VS Code 发送 file:///d%3A/...,DecodeUri 只在字面冒号时去前导 /,解出 /d:/... 后 FindProject 失败,项目未采纳、索引 roots 落到工作区根,跨模块成员全部失去高亮与跳转。改为解码完成后判断盘符;gate10 增加百分号编码 rootUri 的回归断言。
285 lines
8.6 KiB
Plaintext
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
|