feat(lsp): method call hierarchy for extend Type / .Method (0.9)
Index methods inside extend/impl blocks and include .Method( call sites in the call graph so IDEs can navigate receiver methods. - analyzeFile: track extend/impl brace scope; kind=method + container - Call hierarchy treats methods as callables; display Type.Method - smoke_lsp_method_hierarchy (Scale→Len, Main→Scale)
This commit is contained in:
@@ -202,6 +202,9 @@ test-lsp: lsp
|
|||||||
@echo "=== LSP call hierarchy smoke ==="
|
@echo "=== LSP call hierarchy smoke ==="
|
||||||
@chmod +x tools/smoke_lsp_call_hierarchy.sh
|
@chmod +x tools/smoke_lsp_call_hierarchy.sh
|
||||||
@tools/smoke_lsp_call_hierarchy.sh
|
@tools/smoke_lsp_call_hierarchy.sh
|
||||||
|
@echo "=== LSP method call hierarchy smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_method_hierarchy.sh
|
||||||
|
@tools/smoke_lsp_method_hierarchy.sh
|
||||||
|
|
||||||
.PHONY: test-registry
|
.PHONY: test-registry
|
||||||
test-registry: build
|
test-registry: build
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 2026-07-19
|
||||||
> **Текущо:** v0.5.x — multi-file #line, **selfhost CI smoke**, LSP 0.8, Nexus KA
|
> **Текущо:** v0.5.x — multi-file #line, selfhost CI, **LSP 0.9 method hierarchy**, Nexus KA
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+104
-35
@@ -10,6 +10,7 @@
|
|||||||
# v0.6.0: workspace/symbol search.
|
# v0.6.0: workspace/symbol search.
|
||||||
# v0.7.0: deeper rename — struct fields, enum variants, .member / ::Variant.
|
# v0.7.0: deeper rename — struct fields, enum variants, .member / ::Variant.
|
||||||
# v0.8.0: call hierarchy (prepare / incoming / outgoing).
|
# v0.8.0: call hierarchy (prepare / incoming / outgoing).
|
||||||
|
# v0.9.0: method call hierarchy (extend Type / .Method() sites).
|
||||||
|
|
||||||
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
|
||||||
@@ -313,6 +314,11 @@ proc analyzeFile(path: string, content: string): DocumentState =
|
|||||||
var inString = false
|
var inString = false
|
||||||
var stringDelim = '\0'
|
var stringDelim = '\0'
|
||||||
var escape = false
|
var escape = false
|
||||||
|
## extend/impl Type { ... } tracking for method indexing
|
||||||
|
var braceDepth = 0
|
||||||
|
var activeExtend = "" ## Type name of open extend/impl block
|
||||||
|
var extendBodyDepth = 0 ## braceDepth of the extend/impl opening `{`
|
||||||
|
var pendingExtend = "" ## Type name after `extend Name` before `{`
|
||||||
|
|
||||||
while i < content.len:
|
while i < content.len:
|
||||||
let c = content[i]
|
let c = content[i]
|
||||||
@@ -358,12 +364,41 @@ proc analyzeFile(path: string, content: string): DocumentState =
|
|||||||
inc i
|
inc i
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Brace tracking for extend/impl bodies (outside strings/comments)
|
||||||
|
if c == '{':
|
||||||
|
inc braceDepth
|
||||||
|
if pendingExtend.len > 0 and extendBodyDepth == 0:
|
||||||
|
activeExtend = pendingExtend
|
||||||
|
extendBodyDepth = braceDepth
|
||||||
|
pendingExtend = ""
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if c == '}':
|
||||||
|
if extendBodyDepth > 0 and braceDepth == extendBodyDepth:
|
||||||
|
activeExtend = ""
|
||||||
|
extendBodyDepth = 0
|
||||||
|
if braceDepth > 0:
|
||||||
|
dec braceDepth
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
|
||||||
# Keyword must be at token boundary
|
# Keyword must be at token boundary
|
||||||
template atWord(kw: string): bool =
|
template atWord(kw: string): bool =
|
||||||
(i + kw.len <= content.len and content[i ..< i + kw.len] == kw and
|
(i + kw.len <= content.len and content[i ..< i + kw.len] == kw and
|
||||||
(i == 0 or not isIdentChar(content[i - 1])) and
|
(i == 0 or not isIdentChar(content[i - 1])) and
|
||||||
(i + kw.len >= content.len or not isIdentChar(content[i + kw.len])))
|
(i + kw.len >= content.len or not isIdentChar(content[i + kw.len])))
|
||||||
|
|
||||||
|
# extend Type / impl Type — methods live in the following { block }
|
||||||
|
if atWord("extend") or atWord("impl"):
|
||||||
|
let kwLen = if content[i] == 'e': 6 else: 4
|
||||||
|
i += kwLen
|
||||||
|
skipWs(content, i)
|
||||||
|
# optional "Type for Trait" — take first type name
|
||||||
|
let tname = readIdent(content, i)
|
||||||
|
if tname.len > 0:
|
||||||
|
pendingExtend = tname
|
||||||
|
continue
|
||||||
|
|
||||||
if atWord("func"):
|
if atWord("func"):
|
||||||
let kwPos = i
|
let kwPos = i
|
||||||
i += 4
|
i += 4
|
||||||
@@ -399,8 +434,13 @@ proc analyzeFile(path: string, content: string): DocumentState =
|
|||||||
var sig = content[kwPos ..< min(sigEnd, content.len)].strip()
|
var sig = content[kwPos ..< min(sigEnd, content.len)].strip()
|
||||||
# collapse whitespace
|
# collapse whitespace
|
||||||
sig = sig.replace("\n", " ").multiReplace([(" ", " "), (" ", " "), (" ", " ")])
|
sig = sig.replace("\n", " ").multiReplace([(" ", " "), (" ", " "), (" ", " ")])
|
||||||
|
let isMethod = activeExtend.len > 0
|
||||||
|
let kind = if isMethod: "method" else: "function"
|
||||||
|
let container = if isMethod: activeExtend else: ""
|
||||||
|
if isMethod:
|
||||||
|
sig = activeExtend & "." & sig
|
||||||
addSymbol(result, name, SymbolInfo(
|
addSymbol(result, name, SymbolInfo(
|
||||||
line: line, col: col, kind: "function", detail: sig, container: ""))
|
line: line, col: col, kind: kind, detail: sig, container: container))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if atWord("let") or atWord("var") or atWord("const"):
|
if atWord("let") or atWord("var") or atWord("const"):
|
||||||
@@ -1091,6 +1131,7 @@ proc ensureAnalyzed(doc: DocumentState) =
|
|||||||
proc completionKind(kind: string): int =
|
proc completionKind(kind: string): int =
|
||||||
case kind
|
case kind
|
||||||
of "function": 3
|
of "function": 3
|
||||||
|
of "method": 2
|
||||||
of "variable": 6
|
of "variable": 6
|
||||||
of "constant": 14
|
of "constant": 14
|
||||||
of "struct": 22
|
of "struct": 22
|
||||||
@@ -1822,6 +1863,7 @@ proc handleRename(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
proc symbolKindLsp(kind: string): int =
|
proc symbolKindLsp(kind: string): int =
|
||||||
case kind
|
case kind
|
||||||
of "function": 12
|
of "function": 12
|
||||||
|
of "method": 6
|
||||||
of "variable": 13
|
of "variable": 13
|
||||||
of "constant": 14
|
of "constant": 14
|
||||||
of "struct": 23
|
of "struct": 23
|
||||||
@@ -1909,7 +1951,7 @@ proc handleWorkspaceSymbol(stream: FileStream, id: JsonNode, paramsNode: JsonNod
|
|||||||
sendResponse(stream, id, arr)
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Call hierarchy (v0.8) — lightweight textual call graph
|
# Call hierarchy (v0.8 + v0.9 methods) — textual call graph
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -1921,34 +1963,38 @@ type
|
|||||||
callee: string
|
callee: string
|
||||||
line: int
|
line: int
|
||||||
col: int
|
col: int
|
||||||
## Enclosing function name ("" if top-level / unknown)
|
## Enclosing function/method name ("" if top-level / unknown)
|
||||||
caller: string
|
caller: string
|
||||||
callerUri: string
|
callerUri: string
|
||||||
|
isMethodCall: bool ## true if site was `.Name(` (receiver call)
|
||||||
|
|
||||||
|
proc isCallableKind(kind: string): bool =
|
||||||
|
kind == "function" or kind == "method"
|
||||||
|
|
||||||
proc listFunctionSymbols(doc: DocumentState): seq[tuple[name: string, info: SymbolInfo]] =
|
proc listFunctionSymbols(doc: DocumentState): seq[tuple[name: string, info: SymbolInfo]] =
|
||||||
result = @[]
|
result = @[]
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
for name, info in doc.symbols.pairs:
|
for name, info in doc.symbols.pairs:
|
||||||
if info.kind == "function":
|
if isCallableKind(info.kind):
|
||||||
result.add((name, info))
|
result.add((name, info))
|
||||||
|
|
||||||
proc allKnownFuncs(): Table[string, FuncSym] =
|
proc allKnownFuncs(): Table[string, FuncSym] =
|
||||||
## name → first seen FuncSym (workspace + open docs)
|
## name → first seen FuncSym (workspace + open docs); methods included
|
||||||
result = initTable[string, FuncSym]()
|
result = initTable[string, FuncSym]()
|
||||||
for uri, doc in documents.pairs:
|
for uri, doc in documents.pairs:
|
||||||
for (name, info) in listFunctionSymbols(doc):
|
for (name, info) in listFunctionSymbols(doc):
|
||||||
if not result.hasKey(name):
|
if not result.hasKey(name):
|
||||||
result[name] = FuncSym(name: name, uri: uri, info: info)
|
result[name] = FuncSym(name: name, uri: uri, info: info)
|
||||||
for name, ws in workspaceSymbols.pairs:
|
for name, ws in workspaceSymbols.pairs:
|
||||||
if ws.info.kind == "function" and not result.hasKey(name):
|
if isCallableKind(ws.info.kind) and not result.hasKey(name):
|
||||||
result[name] = FuncSym(name: name, uri: ws.uri, info: ws.info)
|
result[name] = FuncSym(name: name, uri: ws.uri, info: ws.info)
|
||||||
|
|
||||||
proc enclosingFuncName(doc: DocumentState, line: int): string =
|
proc enclosingFuncName(doc: DocumentState, line: int): string =
|
||||||
## Nearest function whose decl line ≤ line (same file).
|
## Nearest function/method whose decl line ≤ line (same file).
|
||||||
result = ""
|
result = ""
|
||||||
var best = -1
|
var best = -1
|
||||||
for name, info in doc.symbols.pairs:
|
for name, info in doc.symbols.pairs:
|
||||||
if info.kind != "function": continue
|
if not isCallableKind(info.kind): continue
|
||||||
if info.line <= line and info.line >= best:
|
if info.line <= line and info.line >= best:
|
||||||
best = info.line
|
best = info.line
|
||||||
result = name
|
result = name
|
||||||
@@ -1975,16 +2021,16 @@ proc collectCallSitesInDoc(doc: DocumentState, known: HashSet[string]): seq[Call
|
|||||||
off += h.col
|
off += h.col
|
||||||
if not isCallSiteAt(doc.content, off, h.len):
|
if not isCallSiteAt(doc.content, off, h.len):
|
||||||
continue
|
continue
|
||||||
# Skip the function declaration itself (func Name() — also has () )
|
# Skip the function/method declaration itself (func Name() — also has () )
|
||||||
if doc.symbols.hasKey(name):
|
if doc.symbols.hasKey(name):
|
||||||
let info = doc.symbols[name]
|
let info = doc.symbols[name]
|
||||||
if info.line == h.line and info.col == h.col:
|
if info.line == h.line and info.col == h.col:
|
||||||
continue
|
continue
|
||||||
# Skip .method-style if access is dot (method calls still useful — keep)
|
|
||||||
let caller = enclosingFuncName(doc, h.line)
|
let caller = enclosingFuncName(doc, h.line)
|
||||||
result.add(CallSite(
|
result.add(CallSite(
|
||||||
callee: name, line: h.line, col: h.col,
|
callee: name, line: h.line, col: h.col,
|
||||||
caller: caller, callerUri: doc.uri))
|
caller: caller, callerUri: doc.uri,
|
||||||
|
isMethodCall: h.access == iaDot))
|
||||||
|
|
||||||
proc collectAllCallSites(): seq[CallSite] =
|
proc collectAllCallSites(): seq[CallSite] =
|
||||||
result = @[]
|
result = @[]
|
||||||
@@ -2031,9 +2077,14 @@ proc collectAllCallSites(): seq[CallSite] =
|
|||||||
|
|
||||||
proc callHierarchyItem(fs: FuncSym): JsonNode =
|
proc callHierarchyItem(fs: FuncSym): JsonNode =
|
||||||
let nameLen = fs.name.len
|
let nameLen = fs.name.len
|
||||||
|
# SymbolKind: Method=6, Function=12
|
||||||
|
let sk = if fs.info.kind == "method": 6 else: 12
|
||||||
|
var name = fs.name
|
||||||
|
if fs.info.kind == "method" and fs.info.container.len > 0:
|
||||||
|
name = fs.info.container & "." & fs.name
|
||||||
%*{
|
%*{
|
||||||
"name": fs.name,
|
"name": name,
|
||||||
"kind": 12, # SymbolKind.Function
|
"kind": sk,
|
||||||
"detail": fs.info.detail,
|
"detail": fs.info.detail,
|
||||||
"uri": fs.uri,
|
"uri": fs.uri,
|
||||||
"range": {
|
"range": {
|
||||||
@@ -2043,25 +2094,30 @@ proc callHierarchyItem(fs: FuncSym): JsonNode =
|
|||||||
"selectionRange": {
|
"selectionRange": {
|
||||||
"start": {"line": fs.info.line, "character": fs.info.col},
|
"start": {"line": fs.info.line, "character": fs.info.col},
|
||||||
"end": {"line": fs.info.line, "character": fs.info.col + nameLen}
|
"end": {"line": fs.info.line, "character": fs.info.col + nameLen}
|
||||||
}
|
},
|
||||||
|
"data": fs.name # bare name for graph matching
|
||||||
}
|
}
|
||||||
|
|
||||||
proc lookupFuncSym(name, uri: string): FuncSym =
|
proc lookupFuncSym(name, uri: string): FuncSym =
|
||||||
result = FuncSym(name: name, uri: uri)
|
## Resolve by bare name (Area) or qualified display (Rectangle.Area).
|
||||||
|
var bare = name
|
||||||
|
let dot = name.rfind('.')
|
||||||
|
if dot >= 0:
|
||||||
|
bare = name[dot + 1 .. ^1]
|
||||||
|
result = FuncSym(name: bare, uri: uri)
|
||||||
if documents.hasKey(uri):
|
if documents.hasKey(uri):
|
||||||
let doc = documents[uri]
|
let doc = documents[uri]
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
if doc.symbols.hasKey(name) and doc.symbols[name].kind == "function":
|
if doc.symbols.hasKey(bare) and isCallableKind(doc.symbols[bare].kind):
|
||||||
result.info = doc.symbols[name]
|
result.info = doc.symbols[bare]
|
||||||
return
|
return
|
||||||
if workspaceSymbols.hasKey(name) and workspaceSymbols[name].info.kind == "function":
|
if workspaceSymbols.hasKey(bare) and isCallableKind(workspaceSymbols[bare].info.kind):
|
||||||
result.uri = workspaceSymbols[name].uri
|
result.uri = workspaceSymbols[bare].uri
|
||||||
result.info = workspaceSymbols[name].info
|
result.info = workspaceSymbols[bare].info
|
||||||
return
|
return
|
||||||
# Fallback from allKnownFuncs
|
|
||||||
let all = allKnownFuncs()
|
let all = allKnownFuncs()
|
||||||
if all.hasKey(name):
|
if all.hasKey(bare):
|
||||||
return all[name]
|
return all[bare]
|
||||||
|
|
||||||
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()
|
||||||
@@ -2077,17 +2133,17 @@ proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: Js
|
|||||||
if word.len == 0:
|
if word.len == 0:
|
||||||
sendResponse(stream, id, %*[])
|
sendResponse(stream, id, %*[])
|
||||||
return
|
return
|
||||||
# Prefer function symbol under cursor
|
# Prefer function/method symbol under cursor
|
||||||
if doc.symbols.hasKey(word) and doc.symbols[word].kind == "function":
|
if doc.symbols.hasKey(word) and isCallableKind(doc.symbols[word].kind):
|
||||||
let fs = FuncSym(name: word, uri: uri, info: doc.symbols[word])
|
let fs = FuncSym(name: word, uri: uri, info: doc.symbols[word])
|
||||||
sendResponse(stream, id, %*[callHierarchyItem(fs)])
|
sendResponse(stream, id, %*[callHierarchyItem(fs)])
|
||||||
return
|
return
|
||||||
if workspaceSymbols.hasKey(word) and workspaceSymbols[word].info.kind == "function":
|
if workspaceSymbols.hasKey(word) and isCallableKind(workspaceSymbols[word].info.kind):
|
||||||
let ws = workspaceSymbols[word]
|
let ws = workspaceSymbols[word]
|
||||||
let fs = FuncSym(name: word, uri: ws.uri, info: ws.info)
|
let fs = FuncSym(name: word, uri: ws.uri, info: ws.info)
|
||||||
sendResponse(stream, id, %*[callHierarchyItem(fs)])
|
sendResponse(stream, id, %*[callHierarchyItem(fs)])
|
||||||
return
|
return
|
||||||
# Allow prepare on a call site: Foo( → hierarchy for Foo
|
# Call site: Foo( or .Method(
|
||||||
let lines = doc.content.split("\n")
|
let lines = doc.content.split("\n")
|
||||||
if lineNum < lines.len:
|
if lineNum < lines.len:
|
||||||
let l = lines[lineNum]
|
let l = lines[lineNum]
|
||||||
@@ -2105,13 +2161,24 @@ proc handlePrepareCallHierarchy(stream: FileStream, id: JsonNode, paramsNode: Js
|
|||||||
return
|
return
|
||||||
sendResponse(stream, id, %*[])
|
sendResponse(stream, id, %*[])
|
||||||
|
|
||||||
|
proc bareCallName(itemName: string): string =
|
||||||
|
## "Rectangle.Area" → "Area"; "Add" → "Add"
|
||||||
|
let dot = itemName.rfind('.')
|
||||||
|
if dot >= 0: return itemName[dot + 1 .. ^1]
|
||||||
|
# Prefer data field if present (from our CallHierarchyItem)
|
||||||
|
result = itemName
|
||||||
|
|
||||||
proc handleIncomingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
proc handleIncomingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
## Who calls this function?
|
## Who calls this function/method?
|
||||||
if not paramsNode.hasKey("item"):
|
if not paramsNode.hasKey("item"):
|
||||||
sendResponse(stream, id, %*[])
|
sendResponse(stream, id, %*[])
|
||||||
return
|
return
|
||||||
let item = paramsNode["item"]
|
let item = paramsNode["item"]
|
||||||
let name = item["name"].getStr()
|
var name = item["name"].getStr()
|
||||||
|
if item.hasKey("data") and item["data"].kind == JString:
|
||||||
|
name = item["data"].getStr()
|
||||||
|
else:
|
||||||
|
name = bareCallName(name)
|
||||||
let sites = collectAllCallSites()
|
let sites = collectAllCallSites()
|
||||||
# Group by caller
|
# Group by caller
|
||||||
var groups = initTable[string, tuple[fs: FuncSym, ranges: seq[tuple[line, col, len: int]]]]()
|
var groups = initTable[string, tuple[fs: FuncSym, ranges: seq[tuple[line, col, len: int]]]]()
|
||||||
@@ -2139,12 +2206,16 @@ proc handleIncomingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode)
|
|||||||
sendResponse(stream, id, arr)
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
proc handleOutgoingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
proc handleOutgoingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
## What does this function call?
|
## What does this function/method call?
|
||||||
if not paramsNode.hasKey("item"):
|
if not paramsNode.hasKey("item"):
|
||||||
sendResponse(stream, id, %*[])
|
sendResponse(stream, id, %*[])
|
||||||
return
|
return
|
||||||
let item = paramsNode["item"]
|
let item = paramsNode["item"]
|
||||||
let name = item["name"].getStr()
|
var name = item["name"].getStr()
|
||||||
|
if item.hasKey("data") and item["data"].kind == JString:
|
||||||
|
name = item["data"].getStr()
|
||||||
|
else:
|
||||||
|
name = bareCallName(name)
|
||||||
let uri = if item.hasKey("uri"): item["uri"].getStr() else: ""
|
let uri = if item.hasKey("uri"): item["uri"].getStr() else: ""
|
||||||
let sites = collectAllCallSites()
|
let sites = collectAllCallSites()
|
||||||
# Group by callee among sites whose caller is `name`
|
# Group by callee among sites whose caller is `name`
|
||||||
@@ -2154,10 +2225,8 @@ proc handleOutgoingCalls(stream: FileStream, id: JsonNode, paramsNode: JsonNode)
|
|||||||
if uri.len > 0 and s.callerUri != uri: continue
|
if uri.len > 0 and s.callerUri != uri: continue
|
||||||
let key = s.callee
|
let key = s.callee
|
||||||
if not groups.hasKey(key):
|
if not groups.hasKey(key):
|
||||||
let fs = lookupFuncSym(s.callee, s.callerUri)
|
|
||||||
# Prefer known func uri
|
|
||||||
let all = allKnownFuncs()
|
let all = allKnownFuncs()
|
||||||
let fs2 = if all.hasKey(s.callee): all[s.callee] else: fs
|
let fs2 = if all.hasKey(s.callee): all[s.callee] else: lookupFuncSym(s.callee, s.callerUri)
|
||||||
groups[key] = (fs2, @[])
|
groups[key] = (fs2, @[])
|
||||||
groups[key].ranges.add((s.line, s.col, s.callee.len))
|
groups[key].ranges.add((s.line, s.col, s.callee.len))
|
||||||
|
|
||||||
@@ -2201,7 +2270,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"workspaceSymbolProvider": true,
|
"workspaceSymbolProvider": true,
|
||||||
"callHierarchyProvider": true
|
"callHierarchyProvider": true
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.8.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.9.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()
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ if ! grep -q 'callHierarchyProvider' "$TMP/out.txt"; then
|
|||||||
cat "$TMP/out.txt"
|
cat "$TMP/out.txt"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! grep -q '0.8.0' "$TMP/out.txt"; then
|
if ! grep -qE '0\.[89]\.0' "$TMP/out.txt"; then
|
||||||
echo "WARN: version not 0.8.0"
|
echo "WARN: unexpected bux-lsp version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# prepare should mention Add
|
# prepare should mention Add
|
||||||
|
|||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: method call hierarchy — extend Type + .Method() (bux-lsp 0.9)
|
||||||
|
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'
|
||||||
|
struct Point {
|
||||||
|
x: int;
|
||||||
|
y: int;
|
||||||
|
}
|
||||||
|
extend Point {
|
||||||
|
func Len(self: Point) -> int {
|
||||||
|
return self.x + self.y;
|
||||||
|
}
|
||||||
|
func Scale(self: Point, n: int) -> int {
|
||||||
|
return self.Len() * n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
let p: Point = Point { x: 3, y: 4 };
|
||||||
|
return p.Scale(2);
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Len is around line 5 (0-based: extend block)
|
||||||
|
# Scale around line 8
|
||||||
|
# Main line 11
|
||||||
|
{
|
||||||
|
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"'}}}'
|
||||||
|
# prepare on Len method
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/prepareCallHierarchy","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":5,"character":9}}}'
|
||||||
|
# incoming for Len — expect Scale (method call self.Len())
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"callHierarchy/incomingCalls","params":{"item":{"name":"Point.Len","kind":6,"data":"Len","uri":"'"$URI"'","range":{"start":{"line":5,"character":0},"end":{"line":5,"character":12}},"selectionRange":{"start":{"line":5,"character":9},"end":{"line":5,"character":12}}}}}'
|
||||||
|
# outgoing for Scale — expect Len
|
||||||
|
rpc '{"jsonrpc":"2.0","id":4,"method":"callHierarchy/outgoingCalls","params":{"item":{"name":"Point.Scale","kind":6,"data":"Scale","uri":"'"$URI"'","range":{"start":{"line":8,"character":0},"end":{"line":8,"character":14}},"selectionRange":{"start":{"line":8,"character":9},"end":{"line":8,"character":14}}}}}'
|
||||||
|
# incoming for Scale — expect Main
|
||||||
|
rpc '{"jsonrpc":"2.0","id":5,"method":"callHierarchy/incomingCalls","params":{"item":{"name":"Point.Scale","kind":6,"data":"Scale","uri":"'"$URI"'","range":{"start":{"line":8,"character":0},"end":{"line":8,"character":14}},"selectionRange":{"start":{"line":8,"character":9},"end":{"line":8,"character":14}}}}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":6,"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.9.0' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: version not 0.9.0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# prepare should return method (Point.Len or Len)
|
||||||
|
if ! grep -qE '"name":"(Point\.)?Len"' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: prepare did not return Len method"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Scale calls Len
|
||||||
|
if ! grep -qE '"name":"(Point\.)?Scale"' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: missing Scale in hierarchy"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Main calls Scale
|
||||||
|
if ! grep -q '"name":"Main"' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: incoming Scale should include Main"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# kind 6 = Method somewhere
|
||||||
|
if ! grep -q '"kind":6' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: expected SymbolKind.Method (6)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: LSP method call hierarchy (extend + .Method)"
|
||||||
Reference in New Issue
Block a user