diff --git a/App/lib/lsp/LspCompletion.vp b/App/lib/lsp/LspCompletion.vp new file mode 100644 index 0000000..91ddbb0 --- /dev/null +++ b/App/lib/lsp/LspCompletion.vp @@ -0,0 +1,764 @@ +from stdint import * +import string +import os +import os.path +import json +import memhub +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.LspFiles as LspFiles +import lib.lsp.LspTransport as LspTransport +import lib.lsp.LspRpc as LspRpc + + +define LSP_COMPLETION_CAP = 500 +define LSP_IMPORT_COMPLETION_CAP = 200 +define LSP_DIR_LIST_CAP = 512 + +# CompletionItemKind values. +define CK_METHOD = 2 +define CK_FUNCTION = 3 +define CK_FIELD = 5 +define CK_VARIABLE = 6 +define CK_CLASS = 7 +define CK_MODULE = 9 +define CK_CONSTANT = 21 + + +def _Doc(pool: memhub.MemManager*, docs: LspDocuments.LspDocuments*, + params: json.JsonValue*) -> LspDocuments.LspDocument*: + if docs is None or params is None: + return None + td: json.JsonValue* = params["textDocument"] + uri: str = LspRpc.GetStringField(td, "uri") + if uri is None: + return None + return docs.Find(uri) + + +def _LinePrefix(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + line0: int, utf16_col: int) -> str: + if doc is None or doc.index is None or doc.index.Count() <= 0: + return None + byte_col: int = doc.index.Utf16ColToByte(line0, utf16_col) + return LspSource.SliceBytes(pool, doc, line0, 0, line0, byte_col) + + +def _IsIdentChar(ch: char) -> int: + return LspSource._IsIdentChar(ch) + + +def _IsIdentStart(ch: char) -> int: + return LspSource._IsIdentStart(ch) + + +def _ContainsIdentChar(hay: str) -> int: + if hay is None: + return 0 + i: sizet = 0 + while hay[i] != 0: + ch: char = hay[i] + if _IsIdentChar(ch) != 0 or ch == '.': + i += 1 + continue + return 0 + return 1 + + +def _StartsWith(hay: str, needle: str) -> int: + if hay is None or needle is None: + return 0 + n: sizet = string.strlen(needle) + if string.strlen(hay) < n: + return 0 + return 1 if string.strncmp(hay, needle, n) == 0 else 0 + + +def _TrailingIdent(pool: memhub.MemManager*, text: str) -> str: + if text is None: + return None + n: sizet = string.strlen(text) + i: sizet = n + while i > 0 and _IsIdentChar(text[i - 1]) != 0: + i -= 1 + if i >= n: + return "" + return LspSymbols._CopyStr(pool, text + i, 256) + + +def _PrefixMatch(name: str, filter: str) -> int: + if filter is None or filter[0] == 0: + return 1 + if name is None: + return 0 + fn: sizet = string.strlen(filter) + if string.strlen(name) < fn: + return 0 + i: sizet = 0 + while i < fn: + a: char = name[i] + b: char = filter[i] + if a >= 'A' and a <= 'Z': + a = char(int(a) + 32) + if b >= 'A' and b <= 'Z': + b = char(int(b) + 32) + if a != b: + return 0 + i += 1 + return 1 + + +def _Seen(seen: list[str]*, name: str) -> int: + if seen is None or name is None: + return 0 + i: int = 0 + while i < int(len(seen)): + if string.strcmp(seen.get(sizet(i)), name) == 0: + return 1 + i += 1 + return 0 + + +def _MarkSeen(pool: memhub.MemManager*, seen: list[str]*, name: str): + if seen is None or name is None: + return + seen.append(LspSymbols._CopyStr(pool, name, 256)) + + +def _KindToItem(kind: int) -> int: + if kind == LspSymbols.LSP_KIND_CLASS: + return CK_CLASS + if kind == LspSymbols.LSP_KIND_METHOD: + return CK_METHOD + if kind == LspSymbols.LSP_KIND_FUNCTION: + return CK_FUNCTION + if kind == LspSymbols.LSP_KIND_FIELD: + return CK_FIELD + if kind == LspSymbols.LSP_KIND_CONSTANT: + return CK_CONSTANT + if kind == LspSymbols.LSP_KIND_MODULE: + return CK_MODULE + return CK_VARIABLE + + +def _SymbolItem(pool: memhub.MemManager*, + s: LspSymbols.LspSymbol*) -> json.JsonValue*: + if s is None or s.name is None: + return None + item: json.JsonValue* = json.object(pool) + if item is None: + return None + json.object_set(pool, item, "label", json.string_val(pool, s.name)) + json.object_set(pool, item, "kind", json.int_val(pool, int64(_KindToItem(s.kind)))) + if s.detail is not None: + json.object_set(pool, item, "detail", json.string_val(pool, s.detail)) + if s.doc is not None: + json.object_set(pool, item, "documentation", json.string_val(pool, s.doc)) + json.object_set(pool, item, "sortText", json.string_val(pool, s.name)) + return item + + +def _AddBuiltin(pool: memhub.MemManager*, out: json.JsonValue*, + seen: list[str]*, filter: str, name: str): + if _PrefixMatch(name, filter) == 0 or _Seen(seen, name) != 0: + return + item: json.JsonValue* = json.object(pool) + if item is None: + return + json.object_set(pool, item, "label", json.string_val(pool, name)) + json.object_set(pool, item, "kind", json.int_val(pool, int64(CK_FUNCTION))) + json.object_set(pool, item, "detail", json.string_val(pool, "builtin")) + json.object_set(pool, item, "sortText", json.string_val(pool, name)) + json.array_append(pool, out, item) + _MarkSeen(pool, seen, name) + + +def _AddBuiltinItems(pool: memhub.MemManager*, out: json.JsonValue*, + seen: list[str]*, filter: str): + _AddBuiltin(pool, out, seen, filter, "print") + _AddBuiltin(pool, out, seen, filter, "len") + _AddBuiltin(pool, out, seen, filter, "range") + _AddBuiltin(pool, out, seen, filter, "str") + _AddBuiltin(pool, out, seen, filter, "int") + _AddBuiltin(pool, out, seen, filter, "float") + _AddBuiltin(pool, out, seen, filter, "bool") + _AddBuiltin(pool, out, seen, filter, "bytes") + _AddBuiltin(pool, out, seen, filter, "list") + _AddBuiltin(pool, out, seen, filter, "dict") + _AddBuiltin(pool, out, seen, filter, "set") + _AddBuiltin(pool, out, seen, filter, "tuple") + _AddBuiltin(pool, out, seen, filter, "malloc") + _AddBuiltin(pool, out, seen, filter, "free") + _AddBuiltin(pool, out, seen, filter, "sizeof") + _AddBuiltin(pool, out, seen, filter, "memcpy") + _AddBuiltin(pool, out, seen, filter, "memset") + _AddBuiltin(pool, out, seen, filter, "exit") + _AddBuiltin(pool, out, seen, filter, "abort") + _AddBuiltin(pool, out, seen, filter, "min") + _AddBuiltin(pool, out, seen, filter, "max") + _AddBuiltin(pool, out, seen, filter, "abs") + _AddBuiltin(pool, out, seen, filter, "ord") + _AddBuiltin(pool, out, seen, filter, "chr") + + +def _ScopeAt(a: LspSymbols.LspAnalysis*, line0: int, ch: int) -> LspSymbols.LspScope*: + if a is None: + return None + best: LspSymbols.LspScope* = a.module_scope + best_span: int = 2147483647 + i: int = 0 + while i < int(len(a.symbols)): + s: LspSymbols.LspSymbol* = a.symbols.get(sizet(i)) + i += 1 + if s is None or s.own_scope is None or s.rng is None: + continue + r: LspSource.LspRange* = s.rng + inside: int = 0 + if line0 > r.sl or (line0 == r.sl and ch >= r.sc): + if line0 < r.el or (line0 == r.el and ch <= r.ec): + inside = 1 + if inside == 0: + continue + span: int = (r.el - r.sl) * 4096 + (r.ec - r.sc) + if span < best_span: + best_span = span + best = s.own_scope + return best + + +def _LookupLocal(a: LspSymbols.LspAnalysis*, scope: LspSymbols.LspScope*, + name: str) -> LspSymbols.LspSymbol*: + s: LspSymbols.LspScope* = scope + while s is not None: + found: LspSymbols.LspSymbol* = LspSymbols._FindSymbol(s, name) + if found is not None: + return found + p: LspSymbols.LspScope* = s.parent + if s.kind == LspSymbols.LSP_SCOPE_FUNCTION and p is not None and p.kind == LspSymbols.LSP_SCOPE_CLASS: + p = p.parent + s = p + return None + + +def _ScopeItems(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + scope: LspSymbols.LspScope*, filter: str, + out: json.JsonValue*): + seen: list[str]* = list[str](pool, 16) + s: LspSymbols.LspScope* = scope + while s is not None: + if s.syms is not None: + i: int = 0 + while i < int(len(s.syms)): + sym: LspSymbols.LspSymbol* = s.syms.get(sizet(i)) + i += 1 + if sym is None or sym.name is None: + continue + if _PrefixMatch(sym.name, filter) == 0 or _Seen(seen, sym.name) != 0: + continue + item: json.JsonValue* = _SymbolItem(pool, sym) + if item is not None: + json.array_append(pool, out, item) + _MarkSeen(pool, seen, sym.name) + if int(len(out)) >= LSP_COMPLETION_CAP: + return + p: LspSymbols.LspScope* = s.parent + if s.kind == LspSymbols.LSP_SCOPE_FUNCTION and p is not None and p.kind == LspSymbols.LSP_SCOPE_CLASS: + p = p.parent + s = p + _AddBuiltinItems(pool, out, seen, filter) + + +def _MemberCtx(pool: memhub.MemManager*, prefix: str, out_base: str*, + out_filter: str*) -> int: + if prefix is None: + return 0 + n: sizet = string.strlen(prefix) + i: sizet = n + while i > 0: + ch: char = prefix[i - 1] + if _IsIdentChar(ch) != 0 or ch == '.': + i -= 1 + continue + break + run: str = prefix + i + rn: sizet = string.strlen(run) + # Find the last dot inside the identifier run. + dot: sizet = rn + j: sizet = rn + while j > 0: + if run[j - 1] == '.': + dot = j - 1 + break + j -= 1 + if dot >= rn: + return 0 + if dot >= 256: + return 0 + base: str = LspSymbols._CopyStr(pool, run, 256) + if base is None: + return 0 + base[dot] = 0 + if dot == 0 or (base[0] >= '0' and base[0] <= '9'): + return 0 + out_base[0] = str(base) + out_filter[0] = LspSymbols._CopyStr(pool, run + dot + 1, 256) + return 1 + + +def _ImportCtx(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + prefix: str, out_typed: str*, out_level: int*) -> int: + if prefix is None: + return 0 + # Strip leading whitespace. + p: str = prefix + n: sizet = string.strlen(p) + i: sizet = 0 + while i < n: + ch: char = p[i] + if ch == ' ' or ch == '\t': + i += 1 + continue + break + p = p + i + if _StartsWith(p, "from ") != 0: + rest: str = p + 5 + if _FindKeyword(rest, "import") >= 0: + return 2 + level: int = 0 + j: sizet = 0 + while rest[j] == '.': + level += 1 + j += 1 + typed: str = rest + j + if _ContainsIdentChar(typed) == 0: + return 2 + if level == 0: + level = 0 + out_level[0] = level + out_typed[0] = LspSymbols._CopyStr(pool, typed, 512) + return 1 + if _StartsWith(p, "import ") != 0: + rest2: str = p + 7 + if _ContainsIdentChar(rest2) == 0: + return 2 + out_level[0] = 0 + out_typed[0] = LspSymbols._CopyStr(pool, rest2, 512) + return 1 + return 0 + + +def _FindKeyword(s: str, word: str) -> int: + """Index of word at an identifier boundary, or -1.""" + if s is None or word is None: + return -1 + n: sizet = string.strlen(s) + w: sizet = string.strlen(word) + i: sizet = 0 + while i + w <= n: + if string.strncmp(s + i, word, w) == 0: + prev_ok: int = 1 + if i > 0 and _IsIdentChar(s[i - 1]) != 0: + prev_ok = 0 + next_ok: int = 1 + if i + w < n and _IsIdentChar(s[i + w]) != 0: + next_ok = 0 + if prev_ok != 0 and next_ok != 0: + return int(i) + i += 1 + return -1 + + +def _TargetForBase(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, + scope: LspSymbols.LspScope*, base: str) -> LspIndex.LspTarget*: + if base is None: + return None + h: str = None + r: str = None + LspSymbols._SplitHead(pool, base, &h, &r) + if h is None: + return None + head: LspSymbols.LspSymbol* = _LookupLocal(a, scope, h) + if head is None: + return None + if r is None: + if head.class_ref is not None: + return LspIndex.TargetFor(pool, head.class_ref) + if head.import_kind != LspSymbols.LSP_IMPORT_NONE: + return LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, head, 0) + if head.kind == LspSymbols.LSP_KIND_CLASS: + return LspIndex.TargetFor(pool, head) + if head.kind == LspSymbols.LSP_KIND_FUNCTION or head.kind == LspSymbols.LSP_KIND_METHOD: + return LspIndex.TargetFor(pool, head) + return _TypeTarget(pool, a, ws, table_pool, scope, head.detail) + if head.import_kind != LspSymbols.LSP_IMPORT_NONE: + return LspIndex.ResolveChain(ws, pool, table_pool, a.file, head, r) + cur: LspSymbols.LspSymbol* = head + if cur.class_ref is not None: + cur = cur.class_ref + segs: str = r + while segs is not None: + sh: str = None + sr: str = None + LspSymbols._SplitHead(pool, segs, &sh, &sr) + if sh is None or cur.kind != LspSymbols.LSP_KIND_CLASS or cur.own_scope is None: + return _TypeTarget(pool, a, ws, table_pool, scope, head.detail) + nxt: LspSymbols.LspSymbol* = LspSymbols._FindSymbol(cur.own_scope, sh) + if nxt is None: + return _TypeTarget(pool, a, ws, table_pool, scope, head.detail) + cur = nxt + segs = sr + if cur.kind == LspSymbols.LSP_KIND_CLASS: + return LspIndex.TargetFor(pool, cur) + if cur.kind == LspSymbols.LSP_KIND_FUNCTION or cur.kind == LspSymbols.LSP_KIND_METHOD: + return LspIndex.TargetFor(pool, cur) + return _TypeTarget(pool, a, ws, table_pool, scope, cur.detail) + + +def _TypeTarget(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, + scope: LspSymbols.LspScope*, detail: str) -> LspIndex.LspTarget*: + """Resolve the first identifier in a type annotation that names a class.""" + if detail is None: + return None + n: sizet = string.strlen(detail) + i: sizet = 0 + while i < n: + if _IsIdentStart(detail[i]) == 0: + i += 1 + continue + start: sizet = i + while i < n and _IsIdentChar(detail[i]) != 0: + i += 1 + tok: bytes = pool.alloc(i - start + 1) + if tok is None: + return None + string.memcpy(tok, detail + start, i - start) + tok[i - start] = 0 + name: str = str(tok) + sym: LspSymbols.LspSymbol* = _LookupLocal(a, scope, name) + if sym is not None: + if sym.kind == LspSymbols.LSP_KIND_CLASS: + return LspIndex.TargetFor(pool, sym) + if sym.import_kind != LspSymbols.LSP_IMPORT_NONE: + t: LspIndex.LspTarget* = LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, sym, 0) + if t is not None and t.kind == LspSymbols.LSP_KIND_CLASS: + return t + t2: LspIndex.LspTarget* = None + idx_sym: LspSymbols.LspSymbol* = LspIndex.FindSymbol(a.file, None, name) + if idx_sym is not None: + if idx_sym.import_kind != LspSymbols.LSP_IMPORT_NONE: + t2 = LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, idx_sym, 0) + elif idx_sym.kind == LspSymbols.LSP_KIND_CLASS: + t2 = LspIndex.TargetFor(pool, idx_sym) + if t2 is not None and t2.kind == LspSymbols.LSP_KIND_CLASS: + return t2 + return None + + +def _LiveClass(a: LspSymbols.LspAnalysis*, + target: LspIndex.LspTarget*) -> LspSymbols.LspSymbol*: + if a is None or target is None or target.sel is None: + return None + i: int = 0 + while i < int(len(a.symbols)): + s: LspSymbols.LspSymbol* = a.symbols.get(sizet(i)) + i += 1 + if s is None or s.kind != LspSymbols.LSP_KIND_CLASS or s.sel is None: + continue + if s.sel.sl == target.sel.sl and s.sel.sc == target.sel.sc: + return s + if s.name is not None and target.name is not None: + if string.strcmp(s.name, target.name) == 0 and s.container is not None and target.container is not None: + if string.strcmp(s.container, target.container) == 0: + return s + return None + + +def _AddTargetMembers(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, + target: LspIndex.LspTarget*, filter: str, + out: json.JsonValue*): + if target is None or target.file is None: + return + seen: list[str]* = list[str](pool, 16) + if target.kind == LspSymbols.LSP_KIND_CLASS: + if string.strcmp(target.file, a.file) == 0: + cls: LspSymbols.LspSymbol* = _LiveClass(a, target) + if cls is not None and cls.own_scope is not None and cls.own_scope.syms is not None: + i: int = 0 + while i < int(len(cls.own_scope.syms)): + s: LspSymbols.LspSymbol* = cls.own_scope.syms.get(sizet(i)) + i += 1 + if s is None or s.name is None: + continue + if _PrefixMatch(s.name, filter) == 0 or _Seen(seen, s.name) != 0: + continue + item: json.JsonValue* = _SymbolItem(pool, s) + if item is not None: + json.array_append(pool, out, item) + _MarkSeen(pool, seen, s.name) + return + members: list[LspSymbols.LspSymbol*]* = LspIndex.MembersOf(pool, target.file, target.name) + if members is None: + all_syms: list[LspSymbols.LspSymbol*]* = LspIndex.LoadSymbols(pool, table_pool, target.file) + if all_syms is not None: + i: int = 0 + while i < int(len(all_syms)): + s: LspSymbols.LspSymbol* = all_syms.get(sizet(i)) + i += 1 + if s is None or s.container is None or s.name is None: + continue + if string.strcmp(s.container, target.name) != 0: + continue + if _PrefixMatch(s.name, filter) == 0 or _Seen(seen, s.name) != 0: + continue + item: json.JsonValue* = _SymbolItem(pool, s) + if item is not None: + json.array_append(pool, out, item) + _MarkSeen(pool, seen, s.name) + return + i = 0 + while i < int(len(members)): + s: LspSymbols.LspSymbol* = members.get(sizet(i)) + i += 1 + if s is None or s.name is None: + continue + if _PrefixMatch(s.name, filter) == 0 or _Seen(seen, s.name) != 0: + continue + item: json.JsonValue* = _SymbolItem(pool, s) + if item is not None: + json.array_append(pool, out, item) + _MarkSeen(pool, seen, s.name) + return + if target.kind == LspSymbols.LSP_KIND_MODULE: + syms: list[LspSymbols.LspSymbol*]* = LspIndex.LoadSymbols(pool, table_pool, target.file) + if syms is None: + return + i: int = 0 + while i < int(len(syms)): + s: LspSymbols.LspSymbol* = syms.get(sizet(i)) + i += 1 + if s is None or s.container is not None or s.name is None: + continue + if _PrefixMatch(s.name, filter) == 0 or _Seen(seen, s.name) != 0: + continue + item: json.JsonValue* = _SymbolItem(pool, s) + if item is not None: + json.array_append(pool, out, item) + _MarkSeen(pool, seen, s.name) + return + + +def _ListDir(pool: memhub.MemManager*, dir: str, + out: list[str]*, cap: int): + if dir is None or out is None or not os.isdir(dir): + return + names: str* = (str*)(pool.alloc(sizet(LSP_DIR_LIST_CAP) * 8)) + if names is None: + return + i: int = 0 + while i < LSP_DIR_LIST_CAP: + names[i] = None + i += 1 + count: int = int(os.listdir(dir, names, ULONG(LSP_DIR_LIST_CAP))) + i = 0 + while i < count: + name: str = names[i] + i += 1 + if name is None or name[0] == 0 or name[0] == '.': + continue + full: str = LspFiles.Join(pool, dir, name) + if full is None: + continue + label: str = None + if os.isdir(full): + label = LspSymbols._CopyStr(pool, name, 256) + elif LspFiles.IsSourceFile(name) != 0: + if string.strcmp(name, "__init__") == 0: + continue + n: sizet = string.strlen(name) + dot: sizet = n + k: sizet = n + while k > 0: + if name[k - 1] == '.': + dot = k - 1 + break + k -= 1 + if dot <= 0: + continue + buf: bytes = pool.alloc(dot + 1) + if buf is None: + continue + string.memcpy(buf, name, dot) + buf[dot] = 0 + if string.strcmp(str(buf), "__init__") == 0: + continue + label = str(buf) + if label is None or _Seen(out, label) != 0: + continue + out.append(label) + if int(len(out)) >= cap: + return + + +def _RootsFor(a: LspSymbols.LspAnalysis*, ws: LspWorkspace.LspWorkspace*, + level: int, out: list[str]*): + if level > 0: + base: str = None + if a is not None and a.file is not None: + base = a.file + if base is not None: + d: str = base + steps: int = level + while steps > 0: + d = LspFiles.DirName(a.pool, d) + steps -= 1 + if d is not None: + out.append(d) + return + roots: list[str]* = LspIndex.Roots() + if roots is not None: + i: int = 0 + while i < int(len(roots)): + out.append(roots.get(sizet(i))) + i += 1 + return + if ws is None: + return + sd: str = ws.SourceDir() + if sd is not None: + out.append(sd) + inc: str = ws.Includes() + if inc is None: + return + n: sizet = string.strlen(inc) + start: sizet = 0 + i: sizet = 0 + while i <= n: + if i == n or inc[i] == ' ': + if i > start: + part: bytes = a.pool.alloc(i - start + 1) + if part is not None: + string.memcpy(part, inc + start, i - start) + part[i - start] = 0 + out.append(str(part)) + start = i + 1 + i += 1 + + +def _ImportItems(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + ws: LspWorkspace.LspWorkspace*, typed: str, level: int, + out: json.JsonValue*): + if typed is None: + return + # Split into segments; the trailing partial segment is the filter. + n: sizet = string.strlen(typed) + segments: list[str]* = list[str](pool, 8) + start: sizet = 0 + i: sizet = 0 + while i <= n: + if i == n or typed[i] == '.': + part: bytes = pool.alloc(i - start + 1) + if part is not None: + string.memcpy(part, typed + start, i - start) + part[i - start] = 0 + segments.append(str(part)) + start = i + 1 + i += 1 + filter: str = "" + prefix_n: int = int(len(segments)) + if n > 0 and typed[n - 1] != '.': + if prefix_n > 0: + filter = segments.get(sizet(prefix_n - 1)) + prefix_n -= 1 + roots: list[str]* = list[str](pool, 8) + _RootsFor(a, ws, level, roots) + seen: list[str]* = list[str](pool, 16) + ri: int = 0 + while ri < int(len(roots)): + dir: str = roots.get(sizet(ri)) + ri += 1 + si: int = 0 + ok: int = 1 + while si < prefix_n: + seg: str = segments.get(sizet(si)) + si += 1 + dir = LspFiles.Join(pool, dir, seg) + if dir is None or not os.isdir(dir): + ok = 0 + break + if ok == 0: + continue + labels: list[str]* = list[str](pool, 64) + _ListDir(pool, dir, labels, LSP_IMPORT_COMPLETION_CAP) + li: int = 0 + while li < int(len(labels)): + label: str = labels.get(sizet(li)) + li += 1 + if _PrefixMatch(label, filter) == 0 or _Seen(seen, label) != 0: + continue + _MarkSeen(pool, seen, label) + item: json.JsonValue* = json.object(pool) + if item is None: + continue + json.object_set(pool, item, "label", json.string_val(pool, label)) + json.object_set(pool, item, "kind", json.int_val(pool, int64(CK_MODULE))) + json.object_set(pool, item, "detail", json.string_val(pool, "module")) + json.object_set(pool, item, "sortText", json.string_val(pool, label)) + json.array_append(pool, out, item) + if int(len(out)) >= LSP_IMPORT_COMPLETION_CAP: + return + + +def Completion(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, + docs: LspDocuments.LspDocuments*, ws: LspWorkspace.LspWorkspace*, + id_node: json.JsonValue*, params: json.JsonValue*) -> int: + result: json.JsonValue* = json.object(pool) + items: json.JsonValue* = json.array(pool) + if result is None or items is None: + return -1 + json.object_set(pool, result, "isIncomplete", json.bool_val(pool, False)) + json.object_set(pool, result, "items", items) + + if params is None: + return LspRpc.WriteResponse(pool, tr, id_node, result) + doc: LspDocuments.LspDocument* = _Doc(pool, docs, params) + pos: json.JsonValue* = params["position"] + if doc is None or pos is None: + return LspRpc.WriteResponse(pool, tr, id_node, result) + line0: int = LspRpc.GetIntField(pos, "line", 0) + ch: int = LspRpc.GetIntField(pos, "character", 0) + file: str = LspWorkspace.DecodeUri(pool, LspRpc.GetStringField( + params["textDocument"], "uri")) + a: LspSymbols.LspAnalysis* = LspSymbols.Analyze(pool, docs.pool, doc, file) + if a is None: + return LspRpc.WriteResponse(pool, tr, id_node, result) + + prefix: str = _LinePrefix(pool, doc, line0, ch) + typed: str = None + level: int = 0 + import_mode: int = _ImportCtx(pool, a, prefix, &typed, &level) + if import_mode == 1: + _ImportItems(pool, a, ws, typed, level, items) + return LspRpc.WriteResponse(pool, tr, id_node, result) + if import_mode == 2: + return LspRpc.WriteResponse(pool, tr, id_node, result) + + scope: LspSymbols.LspScope* = _ScopeAt(a, line0, ch) + if scope is None: + scope = a.module_scope + base: str = None + filter: str = "" + if _MemberCtx(pool, prefix, &base, &filter) != 0: + target: LspIndex.LspTarget* = _TargetForBase(pool, a, ws, docs.pool, scope, base) + _AddTargetMembers(pool, a, ws, docs.pool, target, filter, items) + return LspRpc.WriteResponse(pool, tr, id_node, result) + + filter2: str = _TrailingIdent(pool, prefix) + _ScopeItems(pool, a, scope, filter2, items) + return LspRpc.WriteResponse(pool, tr, id_node, result) diff --git a/App/lib/lsp/LspIndex.vp b/App/lib/lsp/LspIndex.vp index fa70a06..fb290d6 100644 --- a/App/lib/lsp/LspIndex.vp +++ b/App/lib/lsp/LspIndex.vp @@ -37,6 +37,7 @@ class LspTarget: kind: int detail: str doc: str + container: str sel: LspSource.LspRange* has_sel: int @@ -317,6 +318,64 @@ def Files() -> list[LspFileEntry*]*: return idx.files +def Roots() -> list[str]*: + idx: LspIndex* = _index + if idx is None: + return None + return idx.roots + + +def SymbolsOf(path: str) -> list[LspSymbols.LspSymbol*]*: + e: LspFileEntry* = FileEntry(path) + if e is None: + return None + return e.syms + + +def MembersOf(pool: memhub.MemManager*, path: str, + container: str) -> list[LspSymbols.LspSymbol*]*: + e: LspFileEntry* = FileEntry(path) + if e is None or e.syms is None or container is None: + return None + out: list[LspSymbols.LspSymbol*]* = list[LspSymbols.LspSymbol*](pool, 16) + i: int = 0 + while i < int(len(e.syms)): + s: LspSymbols.LspSymbol* = e.syms.get(sizet(i)) + i += 1 + if s is None or s.container is None: + continue + if string.strcmp(s.container, container) == 0: + out.append(s) + return out + + +def LoadSymbols(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + path: str) -> list[LspSymbols.LspSymbol*]*: + """Indexable symbols of path: index entry when present, otherwise parsed on demand.""" + if path is None: + return None + e: LspFileEntry* = FileEntry(path) + if e is not None and e.syms is not None: + return e.syms + text: str = LspFiles.ReadSource(pool, path) + if text is None: + return None + doc: LspDocuments.LspDocument* = LspSource.NewTempDoc(pool, text) + if doc is None: + return None + a: LspSymbols.LspAnalysis* = LspSymbols.Analyze(pool, table_pool, doc, path) + if a is None: + return None + out: list[LspSymbols.LspSymbol*]* = list[LspSymbols.LspSymbol*](pool, 16) + i: int = 0 + while i < int(len(a.symbols)): + s: LspSymbols.LspSymbol* = a.symbols.get(sizet(i)) + i += 1 + if _Indexable(s, a) != 0: + out.append(s) + return out + + def SetRoots(ws: LspWorkspace.LspWorkspace*) -> int: """Populate the search roots without scanning the workspace (used by navigation).""" idx: LspIndex* = _index @@ -411,11 +470,17 @@ def _SymbolTarget(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*) -> LspTarg t.kind = s.kind t.detail = s.detail t.doc = s.doc + t.container = s.container t.sel = s.sel t.has_sel = 1 return t +def TargetFor(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*) -> LspTarget*: + """Public wrapper used by completion/signature to treat a live symbol as a target.""" + return _SymbolTarget(pool, s) + + def _FileTarget(pool: memhub.MemManager*, file: str, name: str) -> LspTarget*: if file is None: diff --git a/App/lib/lsp/LspServer.vp b/App/lib/lsp/LspServer.vp index 905292c..89cddce 100644 --- a/App/lib/lsp/LspServer.vp +++ b/App/lib/lsp/LspServer.vp @@ -14,6 +14,8 @@ import lib.lsp.LspWorkspace as LspWorkspace import lib.lsp.LspDiagnostics as LspDiagnostics import lib.lsp.LspIndex as LspIndex import lib.lsp.LspNavigation as LspNavigation +import lib.lsp.LspCompletion as LspCompletion +import lib.lsp.LspSignature as LspSignature define LSP_SERVER_NAME = "viperc-lsp" @@ -46,6 +48,23 @@ def _BuildInitializeResult(pool: memhub.MemManager*) -> json.JsonValue*: json.object_set(pool, caps, "referencesProvider", json.bool_val(pool, True)) json.object_set(pool, caps, "documentHighlightProvider", json.bool_val(pool, True)) json.object_set(pool, caps, "hoverProvider", json.bool_val(pool, True)) + + comp: json.JsonValue* = json.object(pool) + comp_trig: json.JsonValue* = json.array(pool) + if comp is not None and comp_trig is not None: + json.array_append(pool, comp_trig, json.string_val(pool, ".")) + json.array_append(pool, comp_trig, json.string_val(pool, "/")) + json.object_set(pool, comp, "triggerCharacters", comp_trig) + json.object_set(pool, comp, "resolveProvider", json.bool_val(pool, False)) + json.object_set(pool, caps, "completionProvider", comp) + + sig: json.JsonValue* = json.object(pool) + sig_trig: json.JsonValue* = json.array(pool) + if sig is not None and sig_trig is not None: + json.array_append(pool, sig_trig, json.string_val(pool, "(")) + json.array_append(pool, sig_trig, json.string_val(pool, ",")) + json.object_set(pool, sig, "triggerCharacters", sig_trig) + json.object_set(pool, caps, "signatureHelpProvider", sig) json.object_set(pool, result, "capabilities", caps) json.object_set(pool, info, "name", name) json.object_set(pool, info, "version", ver) @@ -389,6 +408,10 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int: rc = LspNavigation.DocumentHighlight(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "textDocument/hover") == 0: rc = LspNavigation.Hover(pool, tr, docs, ws, id_node, params) + elif string.strcmp(method, "textDocument/completion") == 0: + rc = LspCompletion.Completion(pool, tr, docs, ws, id_node, params) + elif string.strcmp(method, "textDocument/signatureHelp") == 0: + rc = LspSignature.SignatureHelp(pool, tr, docs, ws, id_node, params) elif string.strcmp(method, "viper/lspStats") == 0: rc = _HandleStats(pool, tr, docs, ws, id_node) elif string.strcmp(method, "viper/lspDocument") == 0: diff --git a/App/lib/lsp/LspSignature.vp b/App/lib/lsp/LspSignature.vp new file mode 100644 index 0000000..90edb38 --- /dev/null +++ b/App/lib/lsp/LspSignature.vp @@ -0,0 +1,382 @@ +from stdint import * +import string +import json +import memhub +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.LspFiles as LspFiles +import lib.lsp.LspCompletion as LspCompletion +import lib.lsp.LspTransport as LspTransport +import lib.lsp.LspRpc as LspRpc + + +define LSP_MAX_SIGNATURES = 8 + + +def _SliceOffsets(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + start: int, end: int) -> str: + if doc is None or doc.index is None or end <= start: + return None + total: int = int(doc.index.Length()) + if start < 0: + start = 0 + if end > total: + end = total + n: sizet = sizet(end - start) + buf: bytes = pool.alloc(n + 1) + if buf is None: + return None + string.memcpy(buf, doc.text + start, n) + buf[n] = 0 + return str(buf) + + +def _FindCallParen(pool: memhub.MemManager*, + doc: LspDocuments.LspDocument*, cursor: int) -> int: + """Byte offset of the innermost unmatched '(' before the cursor, or -1.""" + if doc is None or doc.index is None: + return -1 + total: int = int(doc.index.Length()) + if cursor > total: + cursor = total + stack: list[int]* = list[int](pool, 16) + if stack is None: + return -1 + off: int = 0 + while off < cursor: + ch: char = doc.text[off] + if ch == '#': + while off < cursor and doc.text[off] != '\n': + off += 1 + continue + if ch == '\'' or ch == '"': + off = int(LspSource._SkipQuoted(doc, sizet(off), sizet(cursor))) + continue + if ch == '(': + stack.append(off) + elif ch == ')': + if int(len(stack)) > 0: + stack.pop() + off += 1 + if int(len(stack)) == 0: + return -1 + return stack.get(sizet(int(len(stack)) - 1)) + + +def _CalleeBefore(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*, + paren: int) -> str: + if doc is None or paren <= 0: + return None + end: int = paren + while end > 0: + ch: char = doc.text[end - 1] + if ch == ' ' or ch == '\t' or ch == '\n' or ch == '\r': + end -= 1 + continue + break + start: int = end + while start > 0: + ch: char = doc.text[start - 1] + if LspSource._IsIdentChar(ch) != 0 or ch == '.': + start -= 1 + continue + break + if start >= end: + return None + txt: str = _SliceOffsets(pool, doc, start, end) + return txt + + +def _CountCommas(doc: LspDocuments.LspDocument*, open_off: int, + cursor: int) -> int: + if doc is None or doc.index is None or cursor <= open_off: + return 0 + off: int = open_off + 1 + depth: int = 0 + commas: int = 0 + while off < cursor: + ch: char = doc.text[off] + if ch == '#': + while off < cursor and doc.text[off] != '\n': + off += 1 + continue + if ch == '\'' or ch == '"': + off = int(LspSource._SkipQuoted(doc, sizet(off), sizet(cursor))) + continue + if ch == '(' or ch == '[' or ch == '{': + depth += 1 + elif ch == ')' or ch == ']' or ch == '}': + if depth > 0: + depth -= 1 + elif ch == ',' and depth == 0: + commas += 1 + off += 1 + return commas + + +def _CalleeTarget(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + ws: LspWorkspace.LspWorkspace*, table_pool: memhub.MemManager*, + scope: LspSymbols.LspScope*, chain: str) -> LspIndex.LspTarget*: + if chain is None: + return None + target: LspIndex.LspTarget* = LspCompletion._TargetForBase( + pool, a, ws, table_pool, scope, chain) + if target is None: + return None + if target.kind == LspSymbols.LSP_KIND_FUNCTION or target.kind == LspSymbols.LSP_KIND_METHOD: + return target + if target.kind == LspSymbols.LSP_KIND_CLASS: + return target + # A value with a known class type: use that type's constructor/method signatures. + if target.detail is not None: + return LspCompletion._TypeTarget(pool, a, ws, table_pool, scope, target.detail) + return None + + +def _CollectDefs(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*, + container: str, name: str) -> list[ast.AST*]*: + out: list[ast.AST*]* = list[ast.AST*](pool, 8) + if a is None or a.tree is None or a.tree.children is None or name is None: + return out + i: int = 0 + body: list[ast.AST*]* = a.tree.children + while i < int(len(body)): + st: ast.AST* = body.get(sizet(i)) + i += 1 + if st is None: + continue + k: int = st.kind() + if k == ast.ASTKind.FunctionDef: + fn: ast.FunctionDef* = (ast.FunctionDef*)(st) + if container is None and fn.name is not None: + if string.strcmp(fn.name, name) == 0: + out.append(st) + continue + if k == ast.ASTKind.ClassDef: + cls: ast.ClassDef* = (ast.ClassDef*)(st) + if cls.name is None: + continue + if container is not None and string.strcmp(cls.name, container) == 0: + if cls.children is None: + continue + j: int = 0 + while j < int(len(cls.children)): + ch: ast.AST* = cls.children.get(sizet(j)) + j += 1 + if ch is not None and ch.kind() == ast.ASTKind.FunctionDef: + m: ast.FunctionDef* = (ast.FunctionDef*)(ch) + if m.name is not None and string.strcmp(m.name, name) == 0: + out.append(ch) + return out + + +def _Trim(pool: memhub.MemManager*, s: str) -> str: + if s is None: + return None + n: sizet = string.strlen(s) + start: sizet = 0 + while start < n: + ch: char = s[start] + if ch == ' ' or ch == '\t' or ch == '\r' or ch == '\n': + start += 1 + continue + break + end: sizet = n + while end > start: + ch: char = s[end - 1] + if ch == ' ' or ch == '\t' or ch == '\r' or ch == '\n': + end -= 1 + continue + break + if end <= start: + return "" + return LspSymbols._CopyStr(pool, s + start, int(end - start)) + + +def _ParseParams(pool: memhub.MemManager*, label: str) -> list[str]*: + params: list[str]* = list[str](pool, 8) + if params is None or label is None: + return params + n: sizet = string.strlen(label) + open_i: sizet = 0 + while open_i < n and label[open_i] != '(': + open_i += 1 + if open_i >= n: + return params + depth: int = 0 + start: sizet = open_i + 1 + i: sizet = open_i + while i < n: + ch: char = label[i] + if ch == '(' or ch == '[' or ch == '{': + depth += 1 + elif ch == ')' or ch == ']' or ch == '}': + depth -= 1 + if depth == 0: + part: str = _Trim(pool, _SliceStr(pool, label, start, i)) + if part is not None and part[0] != 0: + params.append(part) + break + elif ch == ',' and depth == 1: + part2: str = _Trim(pool, _SliceStr(pool, label, start, i)) + if part2 is not None and part2[0] != 0: + params.append(part2) + start = i + 1 + i += 1 + # Drop the receiver parameter from method signatures. + if int(len(params)) > 0: + first: str = params.get(sizet(0)) + if first is not None: + if string.strcmp(first, "self") == 0 or string.strcmp(first, "cls") == 0: + params.pop() + return params + + +def _SliceStr(pool: memhub.MemManager*, s: str, start: sizet, end: sizet) -> str: + if s is None or end <= start: + return "" + n: sizet = end - start + buf: bytes = pool.alloc(n + 1) + if buf is None: + return "" + string.memcpy(buf, s + start, n) + buf[n] = 0 + return str(buf) + + +def _FileAnalysis(pool: memhub.MemManager*, table_pool: memhub.MemManager*, + path: str) -> LspSymbols.LspAnalysis*: + text: str = LspFiles.ReadSource(pool, path) + if text is None: + return None + doc: LspDocuments.LspDocument* = LspSource.NewTempDoc(pool, text) + if doc is None: + return None + return LspSymbols.Analyze(pool, table_pool, doc, path) + + +def _SignatureResult(pool: memhub.MemManager*, b: LspSymbols.LspAnalysis*, + defs: list[ast.AST*]*, fallback_label: str, + active_param: int) -> json.JsonValue*: + arr: json.JsonValue* = json.array(pool) + if arr is None: + return None + n_sigs: int = 0 + first_params: int = 0 + i: int = 0 + while i < int(len(defs)) and n_sigs < LSP_MAX_SIGNATURES: + node: ast.AST* = defs.get(sizet(i)) + i += 1 + if node is None: + continue + label: str = LspSymbols._Signature(pool, b, node) + if label is None: + continue + params: list[str]* = _ParseParams(pool, label) + sig: json.JsonValue* = json.object(pool) + if sig is None: + continue + json.object_set(pool, sig, "label", json.string_val(pool, label)) + p_arr: json.JsonValue* = json.array(pool) + if p_arr is not None: + j: int = 0 + while j < int(len(params)): + p: json.JsonValue* = json.object(pool) + if p is not None: + json.object_set(pool, p, "label", + json.string_val(pool, params.get(sizet(j)))) + json.array_append(pool, p_arr, p) + j += 1 + json.object_set(pool, sig, "parameters", p_arr) + doc: str = LspSymbols._Docstring(pool, b, node) + if doc is not None: + json.object_set(pool, sig, "documentation", json.string_val(pool, doc)) + if n_sigs == 0: + first_params = int(len(params)) + n_sigs += 1 + json.array_append(pool, arr, sig) + if n_sigs == 0 and fallback_label is not None: + params2: list[str]* = _ParseParams(pool, fallback_label) + sig2: json.JsonValue* = json.object(pool) + if sig2 is not None: + json.object_set(pool, sig2, "label", json.string_val(pool, fallback_label)) + p_arr2: json.JsonValue* = json.array(pool) + if p_arr2 is not None: + j2: int = 0 + while j2 < int(len(params2)): + p2: json.JsonValue* = json.object(pool) + if p2 is not None: + json.object_set(pool, p2, "label", + json.string_val(pool, params2.get(sizet(j2)))) + json.array_append(pool, p_arr2, p2) + j2 += 1 + json.object_set(pool, sig2, "parameters", p_arr2) + json.array_append(pool, arr, sig2) + first_params = int(len(params2)) + n_sigs = 1 + if n_sigs == 0: + return None + result: json.JsonValue* = json.object(pool) + if result is None: + return None + json.object_set(pool, result, "signatures", arr) + json.object_set(pool, result, "activeSignature", json.int_val(pool, 0)) + ap: int = active_param + if first_params > 0 and ap >= first_params: + ap = first_params - 1 + if ap < 0: + ap = 0 + json.object_set(pool, result, "activeParameter", json.int_val(pool, int64(ap))) + return result + + +def SignatureHelp(pool: memhub.MemManager*, tr: LspTransport.LspTransport*, + docs: LspDocuments.LspDocuments*, ws: LspWorkspace.LspWorkspace*, + id_node: json.JsonValue*, params: json.JsonValue*) -> int: + if params is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + doc: LspDocuments.LspDocument* = LspCompletion._Doc(pool, docs, params) + pos: json.JsonValue* = params["position"] + if doc is None or pos is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + line0: int = LspRpc.GetIntField(pos, "line", 0) + ch: int = LspRpc.GetIntField(pos, "character", 0) + uri: str = LspRpc.GetStringField(params["textDocument"], "uri") + file: str = LspWorkspace.DecodeUri(pool, uri) + a: LspSymbols.LspAnalysis* = LspSymbols.Analyze(pool, docs.pool, doc, file) + if a is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + byte_col: int = doc.index.Utf16ColToByte(line0, ch) + cursor: int = int(doc.index.LineStart(line0)) + byte_col + open_off: int = _FindCallParen(pool, doc, cursor) + if open_off < 0: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + chain: str = _CalleeBefore(pool, doc, open_off) + if chain is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + scope: LspSymbols.LspScope* = LspCompletion._ScopeAt(a, line0, ch) + if scope is None: + scope = a.module_scope + target: LspIndex.LspTarget* = _CalleeTarget(pool, a, ws, docs.pool, scope, chain) + if target is None or target.name is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + b: LspSymbols.LspAnalysis* = a + if target.file is not None and string.strcmp(target.file, a.file) != 0: + b = _FileAnalysis(pool, docs.pool, target.file) + if b is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + defs: list[ast.AST*]* = None + fallback: str = target.detail + if target.kind == LspSymbols.LSP_KIND_CLASS: + defs = _CollectDefs(pool, b, target.name, "__init__") + else: + defs = _CollectDefs(pool, b, target.container, target.name) + commas: int = _CountCommas(doc, open_off, cursor) + result: json.JsonValue* = _SignatureResult(pool, b, defs, fallback, commas) + if result is None: + return LspRpc.WriteResponse(pool, tr, id_node, json.null(pool)) + return LspRpc.WriteResponse(pool, tr, id_node, result) diff --git a/Tools/lsp/fixtures/gate4.vp b/Tools/lsp/fixtures/gate4.vp new file mode 100644 index 0000000..97a4b08 --- /dev/null +++ b/Tools/lsp/fixtures/gate4.vp @@ -0,0 +1,34 @@ +from .gate3_stub import StubThing +from .gate3_helper import helper_func + +GLOBAL_COUNT: int = 2 + + +class Gadget: + label: str + + def __init__(self, label: str): + self.label = label + + def run(self, times: int) -> int: + local: int = times + return local + + +def overload(a: int) -> int: + return a + + +def overload(a: str) -> int: + return 0 + + +def pair(a: int, b: int) -> int: + return a + + +def use(g: Gadget) -> int: + x: int = 1 + Gadget.label = "x" + StubThing.value = 1 + return x diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index 50e7a38..ca60007 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -151,8 +151,8 @@ def lifecycle(exe: str) -> int: raise AssertionError(f"unknown method did not return a null result: {unknown}") unsupported = client.request( - "textDocument/completion", - {"textDocument": {"uri": "file:///unsupported.vp"}, "position": {"line": 0, "character": 0}}, + "textDocument/formatting", + {"textDocument": {"uri": "file:///unsupported.vp"}, "options": {"tabSize": 4, "insertSpaces": True}}, ) if "error" in unsupported or unsupported.get("result") is not None: raise AssertionError(f"unsupported feature should answer null: {unsupported}") @@ -574,12 +574,181 @@ def gate3(exe: str) -> int: return 0 +def _position_after(text: str, needle: str, delta: int = 0) -> dict[str, int]: + pos = _position(text, needle) + pos["character"] += len(needle) + delta + return pos + + +def _end_position(text: str) -> dict[str, int]: + line = text.count("\n") + column = len(text) - (text.rfind("\n") + 1) + return {"line": line, "character": column} + + +def _replace(client: "Client", uri: str, version: int, text: str) -> None: + client.notify( + "textDocument/didChange", + { + "textDocument": {"uri": uri, "version": version}, + "contentChanges": [{"text": text}], + }, + ) + + +def _completion_labels(client: "Client", uri: str, + pos: dict[str, int]) -> list[str]: + result = client.request( + "textDocument/completion", + { + "textDocument": {"uri": uri}, + "position": pos, + "context": {"triggerKind": 1}, + }, + ).get("result") + if not isinstance(result, dict) or not isinstance(result.get("items"), list): + raise AssertionError(f"completion result is malformed: {result}") + return [item["label"] for item in result["items"] if isinstance(item, dict)] + + +def _signature(client: "Client", uri: str, + pos: dict[str, int]) -> dict[str, Any]: + result = client.request( + "textDocument/signatureHelp", + {"textDocument": {"uri": uri}, "position": pos}, + ).get("result") + if result is not None and not isinstance(result, dict): + raise AssertionError(f"signatureHelp result is malformed: {result}") + return result or {} + + +def gate4(exe: str) -> int: + root = Path(__file__).resolve().parents[2] + source = root / "Tools" / "lsp" / "fixtures" / "gate4.vp" + uri = source.as_uri() + text = source.read_text(encoding="utf-8") + + client = Client(exe) + response = client.request( + "initialize", + {"processId": os.getpid(), "rootUri": root.as_uri(), "capabilities": {}}, + ) + capabilities = response.get("result", {}).get("capabilities", {}) + completion_provider = capabilities.get("completionProvider") + if not isinstance(completion_provider, dict): + raise AssertionError(f"missing completionProvider: {response}") + if "." not in completion_provider.get("triggerCharacters", []): + raise AssertionError(f"completion trigger characters are wrong: {completion_provider}") + signature_provider = capabilities.get("signatureHelpProvider") + if not isinstance(signature_provider, dict): + raise AssertionError(f"missing signatureHelpProvider: {response}") + if "(" not in signature_provider.get("triggerCharacters", []): + raise AssertionError(f"signature trigger characters are wrong: {signature_provider}") + + client.notify("initialized", {}) + client.notify( + "textDocument/didOpen", + {"textDocument": {"uri": uri, "languageId": "viper", "version": 1, "text": text}}, + ) + client.wait_notification("textDocument/publishDiagnostics") + + return_pos = _position(text, " return x") + return_pos["character"] = 4 + labels = _completion_labels(client, uri, return_pos) + for required in ("g", "x", "Gadget", "GLOBAL_COUNT", "helper_func", "overload", "pair"): + if required not in labels: + raise AssertionError(f"scope completion missed {required}: {labels}") + for builtin in ("len", "print"): + if builtin not in labels: + raise AssertionError(f"scope completion missed builtin {builtin}: {labels}") + + labels = _completion_labels(client, uri, _position_after(text, "self.")) + for required in ("label", "run", "__init__"): + if required not in labels: + raise AssertionError(f"self member completion missed {required}: {labels}") + + labels = _completion_labels(client, uri, _position_after(text, "Gadget.")) + for required in ("label", "run", "__init__"): + if required not in labels: + raise AssertionError(f"class member completion missed {required}: {labels}") + + labels = _completion_labels(client, uri, _position_after(text, "StubThing.")) + for required in ("value", "run"): + if required not in labels: + raise AssertionError(f".vpi member completion missed {required}: {labels}") + + import_text = "from .gate3_" + _replace(client, uri, 2, import_text) + labels = _completion_labels(client, uri, _end_position(import_text)) + for required in ("gate3_helper", "gate3_stub"): + if required not in labels: + raise AssertionError(f"import completion missed {required}: {labels}") + + overload_text = ( + "def overload(a: int) -> int:\n" + " return a\n" + "\n" + "def overload(a: str) -> int:\n" + " return 0\n" + "\n" + "overload(" + ) + _replace(client, uri, 3, overload_text) + help = _signature(client, uri, _end_position(overload_text)) + signatures = help.get("signatures") + if not isinstance(signatures, list) or len(signatures) != 2: + raise AssertionError(f"overload signatureHelp is wrong: {help}") + for signature in signatures: + if "def overload" not in signature.get("label", ""): + raise AssertionError(f"overload signature label is wrong: {signature}") + if len(signature.get("parameters", [])) != 1: + raise AssertionError(f"overload parameter list is wrong: {signature}") + if help.get("activeParameter") != 0: + raise AssertionError(f"activeParameter should be 0: {help}") + + pair_text = ( + "def pair(a: int, b: int) -> int:\n" + " return a\n" + "\n" + "pair(1, " + ) + _replace(client, uri, 4, pair_text) + help = _signature(client, uri, _end_position(pair_text)) + signatures = help.get("signatures") + if not isinstance(signatures, list) or len(signatures) != 1: + raise AssertionError(f"pair signatureHelp is wrong: {help}") + if len(signatures[0].get("parameters", [])) != 2: + raise AssertionError(f"pair parameter list is wrong: {signatures[0]}") + if help.get("activeParameter") != 1: + raise AssertionError(f"activeParameter should be 1: {help}") + + cross_text = "from .gate3_helper import helper_func\n\nhelper_func(" + _replace(client, uri, 5, cross_text) + help = _signature(client, uri, _end_position(cross_text)) + signatures = help.get("signatures") + if not isinstance(signatures, list) or len(signatures) != 1: + raise AssertionError(f"cross-file signatureHelp is wrong: {help}") + if "def helper_func" not in signatures[0].get("label", ""): + raise AssertionError(f"cross-file signature label is wrong: {signatures[0]}") + + client.notify("textDocument/didClose", {"textDocument": {"uri": uri}}) + client.wait_notification("textDocument/publishDiagnostics") + client.request("shutdown") + code = client.close() + if code != 0: + raise AssertionError(f"exit code is {code}, expected 0") + print("completion: OK") + print("signature: OK") + return 0 + + def main() -> int: parser = argparse.ArgumentParser(description="ViperC LSP probe") parser.add_argument("--selfcheck", action="store_true", help="run pure-Python framing checks") parser.add_argument("--gate1", action="store_true", help="run document/position checks") parser.add_argument("--gate2", action="store_true", help="run syntax diagnostic checks") parser.add_argument("--gate3", action="store_true", help="run symbol/navigation checks") + parser.add_argument("--gate4", action="store_true", help="run completion/signature checks") parser.add_argument("--exe", help="path to viperc.exe") args = parser.parse_args() @@ -591,6 +760,8 @@ def main() -> int: if not os.path.isfile(exe): print(f"probe: executable not found: {exe}", file=sys.stderr) return 2 + if args.gate4: + return gate4(exe) if args.gate3: return gate3(exe) if args.gate2: