ludic-lsp: every declaration by its own kind, every field a child, attribute arguments resolve

documentSymbol named `export property X` and `export registry` a function called
"property" / "registry": `export`, `unsafe` and leading @attributes are now a
lead-in to the declaration after them, which keeps its doc comment and is marked
exported. A record's field default was skipped with padv, which crosses lines, so
a property showed only its first field; a member now ends at a comma, the brace
or its line, past a fn type's parameters and a generic's arguments, and an
attribute's arguments are skipped rather than read as fields.

New symbols: state, event (and cancellable), action and port as records (struct,
event, event, interface) with their members; registry (with its record, for
go-to-type, and its line as detail); enum and its members; component and view
with props, state, fields, functions and events; reducer, named "S on A"; a
module-level let as a constant; def and bind bodies, module / friend / numbers
lines read past. A member of a component is not a name for the whole unit.

Hover and definition on an attribute's argument: @Node(model) / @Clip / @Material
to the field of the record it is written in, @Ref(Kits) to the registry (as any
top-level name), and hover on the attribute shows its docs/language page. A
fn-typed or generic field's type is shown whole. lsp_test (test-lsp) holds all of
it on a module file with each kind of declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 23:59:40 +03:00
parent 2ad6edaae3
commit eb2d6106af
3 changed files with 321 additions and 21 deletions

View file

@ -286,6 +286,20 @@ function cmd_test_lsp() -> int {
lsp_notify("textDocument/didSave", `{{"textDocument":{lsp_doc(ccf)}}}`)
lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(ccf)}}}`)
# ---- the declarations a module file holds (R8): exported records, a registry with its
# attributes, states, events, actions, ports, enums, reducers and components, every member
let dsrc = "module kits uses base\n\n# a kit\nexport property Kit {\n key: string = \"\"\n @Asset(\"gltf\") model: string = \"\"\n @Node(model) grip: string = \"\"\n @Ref(Kits) spare: int = 0\n pick: fn(int, float) -> bool\n}\n\n# every kit\n@AppendOnly @ByKey\nexport registry Kits of Kit as KIT from \"kits.lres\"\nexport state KitState {\n held: int = -1\n seen: []int\n}\nexport event KitDropped { kit: int, x: float }\nexport action PickKit { kit: int }\nexport port KitWorld {\n ground: fn(float, float) -> float\n}\nenum Grade { Low, High }\nreducer KitState on PickKit(st: mut KitState, a: PickKit) {\n st.held = a.kit\n}\ncomponent Tray(k: KitState) {\n prop step: int = 1\n state open: bool = false\n on press() { }\n}\nexport function kit_weight(k: Kit) -> int { return k.spare }\n"
let dfile = tmp_path("lsp_decls.ludic")
write_file(dfile, dsrc)
lsp_open(dfile, "ludic")
let duri = lsp_doc(dfile)
let dsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{duri}}}`)
let dref = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 6)}}}`)
let dhov = lsp_request("textDocument/hover", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 6)}}}`)
let dnode = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Node(model)", 7)}}}`)
let dty = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "kit_weight(k: Kit)", 14)}}}`)
let dat = lsp_request("textDocument/hover", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 1)}}}`)
# ---- workspace-wide symbol search ---------------------------------------
let ws = lsp_request("workspace/symbol", `{{"query":"Stats"}}`)
@ -456,5 +470,43 @@ function cmd_test_lsp() -> int {
check_true("a name inside a template hole resolves", Text.contains(j_get(j_get(lsp_result(thov), "contents"), "value").s, "param variant: int"))
check("find-usages reaches into template holes", loc_lines_key(lsp_result(trefs)), "1,3,4")
check_true("positions after a multi-line template stay on the right line", Text.contains(j_get(j_get(lsp_result(tafter), "contents"), "value").s, "let multi"))
# a module file's declarations, each by its own name and kind, with every member as a child
let dk = lsp_result(dsyms)
check_true("an exported property is a struct named for itself, all its fields children", lsp_sym_is(dk, "Kit", 23, 5))
check_true("a registry is a symbol of its own, with what it holds", lsp_sym_is(dk, "Kits", 18, 0) and Text.contains(lsp_sym_detail(dk, "Kits"), "of Kit as KIT"))
check_true("a state is a struct with its fields", lsp_sym_is(dk, "KitState", 23, 2))
check_true("an event and an action are events", lsp_sym_is(dk, "KitDropped", 24, 2) and lsp_sym_is(dk, "PickKit", 24, 1))
check_true("a port is an interface, a fn-typed member written out", lsp_sym_is(dk, "KitWorld", 11, 1) and Text.contains(lsp_sym_detail(dk, "KitWorld"), "fn(float, float) -> float"))
check_true("an enum and its members", lsp_sym_is(dk, "Grade", 10, 2))
check_true("a reducer is named for its state and its action", lsp_sym_is(dk, "KitState on PickKit", 12, -1))
check_true("a component holds its props, state and events", lsp_sym_is(dk, "Tray", 5, 3))
check_true("an exported function is still a function", lsp_sym_is(dk, "kit_weight", 12, -1))
check_true("go-to-definition on @Ref(Kits) reaches the registry", j_get(j_get(j_get(lsp_result(dref), "range"), "start"), "line").num == 13)
check_true("hover on @Ref(Kits) shows the registry", Text.contains(j_get(j_get(lsp_result(dhov), "contents"), "value").s, "registry Kits of Kit"))
check_true("go-to-definition on @Node(model) reaches the field it names", j_get(j_get(j_get(lsp_result(dnode), "range"), "start"), "line").num == 5)
check_true("go-to-definition on an exported record's type name", j_get(j_get(j_get(lsp_result(dty), "range"), "start"), "line").num == 3)
check_true("hover on an attribute shows its reference page", Text.contains(j_get(j_get(lsp_result(dat), "contents"), "value").s, "@Ref(Registry)"))
return report()
}
# the symbol named `name` among `syms` has LSP kind `kind` and, unless n is -1, n children
function lsp_sym_is(syms: JVal, name: pointer, kind: int, n: int) -> bool {
var i = 0
while i < len(syms.kids) {
let y = syms.kids[i]
if j_get(y, "name").s == name {
if j_get(y, "kind").num != kind { return false }
return n < 0 or len(j_get(y, "children").kids) == n
}
i += 1
}
return false
}
function lsp_sym_detail(syms: JVal, name: pointer) -> pointer {
var i = 0
while i < len(syms.kids) {
if j_get(syms.kids[i], "name").s == name { return j_get(syms.kids[i], "detail").s }
i += 1
}
return ""
}

View file

@ -48,6 +48,11 @@ program LudicLsp {
const LS_QUERYVAR: int = 15
const LS_STATE: int = 16
const LS_IMPORT: int = 17
const LS_REGISTRY: int = 18 # registry R of T: a table of named entries
const LS_ENUM: int = 19
const LS_ENUMMEMBER: int = 20
const LS_VIEW: int = 21 # a UI component or a view
const LS_REDUCER: int = 22 # reducer S on A: named "S on A" in the outline
# ---- semantic classes ----
const SC_NONE: int = 0
@ -175,6 +180,7 @@ program LudicLsp {
exported: int = 0
init: int = 0 # first token of the initializer (let/var/const), or -1
mutable: int = 0 # a `var` local
decl: pointer = null # the word that declared it (property, state, event, action, port, ...)
}
property Doc {
path: pointer = null
@ -466,7 +472,7 @@ program LudicLsp {
function sym_add(kind: int, nametok: int, parent: int) -> int {
let s = new Sym
s.kind = kind; s.tok = nametok; s.parent = parent
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0; s.decl = ""
if nametok >= 0 { s.start = pD.tk_start[nametok]; s.end = pD.tk_end[nametok]; s.name = ttext(pD, nametok) }
s.bstart = 0; s.bend = cstr_len(pD.text)
push(pD.syms, s)
@ -474,6 +480,62 @@ program LudicLsp {
}
function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 }
function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() }
# past a bracketed group, staying on its line (skip_group also takes the newlines after it)
function lp_jump_group() -> void { let m = cur_match(); if m < 0 { pi += 1 } else { pi = m + 1 } }
# the tokens up to the end of this line as written, joined by single spaces (after a leading one)
function lp_rest_of_line() -> pointer {
let b = buf_new()
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
if pk() != LT_COMMENT { buf_putc(b, ' '); buf_puts(b, ttext(pD, pi)) }
pi += 1
}
return buf_str(b)
}
# a component's or a view's body: its props and state (and a view's fields) as fields, its
# functions and events as functions
function lp_view_body(sidx: int) -> void {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
padv()
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
let before = pi
if pis("function") { lp_decl(sidx) }
else if pis("on") {
padv()
let nt = pname()
if nt >= 0 {
let f = sym_add(LS_FN, nt, sidx)
pD.syms[f].detail = "on " + pD.syms[f].name
if pis("(") { lp_params(f, LS_PARAM, buf_new()) }
}
while pi < ntok(pD) and pk() != LT_EOF and not pis("{") and pk() != LT_NL { pi += 1 }
if pis("{") { skip_group() }
}
else {
var word = ""
if pis("prop") or pis("state") { word = pword(); padv() }
let ft = pname()
if ft >= 0 and (pis(":") or pis("=")) {
let f = sym_add(LS_FIELD, ft, sidx)
if pis(":") { padv(); pD.syms[f].ty = lp_type_text() }
var head = word
if (head == "") { head = pD.syms[sidx].name + "." }
else { head = head + " " }
pD.syms[f].detail = head + pD.syms[f].name + ": " + pD.syms[f].ty
}
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
if close >= 0 and pi >= close { break }
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
}
}
if pi == before { padv() } else { pskipnl() }
if pis(",") { padv() }
}
if close >= 0 { pi = close + 1 } else { padv() }
pskipnl()
}
# `name: Type` pairs inside ( ) — record params, build the signature into sig
function lp_params(owner: int, kind: int, sig: Buf) -> void {
@ -515,7 +577,28 @@ program LudicLsp {
}
let tt = pname()
if tt < 0 { return "" }
return pre + ttext(pD, tt)
var ty = pre + ttext(pD, tt)
# a function type, `fn(int, float) -> bool`, and a generic's arguments, `Pool<Item>`, as written
if (ttext(pD, tt) == "fn") and pis("(") {
let m = cur_match()
if m >= 0 {
ty = ty + pD.src[pD.tk_start[pi]..pD.tk_end[m]]
pi = m + 1
if pis("->") { pi += 1; ty = ty + " -> " + lp_type_text() }
}
}
else if pis("<") {
let a = pi
var depth = 0
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
if pis("<") { depth += 1 }
if pis(">") { depth -= 1 }
pi += 1
if depth == 0 { break }
}
ty = ty + pD.src[pD.tk_start[a]..pD.tk_end[pi - 1]]
}
return ty
}
# (a, b) variable list -> token indices into out; returns count
@ -658,10 +741,33 @@ program LudicLsp {
}
}
var lp_anchor: int = -1 # where a declaration begins when `export`, `unsafe` or @attributes lead it
# one top-level declaration
function lp_decl(parent: int) -> void {
let w = pword()
let decl_tok = pi
var decl_tok = pi
if lp_anchor >= 0 { decl_tok = lp_anchor; lp_anchor = -1 }
# a lead-in: @Attr(args) (a registry's @AppendOnly, a handler's @On(E)), `export` or `unsafe`
# before a declaration - read past, the declaration keeps where it began and says it is exported
if pk() == LT_ANNO {
padv()
if pis("(") { skip_group() }
lp_anchor = decl_tok
lp_decl(parent)
return
}
if (w == "export" or w == "unsafe") and pi + 1 < ntok(pD) and not tis(pD, pi + 1, "function") and not tis(pD, pi + 1, "pure") {
let first = len(pD.syms)
padv()
lp_anchor = decl_tok
lp_decl(parent)
if w == "export" {
var q = first
while q < len(pD.syms) { if pD.syms[q].parent == parent { pD.syms[q].exported = 1 }; q += 1 }
}
return
}
if w == "import" {
padv()
@ -677,18 +783,23 @@ program LudicLsp {
}
return
}
var wd = w
if w == "export" and tis(pD, pi + 1, "state") { padv(); wd = "state" }
if (wd == "property") or (wd == "model") or (wd == "state") { # a state: a record with one instance
let is_comp = (wd == "property") or (wd == "state")
padv(); let nt = pname(); if nt < 0 { return }
let wd = w
# records: a property, a state (one instance), an event, an action, a port (its members are
# fn-typed), a model (its members are components)
if (wd == "property") or (wd == "model") or (wd == "state") or (wd == "event") or (wd == "action") or (wd == "port") {
let is_comp = not (wd == "model")
padv()
if wd == "event" and pis("cancellable") { padv() }
let nt = pname(); if nt < 0 { return }
var kind = LS_ARCHETYPE
if is_comp { kind = LS_COMPONENT }
let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].decl = wd
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
while pk() == LT_ANNO { padv() }
if pis("<") { while pi < ntok(pD) and not pis(">") and pk() != LT_EOF { padv() }; padv() } # property Pool<T>
while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } }
let det = buf_new()
buf_puts(det, w); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {")
buf_puts(det, wd); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {")
if not pis("{") { pD.syms[sidx].detail = buf_str(det); return }
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
@ -698,14 +809,19 @@ program LudicLsp {
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
if pis(",") { padv(); continue }
while pk() == LT_ANNO { padv() } # @Sync / @Computed on a field
while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } } # @Ref(Items), @Range(0, 1), @Sync ...
let ft = pname(); if ft < 0 { padv(); continue }
let f = sym_add(LS_FIELD, ft, sidx)
var fty = ""
if pis(":") { padv(); fty = lp_type_text() }
pD.syms[f].ty = fty
pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty))
if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } }
# the rest of the member - a fn type's parameters and result, a generic's arguments, the
# default - ends at a comma, the closing brace or the end of its line
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
if close >= 0 and pi >= close { break }
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
}
if not first { buf_putc(det, ',') }
buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty)
first = false
@ -716,9 +832,86 @@ program LudicLsp {
pskipnl()
return
}
if (w == "const") or (w == "var") {
if w == "open" and tis(pD, pi + 1, "registry") { padv(); lp_anchor = decl_tok; lp_decl(parent); return }
if w == "registry" { # registry Name of Record [as P] [from "file.lres"]
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_REGISTRY, nt, parent)
pD.syms[sidx].decl = "registry"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis("of") { pD.syms[sidx].ty = ttext(pD, pi + 1) } # its record, for go-to-type
pD.syms[sidx].detail = "registry " + pD.syms[sidx].name + lp_rest_of_line()
pskipnl()
return
}
if w == "enum" { # enum Name { A, B = 2, C }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_ENUM, nt, parent)
pD.syms[sidx].decl = "enum"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
pD.syms[sidx].detail = "enum " + pD.syms[sidx].name
if pis("{") {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] }
padv()
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
let mt = pname()
if mt < 0 { padv(); continue }
let m = sym_add(LS_ENUMMEMBER, mt, sidx)
pD.syms[m].detail = pD.syms[sidx].name + "." + pD.syms[m].name
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") { pi += 1 }
if pis(",") { padv() } else { pskipnl() }
}
if close >= 0 { pi = close + 1 } else { padv() }
pskipnl()
}
return
}
if (w == "component") or (w == "view") { # component Name[(states)] { prop / state / function / on }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_VIEW, nt, parent)
pD.syms[sidx].decl = w
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
pD.syms[sidx].detail = w + " " + pD.syms[sidx].name
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
if pis("{") { lp_view_body(sidx) }
return
}
if w == "reducer" { # reducer State on Action(states, a: Action) { ... }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_REDUCER, nt, parent)
pD.syms[sidx].decl = "reducer"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis("on") { padv(); let at = pname(); if at >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " on " + ttext(pD, at) } }
pD.syms[sidx].detail = "reducer " + pD.syms[sidx].name
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
if pis("{") {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
var q = 0
while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 }
lp_block(sidx, pD.syms[sidx].bend)
}
return
}
# a def's entry and a bind's answers are data: their bodies hold no declaration
if (w == "def") or (w == "bind") {
padv()
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis("{") { pi += 1 }
if pis("{") { skip_group() } else { pskipnl() }
return
}
# a line that says what a file is: module / friend module / numbers float
if (w == "module") or (w == "friend") or (w == "numbers") {
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL { pi += 1 }
pskipnl()
return
}
if (w == "const") or (w == "var") or (w == "let") {
var kind = LS_VAR
if w == "const" { kind = LS_CONST }
if (w == "const") or (w == "let") { kind = LS_CONST }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
@ -939,10 +1132,10 @@ program LudicLsp {
# ============================================================================
function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE }
function is_toplevel_kind(k: int) -> bool {
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT or k == LS_REGISTRY or k == LS_ENUM or k == LS_VIEW
}
function is_outline_kind(k: int) -> bool {
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_REGISTRY or k == LS_ENUM or k == LS_ENUMMEMBER or k == LS_VIEW or k == LS_REDUCER
}
# ---- paths & URIs ----
@ -1216,6 +1409,37 @@ program LudicLsp {
}
return best
}
# a name the whole unit sees: a top-level kind, and not a member of a component or a view
function is_top_sym(D: Doc, k: int) -> bool {
let y = D.syms[k]
if not is_toplevel_kind(y.kind) { return false }
return y.parent < 0 or D.syms[y.parent].kind != LS_VIEW
}
# the field an attribute's argument names - `@Node(model)`, `@Clip(model)` - in the record the
# attribute is written in, or -1 (a registry, `@Ref(Items)`, and a constant resolve as any name)
function attr_arg_field(D: Doc, tok: int) -> int {
var p = prev_sig(D, tok)
while p >= 0 and (tis(D, p, ",") or D.tk_kind[p] == LT_ID or D.tk_kind[p] == LT_STR or D.tk_kind[p] == LT_INT or D.tk_kind[p] == LT_FLOAT) { p = prev_sig(D, p) }
if p < 0 or not tis(D, p, "(") { return -1 }
let a = prev_sig(D, p)
if a < 0 or D.tk_kind[a] != LT_ANNO { return -1 }
let off = D.tk_start[tok]
let name = ttext(D, tok)
var rec = -1
var i = 0
while i < len(D.syms) {
let y = D.syms[i]
if (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) and off >= y.bstart and off <= y.bend and y.bend > y.bstart { rec = i }
i += 1
}
if rec < 0 { return -1 }
i = 0
while i < len(D.syms) {
if D.syms[i].kind == LS_FIELD and D.syms[i].parent == rec and (D.syms[i].name == name) { return i }
i += 1
}
return -1
}
# returns sym index in g_owner (set), or -1
function find_top(D: Doc, name: pointer) -> int {
let rel = related(D)
@ -1223,7 +1447,7 @@ program LudicLsp {
while i < len(rel) {
var k = 0
while k < len(rel[i].syms) {
if is_toplevel_kind(rel[i].syms[k].kind) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k }
if is_top_sym(rel[i], k) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k }
k += 1
}
i += 1
@ -1232,7 +1456,7 @@ program LudicLsp {
while i < len(g_docs) {
var k = 0
while k < len(g_docs[i].syms) {
if is_toplevel_kind(g_docs[i].syms[k].kind) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k }
if is_top_sym(g_docs[i], k) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k }
k += 1
}
i += 1
@ -1276,6 +1500,8 @@ program LudicLsp {
}
let l = find_local(D, name, D.tk_start[tok])
if l >= 0 { g_owner = D; g_symidx = l; return tok }
let af = attr_arg_field(D, tok)
if af >= 0 { g_owner = D; g_symidx = af; return tok }
let t = find_top(D, name)
if t >= 0 { g_symidx = t; return tok }
g_owner = null; g_symidx = -1
@ -1681,6 +1907,10 @@ program LudicLsp {
if k == LS_PARAM { return SC_PARAM }
if k == LS_QUERYVAR or k == LS_LOCAL or k == LS_STATE { return SC_VARIABLE }
if k == LS_UNIT { return SC_UI }
if k == LS_REGISTRY or k == LS_ENUMMEMBER { return SC_CONST }
if k == LS_ENUM { return SC_TYPE }
if k == LS_VIEW { return SC_UI }
if k == LS_REDUCER { return SC_FUNCTION }
return SC_UNKNOWN
}
function classify(D: Doc) -> void {
@ -2245,8 +2475,21 @@ program LudicLsp {
if k == LS_WIDGET { return 14 }
if k == LS_SCENE { return 3 }
if k == LS_LAYER { return 3 }
if k == LS_REGISTRY { return 18 }
if k == LS_ENUM { return 10 }
if k == LS_ENUMMEMBER { return 22 }
if k == LS_VIEW { return 5 }
if k == LS_REDUCER { return 12 }
return 13
}
# a symbol's LSP SymbolKind: a record says which it is by the word that declared it
function sym_lsp_kind(y: Sym) -> int {
if y.kind == LS_COMPONENT {
if (y.decl == "event") or (y.decl == "action") { return 24 }
if (y.decl == "port") { return 11 }
}
return lsp_symbol_kind(y.kind)
}
function comp_kind_of(k: int) -> int {
if k == LS_COMPONENT { return 22 }
if k == LS_ARCHETYPE { return 7 }
@ -2259,6 +2502,10 @@ program LudicLsp {
if k == LS_SCENE { return 11 }
if k == LS_LAYER { return 11 }
if k == LS_FIELD { return 5 }
if k == LS_REGISTRY { return 21 }
if k == LS_ENUM { return 13 }
if k == LS_ENUMMEMBER { return 20 }
if k == LS_VIEW { return 7 }
return 6
}
@ -2370,7 +2617,7 @@ program LudicLsp {
first = 0
buf_puts(o, "{\"name\":"); jstr_out(o, y.name)
buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail)
buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind))
buf_puts(o, ",\"kind\":"); buf_puti(o, sym_lsp_kind(y))
buf_puts(o, ",\"range\":")
var a = y.start; var b = y.end
if y.bend > y.end and y.bstart >= y.start { b = y.bend }
@ -2767,7 +3014,7 @@ program LudicLsp {
if keep {
if first == 0 { buf_putc(o, ',') }
first = 0
buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}')
buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, sym_lsp_kind(y)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}')
}
}
k += 1

View file

@ -166,7 +166,8 @@ function sym_hover_code(O: Doc, si: int) -> pointer {
function record_hover_code(O: Doc, si: int) -> pointer {
let y = O.syms[si]
let b = buf_new()
if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") }
if not (y.decl == "") { buf_puts(b, y.decl); buf_puts(b, " ") }
else if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") }
buf_puts(b, y.name)
buf_puts(b, " {")
var n = 0