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).
This commit is contained in:
@@ -185,6 +185,9 @@ test-lsp: lsp
|
|||||||
@echo "==> LSP path rename smoke"
|
@echo "==> LSP path rename smoke"
|
||||||
@chmod +x tools/smoke_lsp_rename_path.sh
|
@chmod +x tools/smoke_lsp_rename_path.sh
|
||||||
@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
|
.PHONY: test-registry
|
||||||
test-registry: build
|
test-registry: build
|
||||||
|
|||||||
+20
-5
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 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.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден 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)
|
1. Workspace-wide import path index without open documents (optional polish)
|
||||||
2. Workspace-wide import path index without open documents (optional polish)
|
2. Expr/Stmt-level sourceFile if macros / cross-file inlining land
|
||||||
3. Expr/Stmt-level sourceFile if macros / cross-file inlining land
|
3. Fix selfhost C backend so buxc2→buxc3 fixed-point is green
|
||||||
4. Fix selfhost C backend so buxc2→buxc3 fixed-point is green
|
4. Main PR CI workflow (`make test`) beyond optional selfhost-loop
|
||||||
|
|||||||
+148
-2
@@ -14,6 +14,7 @@
|
|||||||
# v0.10.0: method rename + qualified path / extend Type rename edges.
|
# v0.10.0: method rename + qualified path / extend Type rename edges.
|
||||||
# v0.11.0: interface dispatch in call hierarchy (extend Type for Trait).
|
# 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.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 std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
||||||
import lexer, parser, ast, sema, types, scope, source_location
|
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
|
if seen.contains(key): continue
|
||||||
seen.incl(key)
|
seen.incl(key)
|
||||||
result.add(FuncSym(name: meth, uri: uri, info: info))
|
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) =
|
proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
let uri = paramsNode["textDocument"]["uri"].getStr()
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
@@ -2769,9 +2911,10 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"referencesProvider": true,
|
"referencesProvider": true,
|
||||||
"renameProvider": {"prepareProvider": true},
|
"renameProvider": {"prepareProvider": true},
|
||||||
"workspaceSymbolProvider": 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:
|
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
||||||
rootPath = paramsNode["rootPath"].getStr()
|
rootPath = paramsNode["rootPath"].getStr()
|
||||||
@@ -2871,6 +3014,9 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
of "callHierarchy/outgoingCalls":
|
of "callHierarchy/outgoingCalls":
|
||||||
handleOutgoingCalls(stream, id, paramsNode)
|
handleOutgoingCalls(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/implementation":
|
||||||
|
handleImplementation(stream, id, paramsNode)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if id != nil:
|
if id != nil:
|
||||||
sendError(stream, id, -32601, "method not found: " & methodName)
|
sendError(stream, id, -32601, "method not found: " & methodName)
|
||||||
|
|||||||
Executable
+112
@@ -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
|
||||||
Reference in New Issue
Block a user