From a296f1695def0a70ca4cca0e27ad007af5762839 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sun, 19 Jul 2026 23:46:17 +0300 Subject: [PATCH] feat(lsp): textDocument/implementation for interfaces (0.13) Go-to-implementation resolves interface types to implementing types and interface methods to extend Type for Iface method decls (Circle+Square). --- Makefile | 3 + docs/QUALITY_PLAN.md | 25 ++++- tools/lsp_server.nim | 150 +++++++++++++++++++++++++++++- tools/smoke_lsp_implementation.sh | 112 ++++++++++++++++++++++ 4 files changed, 283 insertions(+), 7 deletions(-) create mode 100755 tools/smoke_lsp_implementation.sh diff --git a/Makefile b/Makefile index 75f4679..cf14208 100644 --- a/Makefile +++ b/Makefile @@ -185,6 +185,9 @@ test-lsp: lsp @echo "==> LSP path rename smoke" @chmod +x tools/smoke_lsp_rename_path.sh @tools/smoke_lsp_rename_path.sh + @echo "==> LSP implementation smoke" + @chmod +x tools/smoke_lsp_implementation.sh + @tools/smoke_lsp_implementation.sh .PHONY: test-registry test-registry: build diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index f3d2af9..510c3f6 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) > **Дата:** 2026-07-19 -> **Текущо:** v0.5.x — HirNode sourceFile, **optional selfhost-loop CI**, LSP 0.12, Nexus KA +> **Текущо:** v0.5.x — **LSP 0.13 implementation**, HirNode sourceFile, optional selfhost-loop CI > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -771,9 +771,24 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 49 (LSP 0.13 textDocument/implementation) + +1. **`implementationProvider`** + `textDocument/implementation` +2. **Interface type** under cursor → locations of implementing types + (`extend Type for Iface` / type decl) +3. **Interface method** under cursor → implementor method decls + (reuses `collectImplementorFuncs` + `workspaceImpls`) +4. Also: call-site / shared method name matching known iface methods; + implementor method → sibling implementors of same iface method +5. Smoke: `tools/smoke_lsp_implementation.sh` + - Drawable → ≥2 types; Draw → ≥2 methods (Circle + Square) +6. Version **bux-lsp 0.13.0**; `make test-lsp` + +--- + ## Следващи стъпки -1. LSP find-implementations request (dedicated, beyond call hierarchy) -2. Workspace-wide import path index without open documents (optional polish) -3. Expr/Stmt-level sourceFile if macros / cross-file inlining land -4. Fix selfhost C backend so buxc2→buxc3 fixed-point is green +1. Workspace-wide import path index without open documents (optional polish) +2. Expr/Stmt-level sourceFile if macros / cross-file inlining land +3. Fix selfhost C backend so buxc2→buxc3 fixed-point is green +4. Main PR CI workflow (`make test`) beyond optional selfhost-loop diff --git a/tools/lsp_server.nim b/tools/lsp_server.nim index 73bb73e..4307bd5 100644 --- a/tools/lsp_server.nim +++ b/tools/lsp_server.nim @@ -14,6 +14,7 @@ # v0.10.0: method rename + qualified path / extend Type rename edges. # v0.11.0: interface dispatch in call hierarchy (extend Type for Trait). # v0.12.0: module-path segment rename (import Std::Io / Std::Io::{…}). +# v0.13.0: textDocument/implementation (interface → types / methods). import std/[json, os, strutils, streams, tables, osproc, sequtils, sets] import lexer, parser, ast, sema, types, scope, source_location @@ -2579,6 +2580,147 @@ proc collectImplementorFuncs(iface, meth: string): seq[FuncSym] = if seen.contains(key): continue seen.incl(key) result.add(FuncSym(name: meth, uri: uri, info: info)) + # Closed-file index from workspace scan + let wkey = iface & "." & meth + if workspaceImpls.hasKey(wkey): + for impl in workspaceImpls[wkey]: + let key = impl.uri & "#" & impl.typeName & "." & meth + if seen.contains(key): continue + seen.incl(key) + if documents.hasKey(impl.uri): + let doc = documents[impl.uri] + ensureAnalyzed(doc) + if doc.symbols.hasKey(meth) and doc.symbols[meth].kind == "method" and + doc.symbols[meth].container == impl.typeName: + result.add(FuncSym(name: meth, uri: impl.uri, info: doc.symbols[meth])) + continue + if workspaceSymbols.hasKey(meth) and workspaceSymbols[meth].uri == impl.uri: + var info = workspaceSymbols[meth].info + info.container = impl.typeName + result.add(FuncSym(name: meth, uri: impl.uri, info: info)) + else: + result.add(FuncSym( + name: meth, uri: impl.uri, + info: SymbolInfo(line: 0, col: 0, kind: "method", + detail: impl.typeName & "." & meth, container: impl.typeName))) + +proc collectTypeImplementorLocs(iface: string): seq[JsonNode] = + ## Locations of types that `extend Type for iface`. + result = @[] + var seen = initHashSet[string]() + for uri, doc in documents.pairs: + ensureAnalyzed(doc) + for impl in doc.impls: + if impl.iface != iface: continue + let key = uri & "#" & impl.typeName + if seen.contains(key): continue + seen.incl(key) + if doc.symbols.hasKey(impl.typeName): + let info = doc.symbols[impl.typeName] + result.add(locationJson(uri, info.line, info.col, impl.typeName.len)) + else: + # Fall back to the `extend` line + result.add(locationJson(uri, impl.line, 0, max(1, impl.typeName.len))) + # Derive types from workspaceImpls (Iface.Method → typeName) + for wkey, impls in workspaceImpls.pairs: + if not wkey.startsWith(iface & "."): continue + for impl in impls: + let key = impl.uri & "#" & impl.typeName + if seen.contains(key): continue + seen.incl(key) + if documents.hasKey(impl.uri): + let doc = documents[impl.uri] + ensureAnalyzed(doc) + if doc.symbols.hasKey(impl.typeName): + let info = doc.symbols[impl.typeName] + result.add(locationJson(impl.uri, info.line, info.col, impl.typeName.len)) + continue + if workspaceSymbols.hasKey(impl.typeName): + let ws = workspaceSymbols[impl.typeName] + result.add(locationJson(ws.uri, ws.info.line, ws.info.col, impl.typeName.len)) + else: + result.add(locationJson(impl.uri, 0, 0, max(1, impl.typeName.len))) + +proc handleImplementation(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = + ## textDocument/implementation — go to implementors of interface / iface method. + let uri = paramsNode["textDocument"]["uri"].getStr() + let position = paramsNode["position"] + let lineNum = position["line"].getInt() + let col = position["character"].getInt() + let doc = getDoc(uri) + if doc.content == "": + sendResponse(stream, id, %*[]) + return + ensureAnalyzed(doc) + let word = findWordAt(doc.content, lineNum, col) + if word.len == 0: + sendResponse(stream, id, %*[]) + return + + var arr = newJArray() + + # 1) Interface method under cursor → implementor methods + let (iok, im) = findIfaceMethodAt(doc, word, lineNum, col) + if iok: + for fs in collectImplementorFuncs(im.parent, im.name): + arr.add(locationJson(fs.uri, fs.info.line, fs.info.col, fs.name.len)) + sendResponse(stream, id, arr) + return + + # 2) Interface type name → implementing types (extend Type for Iface) + var isIface = false + if doc.symbols.hasKey(word) and doc.symbols[word].kind == "interface": + isIface = true + elif workspaceSymbols.hasKey(word) and workspaceSymbols[word].info.kind == "interface": + isIface = true + if isIface: + for loc in collectTypeImplementorLocs(word): + arr.add(loc) + sendResponse(stream, id, arr) + return + + # 3) Call site / method name that matches a known iface method + # (e.g. cursor on Draw in c.Draw() or on implementor name shared with iface) + for m in doc.ifaceMethods: + if m.name != word: continue + for fs in collectImplementorFuncs(m.parent, m.name): + arr.add(locationJson(fs.uri, fs.info.line, fs.info.col, fs.name.len)) + if arr.len > 0: + sendResponse(stream, id, arr) + return + + # 4) Method registered as implementor of some interface — still list siblings? + # Prefer: if word is method on a type that implements I, and I has that method, + # return all implementors of I.Method (including self). + if doc.symbols.hasKey(word) and doc.symbols[word].kind == "method": + let container = doc.symbols[word].container + for impl in doc.impls: + if impl.typeName != container: continue + # This type implements impl.iface; if method is an iface method, list all + for m in doc.ifaceMethods: + if m.parent == impl.iface and m.name == word: + for fs in collectImplementorFuncs(impl.iface, word): + arr.add(locationJson(fs.uri, fs.info.line, fs.info.col, fs.name.len)) + if arr.len > 0: + sendResponse(stream, id, arr) + return + # workspaceImpls reverse lookup + for wkey, impls in workspaceImpls.pairs: + if not wkey.endsWith("." & word): continue + let iface = wkey[0 ..< wkey.len - word.len - 1] + var onType = false + for impl in impls: + if impl.typeName == container or impl.uri == uri: + onType = true + break + if onType: + for fs in collectImplementorFuncs(iface, word): + arr.add(locationJson(fs.uri, fs.info.line, fs.info.col, fs.name.len)) + if arr.len > 0: + sendResponse(stream, id, arr) + return + + sendResponse(stream, id, arr) proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = let uri = paramsNode["textDocument"]["uri"].getStr() @@ -2769,9 +2911,10 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = "referencesProvider": true, "renameProvider": {"prepareProvider": true}, "workspaceSymbolProvider": true, - "callHierarchyProvider": true + "callHierarchyProvider": true, + "implementationProvider": true }, - "serverInfo": {"name": "bux-lsp", "version": "0.12.0"} + "serverInfo": {"name": "bux-lsp", "version": "0.13.0"} }) if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull: rootPath = paramsNode["rootPath"].getStr() @@ -2871,6 +3014,9 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = of "callHierarchy/outgoingCalls": handleOutgoingCalls(stream, id, paramsNode) + of "textDocument/implementation": + handleImplementation(stream, id, paramsNode) + else: if id != nil: sendError(stream, id, -32601, "method not found: " & methodName) diff --git a/tools/smoke_lsp_implementation.sh b/tools/smoke_lsp_implementation.sh new file mode 100755 index 0000000..3a49af6 --- /dev/null +++ b/tools/smoke_lsp_implementation.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Smoke: textDocument/implementation (bux-lsp 0.13) +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +LSP="$ROOT/tools/bux-lsp" +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +if [[ ! -x "$LSP" ]]; then + (cd "$ROOT" && make lsp >/dev/null) +fi + +cat > "$TMP/Main.bux" <<'EOF' +interface Drawable { + func Draw(self: &Self); +} +struct Circle { + radius: int; +} +struct Square { + side: int; +} +extend Circle for Drawable { + func Draw(self: &Circle) { + let r: int = self.radius; + } +} +extend Square for Drawable { + func Draw(self: &Square) { + let s: int = self.side; + } +} +func Render(c: Circle) { + c.Draw(); +} +func Main() -> int { + return 0; +} +EOF + +rpc() { + local body="$1" + local len + len=$(printf '%s' "$body" | wc -c) + printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body" +} + +CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux") +URI="file://$TMP/Main.bux" + +# Drawable interface name ~ line 0 character 10 +# Draw iface method ~ line 1 character 9 +# c.Draw() call ~ line 22 character 6 (approx) +{ + rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}' + rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}' + rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}' + # implementation on interface type Drawable + rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/implementation","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":10}}}' + # implementation on iface method Draw + rpc '{"jsonrpc":"2.0","id":3,"method":"textDocument/implementation","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":9}}}' + rpc '{"jsonrpc":"2.0","id":4,"method":"shutdown","params":null}' + rpc '{"jsonrpc":"2.0","method":"exit","params":null}' +} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt" + +if ! grep -q '0.13.0' "$TMP/out.txt"; then + echo "WARN: version not 0.13.0" +fi + +if ! grep -q 'implementationProvider' "$TMP/out.txt"; then + echo "FAIL: missing implementationProvider capability" + cat "$TMP/out.txt" + exit 1 +fi + +# id 2: interface → Circle + Square type locations +# Count locations in response for id 2 roughly via line ranges mentioning Circle/Square +# Parse with python for robustness +python3 - <<'PY' "$TMP/out.txt" +import json, sys, re +raw = open(sys.argv[1]).read() +# Split Content-Length messages into JSON bodies +bodies = [] +for m in re.finditer(r'Content-Length:\s*(\d+)\s*\n\s*\n', raw): + pass +# Simpler: find all JSON objects with "id" +parts = re.split(r'Content-Length:\s*\d+\s*', raw) +for p in parts: + p = p.strip() + if not p.startswith('{'): + continue + try: + j = json.loads(p) + except Exception: + continue + if j.get('id') == 2: + r = j.get('result') or [] + if len(r) < 2: + print(f"FAIL: interface Drawable expected ≥2 implementor types, got {len(r)}") + print(json.dumps(j, indent=2)[:800]) + sys.exit(1) + print(f" interface Drawable → {len(r)} type location(s)") + if j.get('id') == 3: + r = j.get('result') or [] + if len(r) < 2: + print(f"FAIL: iface method Draw expected ≥2 implementors, got {len(r)}") + print(json.dumps(j, indent=2)[:800]) + sys.exit(1) + # Expect two method sites (Circle.Draw + Square.Draw) + print(f" iface Draw → {len(r)} method location(s)") +print("PASS: LSP textDocument/implementation (0.13)") +PY