# lsp_test.ludic — drive bin/ludic-lsp over real LSP traffic against the real # source tree (`ludic-dev test-lsp`; replaces tools/test-lsp.py). Every assertion is # about a feature an editor actually calls: if this passes, VS Code, JetBrains, # Neovim and the rest are exercising code paths that have been checked, because # they all speak to this one binary. # # The server is driven in one batch: every request is framed into a single # stdin stream (positions are computed from the same files the server reads), # the server runs to `exit`, and its stdout is parsed back into a result table # keyed by request id plus the last publishDiagnostics per file — exactly what # an interactive client would have observed, without a bidirectional pipe. # ---- the request stream ------------------------------------------------------- var lsp_req: Sb = null var lsp_next_id: int = 0 # one framed JSON-RPC message appended to the stream function lsp_frame(body: pointer) -> void { sb_puts(lsp_req, "Content-Length: "); sb_puts(lsp_req, string(len(body))) sb_puts(lsp_req, "\r\n\r\n"); sb_puts(lsp_req, body) } # queue a request; returns its id (the key into lsp_res after the run) function lsp_request(method: pointer, params: pointer) -> int { lsp_next_id += 1 lsp_frame(`{{"jsonrpc":"2.0","id":{lsp_next_id},"method":"{method}","params":{params}}}`) return lsp_next_id } function lsp_notify(method: pointer, params: pointer) -> void { lsp_frame(`{{"jsonrpc":"2.0","method":"{method}","params":{params}}}`) } # didOpen for a file on disk; returns its text (for position lookups) function lsp_open(path: pointer, lang: pointer) -> pointer { let text = read_file(path) lsp_notify("textDocument/didOpen", `{{"textDocument":{{"uri":"file://{path}","languageId":"{lang}","version":1,"text":{j_quote(text)}}}}}`) return text } function lsp_doc(path: pointer) -> pointer { return `{{"uri":"file://{path}"}}` } # the LSP position of the first occurrence of `needle` (+ `off` characters) function position_of(text: pointer, needle: pointer, off: int) -> pointer { let n = len(text) var line = 0; var a = 0 while a <= n { var b = a while b < n and text[b] != '\n' { b += 1 } let row = text[a..b] let at = Text.index_of(row, needle) if at >= 0 { return `{{"line":{line},"character":{at + off}}}` } line += 1; a = b + 1 } err(`lsp_test: {needle} not found\n`) exit(1) return "" } # ---- the response stream ------------------------------------------------------ property DiagSet { file: pointer = "", msgs: []pointer } var lsp_res: []JVal = new []JVal # index = request id (0 unused) var lsp_diags: []DiagSet = new []DiagSet # the last publish per file basename function lsp_result(id: int) -> JVal { if id < len(lsp_res) { return lsp_res[id] } return jval_new(JV_NULL) } function lsp_diag_set(file: pointer) -> DiagSet { var i = 0 while i < len(lsp_diags) { if lsp_diags[i].file == file { return lsp_diags[i] }; i += 1 } return null } # messages published for `file` (an empty slice when nothing was published) function lsp_diag_msgs(file: pointer) -> []pointer { let d = lsp_diag_set(file) if d == null { return new []pointer } return d.msgs } function basename_of(uri: pointer) -> pointer { var i = len(uri) while i > 0 and uri[i - 1] != '/' { i -= 1 } return uri[i..len(uri)] } function lsp_record(msg: JVal) -> void { let id = j_get(msg, "id") if id.t == JV_NUM { while len(lsp_res) <= id.num { push(lsp_res, jval_new(JV_NULL)) } lsp_res[id.num] = j_get(msg, "result") return } if j_get(msg, "method").s == "textDocument/publishDiagnostics" { let p = j_get(msg, "params") let file = basename_of(j_get(p, "uri").s) let msgs = new []pointer let list = j_get(p, "diagnostics") var i = 0 while i < len(list.kids) { push(msgs, j_get(list.kids[i], "message").s); i += 1 } var d = lsp_diag_set(file) if d == null { d = new DiagSet; d.file = file; push(lsp_diags, d) } d.msgs = msgs } } # split a Content-Length framed stream into messages function lsp_parse_stream(s: pointer) -> void { let n = len(s) var p = 0 while p < n { let h = Text.index_of(s[p..n], "Content-Length:") if h < 0 { return } var q = p + h + 15 while s[q] == ' ' { q += 1 } var size = 0 while s[q] >= '0' and s[q] <= '9' { size = size * 10 + (s[q] - '0'); q += 1 } let body_at = Text.index_of(s[q..n], "\r\n\r\n") if body_at < 0 { return } let start = q + body_at + 4 lsp_record(json_parse(s[start..start + size])) p = start + size } } # ---- JVal conveniences -------------------------------------------------------- function j_has_key(v: JVal, key: pointer) -> bool { return j_get(v, key).t != JV_NULL } # does any element of array `arr` have field `key` equal to `want`? function j_any_field(arr: JVal, key: pointer, want: pointer) -> bool { var i = 0 while i < len(arr.kids) { if j_get(arr.kids[i], key).s == want { return true }; i += 1 } return false } # the sorted, de-duplicated start lines of a TextEdit array, as "1,7" function lines_key(edits: JVal) -> pointer { let lines = new []int var i = 0 while i < len(edits.kids) { let l = j_get(j_get(j_get(edits.kids[i], "range"), "start"), "line").num if not List.contains(lines, l) { push(lines, l) } i += 1 } List.sort(lines) var out = "" i = 0 while i < len(lines) { if i > 0 { out += "," }; out += string(lines[i]); i += 1 } return out } # the sorted, de-duplicated start lines of a Location array, as "2,11" function loc_lines_key(locs: JVal) -> pointer { let lines = new []int var i = 0 while i < len(locs.kids) { let l = j_get(j_get(j_get(locs.kids[i], "range"), "start"), "line").num if not List.contains(lines, l) { push(lines, l) } i += 1 } List.sort(lines) var out = "" i = 0 while i < len(lines) { if i > 0 { out += "," }; out += string(lines[i]); i += 1 } return out } # the edits of a WorkspaceEdit's first (only) changed document function first_change(ws: JVal) -> JVal { let changes = j_get(ws, "changes") if len(changes.kids) == 0 { return jval_new(JV_ARR) } return changes.kids[0] } # how many distinct file basenames a Location array spans function distinct_files(locs: JVal) -> int { let seen = new []pointer var i = 0 while i < len(locs.kids) { let f = basename_of(j_get(locs.kids[i], "uri").s) if not set_has(seen, f) { push(seen, f) } i += 1 } return len(seen) } function check_true(label: pointer, cond: bool) -> void { if cond { ok(label) } else { bad(label) } } # ---- the suite ---------------------------------------------------------------- # Exit 2 when there is no server to test (the caller reports a skip, not a pass). function cmd_test_lsp() -> int { let server = getenv_or("LUDIC_LSP", "bin/ludic-lsp") if not is_exec(server) { skip("ludic-lsp missing"); return 2 } let root = capture_line("pwd") lsp_req = sb_new() let init = lsp_request("initialize", `{{"rootUri":"file://{root}","capabilities":{{}},"initializationOptions":{{"compilerDiagnostics":true,"compilerPath":"{root}/bin/ludicc"}}}}`) lsp_notify("initialized", "{}") # ---- a fragment file: no `program` block, only reachable through an import -- let frag = `{root}/examples/games/chronorift/combat.ludic` let text = lsp_open(frag, "ludic") let uri = lsp_doc(frag) let syms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{uri}}}`) # a field reached through a query binding, declared in ANOTHER file let pos = position_of(text, "st.guard", 3) let hov = lsp_request("textDocument/hover", `{{"textDocument":{uri},"position":{pos}}}`) let def = lsp_request("textDocument/definition", `{{"textDocument":{uri},"position":{pos}}}`) # a binding is not yet inside its own scope at the point it is written, so # resolving it has to go through the declaration table, not scope lookup let qpos = position_of(text, "for (e) in query", 5) let qhov = lsp_request("textDocument/hover", `{{"textDocument":{uri},"position":{qpos}}}`) let comp = lsp_request("textDocument/completion", `{{"textDocument":{uri},"position":{pos}}}`) # a query's term list admits properties and models and nothing else — # not the loop variables, not builtins, not keywords let qsrc = "program QT {\n property Pos { x: int = 0 }\n property Vel { dx: int = 0 }\n handler S phase Update {\n for (a, b) in query [Pos, ] { }\n }\n}\n" let qfile = tmp_path("lsp_query.ludic") write_file(qfile, qsrc) lsp_open(qfile, "ludic") let quri = lsp_doc(qfile) let qcomp = lsp_request("textDocument/completion", `{{"textDocument":{quri},"position":{position_of(qsrc, "[Pos, ]", 6)}}}`) let body = lsp_request("textDocument/completion", `{{"textDocument":{quri},"position":{position_of(qsrc, "] { }", 3)}}}`) let refs = lsp_request("textDocument/references", `{{"textDocument":{uri},"position":{position_of(text, "query [Stats", 7)},"context":{{"includeDeclaration":true}}}}`) let hints = lsp_request("textDocument/inlayHint", `{{"textDocument":{uri},"range":{{"start":{{"line":0,"character":0}},"end":{{"line":9999,"character":0}}}}}}`) # a namespaced call resolves to its underlying bare-builtin signature let sig = lsp_request("textDocument/signatureHelp", `{{"textDocument":{uri},"position":{position_of(text, "Random.chance(", 14)}}}`) let toks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{uri}}}`) let folds = lsp_request("textDocument/foldingRange", `{{"textDocument":{uri}}}`) # ---- structural diagnostics arrive while typing -------------------------- lsp_notify("textDocument/didChange", `{{"textDocument":{{"uri":"file://{frag}","version":2}},"contentChanges":[{{"text":{j_quote(text + "\n{ oops")}}}]}}`) lsp_request("textDocument/documentSymbol", `{{"textDocument":{uri}}}`) # ---- the LSP analyses opened files on its own ---------------------------- let badf = tmp_path("lsp_bad.ludic") write_file(badf, "program Bad {\n property P { x: nosuchtype = 0 }\n}\n") lsp_open(badf, "ludic") let bsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(badf)}}}`) # ---- markdown: ```ludic fences are real Ludic --------------------------- let md = tmp_path("lsp_test.md") write_file(md, "# Title\n\nprose\n\n```ludic\nprogram D { property Pt { x: int = 0 } }\n```\n\n```python\nz = 1\n```\n") lsp_open(md, "markdown") let mduri = lsp_doc(md) let mdsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{mduri}}}`) let mdtoks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{mduri}}}`) let fmt = lsp_request("textDocument/formatting", `{{"textDocument":{mduri},"options":{{"tabSize":2,"insertSpaces":true}}}}`) # the repository's own documentation must not light up with false errors let docs = ["LANGUAGE.md", "README.md", "COMPILING.md"] var di = 0 while di < len(docs) { let path = `{root}/{docs[di]}` if shq(`test -f {path}`) { lsp_open(path, "markdown") lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(path)}}}`) } di += 1 } let langdoc = lsp_doc(`{root}/LANGUAGE.md`) let langtoks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{langdoc}}}`) # ---- rename has to be resolution-based, not textual ---------------------- # `x` here is a field of Pos, a field of Vel, a local, and a parameter. # Renaming any one of them must leave the other three alone. let amb = tmp_path("lsp_rename.ludic") write_file(amb, "program R {\n property Pos { x: int = 0, y: int = 0 }\n property Vel { x: int = 0 }\n function helper(x: int) -> int { return x + 1 }\n handler A phase Update {\n for (p) in query [Pos] {\n let x = 5\n p.x = p.x + x\n }\n }\n handler B phase Update {\n for (v) in query [Vel] { v.x = v.x + 1 }\n }\n}\n") let atext = lsp_open(amb, "ludic") let auri = lsp_doc(amb) let field = lsp_request("textDocument/rename", `{{"textDocument":{auri},"position":{position_of(atext, "property Pos { x", 15)},"newName":"ZZZ"}}`) let local = lsp_request("textDocument/rename", `{{"textDocument":{auri},"position":{position_of(atext, "let x = 5", 4)},"newName":"ZZZ"}}`) let param = lsp_request("textDocument/rename", `{{"textDocument":{auri},"position":{position_of(atext, "function helper(x", 16)},"newName":"ZZZ"}}`) # find-usages must be equally precise let refs_x = lsp_request("textDocument/references", `{{"textDocument":{auri},"position":{position_of(atext, "property Vel { x", 15)},"context":{{"includeDeclaration":true}}}}`) # ---- the compiler's own errors arrive on save ---------------------------- let ccf = tmp_path("lsp_cc.ludic") write_file(ccf, "program CcErr {\n entry {\n let x = (5 +\n }\n}\n") lsp_open(ccf, "ludic") 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"}}`) # ---- navigation an IDE leans on -------------------------------------------- # package imports resolve from the toolchain's packages/, import strings are # links, locals of both kinds have a type, built-ins lead to their reference # page, and a call's argument list completes its parameter names. let nsrc = "program Nav {\n import \"ludic.render3d/fmath.ludic\"\n function area(width: int, height: int) -> int { return width * height }\n entry {\n var total = 0\n let name = \"x\"\n total = total + area(width: 2, )\n Screen.fill_circle(x: 1, y: f_clamp(1, 0, 2), )\n let s = Screen.\n }\n}\n" let nfile = tmp_path("lsp_nav.ludic") write_file(nfile, nsrc) lsp_open(nfile, "ludic") let nuri = lsp_doc(nfile) let links = lsp_request("textDocument/documentLink", `{{"textDocument":{nuri}}}`) let pkgdef = lsp_request("textDocument/definition", `{{"textDocument":{nuri},"position":{position_of(nsrc, "ludic.render3d", 3)}}}`) let xdef = lsp_request("textDocument/definition", `{{"textDocument":{nuri},"position":{position_of(nsrc, "f_clamp", 2)}}}`) let vhov = lsp_request("textDocument/hover", `{{"textDocument":{nuri},"position":{position_of(nsrc, "total = total", 10)}}}`) let lhov = lsp_request("textDocument/hover", `{{"textDocument":{nuri},"position":{position_of(nsrc, "name = ", 1)}}}`) let vrefs = lsp_request("textDocument/references", `{{"textDocument":{nuri},"position":{position_of(nsrc, "var total", 5)},"context":{{"includeDeclaration":true}}}}`) let bdef = lsp_request("textDocument/definition", `{{"textDocument":{nuri},"position":{position_of(nsrc, "fill_circle", 2)}}}`) let bhov = lsp_request("textDocument/hover", `{{"textDocument":{nuri},"position":{position_of(nsrc, "fill_circle", 2)}}}`) let tdef = lsp_request("textDocument/definition", `{{"textDocument":{nuri},"position":{position_of(nsrc, "int, height", 1)}}}`) let acomp = lsp_request("textDocument/completion", `{{"textDocument":{nuri},"position":{position_of(nsrc, "2, )", 3)}}}`) let nscomp = lsp_request("textDocument/completion", `{{"textDocument":{nuri},"position":{position_of(nsrc, "= Screen.", 9)}}}`) let ahelp = lsp_request("textDocument/signatureHelp", `{{"textDocument":{nuri},"position":{position_of(nsrc, "2), )", 4)}}}`) # contextual keywords, highlights, barrels, semantic modifiers let kdir = tmp_path("lsp_barrel") shell(`mkdir -p {kdir}/camp`) write_file(`{kdir}/camp/index.ludic`, "import \"tent.ludic\"\n") write_file(`{kdir}/camp/tent.ludic`, "function pitch(size: int) -> int { return size }\n") let ksrc = "program K {\n import \"camp\"\n import \"ludic.render3d/fmath.ludic\"\n function go() -> int {\n var model = pitch(2)\n model = model + f_clamp(1, 0, 2)\n Screen.clear(0)\n return model\n }\n}\n" let kfile = `{kdir}/k.ludic` write_file(kfile, ksrc) lsp_open(kfile, "ludic") let kuri = lsp_doc(kfile) let khov = lsp_request("textDocument/hover", `{{"textDocument":{kuri},"position":{position_of(ksrc, "var model", 5)}}}`) let krefs = lsp_request("textDocument/references", `{{"textDocument":{kuri},"position":{position_of(ksrc, "var model", 5)},"context":{{"includeDeclaration":true}}}}`) let kbarrel = lsp_request("textDocument/definition", `{{"textDocument":{kuri},"position":{position_of(ksrc, "pitch(2)", 1)}}}`) let khl = lsp_request("textDocument/documentHighlight", `{{"textDocument":{kuri},"position":{position_of(ksrc, "f_clamp", 1)}}}`) let ktoks = lsp_request("textDocument/semanticTokens/full", `{{"textDocument":{kuri}}}`) # names inside `template {holes}`, and positions after a multi-line template let tsrc = "program TT {\n function tag(variant: int) -> pointer {\n let multi = `a\nb {variant}`\n return `rock_{variant}` + multi\n }\n}\n" let tfile = tmp_path("lsp_tmpl.ludic") write_file(tfile, tsrc) lsp_open(tfile, "ludic") let turi = lsp_doc(tfile) let thov = lsp_request("textDocument/hover", `{{"textDocument":{turi},"position":{position_of(tsrc, "rock_{variant", 7)}}}`) let trefs = lsp_request("textDocument/references", `{{"textDocument":{turi},"position":{position_of(tsrc, "tag(variant", 5)},"context":{{"includeDeclaration":true}}}}`) let tafter = lsp_request("textDocument/hover", `{{"textDocument":{turi},"position":{position_of(tsrc, "+ multi", 3)}}}`) lsp_request("shutdown", "null") lsp_notify("exit", "null") # ---- run the server over the whole stream -------------------------------- let req_path = tmp_path("lsp_req.bin") let res_path = tmp_path("lsp_res.bin") write_file(req_path, sb_str(lsp_req)) shell(`{server} --stdio < {req_path} > {res_path} 2>{tmp_path("lsp_err.txt")}`) let stream = read_file(res_path) if stream == null { bad("ludic-lsp produced no output"); return report() } lsp_parse_stream(stream) # ---- and judge ---------------------------------------------------------- let caps = j_get(lsp_result(init), "capabilities") let wanted = ["completionProvider", "hoverProvider", "definitionProvider", "referencesProvider", "renameProvider", "documentSymbolProvider", "workspaceSymbolProvider", "documentFormattingProvider", "semanticTokensProvider", "foldingRangeProvider", "signatureHelpProvider", "inlayHintProvider", "documentLinkProvider"] var wi = 0 while wi < len(wanted) { check_true(`advertises {wanted[wi]}`, j_has_key(caps, wanted[wi])); wi += 1 } check_true("document symbols in an imported fragment", len(lsp_result(syms).kids) >= 3) let hov_text = j_get(j_get(lsp_result(hov), "contents"), "value").s check_true("hover resolves a field through a query binding", Text.contains(hov_text, "Stats.guard: int")) check_true("go-to-definition crosses files", Text.ends_with(j_get(lsp_result(def), "uri").s, "world.ludic")) let qhov_text = j_get(j_get(lsp_result(qhov), "contents"), "value").s check_true("hover works on a query binding at its declaration site", Text.contains(qhov_text, "query binding")) let items = j_get(lsp_result(comp), "items") check_true("completion after '.' offers only that property's fields", j_any_field(items, "label", "guard") and j_any_field(items, "label", "hp") and not j_any_field(items, "label", "clear")) let qitems = j_get(lsp_result(qcomp), "items") check_true("completion inside query terms lists properties only", len(qitems.kids) == 2 and j_any_field(qitems, "label", "Pos") and j_any_field(qitems, "label", "Vel")) let bitems = j_get(lsp_result(body), "items") check_true("completion in the loop body still has locals and builtins", j_any_field(bitems, "label", "a") and j_any_field(bitems, "label", "clear")) check_true("find-usages spans the compilation unit", distinct_files(lsp_result(refs)) >= 3) check_true("inlay hints name query binding types", j_any_field(lsp_result(hints), "label", ": Stats")) let sigs = j_get(lsp_result(sig), "signatures") # the documented, namespaced spelling (docs/language) when the reference is # present, else the builtin the method lowers to check_true("signature help for a namespaced builtin", len(sigs.kids) > 0 and (Text.starts_with(j_get(sigs.kids[0], "label").s, "Random.chance(") or Text.starts_with(j_get(sigs.kids[0], "label").s, "rng_chance("))) check_true("signature help splits the parameters", len(sigs.kids) > 0 and len(j_get(sigs.kids[0], "parameters").kids) == 1) let tdata = j_get(lsp_result(toks), "data") check_true("semantic tokens produced", len(tdata.kids) / 5 > 100) check_true("semantic token stream is well formed", len(tdata.kids) % 5 == 0) check_true("folding ranges produced", len(lsp_result(folds).kids) > 5) let cmsgs = lsp_diag_msgs("combat.ludic") var unclosed = false var ci = 0 while ci < len(cmsgs) { if Text.contains(cmsgs[ci], "unclosed") { unclosed = true }; ci += 1 } check_true("unbalanced brace reported while typing", unclosed) check_true("LSP opens and analyses a file with no compiler present", j_any_field(lsp_result(bsyms), "name", "Bad")) let mdnames = lsp_result(mdsyms) check_true("symbols found inside a markdown fence", len(mdnames.kids) == 1 and j_get(mdnames.kids[0], "name").s == "D") let mdata = j_get(lsp_result(mdtoks), "data") var first_tok = -1 if len(mdata.kids) > 0 { first_tok = mdata.kids[0].num } check("markdown tokens start on the fence body line", string(first_tok), "5") # A fence is a snippet: elisions and half-shown blocks are the documentation # doing its job, so errors there would be noise. Highlighting still applies. check_true("no diagnostics are reported for markdown", lsp_diag_set("lsp_test.md") != null and len(lsp_diag_msgs("lsp_test.md")) == 0) let fedits = lsp_result(fmt) var ftext = "" if len(fedits.kids) > 0 { ftext = j_get(fedits.kids[0], "newText").s } check_true("markdown formatting leaves prose and other fences alone", Text.contains(ftext, "prose") and Text.contains(ftext, "z = 1")) di = 0 while di < len(docs) { check(`{docs[di]} opens with no diagnostics`, string(len(lsp_diag_msgs(docs[di]))), "0") di += 1 } check_true("LANGUAGE.md fences are highlighted", len(j_get(lsp_result(langtoks), "data").kids) / 5 > 100) check("renaming Pos.x touches only its declaration and its uses", lines_key(first_change(lsp_result(field))), "1,7") check("renaming a local touches only that local", lines_key(first_change(lsp_result(local))), "6,7") check("renaming a parameter touches only that function", lines_key(first_change(lsp_result(param))), "3") check("renaming a parameter makes exactly two edits", string(len(first_change(lsp_result(param)).kids)), "2") check("find-usages of Vel.x does not pick up Pos.x", loc_lines_key(lsp_result(refs_x)), "2,11") check_true("workspace symbol search finds a property", j_any_field(lsp_result(ws), "name", "Stats")) let ccmsgs = lsp_diag_msgs("lsp_cc.ludic") var cc_seen = false ci = 0 while ci < len(ccmsgs) { if Text.contains(ccmsgs[ci], "expected expression") { cc_seen = true }; ci += 1 } check_true("ludicc's own error is published on save (compilerDiagnostics)", cc_seen) let link0 = lsp_result(links) check_true("an import string is a link to its file", len(link0.kids) == 1 and Text.ends_with(j_get(link0.kids[0], "target").s, "packages/ludic.render3d/fmath.ludic")) let pd = lsp_result(pkgdef) check_true("go-to-definition on a package import opens the package file", len(pd.kids) == 1 and Text.ends_with(j_get(pd.kids[0], "uri").s, "packages/ludic.render3d/fmath.ludic")) check_true("a name from an imported package resolves into it", Text.ends_with(j_get(lsp_result(xdef), "uri").s, "fmath.ludic")) check_true("hover gives a var local its inferred type", Text.contains(j_get(j_get(lsp_result(vhov), "contents"), "value").s, "var total: int")) check_true("hover gives a let local its inferred type", Text.contains(j_get(j_get(lsp_result(lhov), "contents"), "value").s, "let name: string")) check("find-usages of a var local", loc_lines_key(lsp_result(vrefs)), "4,6") check_true("go-to-definition on a namespace method opens its reference page", Text.ends_with(j_get(lsp_result(bdef), "uri").s, "screen-fill_circle.md")) check_true("hover on a namespace method links its documentation", Text.contains(j_get(j_get(lsp_result(bhov), "contents"), "value").s, "screen-fill_circle.html")) check_true("go-to-definition on a built-in type opens its reference page", Text.ends_with(j_get(lsp_result(tdef), "uri").s, "type-int.md")) let aitems = j_get(lsp_result(acomp), "items") check_true("an argument list completes the parameters not given yet", j_any_field(aitems, "label", "height:") and not j_any_field(aitems, "label", "width:") and not j_any_field(aitems, "label", "let")) check_true("Namespace. completes its members", j_any_field(j_get(lsp_result(nscomp), "items"), "label", "fill_circle")) let ah = lsp_result(ahelp) check_true("signature help tracks the argument after a named one", j_get(ah, "activeParameter").num == 2 and len(j_get(j_get(ah, "signatures").kids[0], "parameters").kids) == 4) check_true("a declaration word used as a variable is a variable", Text.contains(j_get(j_get(lsp_result(khov), "contents"), "value").s, "var model: int")) check("find-usages of a local named like a keyword", loc_lines_key(lsp_result(krefs)), "4,5,7") check_true("an import of a directory reads its index.ludic barrel", Text.ends_with(j_get(lsp_result(kbarrel), "uri").s, "camp/tent.ludic")) let hl = lsp_result(khl) check_true("highlight usages stays inside the document", len(hl.kids) == 1 and j_get(j_get(j_get(hl.kids[0], "range"), "start"), "line").num == 5) let kd = j_get(lsp_result(ktoks), "data") var vendor_seen = false var lib_seen = false var ti = 0 while ti + 4 < len(kd.kids) { if (kd.kids[ti + 4].num & 8) != 0 { vendor_seen = true } if (kd.kids[ti + 4].num & 4) != 0 and kd.kids[ti + 3].num == 5 { lib_seen = true } ti += 5 } check_true("semantic tokens mark package symbols as vendor", vendor_seen) check_true("semantic tokens mark library calls as defaultLibrary", lib_seen) 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 "" }