fix(lsp): 点号导入逐段高亮与 project includes 路径修复
import a.b.c / from a.b import X 的每个模块段都有 namespace token 并可跳转到对应模块文件;预绑定出现点不再被重解析覆盖;project source/includes 规范化为无 .. 的路径,避免索引路径失配后按需反复解析导致崩溃。
This commit is contained in:
1 parent
c6b0b03bdc
commit
b38d103c17
8 files changed
+182
-10
No files matched your search
+24
-4
@@ -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:
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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."""
|
||||
@@ -0,0 +1,2 @@
|
||||
def mod_fn(x: int) -> int:
|
||||
return x
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user