diff --git a/App/lib/lsp/LspSemantic.vp b/App/lib/lsp/LspSemantic.vp index f909be3..2ef4c3c 100644 --- a/App/lib/lsp/LspSemantic.vp +++ b/App/lib/lsp/LspSemantic.vp @@ -136,6 +136,18 @@ def _Overlaps(last: LspSemToken*, t: LspSemToken*) -> int: return 0 +def _SymbolTokenValid(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + t: LspSemToken*, name: str) -> int: + if t is None or name is None: + return 0 + sc: int = LspSource.Utf16ColToByte(a.doc, t.sl, t.sc) + ec: int = LspSource.Utf16ColToByte(a.doc, t.el, t.ec) + txt: str = LspSource.SliceBytes(pool, a.doc, t.sl, sc, t.el, ec) + if txt is None: + return 0 + return 1 if string.strcmp(txt, name) == 0 else 0 + + def _AstStream(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*) -> list[LspSemToken*]*: out: list[LspSemToken*]* = list[LspSemToken*](pool, 64) @@ -146,7 +158,11 @@ def _AstStream(pool: memhub.MemManager*, while si < int(len(syms)) or oi < int(len(occs)): sym_t: LspSemToken* = None if si < int(len(syms)): - sym_t = _TokenForSymbol(pool, syms.get(sizet(si))) + s: LspSymbols.LspSymbol* = syms.get(sizet(si)) + sym_t = _TokenForSymbol(pool, s) + if sym_t is not None and s is not None: + if _SymbolTokenValid(pool, a, sym_t, s.name) == 0: + sym_t.typ = -1 occ_t: LspSemToken* = None if oi < int(len(occs)): occ_t = _TokenForOcc(pool, occs.get(sizet(oi))) @@ -301,6 +317,8 @@ def _Encode(pool: memhub.MemManager*, out: json.JsonValue*, li += 1 if t is None: continue + if t.typ < 0: + continue if has_range != 0 and _InRange(t, start_line, end_line) == 0: continue if _Overlaps(last, t) != 0: diff --git a/App/lib/lsp/LspSource.vp b/App/lib/lsp/LspSource.vp index c032933..41041ea 100644 --- a/App/lib/lsp/LspSource.vp +++ b/App/lib/lsp/LspSource.vp @@ -281,13 +281,20 @@ def FindHeaderColon(doc: LspDocuments.LspDocument*, line: int, byte_col: int, def FindDeclName(doc: LspDocuments.LspDocument*, line: int, byte_col: int, name: str, want_class: int, out_line: int*, out_col: int*) -> int: - """Locate the declared identifier inside a def/class header; 1 when found.""" + """Locate the declared identifier inside a def/class header; 1 when found. + + Declaration-only forms (e.g. `extern lib name(...) -> T`) have no header colon, so the scan + falls back to the end of the declaration line. + """ if doc is None or doc.index is None or doc.index.Count() <= 0 or name is None: return 0 + if line < 0: + return 0 el: int = 0 ec: int = 0 if FindHeaderColon(doc, line, byte_col, &el, &ec) == 0: - return 0 + el = line + ec = LineByteLength(doc, line) start: sizet = doc.index.LineStart(line) + sizet(byte_col) end: sizet = doc.index.LineStart(el) + sizet(ec) if end <= start: diff --git a/App/lib/lsp/LspSymbols.vp b/App/lib/lsp/LspSymbols.vp index 7612d75..e4ebaca 100644 --- a/App/lib/lsp/LspSymbols.vp +++ b/App/lib/lsp/LspSymbols.vp @@ -403,6 +403,57 @@ def _AddAttrOcc(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, return o +def _RangeTextIsName(pool: memhub.MemManager*, a: LspAnalysis*, + r: LspSource.LspRange*, name: str) -> int: + if r is None or name is None: + return 0 + sc: int = LspSource.Utf16ColToByte(a.doc, r.sl, r.sc) + ec: int = LspSource.Utf16ColToByte(a.doc, r.el, r.ec) + txt: str = LspSource.SliceBytes(pool, a.doc, r.sl, sc, r.el, ec) + if txt is None: + return 0 + return 1 if string.strcmp(txt, name) == 0 else 0 + + +def _NameOnLine(pool: memhub.MemManager*, a: LspAnalysis*, line1: int, + start_col: int, name: str) -> LspSource.LspRange*: + """Find the declared name as a whole word on its line. + + Soft-keyword declarations (`define X = ...`, `static X`, ...) and `extern lib name(...)` + record the statement or modifier position, not the identifier position. The name is the + first whole-word match at/after the recorded column, else the first on the line. + """ + if a is None or a.doc is None or name is None or line1 <= 0: + return None + line0: int = line1 - 1 + if line0 >= a.doc.index.Count(): + return None + limit: int = LspSource.LineByteLength(a.doc, line0) + line_start: sizet = a.doc.index.LineStart(line0) + off: sizet = line_start + end: sizet = line_start + sizet(limit) + nlen: sizet = string.strlen(name) + first_col: int = -1 + while off + nlen <= end: + ch: char = a.doc.text[off] + if LspSource._IsIdentStart(ch) != 0: + tok_start: sizet = off + while off < end and LspSource._IsIdentChar(a.doc.text[off]) != 0: + off += 1 + if off - tok_start == nlen: + if string.strncmp(str(a.doc.text + tok_start), name, nlen) == 0: + col: int = int(tok_start - line_start) + if col >= start_col: + return LspSource.ByteSpan(pool, a.doc, line0, col, int(nlen)) + if first_col < 0: + first_col = col + continue + off += 1 + if first_col >= 0: + return LspSource.ByteSpan(pool, a.doc, line0, first_col, int(nlen)) + return None + + def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, name: str, kind: int, decl: ast.AST*, line: int, col: int, detail: str) -> LspSymbol*: @@ -417,6 +468,9 @@ def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*, s.file = a.file s.detail = detail 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): + s.sel = _NameOnLine(pool, a, line, col, name) s.rng = s.sel _AddSymbol(a, scope, s) return s diff --git a/Tools/lsp/fixtures/gate5.vp b/Tools/lsp/fixtures/gate5.vp index bdb022a..a3be5b9 100644 --- a/Tools/lsp/fixtures/gate5.vp +++ b/Tools/lsp/fixtures/gate5.vp @@ -22,3 +22,8 @@ def use(x: int) -> int: # gate5 comment t: Thing = Thing() return t.field + x + + +define APP_NAME = "Gate5" + +extern add_c2 add(a: int, b: int) -> int diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index a7e8149..a379a3f 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -896,6 +896,22 @@ def gate5(exe: str) -> int: if not _find_token(tokens, number_pos["line"], number_pos["character"], 10): raise AssertionError(f"missing number token: {tokens[:20]}") + define_pos = _position(text, "APP_NAME") + if not _find_token(tokens, define_pos["line"], define_pos["character"], 5): + raise AssertionError(f"define name token is misplaced: {define_pos} {tokens}") + for token in tokens: + if token["line"] == define_pos["line"] and token["character"] == 0: + if token["length"] == len("APP_NAME"): + raise AssertionError(f"define name token is at column 0: {token}") + + extern_pos = _position(text, "add(a: int, b: int)") + if not _find_token(tokens, extern_pos["line"], extern_pos["character"], 2): + raise AssertionError(f"extern name token is misplaced: {extern_pos} {tokens}") + for token in tokens: + if token["line"] == extern_pos["line"] and token["character"] == 0: + if token["length"] == len("add"): + raise AssertionError(f"extern name token is at column 0: {token}") + ranged = client.request( "textDocument/semanticTokens/range", {