feat(lsp): 支持 from module import * 与相对 includes

通配导入的名字通过索引解析(高亮/悬停/跳转/成员),typedef 使用 type token;project.vpj 中任意相对 source/includes 按项目目录解析;修复通配符号表步长导致的越界崩溃;关闭文档释放文档对象,didOpen 不再强制重建索引。
This commit is contained in:
TermiNexus committed 2026-10-27 02:41:45 +08:00
1 parent 879cf7ab9f
commit d13eb6b063
11 files changed
+245 -7

No files matched your search

+4
View File
@@ -85,6 +85,10 @@ class LspDocuments:
if doc.spare is not None:
doc.spare.Destroy()
doc.spare = None
if doc.uri is not None:
self.pool.free(rawptr(doc.uri))
doc.uri = None
self.pool.free(rawptr(doc))
return 0
def Open(self, uri: str, text: str, version: int) -> LspDocument*:
+33
View File
@@ -62,6 +62,7 @@ class LspTarget:
sel: LspSource.LspRange*
has_sel: int
is_macro: int
is_alias: int
def __new__(self, pool: memhub.MemManager*) -> rawptr:
p: LspTarget* = (LspTarget*)(pool.alloc(LspTarget.__sizeof__()))
@@ -250,6 +251,7 @@ def _CopySymbol(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*,
c.name = LspPools.DupStr(pool, s.name)
c.kind = s.kind
c.is_macro = s.is_macro
c.is_alias = s.is_alias
c.detail = LspSymbols._CopyStr(pool, s.detail, LspSymbols.LSP_SIGNATURE_CAP)
c.doc = LspSymbols._CopyStr(pool, s.doc, LspSymbols.LSP_DOC_CAP)
c.file = file
@@ -742,6 +744,7 @@ def _SymbolTarget(pool: memhub.MemManager*, s: LspSymbols.LspSymbol*) -> LspTarg
t.name = s.name
t.kind = s.kind
t.is_macro = s.is_macro
t.is_alias = s.is_alias
t.detail = s.detail
t.doc = s.doc
t.container = s.container
@@ -954,6 +957,34 @@ def ResolveSymbol(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
return _SymbolTarget(pool, s)
def ResolveWildcard(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
table_pool: memhub.MemManager*, from_file: str,
wildcard: LspSymbols.LspSymbol*,
name: str) -> LspTarget*:
"""Resolve a name (optionally `Name.member...`) supplied by `from module import *`."""
if wildcard is None or name is None:
return None
file: str = _ResolveFromFile(ws, pool, from_file, wildcard)
if file is None or not os.isfile(file):
return None
h: str = None
r: str = None
LspSymbols._SplitHead(pool, name, &h, &r)
if h is None:
return None
member: LspSymbols.LspSymbol* = ResolveInFile(pool, table_pool, file, None, h)
if member is None:
return None
base: LspTarget* = None
if member.import_kind != LspSymbols.LSP_IMPORT_NONE:
base = _ResolveFromTarget(ws, pool, table_pool, member.file, member, 0)
else:
base = _SymbolTarget(pool, member)
if r is None:
return base
return ResolveMembers(pool, table_pool, base, r)
def ResolveMembers(pool: memhub.MemManager*, table_pool: memhub.MemManager*,
base: LspTarget*, rest: str) -> LspTarget*:
"""Walk class members from an already-resolved base target (no import re-resolution)."""
@@ -986,6 +1017,8 @@ def ResolveChain(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
rest: str) -> LspTarget*:
if head is None:
return None
if head.import_attr is not None and string.strcmp(head.import_attr, "*") == 0:
return ResolveWildcard(ws, pool, table_pool, from_file, head, rest)
if head.import_kind == LspSymbols.LSP_IMPORT_FROM:
base: LspTarget* = _ResolveFromTarget(ws, pool, table_pool, from_file, head, 0)
if base is None or rest is None:
+38 -1
View File
@@ -161,6 +161,32 @@ def _LiveSymbol(a: LspSymbols.LspAnalysis*, container: str,
return None
def _WildcardTarget(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
table_pool: memhub.MemManager*,
a: LspSymbols.LspAnalysis*,
o: LspSymbols.LspOccurrence*) -> LspIndex.LspTarget*:
if a is None or a.wildcards is None or o is None or o.name is None:
return None
i: int = 0
while i < int(len(a.wildcards)):
w: LspSymbols.LspSymbol* = a.wildcards.get(sizet(i))
i += 1
t: LspIndex.LspTarget* = LspIndex.ResolveWildcard(
ws, pool, table_pool, a.file, w, o.name)
if t is not None:
return t
return None
def WildcardNameMismatch(o: LspSymbols.LspOccurrence*, target: str) -> int:
"""Skip wildcard resolution for occurrences whose name cannot match the target."""
if o is None or target is None or o.name is None:
return 0
if o.sym is not None or o.head is not None:
return 0
return 1 if string.strcmp(o.name, target) != 0 else 0
def _TargetAt(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
table_pool: memhub.MemManager*,
a: LspSymbols.LspAnalysis*, doc: LspDocuments.LspDocument*,
@@ -173,7 +199,7 @@ def _TargetAt(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
return LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, occ.sym, 0)
if occ.head is not None and occ.rest is not None:
return LspIndex.ResolveChain(ws, pool, table_pool, a.file, occ.head, occ.rest)
return None
return _WildcardTarget(ws, pool, table_pool, a, occ)
s: LspSymbols.LspSymbol* = _SymAt(a, line0, ch)
if s is not None:
return LspIndex.ResolveSymbol(ws, pool, table_pool, a.file, s, 0)
@@ -400,6 +426,13 @@ def _IdentityForOcc(pool: memhub.MemManager*, a: LspSymbols.LspAnalysis*,
out_container[0] = t.container
out_name[0] = t.name
return 1
return 0
t2: LspIndex.LspTarget* = _WildcardTarget(ws, pool, table_pool, a, o)
if t2 is not None and t2.name is not None and t2.file is not None:
out_file[0] = LspFiles.Normalize(pool, t2.file)
out_container[0] = t2.container
out_name[0] = t2.name
return 1
return 0
@@ -531,6 +564,8 @@ def References(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
while j < int(len(oa.occs)):
o: LspSymbols.LspOccurrence* = oa.occs.get(sizet(j))
j += 1
if WildcardNameMismatch(o, tn) != 0:
continue
of: str = None
oc: str = None
on: str = None
@@ -636,6 +671,8 @@ def DocumentHighlight(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
while i2 < int(len(a.occs)):
o2: LspSymbols.LspOccurrence* = a.occs.get(sizet(i2))
i2 += 1
if WildcardNameMismatch(o2, tn) != 0:
continue
of: str = None
oc: str = None
on: str = None
+2
View File
@@ -278,6 +278,8 @@ def Rename(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
while j < int(len(oa.occs)):
o: LspSymbols.LspOccurrence* = oa.occs.get(sizet(j))
j += 1
if LspNavigation.WildcardNameMismatch(o, tn) != 0:
continue
of: str = None
oc: str = None
on: str = None
+26 -2
View File
@@ -31,6 +31,7 @@ define ST_NUMBER = 10
define ST_OPERATOR = 11
define ST_DECORATOR = 12
define ST_MACRO = 13
define ST_TYPE = 14
class LspSemToken:
@@ -83,6 +84,7 @@ def Provider(pool: memhub.MemManager*) -> json.JsonValue*:
json.array_append(pool, types, json.string_val(pool, "operator"))
json.array_append(pool, types, json.string_val(pool, "decorator"))
json.array_append(pool, types, json.string_val(pool, "macro"))
json.array_append(pool, types, json.string_val(pool, "type"))
json.array_append(pool, mods, json.string_val(pool, "declaration"))
json.array_append(pool, mods, json.string_val(pool, "readonly"))
json.object_set(pool, legend, "tokenTypes", types)
@@ -105,6 +107,8 @@ def _IsDefineMacro(s: LspSymbols.LspSymbol*) -> int:
def _TypeFor(s: LspSymbols.LspSymbol*) -> int:
if s is None:
return ST_VARIABLE
if s.is_alias != 0:
return ST_TYPE
if s.kind == LspSymbols.LSP_KIND_MODULE:
return ST_NAMESPACE
if s.kind == LspSymbols.LSP_KIND_CLASS:
@@ -128,6 +132,8 @@ def _ModsFor(s: LspSymbols.LspSymbol*) -> int:
"""readonly = bit 1 in the legend; constants use the theme's constant color (as Vipeye does)."""
if s is None:
return 0
if s.is_alias != 0:
return 0
if s.kind == LspSymbols.LSP_KIND_CONSTANT:
return 2
return 0
@@ -137,6 +143,7 @@ class LspSemCtx:
ws: LspWorkspace.LspWorkspace*
table_pool: memhub.MemManager*
file: str
wildcards: list[LspSymbols.LspSymbol*]*
sym_keys: list[LspSymbols.LspSymbol*]*
sym_tgts: list[LspIndex.LspTarget*]*
chain_heads: list[LspSymbols.LspSymbol*]*
@@ -153,6 +160,8 @@ class LspSemCtx:
def _TypeForTarget(t: LspIndex.LspTarget*) -> int:
if t is None:
return -1
if t.is_alias != 0:
return ST_TYPE
if t.kind == LspSymbols.LSP_KIND_MODULE:
return ST_NAMESPACE
if t.kind == LspSymbols.LSP_KIND_CLASS:
@@ -167,7 +176,7 @@ def _TypeForTarget(t: LspIndex.LspTarget*) -> int:
def _ModsForTarget(t: LspIndex.LspTarget*) -> int:
if t is not None and t.kind == LspSymbols.LSP_KIND_CONSTANT:
if t is not None and t.is_alias == 0 and t.kind == LspSymbols.LSP_KIND_CONSTANT:
return 2
return 0
@@ -209,12 +218,16 @@ def _CachedChain(ctx: LspSemCtx*, pool: memhub.MemManager*,
if ctx is None or head is None or rest is None:
return None
slot: sizet = (_SemHash(rest) ^ _SemHash(head.name)) & sizet(LSP_SEM_CACHE_MASK)
if head.import_mod is not None:
slot = (slot ^ _SemHash(head.import_mod)) & sizet(LSP_SEM_CACHE_MASK)
if ctx.chain_heads.get(slot) == head:
cached: str = ctx.chain_rests.get(slot)
if cached is not None and string.strcmp(cached, rest) == 0:
return ctx.chain_tgts.get(slot)
t: LspIndex.LspTarget* = None
if head.import_kind == LspSymbols.LSP_IMPORT_FROM:
if head.import_attr is not None and string.strcmp(head.import_attr, "*") == 0:
t = LspIndex.ResolveWildcard(ctx.ws, pool, ctx.table_pool, ctx.file, head, rest)
elif head.import_kind == LspSymbols.LSP_IMPORT_FROM:
base: LspIndex.LspTarget* = _CachedSymbol(ctx, pool, head)
if base is not None:
t = LspIndex.ResolveMembers(pool, ctx.table_pool, base, rest)
@@ -263,6 +276,16 @@ def _TokenForOcc(ctx: LspSemCtx*, pool: memhub.MemManager*,
if t is not None:
typ = _TypeForTarget(t)
mods = _ModsForTarget(t)
if typ < 0 and o.head is None and o.rest is None and o.name is not None:
if ctx is not None and ctx.wildcards is not None:
wi: int = 0
while wi < int(len(ctx.wildcards)) and typ < 0:
w: LspSymbols.LspSymbol* = ctx.wildcards.get(sizet(wi))
wi += 1
tw: LspIndex.LspTarget* = _CachedChain(ctx, pool, w, o.name)
if tw is not None:
typ = _TypeForTarget(tw)
mods = _ModsForTarget(tw)
if typ < 0:
# Builtin type names have no declaration; color them like class types.
if o.builtin_type == 0:
@@ -568,6 +591,7 @@ def Tokens(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
ctx.ws = ws
ctx.table_pool = docs.pool
ctx.file = a.file
ctx.wildcards = a.wildcards
ctx.sym_keys = list[LspSymbols.LspSymbol*](pool, 8)
ctx.sym_tgts = list[LspIndex.LspTarget*](pool, 8)
ctx.chain_heads = list[LspSymbols.LspSymbol*](pool, 8)
-1
View File
@@ -474,7 +474,6 @@ def Run(pool: memhub.MemBuddy*, perm_pool: memhub.MemBuddy*) -> int:
pass
elif string.strcmp(method, "textDocument/didOpen") == 0:
_HandleDidOpen(pool, tr, docs, ws, params)
LspIndex.Invalidate()
elif string.strcmp(method, "textDocument/didChange") == 0:
_HandleDidChange(pool, tr, docs, params)
elif string.strcmp(method, "textDocument/didClose") == 0:
+39
View File
@@ -48,6 +48,8 @@ class LspSymbol:
kind: int
# 1 for `define` macro functions (colored as macro), 0 otherwise.
is_macro: int
# 1 for typedef/type aliases (colored as type), 0 otherwise.
is_alias: int
detail: str
doc: str
file: str
@@ -108,6 +110,7 @@ class LspAnalysis:
module_scope: LspScope*
symbols: list[LspSymbol*]*
occs: list[LspOccurrence*]*
wildcards: list[LspSymbol*]*
root: list[LspSymbol*]*
def __new__(self, pool: memhub.MemManager*) -> rawptr:
@@ -464,6 +467,12 @@ def _BindType(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
return
head: LspSymbol* = _Lookup(a, scope, nm.id, 0)
if head is None:
# A type imported by `from module import *` has no local binding; bind it to the
# wildcard module so member access can resolve through the index.
if _IsBuiltinTypeName(nm.id) == 0 and a.wildcards is not None:
if int(len(a.wildcards)) > 0:
sym.type_sym = a.wildcards.get(0)
sym.type_rest = nm.id
return
sym.type_sym = head
sym.type_rest = None
@@ -619,6 +628,22 @@ def _NameOnLine(pool: memhub.MemManager*, a: LspAnalysis*, line1: int,
return None
def _IsTypeAliasDecl(decl: ast.AST*) -> int:
"""1 when decl is `name: t.TypedefT = type` (the canonical typedef form)."""
if decl is None or decl.kind() != ast.ASTKind.AnnAssign:
return 0
ann: ast.AST* = ((ast.AnnAssign*)(decl)).annotation
if ann is None:
return 0
if ann.kind() == ast.ASTKind.Name:
nm: ast.Name* = (ast.Name*)(ann)
return 1 if nm.id is not None and string.strcmp(nm.id, "TypedefT") == 0 else 0
if ann.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = (ast.Attribute*)(ann)
return 1 if at.attr is not None and string.strcmp(at.attr, "CTypedef") == 0 else 0
return 0
def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
name: str, kind: int, decl: ast.AST*, line: int, col: int,
detail: str) -> LspSymbol*:
@@ -636,6 +661,8 @@ def _Declare(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
fd: ast.FunctionDef* = (ast.FunctionDef*)(decl)
if (fd.flags & ast.FLAG_IS_DEFINE) != 0:
s.is_macro = 1
if _IsTypeAliasDecl(decl) != 0:
s.is_alias = 1
s.sel = _NameSpan(pool, a.doc, line, col, name)
if s.sel is None or _RangeTextIsName(pool, a, s.sel, name) == 0:
if decl is None or (decl.kind() != ast.ASTKind.Import and decl.kind() != ast.ASTKind.ImportFrom):
@@ -1188,6 +1215,17 @@ def _WalkStmt(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
sym.import_attr = al.name
sym.import_level = ifr.level
_DeclImportOcc(pool, a, scope, al, bound, sym)
elif ifr.module is not None and a.wildcards is not None:
# Wildcard imports bind no local names; keep the module for on-demand
# resolution of unresolved names (highlighting/navigation).
w: LspSymbol* = _NewSymbol(pool, "*", LSP_KIND_MODULE, node, scope)
if w is not None:
w.file = a.file
w.import_kind = LSP_IMPORT_FROM
w.import_mod = ifr.module
w.import_attr = "*"
w.import_level = ifr.level
a.wildcards.append(w)
i += 1
return
# Pass/Break/Continue/FallThrough and unknown statements carry no names.
@@ -1484,6 +1522,7 @@ def Analyze(pool: memhub.MemManager*, table_pool: memhub.MemManager*,
a.file = file
a.symbols = list[LspSymbol*](pool, 64)
a.occs = list[LspOccurrence*](pool, 128)
a.wildcards = list[LspSymbol*](pool, 8)
a.module_scope = _NewScope(pool, LSP_SCOPE_MODULE, None, None, None)
a.tree = ast.parse_ex(doc.text, pool, table_pool)
a.errors = ast.get_parse_errors()
+65 -2
View File
@@ -10,6 +10,68 @@ import lib.lsp.LspTransport as LspTransport
import lib.Projectrans.Config as Config
def _AbsJoin(pool: memhub.MemManager*, base: str, path: str) -> str:
"""Resolve a project-relative path against the project directory (any relative form)."""
if path is None or path[0] == 0:
return path
plen: sizet = string.strlen(path)
if path[0] == '/' or path[0] == '\\':
return LspPools.DupStr(pool, path)
if plen >= 2 and path[1] == ':':
return LspPools.DupStr(pool, path)
if base is None or base[0] == 0:
return LspPools.DupStr(pool, path)
joined: str = os.path.join(base, path)
if joined is None:
return LspPools.DupStr(pool, path)
norm: str = os.path.normpath(joined)
if norm is None:
norm = joined
return LspPools.DupStr(pool, norm)
def _AbsList(pool: memhub.MemManager*, base: str, dirs: str) -> str:
"""Resolve a space-separated directory list against base."""
if dirs is None or dirs[0] == 0:
return dirs
n: sizet = string.strlen(dirs)
out: str = pool.alloc(n * 3 + 256)
if out is None:
return dirs
out[0] = 0
pos: sizet = 0
first: int = 1
i: sizet = 0
while i < n:
while i < n and dirs[i] == ' ':
i += 1
if i >= n:
break
start: sizet = i
while i < n and dirs[i] != ' ':
i += 1
seg_len: sizet = i - start
if seg_len == 0:
continue
seg: str = pool.alloc(seg_len + 1)
if seg is None:
continue
string.memcpy(seg, dirs + start, seg_len)
seg[seg_len] = 0
resolved: str = _AbsJoin(pool, base, str(seg))
if resolved is None:
continue
alen: sizet = string.strlen(resolved)
if first == 0:
out[pos] = ' '
pos += 1
string.strcpy(out + pos, resolved)
pos += alen
first = 0
out[pos] = 0
return str(out)
def _HexVal(ch: char) -> int:
if ch >= '0' and ch <= '9':
return int(ch) - int('0')
@@ -145,8 +207,9 @@ class LspWorkspace:
rc: int = Config.Load_project_config(project)
if rc == 0:
Config.resolve_paths(os.path.dirname(project))
self.source_dir = Config.SourceDir
self.includes = Config.IncludesDirs
base: str = os.path.dirname(project)
self.source_dir = _AbsJoin(self.pool, base, Config.SourceDir)
self.includes = _AbsList(self.pool, base, Config.IncludesDirs)
Config._mbuddy = old_pool
return rc
+12
View File
@@ -0,0 +1,12 @@
"""Gate 10 fixture: exports consumed through `from module import *`."""
class Extra:
amount: int
define EXTRA_CONST = 5
def extra_fn(x: int) -> int:
return x
+5
View File
@@ -2,9 +2,14 @@
from .gate10_types import Widget, DOUBLE
from .gate10_stub import StubThing
from .gate10_extra import *
import gate10_types
def star_use(e: Extra) -> int:
return extra_fn(EXTRA_CONST) + e.amount
def macro_use(n: int) -> int:
return DOUBLE(n)
+21 -1
View File
@@ -1420,7 +1420,7 @@ def gate10(exe: str) -> int:
# The fixtures live outside the project's source roots; copy them into an isolated workspace
# so the cross-file index contains the module, the stub and the consumer.
workspace = Path(tempfile.mkdtemp(prefix="transpyc_lsp_gate10_"))
names = ("gate10_types.vp", "gate10_stub.vpi", "gate10_use.vp")
names = ("gate10_types.vp", "gate10_stub.vpi", "gate10_extra.vp", "gate10_use.vp")
for name in names:
shutil.copy(fixtures / name, workspace / name)
type_path = workspace / "gate10_types.vp"
@@ -1449,6 +1449,7 @@ def gate10(exe: str) -> int:
legend = response.get("result", {}).get("capabilities", {}).get("semanticTokensProvider", {}).get("legend", {})
sem_types = legend.get("tokenTypes", [])
sem_mods = legend.get("tokenModifiers", [])
semantic = client.request(
"textDocument/semanticTokens/full", {"textDocument": {"uri": uri}}, timeout=120.0
).get("result")
@@ -1461,11 +1462,30 @@ def gate10(exe: str) -> int:
("macro call", "return DOUBLE", 7, "macro"),
("member field", "s.value", 2, "property"),
("member method", "s.resize", 2, "method"),
("star class", "e: Extra", 3, "class"),
("star function", "return extra_fn", 7, "function"),
("star constant", "EXTRA_CONST", 0, "variable"),
("star member", "e.amount", 2, "property"),
):
pos = _position(text, needle)
pos["character"] += delta
if not _find_token(sem_tokens, pos["line"], pos["character"], sem_types.index(token_name)):
raise AssertionError(f"missing cross-module semantic token for {label}: {pos}")
const_pos = _position(text, "EXTRA_CONST")
if not _find_token_mods(sem_tokens, const_pos["line"], const_pos["character"],
sem_types.index("variable"),
1 << sem_mods.index("readonly")):
raise AssertionError(f"wildcard constant is not readonly: {const_pos}")
star_def = client.request(
"textDocument/definition",
{"textDocument": {"uri": uri}, "position": {"line": _position(text, "e: Extra")["line"],
"character": _position(text, "e: Extra")["character"] + 3}},
timeout=120.0,
).get("result")
if not isinstance(star_def, list) or not star_def:
raise AssertionError(f"wildcard definition failed: {star_def}")
if not star_def[0]["uri"].endswith("gate10_extra.vp"):
raise AssertionError(f"wildcard definition target is wrong: {star_def}")
def at(needle: str, delta: int = 0, occurrence: int = 0) -> dict[str, int]:
pos = _position(text, needle, occurrence)