feat(lsp): 增加补全与签名帮助
作用域/成员/import 路径补全;签名帮助支持当前参数与同名重载;成员来自 live 文档、索引或按需解析的 .vpi。
This commit is contained in:
1 parent
4aba50d60e
commit
ebb291daeb
6 files changed
+1441
-2
No files matched your search
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user