Files
ViperC/App/lib/lsp/LspFiles.vp
T
TermiNexus 8dbf2415d4 feat(lsp): 支持跨文件引用与重命名
索引记录已解析出现点;references/rename 按文件分组,支持 from-import 与 module.Member;路径比较归一化;多开文档时跨文件重命名返回 null。
2026-10-26 20:13:23 +08:00

219 lines
5.9 KiB
Plaintext

from stdint import *
import string
import os
import os.path
import io
import memhub
import lib.lsp.LspPools as LspPools
define LSP_SOURCE_CAP = 1048576
define LSP_LIST_CAP = 512
define LSP_WALK_DEPTH = 24
def IsSourceFile(name: str) -> int:
if name is None:
return 0
n: sizet = string.strlen(name)
if n > 3 and string.strncmp(name + (n - 3), ".vp", 3) == 0:
return 1
if n > 4 and string.strncmp(name + (n - 4), ".vpi", 4) == 0:
return 1
return 0
def SkipDir(name: str) -> int:
"""Directories that never contain workspace sources (build output and VCS data)."""
if name is None or name[0] == '.':
return 1
if string.strcmp(name, "__vpcbuild__") == 0:
return 1
if string.strcmp(name, "__vpcbuild_2__") == 0:
return 1
if string.strcmp(name, "temp") == 0:
return 1
if string.strcmp(name, "output") == 0:
return 1
if string.strcmp(name, "node_modules") == 0:
return 1
return 0
def Join(pool: memhub.MemManager*, a: str, b: str) -> str:
if a is None or b is None:
return None
p: str = os.path.join(a, b)
if p is None:
return None
return LspPools.DupStr(pool, p)
def Normalize(pool: memhub.MemManager*, path: str) -> str:
"""Copy path with '/' separators so identities compare consistently on Windows."""
if pool is None or path is None:
return None
n: sizet = string.strlen(path)
buf: bytes = pool.alloc(n + 1)
if buf is None:
return None
i: sizet = 0
while i < n:
ch: char = path[i]
buf[i] = '/' if ch == '\\' else ch
i += 1
buf[n] = 0
return str(buf)
def PathEqual(a: str, b: str) -> int:
"""Compare paths ignoring separator kind and ASCII case (Windows file paths)."""
if a is None or b is None:
return 0
i: sizet = 0
while True:
ca: char = a[i]
cb: char = b[i]
if ca == 0 and cb == 0:
return 1
if ca == 0 or cb == 0:
return 0
if ca == '\\':
ca = '/'
if cb == '\\':
cb = '/'
if ca >= 'A' and ca <= 'Z':
ca = char(int(ca) + 32)
if cb >= 'A' and cb <= 'Z':
cb = char(int(cb) + 32)
if ca != cb:
return 0
i += 1
def DirName(pool: memhub.MemManager*, path: str) -> str:
if path is None:
return None
p: str = os.path.dirname(path)
if p is None:
return None
return LspPools.DupStr(pool, p)
def BaseName(pool: memhub.MemManager*, path: str) -> str:
if path is None:
return None
p: str = os.path.basename(path)
if p is None:
return None
return LspPools.DupStr(pool, p)
def ReadSource(pool: memhub.MemManager*, path: str) -> str:
"""Read a source file into pool (cap 1 MiB); None when unreadable or too large."""
if pool is None or path is None:
return None
f: io.File* = io.File(path, io.MODE.R)
if f is None or f.closed:
if f is not None:
f.close()
return None
size: int64 = f.size()
if size < 0 or size >= int64(LSP_SOURCE_CAP):
f.close()
return None
cap: sizet = sizet(int(size)) + 1
buf: bytes = pool.alloc(cap)
if buf is None:
f.close()
return None
got: int = int(f.read_all(buf, cap))
f.close()
if got < 0:
return None
buf[sizet(int(size))] = 0
return str(buf)
def ModuleName(pool: memhub.MemManager*, root: str, path: str) -> str:
"""Dotted module name of path relative to root; None when path is outside root."""
if pool is None or root is None or path is None:
return None
rlen: sizet = string.strlen(root)
plen: sizet = string.strlen(path)
if plen <= rlen:
return None
rel: str = path + rlen
if rel[0] == '/' or rel[0] == '\\':
rel = rel + 1
n: sizet = string.strlen(rel)
if n <= 3:
return None
body: sizet = n
if IsSourceFile(rel) != 0:
# Strip the extension.
i: sizet = n
while i > 0 and rel[i - 1] != '.':
i -= 1
if i > 0:
body = i - 1
buf: bytes = pool.alloc(body + 1)
if buf is None:
return None
i2: sizet = 0
while i2 < body:
ch: char = rel[i2]
if ch == '/' or ch == '\\':
buf[i2] = '.'
else:
buf[i2] = ch
i2 += 1
buf[body] = 0
out: str = str(buf)
# `<pkg>.__init__` names the package itself.
olen: sizet = string.strlen(out)
if olen > 9 and string.strncmp(out + (olen - 9), ".__init__", 9) == 0:
out[olen - 9] = 0
return out
def CollectSources(pool: memhub.MemManager*, root: str,
out_paths: list[str]*, cap: int, depth: int) -> int:
"""Append absolute .vp/.vpi paths under root to out_paths (bounded)."""
if pool is None or root is None or out_paths is None:
return 0
if depth > LSP_WALK_DEPTH or int(len(out_paths)) >= cap:
return 0
if not os.isdir(root):
return 0
names: str* = (str*)(pool.alloc(sizet(LSP_LIST_CAP) * 8))
if names is None:
return 0
i: int = 0
while i < LSP_LIST_CAP:
names[i] = None
i += 1
count: int = int(os.listdir(root, names, ULONG(LSP_LIST_CAP)))
if count <= 0:
return 0
i = 0
while i < count:
name: str = names[i]
i += 1
if name is None or name[0] == 0:
continue
full: str = Join(pool, root, name)
if full is None:
continue
if os.isdir(full):
if SkipDir(name) == 0:
CollectSources(pool, full, out_paths, cap, depth + 1)
if int(len(out_paths)) >= cap:
return 0
continue
if IsSourceFile(name) != 0:
out_paths.append(full)
if int(len(out_paths)) >= cap:
return 0
return 0