Files
ViperC/App/lib/lsp/LspWorkspace.vp
T
TermiNexus 4770f57567 fix(lsp): 解码百分号编码盘符后去掉前导分隔符
VS Code 发送 file:///d%3A/...,DecodeUri 只在字面冒号时去前导 /,解出 /d:/... 后 FindProject 失败,项目未采纳、索引 roots 落到工作区根,跨模块成员全部失去高亮与跳转。改为解码完成后判断盘符;gate10 增加百分号编码 rootUri 的回归断言。
2026-10-27 17:19:00 +08:00

285 lines
8.6 KiB
Plaintext

from stdint import *
import string
import os
import os._win32
import os._posix
import os.path
import memhub
import lib.lsp.LspPools as LspPools
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] == '\\' 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)
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')
if ch >= 'a' and ch <= 'f':
return int(ch) - int('a') + 10
if ch >= 'A' and ch <= 'F':
return int(ch) - int('A') + 10
return -1
def PathToUri(pool: memhub.MemManager*, path: str) -> str:
"""Convert a local path to a file URI, percent-encoding characters that would break the URI."""
if path is None or path[0] == 0:
return None
n: sizet = string.strlen(path)
# Worst case every byte becomes %XX; plus "file:///" and NUL.
cap: sizet = n * 3 + 16
buf: bytes = pool.alloc(cap)
if buf is None:
return None
o: sizet = 0
string.memcpy(buf, "file:///", 8)
o = 8
i: sizet = 0
if path[0] == '/':
o = 7
while i < n:
ch: char = path[i]
i += 1
if ch == '\\':
ch = '/'
if ch == ' ':
buf[o] = '%'
buf[o + 1] = '2'
buf[o + 2] = '0'
o += 3
continue
if ch == '%' or ch == '#' or ch == '?':
buf[o] = '%'
hi: int = int(ch) >> 4
lo: int = int(ch) & 15
buf[o + 1] = _HexChar(hi)
buf[o + 2] = _HexChar(lo)
o += 3
continue
buf[o] = ch
o += 1
buf[o] = 0
return str(buf)
def _HexChar(v: int) -> char:
if v < 10:
return char(int('0') + v)
return char(int('A') + v - 10)
def DecodeUri(pool: memhub.MemManager*, uri: str) -> str:
if uri is None or uri[0] == 0:
return None
p: str = uri
if string.strncmp(uri, "file://", 7) == 0:
p = uri + 7
n: sizet = string.strlen(p)
out: bytes = pool.alloc(n + 1)
if out is None:
return None
i: sizet = 0
o: sizet = 0
while i < n:
ch: char = p[i]
if ch == '%' and i + 2 < n:
hi: int = _HexVal(p[i + 1])
lo: int = _HexVal(p[i + 2])
if hi >= 0 and lo >= 0:
out[o] = char(hi * 16 + lo)
o += 1
i += 3
continue
out[o] = ch
o += 1
i += 1
out[o] = 0
# VS Code sends the drive colon percent-encoded (file:///d%3A/...), so the leading
# separator must be dropped after decoding, not before.
if string.strncmp(uri, "file://", 7) == 0 and o >= 3 and out[0] == '/' and out[2] == ':':
return str(out + 1)
return str(out)
class LspWorkspace:
pool: memhub.MemBuddy*
root: str
project: str
source_dir: str
includes: str
ready: int
def __new__(self, pool: memhub.MemBuddy*) -> rawptr:
p: LspWorkspace* = (LspWorkspace*)(pool.alloc(LspWorkspace.__sizeof__()))
if p:
string.memset(p, 0, LspWorkspace.__sizeof__())
return rawptr(p)
def __init__(self, pool: memhub.MemBuddy*):
self.pool = pool
self.root = None
self.project = None
self.source_dir = None
self.includes = None
self.ready = 0
def FindProject(self, start: str) -> str:
if start is None or start[0] == 0:
return None
current: str = start
i: int = 0
while i < 32:
cand: str = os.path.join(current, "project.vpj")
if cand is not None and os.isfile(cand):
return cand
parent: str = os.path.dirname(current)
if parent is None or parent[0] == 0:
break
if string.strcmp(parent, current) == 0:
break
current = parent
i += 1
return None
def _ApplyProject(self, project: str) -> int:
self.project = project
old_pool: memhub.MemBuddy* = Config._mbuddy
Config._mbuddy = self.pool
rc: int = Config.Load_project_config(project)
if rc == 0:
Config.resolve_paths(os.path.dirname(project))
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
def _FillFallbacks(self, base: str):
if self.source_dir is None or self.source_dir[0] == 0:
self.source_dir = base
if self.includes is None or self.includes[0] == 0:
sibling: str = os.path.join(base, "includes")
if os.isdir(sibling):
self.includes = os.path.normpath(sibling)
else:
parent_sibling: str = os.path.join(base, "../includes")
if os.isdir(parent_sibling):
self.includes = os.path.normpath(parent_sibling)
else:
self.includes = ""
def Init(self, root_uri: str) -> int:
path: str = DecodeUri(self.pool, root_uri)
if path is None or path[0] == 0:
cwd: str = os.getcwd()
if cwd is None:
cwd = "."
path = LspPools.DupStr(self.pool, cwd)
if path is None:
return -1
self.root = path
project: str = self.FindProject(path)
if project is not None:
self._ApplyProject(project)
self._FillFallbacks(self.root)
self.ready = 1
return 0
def AdoptFromFile(self, file_path: str) -> int:
"""Adopt the nearest project.vpj above an opened file.
A client may open the repository root as the workspace while the project file lives in a
subdirectory (e.g. <repo>/ViperC/project.vpj). The first opened source file then provides
an unambiguous root; returns 1 when a project was adopted, 0 otherwise.
"""
if file_path is None or file_path[0] == 0:
return 0
if self.project is not None:
return 0
directory: str = os.path.dirname(file_path)
project: str = self.FindProject(directory)
if project is None:
return 0
if self._ApplyProject(project) != 0:
self.project = None
return 0
self._FillFallbacks(os.path.dirname(project))
LspTransport.StderrWrite("viperc-lsp: adopted project ")
LspTransport.StderrWrite(project)
LspTransport.StderrWrite("\n")
return 1
def Root(self) -> str:
return self.root
def ProjectPath(self) -> str:
return self.project
def SourceDir(self) -> str:
return self.source_dir
def Includes(self) -> str:
return self.includes