From eb2d6106af9e1f9f4eac914695567cc0156d584f Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 23:59:40 +0300 Subject: [PATCH] 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 --- tools/ludic-cli/lsp_test.ludic | 52 ++++++ tools/ludic-tools/lsp.ludic | 287 +++++++++++++++++++++++++++--- tools/ludic-tools/lsp/types.ludic | 3 +- 3 files changed, 321 insertions(+), 21 deletions(-) diff --git a/tools/ludic-cli/lsp_test.ludic b/tools/ludic-cli/lsp_test.ludic index c20c75fa..640a3be4 100644 --- a/tools/ludic-cli/lsp_test.ludic +++ b/tools/ludic-cli/lsp_test.ludic @@ -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 "" +} diff --git a/tools/ludic-tools/lsp.ludic b/tools/ludic-tools/lsp.ludic index c68ff219..12335a6f 100644 --- a/tools/ludic-tools/lsp.ludic +++ b/tools/ludic-tools/lsp.ludic @@ -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`, 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 + 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 diff --git a/tools/ludic-tools/lsp/types.ludic b/tools/ludic-tools/lsp/types.ludic index a95dd9bd..1bad38b0 100644 --- a/tools/ludic-tools/lsp/types.ludic +++ b/tools/ludic-tools/lsp/types.ludic @@ -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