diff --git a/App/lib/lsp/LspIndex.vp b/App/lib/lsp/LspIndex.vp index b2595c9..46900b8 100644 --- a/App/lib/lsp/LspIndex.vp +++ b/App/lib/lsp/LspIndex.vp @@ -61,6 +61,7 @@ class LspTarget: container: str sel: LspSource.LspRange* has_sel: int + is_macro: int def __new__(self, pool: memhub.MemManager*) -> rawptr: p: LspTarget* = (LspTarget*)(pool.alloc(LspTarget.__sizeof__())) @@ -82,6 +83,8 @@ class LspIndex: refs_built: int last_path: str last_entry: LspFileEntry* + mod_names: list[str]* + mod_paths: list[str]* def __new__(self, owner: memhub.MemManager*) -> rawptr: p: LspIndex* = (LspIndex*)(owner.alloc(LspIndex.__sizeof__())) @@ -94,6 +97,8 @@ _index: LspIndex* = None # During bulk occurrence indexing, resolve only through already-indexed files. On-demand parsing # here would repeat work per occurrence and exhaust the scratch arena. _bulk_resolve: int = 0 +# Semantic-token classification resolves many imports; it must not parse target files on demand. +_resolve_only: int = 0 def Init(owner: memhub.MemManager*) -> int: @@ -124,6 +129,17 @@ def Invalidate(): _index.refs_built = 0 +def BeginResolveOnly(): + """Resolve imports through the built index only (no on-demand parses).""" + global _resolve_only + _resolve_only = 1 + + +def EndResolveOnly(): + global _resolve_only + _resolve_only = 0 + + def _Suffix(pool: memhub.MemManager*, s: str, suffix: str) -> str: if s is None or suffix is None: return None @@ -233,6 +249,7 @@ def _CopySymbol(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*, return None c.name = LspPools.DupStr(pool, s.name) c.kind = s.kind + c.is_macro = s.is_macro c.detail = LspSymbols._CopyStr(pool, s.detail, LspSymbols.LSP_SIGNATURE_CAP) c.doc = LspSymbols._CopyStr(pool, s.doc, LspSymbols.LSP_DOC_CAP) c.file = file @@ -337,6 +354,8 @@ def Ensure(ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*) -> in idx.stale = 0 idx.last_path = None idx.last_entry = None + idx.mod_names = list[str](idx.tree.mb, 16) + idx.mod_paths = list[str](idx.tree.mb, 16) if idx.files is None or idx.roots is None: return -1 if ws is not None: @@ -617,6 +636,27 @@ def SetRoots(ws: LspWorkspace.LspWorkspace*) -> int: return 0 +def RefreshRoots(ws: LspWorkspace.LspWorkspace*) -> int: + """Rebuild roots after the workspace adopts a project.vpj deeper in the tree.""" + idx: LspIndex* = _index + if idx is None or ws is None: + return -1 + if idx.roots is None: + idx.roots = list[str](idx.tree.mb, 8) + if idx.roots is None: + return -1 + idx.roots.clear() + if idx.mod_names is not None: + idx.mod_names.clear() + if idx.mod_paths is not None: + idx.mod_paths.clear() + _AddRoot(idx, ws.SourceDir()) + _AddRootsString(idx, ws.Includes()) + idx.stale = 1 + idx.refs_built = 0 + return 0 + + def FileEntry(path: str) -> LspFileEntry*: idx: LspIndex* = _index if idx is None or path is None or idx.files is None: @@ -644,7 +684,7 @@ def ResolveInFile(pool: memhub.MemManager*, table_pool: memhub.MemManager*, s: LspSymbols.LspSymbol* = FindSymbol(path, container, name) if s is not None: return s - if _bulk_resolve != 0: + if _bulk_resolve != 0 or _resolve_only != 0: return None text: str = LspFiles.ReadSource(pool, path) if text is None: @@ -701,6 +741,7 @@ def _SymbolTarget(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*) -> LspTarg t.file = s.file t.name = s.name t.kind = s.kind + t.is_macro = s.is_macro t.detail = s.detail t.doc = s.doc t.container = s.container @@ -794,18 +835,32 @@ def _ModulePath(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, idx: LspIndex* = _index if idx is None or module is None or idx.roots is None: return None + if idx.mod_names is None: + idx.mod_names = list[str](idx.tree.mb, 16) + idx.mod_paths = list[str](idx.tree.mb, 16) + if idx.mod_names is not None: + i: int = 0 + while i < int(len(idx.mod_names)): + if string.strcmp(idx.mod_names.get(sizet(i)), module) == 0: + return idx.mod_paths.get(sizet(i)) + i += 1 rel: str = _RelPath(pool, module) if rel is None: return None - i: int = 0 + found: str = None + i = 0 while i < int(len(idx.roots)): root: str = idx.roots.get(sizet(i)) i += 1 p: str = None k: int = 0 if _TryPath(pool, root, rel, &p, &k) != 0: - return p - return None + found = p + break + if idx.mod_names is not None and int(len(idx.mod_names)) < 256: + idx.mod_names.append(LspPools.DupStr(idx.tree.mb, module)) + idx.mod_paths.append(LspPools.DupStr(idx.tree.mb, found)) + return found def _ChainFromModule(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, @@ -899,6 +954,32 @@ def ResolveSymbol(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, return _SymbolTarget(pool, s) +def ResolveMembers(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + base: LspTarget*, rest: str) -> LspTarget*: + """Walk class members from an already-resolved base target (no import re-resolution).""" + if base is None or rest is None: + return None + if base.kind != LspSymbols.LSP_KIND_CLASS or base.file is None or base.name is None: + return None + container: str = base.name + file: str = base.file + segs: str = rest + while segs is not None: + h: str = None + r: str = None + LspSymbols._SplitHead(pool, segs, &h, &r) + if h is None: + return None + m: LspSymbols.LspSymbol* = ResolveInFile(pool, table_pool, file, container, h) + if m is None: + return None + if r is None: + return _SymbolTarget(pool, m) + container = m.name + segs = r + return None + + def ResolveChain(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*, table_pool: memhub.MemManager*, from_file: str, head: LspSymbols.LspSymbol*, diff --git a/App/lib/lsp/LspSemantic.vp b/App/lib/lsp/LspSemantic.vp index 77b143f..b9c1926 100644 --- a/App/lib/lsp/LspSemantic.vp +++ b/App/lib/lsp/LspSemantic.vp @@ -6,6 +6,7 @@ import ast import lib.lsp.LspDocuments as LspDocuments import lib.lsp.LspSource as LspSource import lib.lsp.LspSymbols as LspSymbols +import lib.lsp.LspIndex as LspIndex import lib.lsp.LspWorkspace as LspWorkspace import lib.lsp.LspNavigation as LspNavigation import lib.lsp.LspTransport as LspTransport @@ -132,30 +133,145 @@ def _ModsFor(s: LspSymbols.LspSymbol*) -> int: return 0 -def _TokenForSymbol(pool: memhub.MemManager*, +class LspSemCtx: + ws: LspWorkspace.LspWorkspace* + table_pool: memhub.MemManager* + file: str + sym_keys: list[LspSymbols.LspSymbol*]* + sym_tgts: list[LspIndex.LspTarget*]* + chain_heads: list[LspSymbols.LspSymbol*]* + chain_rests: list[str]* + chain_tgts: list[LspIndex.LspTarget*]* + + def __new__(self, pool: memhub.MemManager*) -> rawptr: + p: LspSemCtx* = (LspSemCtx*)(pool.alloc(LspSemCtx.__sizeof__())) + if p: + string.memset(p, 0, LspSemCtx.__sizeof__()) + return rawptr(p) + + +def _TypeForTarget(t: LspIndex.LspTarget*) -> int: + if t is None: + return -1 + if t.kind == LspSymbols.LSP_KIND_MODULE: + return ST_NAMESPACE + if t.kind == LspSymbols.LSP_KIND_CLASS: + return ST_CLASS + if t.kind == LspSymbols.LSP_KIND_FUNCTION: + return ST_MACRO if t.is_macro != 0 else ST_FUNCTION + if t.kind == LspSymbols.LSP_KIND_METHOD: + return ST_MACRO if t.is_macro != 0 else ST_METHOD + if t.kind == LspSymbols.LSP_KIND_FIELD: + return ST_PROPERTY + return ST_VARIABLE + + +def _ModsForTarget(t: LspIndex.LspTarget*) -> int: + if t is not None and t.kind == LspSymbols.LSP_KIND_CONSTANT: + return 2 + return 0 + + +define LSP_SEM_CACHE_SIZE = 4096 +define LSP_SEM_CACHE_MASK = 4095 + + +def _SemHash(s: str) -> sizet: + if s is None: + return 0 + h: uint64 = uint64(1469598103934665603) + i: sizet = 0 + while s[i] != 0: + h = (h ^ uint64(s[i])) * uint64(1099511628211) + i += 1 + return sizet(h & uint64(LSP_SEM_CACHE_MASK)) + + +def _CachedSymbol(ctx: LspSemCtx*, pool: memhub.MemManager*, + sym: LspSymbols.LspSymbol*) -> LspIndex.LspTarget*: + if ctx is None or sym is None: + return None + slot: sizet = _SemHash(sym.name) + if sym.import_mod is not None: + slot = (slot ^ _SemHash(sym.import_mod)) & sizet(LSP_SEM_CACHE_MASK) + if ctx.sym_keys.get(slot) == sym: + return ctx.sym_tgts.get(slot) + t: LspIndex.LspTarget* = LspIndex.ResolveSymbol( + ctx.ws, pool, ctx.table_pool, ctx.file, sym, 0) + ctx.sym_keys.set(slot, sym) + ctx.sym_tgts.set(slot, t) + return t + + +def _CachedChain(ctx: LspSemCtx*, pool: memhub.MemManager*, + head: LspSymbols.LspSymbol*, + rest: str) -> LspIndex.LspTarget*: + if ctx is None or head is None or rest is None: + return None + slot: sizet = (_SemHash(rest) ^ _SemHash(head.name)) & sizet(LSP_SEM_CACHE_MASK) + if ctx.chain_heads.get(slot) == head: + cached: str = ctx.chain_rests.get(slot) + if cached is not None and string.strcmp(cached, rest) == 0: + return ctx.chain_tgts.get(slot) + t: LspIndex.LspTarget* = None + if head.import_kind == LspSymbols.LSP_IMPORT_FROM: + base: LspIndex.LspTarget* = _CachedSymbol(ctx, pool, head) + if base is not None: + t = LspIndex.ResolveMembers(pool, ctx.table_pool, base, rest) + else: + t = LspIndex.ResolveChain(ctx.ws, pool, ctx.table_pool, ctx.file, head, rest) + ctx.chain_heads.set(slot, head) + ctx.chain_rests.set(slot, rest) + ctx.chain_tgts.set(slot, t) + return t + + +def _TokenForSymbol(ctx: LspSemCtx*, pool: memhub.MemManager*, s: LspSymbols.LspSymbol*) -> LspSemToken*: if s is None or s.sel is None: return None - t: LspSemToken* = _NewTok(pool, s.sel.sl, s.sel.sc, s.sel.el, s.sel.ec, - _TypeFor(s)) - if t is not None: - t.mods = _ModsFor(s) - return t + typ: int = _TypeFor(s) + mods: int = _ModsFor(s) + if s.import_kind != LspSymbols.LSP_IMPORT_NONE: + # Import declarations are colored by the imported declaration, not the local alias kind. + t: LspIndex.LspTarget* = _CachedSymbol(ctx, pool, s) + if t is not None: + typ = _TypeForTarget(t) + mods = _ModsForTarget(t) + tok: LspSemToken* = _NewTok(pool, s.sel.sl, s.sel.sc, s.sel.el, s.sel.ec, typ) + if tok is not None: + tok.mods = mods + return tok -def _TokenForOcc(pool: memhub.MemManager*, +def _TokenForOcc(ctx: LspSemCtx*, pool: memhub.MemManager*, o: LspSymbols.LspOccurrence*) -> LspSemToken*: if o is None: return None - if o.sym is None: + typ: int = -1 + mods: int = 0 + if o.sym is not None: + typ = _TypeFor(o.sym) + mods = _ModsFor(o.sym) + if o.sym.import_kind != LspSymbols.LSP_IMPORT_NONE: + t: LspIndex.LspTarget* = _CachedSymbol(ctx, pool, o.sym) + if t is not None: + typ = _TypeForTarget(t) + mods = _ModsForTarget(t) + elif o.head is not None and o.rest is not None and o.head.import_kind != LspSymbols.LSP_IMPORT_NONE: + t = _CachedChain(ctx, pool, o.head, o.rest) + if t is not None: + typ = _TypeForTarget(t) + mods = _ModsForTarget(t) + if typ < 0: # Builtin type names have no declaration; color them like class types. - if o.builtin_type != 0: - return _NewTok(pool, o.line, o.sc, o.line, o.ec, ST_CLASS) - return None - t: LspSemToken* = _NewTok(pool, o.line, o.sc, o.line, o.ec, _TypeFor(o.sym)) - if t is not None: - t.mods = _ModsFor(o.sym) - return t + if o.builtin_type == 0: + return None + typ = ST_CLASS + tok: LspSemToken* = _NewTok(pool, o.line, o.sc, o.line, o.ec, typ) + if tok is not None: + tok.mods = mods + return tok def _Before(a: LspSemToken*, b: LspSemToken*) -> int: @@ -188,8 +304,8 @@ def _SymbolTokenValid(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, return 1 if string.strcmp(txt, name) == 0 else 0 -def _AstStream(pool: memhub.MemManager*, - a: LspSymbols.LspAnalysis*) -> list[LspSemToken*]*: +def _AstStream(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + ctx: LspSemCtx*) -> list[LspSemToken*]*: out: list[LspSemToken*]* = list[LspSemToken*](pool, 64) syms: list[LspSymbols.LspSymbol*]* = a.symbols occs: list[LspSymbols.LspOccurrence*]* = a.occs @@ -204,7 +320,7 @@ def _AstStream(pool: memhub.MemManager*, while si < n: s: LspSymbols.LspSymbol* = syms.get(sizet(si)) si += 1 - t: LspSemToken* = _TokenForSymbol(pool, s) + t: LspSemToken* = _TokenForSymbol(ctx, pool, s) if t is not None and s is not None: if _SymbolTokenValid(pool, a, t, s.name) == 0: t.typ = -1 @@ -213,7 +329,7 @@ def _AstStream(pool: memhub.MemManager*, break occ_t: LspSemToken* = None while oi < m: - t: LspSemToken* = _TokenForOcc(pool, occs.get(sizet(oi))) + t: LspSemToken* = _TokenForOcc(ctx, pool, occs.get(sizet(oi))) oi += 1 if t is not None and t.typ >= 0: occ_t = t @@ -361,6 +477,22 @@ def _InRange(t: LspSemToken*, start_line: int, end_line: int) -> int: return 1 +def _AppendInt(pool: memhub.MemManager*, arr: json.JsonValue*, value: int, + tail: json.JsonValue*) -> json.JsonValue*: + """Append to a JSON array in O(1); json.array_append walks the whole list per call.""" + if arr is None: + return tail + node: json.JsonValue* = json.int_val(pool, int64(value)) + if node is None: + return tail + if tail is None: + arr.child = node + else: + tail.next = node + arr.child_count += 1 + return node + + def _Encode(pool: memhub.MemManager*, out: json.JsonValue*, ast_tokens: list[LspSemToken*]*, lex_tokens: list[LspSemToken*]*, has_range: int, start_line: int, end_line: int): @@ -368,6 +500,7 @@ def _Encode(pool: memhub.MemManager*, out: json.JsonValue*, prev_char: int = 0 count: int = 0 last: LspSemToken* = None + tail: json.JsonValue* = None ai: int = 0 li: int = 0 an: int = int(len(ast_tokens)) @@ -401,11 +534,11 @@ def _Encode(pool: memhub.MemManager*, out: json.JsonValue*, length: int = t.ec - t.sc if length <= 0: continue - json.array_append(pool, out, json.int_val(pool, int64(dl))) - json.array_append(pool, out, json.int_val(pool, int64(dc))) - json.array_append(pool, out, json.int_val(pool, int64(length))) - json.array_append(pool, out, json.int_val(pool, int64(t.typ))) - json.array_append(pool, out, json.int_val(pool, int64(t.mods))) + tail = _AppendInt(pool, out, dl, tail) + tail = _AppendInt(pool, out, dc, tail) + tail = _AppendInt(pool, out, length, tail) + tail = _AppendInt(pool, out, t.typ, tail) + tail = _AppendInt(pool, out, t.mods, tail) prev_line = t.sl prev_char = t.sc last = t @@ -426,7 +559,31 @@ def Tokens(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, return LspRpc.WriteResponse(pool, tr, id_node, result) a: LspSymbols.LspAnalysis* = LspNavigation._Analyze( pool, docs, doc, LspNavigation._Uri(params)) - ast_tokens: list[LspSemToken*]* = _AstStream(pool, a) + # Cross-module coloring resolves imported declarations; the workspace symbol index avoids + # re-parsing the same library file for each occurrence. + LspIndex.SetRoots(ws) + LspIndex.Ensure(ws, docs.pool) + ctx: LspSemCtx* = LspSemCtx(pool) + if ctx is not None: + ctx.ws = ws + ctx.table_pool = docs.pool + ctx.file = a.file + ctx.sym_keys = list[LspSymbols.LspSymbol*](pool, 8) + ctx.sym_tgts = list[LspIndex.LspTarget*](pool, 8) + ctx.chain_heads = list[LspSymbols.LspSymbol*](pool, 8) + ctx.chain_rests = list[str](pool, 8) + ctx.chain_tgts = list[LspIndex.LspTarget*](pool, 8) + ci: int = 0 + while ci < LSP_SEM_CACHE_SIZE: + ctx.sym_keys.append(None) + ctx.sym_tgts.append(None) + ctx.chain_heads.append(None) + ctx.chain_rests.append(None) + ctx.chain_tgts.append(None) + ci += 1 + LspIndex.BeginResolveOnly() + ast_tokens: list[LspSemToken*]* = _AstStream(pool, a, ctx) + LspIndex.EndResolveOnly() lex_tokens: list[LspSemToken*]* = _LexStream(pool, doc) has_range: int = 0 start_line: int = 0 diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index 37fb185..3da785e 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -182,6 +182,7 @@ def _HandleDidOpen(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, file: str = LspWorkspace.DecodeUri(pool, uri) if file is not None and ws is not None: if ws.AdoptFromFile(file) != 0: + LspIndex.RefreshRoots(ws) LspIndex.Invalidate() if docs.Open(uri, text, version) is None: LspTransport.StderrWrite("viperc-lsp: didOpen failed to store document\n") diff --git a/App/lib/lsp/LspSymbols.vp b/App/lib/lsp/LspSymbols.vp index 7f8b7b6..f625b82 100644 --- a/App/lib/lsp/LspSymbols.vp +++ b/App/lib/lsp/LspSymbols.vp @@ -46,6 +46,8 @@ class LspScope: class LspSymbol: name: str kind: int + # 1 for `define` macro functions (colored as macro), 0 otherwise. + is_macro: int detail: str doc: str file: str @@ -630,6 +632,10 @@ def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, return None s.file = a.file s.detail = detail + if decl is not None and decl.kind() == ast.ASTKind.FunctionDef: + fd: ast.FunctionDef* = (ast.FunctionDef*)(decl) + if (fd.flags & ast.FLAG_IS_DEFINE) != 0: + s.is_macro = 1 s.sel = _NameSpan(pool, a.doc, line, col, name) if s.sel is None or _RangeTextIsName(pool, a, s.sel, name) == 0: if decl is None or (decl.kind() != ast.ASTKind.Import and decl.kind() != ast.ASTKind.ImportFrom): diff --git a/Tools/lsp/fixtures/gate10_types.vp b/Tools/lsp/fixtures/gate10_types.vp index a07523c..1da8038 100644 --- a/Tools/lsp/fixtures/gate10_types.vp +++ b/Tools/lsp/fixtures/gate10_types.vp @@ -1,5 +1,7 @@ """Gate 10 fixture: class members used through annotations and module chains.""" +define DOUBLE(x): return x + x + class Widget: value: int diff --git a/Tools/lsp/fixtures/gate10_use.vp b/Tools/lsp/fixtures/gate10_use.vp index e0f365f..f00faed 100644 --- a/Tools/lsp/fixtures/gate10_use.vp +++ b/Tools/lsp/fixtures/gate10_use.vp @@ -1,10 +1,14 @@ """Gate 10 fixture: annotated members, module chains and .vpi members.""" -from .gate10_types import Widget +from .gate10_types import Widget, DOUBLE from .gate10_stub import StubThing import gate10_types +def macro_use(n: int) -> int: + return DOUBLE(n) + + def by_import(s: Widget) -> int: v: int = s.value return s.resize(v) diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index 36f48b5..f1a756b 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -536,6 +536,7 @@ def gate3(exe: str) -> int: "position": ref_pos, "context": {"includeDeclaration": True}, }, + timeout=30.0, ).get("result") if not isinstance(references, list) or len(references) < 3: raise AssertionError(f"references are incomplete: {references}") @@ -866,6 +867,7 @@ def gate5(exe: str) -> int: renamed = client.request( "textDocument/rename", {"textDocument": {"uri": uri}, "position": field_pos, "newName": "value"}, + timeout=30.0, ).get("result") if not isinstance(renamed, dict): raise AssertionError(f"rename failed: {renamed}") @@ -1048,6 +1050,39 @@ def gate7(exe: str) -> int: if "includes" not in includes: raise AssertionError(f"includes is wrong after adoption: {stats}") + # Project-relative imports must resolve before any index rebuild: the adopted project's + # source_dir/includes have to replace the repository-root fallback roots. + app_doc = root / "App" / "lib" / "lsp" / "LspDocuments.vp" + app_text = app_doc.read_text(encoding="utf-8") + app_uri = app_doc.as_uri() + client.notify( + "textDocument/didOpen", + {"textDocument": {"uri": app_uri, "languageId": "viper", "version": 1, "text": app_text}}, + ) + client.wait_notification("textDocument/publishDiagnostics") + pool_pos = _position(app_text, "LspPools.LspPool") + pool_pos["character"] += len("LspPools.") + imported_def = client.request( + "textDocument/definition", {"textDocument": {"uri": app_uri}, "position": pool_pos}, + timeout=60.0, + ).get("result") + if not isinstance(imported_def, list) or not imported_def: + raise AssertionError(f"project import did not resolve after adoption: {imported_def}") + if not imported_def[0]["uri"].endswith("LspPools.vp"): + raise AssertionError(f"project import resolved to the wrong file: {imported_def}") + legend = response.get("result", {}).get("capabilities", {}).get( + "semanticTokensProvider", {}).get("legend", {}) + sem_types = legend.get("tokenTypes", []) + semantic = client.request( + "textDocument/semanticTokens/full", {"textDocument": {"uri": app_uri}}, timeout=60.0 + ).get("result") + sem_tokens = _decode_tokens(semantic.get("data", [])) if isinstance(semantic, dict) else [] + if not _find_token(sem_tokens, pool_pos["line"], pool_pos["character"], + sem_types.index("class")): + raise AssertionError(f"imported class was not highlighted: {pool_pos}") + client.notify("textDocument/didClose", {"textDocument": {"uri": app_uri}}) + client.wait_notification("textDocument/publishDiagnostics") + symbols = client.request( "workspace/symbol", {"query": "LspDocuments"}, timeout=60.0 ).get("result") @@ -1412,6 +1447,26 @@ def gate10(exe: str) -> int: if reported: raise AssertionError(f"gate10 fixture has diagnostics: {reported}") + legend = response.get("result", {}).get("capabilities", {}).get("semanticTokensProvider", {}).get("legend", {}) + sem_types = legend.get("tokenTypes", []) + semantic = client.request( + "textDocument/semanticTokens/full", {"textDocument": {"uri": uri}}, timeout=120.0 + ).get("result") + if not isinstance(semantic, dict): + raise AssertionError(f"semanticTokens/full failed: {semantic}") + sem_tokens = _decode_tokens(semantic.get("data", [])) + for label, needle, delta, token_name in ( + ("imported class", "import Widget", 7, "class"), + ("imported macro", "import Widget", 15, "macro"), + ("macro call", "return DOUBLE", 7, "macro"), + ("member field", "s.value", 2, "property"), + ("member method", "s.resize", 2, "method"), + ): + pos = _position(text, needle) + pos["character"] += delta + if not _find_token(sem_tokens, pos["line"], pos["character"], sem_types.index(token_name)): + raise AssertionError(f"missing cross-module semantic token for {label}: {pos}") + def at(needle: str, delta: int = 0, occurrence: int = 0) -> dict[str, int]: pos = _position(text, needle, occurrence) pos["character"] += delta