diff --git a/App/lib/lsp/LspWorkspace.vp b/App/lib/lsp/LspWorkspace.vp index 0ece618..6902ebf 100644 --- a/App/lib/lsp/LspWorkspace.vp +++ b/App/lib/lsp/LspWorkspace.vp @@ -136,8 +136,6 @@ def DecodeUri(pool: memhub.MemManager*, uri: str) -> str: p: str = uri if string.strncmp(uri, "file://", 7) == 0: p = uri + 7 - if p[0] == '/' and len(p) >= 3 and p[2] == ':': - p = p + 1 n: sizet = string.strlen(p) out: bytes = pool.alloc(n + 1) if out is None: @@ -158,6 +156,10 @@ def DecodeUri(pool: memhub.MemManager*, uri: str) -> str: 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) diff --git a/Tools/lsp/probe.py b/Tools/lsp/probe.py index 86ec566..5b60f70 100644 --- a/Tools/lsp/probe.py +++ b/Tools/lsp/probe.py @@ -1765,6 +1765,50 @@ def gate10(exe: str) -> int: code = client.close() if code != 0: raise AssertionError(f"exit code is {code}, expected 0") + + # VS Code's URI serializer percent-encodes the drive colon (file:///d%3A/...). A decoded + # path starting with "/" prevents project adoption and leaves the index roots empty. + def vscode_uri(path: Path) -> str: + p = path.resolve().as_posix() + if len(p) >= 2 and p[1] == ":": + p = p[0].lower() + "%3A" + p[2:] + return "file:///" + p + + enc_client = Client(exe) + enc_init = enc_client.request( + "initialize", + {"processId": os.getpid(), "rootUri": vscode_uri(workspace), "capabilities": {}}, + ) + if not isinstance(enc_init.get("result"), dict): + raise AssertionError(f"encoded-URI initialize failed: {enc_init}") + enc_client.notify("initialized", {}) + enc_client.notify( + "textDocument/didOpen", + {"textDocument": {"uri": vscode_uri(use_path), "languageId": "viper", + "version": 1, "text": text}}, + ) + enc_client.wait_notification("textDocument/publishDiagnostics") + enc_stats = enc_client.request("viper/lspStats").get("result") or {} + enc_source = str(enc_stats.get("sourceDir") or "") + if enc_source.startswith("/"): + raise AssertionError(f"encoded URI root was not decoded: {enc_stats}") + if Path(enc_source).resolve() != workspace.resolve(): + raise AssertionError(f"encoded URI sourceDir mismatch: {enc_stats}") + enc_sem = enc_client.request( + "textDocument/semanticTokens/full", {"textDocument": {"uri": vscode_uri(use_path)}}, + timeout=120.0, + ).get("result") + if not isinstance(enc_sem, dict): + raise AssertionError(f"encoded URI semanticTokens failed: {enc_sem}") + enc_tokens = _decode_tokens(enc_sem.get("data", [])) + enc_pos = _position(text, "s.value") + enc_pos["character"] += 2 + if not _find_token(enc_tokens, enc_pos["line"], enc_pos["character"], sem_types.index("property")): + raise AssertionError(f"encoded URI lost cross-file member token: {enc_pos}") + enc_client.request("shutdown") + if enc_client.close() != 0: + raise AssertionError("encoded URI client did not exit cleanly") + shutil.rmtree(workspace, ignore_errors=True) print("member-annotations: OK") print("member-module-chain: OK")