fix(lsp): 点号导入逐段高亮与 project includes 路径修复

import a.b.c / from a.b import X 的每个模块段都有 namespace token 并可跳转到对应模块文件;预绑定出现点不再被重解析覆盖;project source/includes 规范化为无 .. 的路径,避免索引路径失配后按需反复解析导致崩溃。
This commit is contained in:
TermiNexus committed 2026-10-27 12:55:43 +08:00
1 parent c6b0b03bdc
commit b38d103c17
8 files changed
+182 -10

No files matched your search

+24 -4
View File
@@ -686,7 +686,9 @@ def ResolveInFile(pool: memhub.MemManager*, table_pool: memhub.MemManager*,
s: LspSymbols.LspSymbol* = FindSymbol(path, container, name)
if s is not None:
return s
if _bulk_resolve != 0 or _resolve_only != 0:
if _bulk_resolve != 0:
return None
if _resolve_only != 0 and FileEntry(path) is not None:
return None
text: str = LspFiles.ReadSource(pool, path)
if text is None:
@@ -872,6 +874,14 @@ def _ChainFromModule(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
hops: int) -> LspTarget*:
segs: str = rest
while segs is not None:
if cur_is_file != 0:
base: str = LspFiles.BaseName(pool, cur)
if base is not None and string.strcmp(base, "__init__.vp") == 0:
# A package's __init__.vp is not a symbol table for its submodules; descend from
# the package directory for the remaining dotted segments.
cur = LspFiles.DirName(pool, cur)
cur_is_file = 0
continue
h: str = None
r: str = None
LspSymbols._SplitHead(pool, segs, &h, &r)
@@ -885,7 +895,7 @@ def _ChainFromModule(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
if member.import_kind != LspSymbols.LSP_IMPORT_NONE:
if member.import_kind == LspSymbols.LSP_IMPORT_FROM:
return _ResolveFromTarget(ws, pool, table_pool, member.file, member, hops + 1)
mod_file: str = _ModulePath(ws, pool, member.import_mod)
mod_file: str = _ModuleFile(ws, pool, member.file, member)
if mod_file is not None:
return _FileTarget(pool, mod_file, member.import_mod)
return None
@@ -950,13 +960,23 @@ def ResolveSymbol(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
if s.import_kind == LspSymbols.LSP_IMPORT_FROM:
return _ResolveFromTarget(ws, pool, table_pool, from_file, s, hops)
if s.import_kind == LspSymbols.LSP_IMPORT_MODULE:
mod_file: str = _ModulePath(ws, pool, s.import_mod)
mod_file: str = _ModuleFile(ws, pool, from_file, s)
if mod_file is None:
return None
return _FileTarget(pool, mod_file, s.import_mod)
return _SymbolTarget(pool, s)
def _ModuleFile(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
from_file: str, s: LspSymbols.LspSymbol*) -> str:
"""Resolve a module import binding, honoring relative import levels."""
if s is None:
return None
if s.import_level > 0:
return _ResolveFromFile(ws, pool, from_file, s)
return _ModulePath(ws, pool, s.import_mod)
def ResolveWildcard(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
table_pool: memhub.MemManager*, from_file: str,
wildcard: LspSymbols.LspSymbol*,
@@ -1043,7 +1063,7 @@ def ResolveChain(ws: LspWorkspace.LspWorkspace*, pool: memhub.MemManager*,
segs = r
return None
if head.import_kind == LspSymbols.LSP_IMPORT_MODULE:
mod_file: str = _ModulePath(ws, pool, head.import_mod)
mod_file: str = _ModuleFile(ws, pool, from_file, head)
if mod_file is None:
return None
if rest is None:
+88
View File
@@ -251,6 +251,18 @@ def _CopyStr(pool: memhub.MemManager*, s: str, cap: int) -> str:
return str(buf)
def _SliceStr(pool: memhub.MemManager*, s: str, start: int, end: int) -> str:
if s is None or end <= start:
return None
n: sizet = sizet(end - start)
buf: bytes = pool.alloc(n + 1)
if buf is None:
return None
string.memcpy(buf, s + start, n)
buf[n] = 0
return str(buf)
def _NameSpan(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
lineno: int, col: int, name: str) -> LspSource.LspRange*:
if lineno <= 0 or name is None:
@@ -764,6 +776,9 @@ def _ResolveOccurrences(a: LspAnalysis*):
i += 1
if o is None or o.path is None:
continue
# Import declarations and dotted module-path segments are pre-bound by the walker.
if o.sym is not None or o.head is not None:
continue
h: str = None
rest: str = None
_SplitHead(a.pool, o.path, &h, &rest)
@@ -894,6 +909,61 @@ def _DeclImportOcc(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
o.sym = sym
def _NewHiddenModule(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
module: str, level: int) -> LspSymbol*:
"""Module head used only for dotted-import navigation (not bound in any scope)."""
if a is None or module is None:
return None
s: LspSymbol* = _NewSymbol(pool, module, LSP_KIND_MODULE, None, scope)
if s is None:
return None
s.file = a.file
s.import_kind = LSP_IMPORT_MODULE
s.import_mod = module
s.import_level = level
return s
def _AddModulePathOccs(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
lineno: int, col: int, path: str, level: int,
skip_first: int, head: LspSymbol*) -> LspSymbol*:
"""Add a namespace occurrence for every segment of a dotted module path.
The first segment resolves through `head` (or a hidden module symbol), later segments are
attributes chained from it, so definition lands on the corresponding module file/directory.
"""
if a is None or scope is None or path is None or path[0] == 0 or lineno <= 0:
return head
plen: int = int(string.strlen(path))
first_end: int = 0
while first_end < plen and path[first_end] != '.':
first_end += 1
if head is None:
first: str = _SliceStr(pool, path, 0, first_end)
head = _NewHiddenModule(pool, a, scope, first, level)
if head is None:
return None
start: int = 0
while start < plen:
end: int = start
while end < plen and path[end] != '.':
end += 1
if not (start == 0 and skip_first != 0):
name: str = _SliceStr(pool, path, start, end)
if name is not None:
o: LspOccurrence* = _AddOcc(pool, a, scope, name, lineno, col + start,
int(string.strlen(name)), ast.ASTCtx.Load, 0)
if o is not None:
if start == 0:
o.sym = head
o.head = head
else:
o.head = head
o.rest = _SliceStr(pool, path, first_end + 1, end)
start = end + 1
return head
def _JoinImportDetail(pool: memhub.MemManager*, a: str, b: str,
c: str) -> str:
if a is None or b is None:
@@ -1191,10 +1261,28 @@ def _WalkStmt(pool: memhub.MemManager*, a: LspAnalysis*, scope: LspScope*,
sym.import_kind = LSP_IMPORT_MODULE
sym.import_mod = bound_mod
_DeclImportOcc(pool, a, scope, al, bound, sym)
if al.asname is not None:
_AddModulePathOccs(pool, a, scope, al.lineno, al.col_offset,
al.name, 0, 0, None)
else:
_AddModulePathOccs(pool, a, scope, al.lineno, al.col_offset,
al.name, 0, 1, sym)
i += 1
return
if k == ast.ASTKind.ImportFrom:
ifr: ast.ImportFrom* = (ast.ImportFrom*)(node)
if ifr.module is not None and ifr.lineno > 0:
mlen: int = int(string.strlen(ifr.module))
first_end: int = 0
while first_end < mlen and ifr.module[first_end] != '.':
first_end += 1
first_seg: str = _SliceStr(pool, ifr.module, 0, first_end)
if first_seg is not None:
mcol: int = _FindWordInLine(a.doc, ifr.lineno - 1,
ifr.col_offset + 4, first_seg)
if mcol >= 0:
_AddModulePathOccs(pool, a, scope, ifr.lineno, mcol,
ifr.module, ifr.level, 0, None)
if ifr.names is not None:
i: int = 0
while i < int(len(ifr.names)):
+7 -6
View File
@@ -15,10 +15,11 @@ def _AbsJoin(pool: memhub.MemManager*, base: str, path: str) -> str:
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 path[0] == '/' or path[0] == '\\' or (plen >= 2 and path[1] == ':'):
norm_abs: str = os.path.normpath(path)
if norm_abs is None:
norm_abs = path
return LspPools.DupStr(pool, norm_abs)
if base is None or base[0] == 0:
return LspPools.DupStr(pool, path)
joined: str = os.path.join(base, path)
@@ -219,11 +220,11 @@ class LspWorkspace:
if self.includes is None or self.includes[0] == 0:
sibling: str = os.path.join(base, "includes")
if os.isdir(sibling):
self.includes = sibling
self.includes = os.path.normpath(sibling)
else:
parent_sibling: str = os.path.join(base, "../includes")
if os.isdir(parent_sibling):
self.includes = parent_sibling
self.includes = os.path.normpath(parent_sibling)
else:
self.includes = ""
@@ -0,0 +1 @@
"""Gate 10 dotted-import package."""
@@ -0,0 +1 @@
"""Gate 10 dotted-import subpackage."""
+2
View File
@@ -0,0 +1,2 @@
def mod_fn(x: int) -> int:
return x
+1
View File
@@ -4,6 +4,7 @@ from .gate10_types import Widget, DOUBLE
from .gate10_stub import StubThing
from .gate10_extra import *
import gate10_types
import gate10_pkg.sub.mod
def star_use(e: Extra) -> int:
+58
View File
@@ -1080,6 +1080,41 @@ def gate7(exe: str) -> int:
if not _find_token(sem_tokens, pool_pos["line"], pool_pos["character"],
sem_types.index("class")):
raise AssertionError(f"imported class was not highlighted: {pool_pos}")
mem_pos = _position(app_text, "memhub.MemManager")
mem_pos["character"] += len("memhub.")
mem_def = client.request(
"textDocument/definition", {"textDocument": {"uri": app_uri}, "position": mem_pos},
timeout=60.0,
).get("result")
if not isinstance(mem_def, list) or not mem_def:
raise AssertionError(f"includes definition failed after adoption: {mem_def}")
if not mem_def[0]["uri"].endswith("memhub.vp"):
raise AssertionError(f"includes definition target is wrong: {mem_def}")
if not _find_token(sem_tokens, mem_pos["line"], mem_pos["character"],
sem_types.index("class")):
raise AssertionError(f"includes class was not highlighted: {mem_pos}")
# A large include-root document must tokenize through the same project roots without
# exhausting the request pool via on-demand re-parsing.
parser_doc = root.parent / "includes" / "ast" / "parser.vp"
parser_text = parser_doc.read_text(encoding="utf-8")
parser_uri = parser_doc.as_uri()
client.notify(
"textDocument/didOpen",
{"textDocument": {"uri": parser_uri, "languageId": "viper", "version": 1,
"text": parser_text}},
)
client.wait_notification("textDocument/publishDiagnostics")
parser_semantic = client.request(
"textDocument/semanticTokens/full", {"textDocument": {"uri": parser_uri}},
timeout=120.0,
).get("result")
if not isinstance(parser_semantic, dict) or not parser_semantic.get("data"):
raise AssertionError(f"include-root semantic tokens failed: {parser_semantic}")
client.notify("textDocument/didClose", {"textDocument": {"uri": parser_uri}})
client.wait_notification("textDocument/publishDiagnostics")
client.notify("textDocument/didClose", {"textDocument": {"uri": app_uri}})
client.wait_notification("textDocument/publishDiagnostics")
@@ -1423,6 +1458,7 @@ def gate10(exe: str) -> int:
names = ("gate10_types.vp", "gate10_stub.vpi", "gate10_extra.vp", "gate10_use.vp")
for name in names:
shutil.copy(fixtures / name, workspace / name)
shutil.copytree(fixtures / "gate10_pkg", workspace / "gate10_pkg")
type_path = workspace / "gate10_types.vp"
stub_path = workspace / "gate10_stub.vpi"
use_path = workspace / "gate10_use.vp"
@@ -1487,6 +1523,28 @@ def gate10(exe: str) -> int:
if not star_def[0]["uri"].endswith("gate10_extra.vp"):
raise AssertionError(f"wildcard definition target is wrong: {star_def}")
# Dotted module paths: every segment is a namespace and ctrl-click opens that module file.
dotted = _position(text, "gate10_pkg.sub.mod")
for delta, segment in ((0, "pkg"), (len("gate10_pkg."), "sub"),
(len("gate10_pkg.sub."), "mod")):
pos = {"line": dotted["line"], "character": dotted["character"] + delta}
if not _find_token(sem_tokens, pos["line"], pos["character"],
sem_types.index("namespace")):
raise AssertionError(f"dotted import segment is not highlighted: {segment} {pos}")
for delta, suffix in ((len("gate10_pkg."), "gate10_pkg/sub/__init__.vp"),
(len("gate10_pkg.sub."), "gate10_pkg/sub/mod.vp")):
pos = {"line": dotted["line"], "character": dotted["character"] + delta}
target = client.request(
"textDocument/definition",
{"textDocument": {"uri": uri}, "position": pos},
timeout=120.0,
).get("result")
if not isinstance(target, list) or not target:
raise AssertionError(f"dotted import definition failed: {pos} -> {target}")
got = target[0]["uri"].replace("\\", "/")
if not got.endswith(suffix):
raise AssertionError(f"dotted import definition target is wrong: {pos} -> {got}")
def at(needle: str, delta: int = 0, occurrence: int = 0) -> dict[str, int]:
pos = _position(text, needle, occurrence)
pos["character"] += delta