feat(lsp): 增加补全与签名帮助

作用域/成员/import 路径补全;签名帮助支持当前参数与同名重载;成员来自 live 文档、索引或按需解析的 .vpi。
This commit is contained in:
TermiNexus committed 2026-10-26 17:11:51 +08:00
1 parent 4aba50d60e
commit ebb291daeb
6 files changed
+1441 -2

No files matched your search

+764
View File
@@ -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)
+65
View File
@@ -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:
+23
View File
@@ -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:
+382
View File
@@ -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)
+34
View File
@@ -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
+173 -2
View File
@@ -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: