feat: LSP references/rename (0.5) and expand CI make test
Add workspace-aware Find All References and Rename for scoped locals and globals, plus registry/apps/dwarf smokes in the default test target. - bux-lsp: textDocument/references, prepareRename, rename - make test-lsp runs smoke_lsp_rename; make test includes registry/dwarf/apps
This commit is contained in:
@@ -19,7 +19,7 @@ dev:
|
|||||||
debug: dev
|
debug: dev
|
||||||
@echo "Debug binary: buxc_debug"
|
@echo "Debug binary: buxc_debug"
|
||||||
|
|
||||||
test: build fmt-check test-examples test-errors test-stdlib
|
test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-apps
|
||||||
@echo "Running lexer tests..."
|
@echo "Running lexer tests..."
|
||||||
$(NIM) c -r tests/lexer_test.nim
|
$(NIM) c -r tests/lexer_test.nim
|
||||||
@echo "Running parser tests..."
|
@echo "Running parser tests..."
|
||||||
@@ -190,6 +190,9 @@ test-lsp: lsp
|
|||||||
@echo "=== LSP hover smoke ==="
|
@echo "=== LSP hover smoke ==="
|
||||||
@chmod +x tools/smoke_lsp_hover.sh
|
@chmod +x tools/smoke_lsp_hover.sh
|
||||||
@tools/smoke_lsp_hover.sh
|
@tools/smoke_lsp_hover.sh
|
||||||
|
@echo "=== LSP references / rename smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_rename.sh
|
||||||
|
@tools/smoke_lsp_rename.sh
|
||||||
|
|
||||||
.PHONY: test-registry
|
.PHONY: test-registry
|
||||||
test-registry: build
|
test-registry: build
|
||||||
|
|||||||
@@ -135,8 +135,10 @@ make test-examples # all examples/ programs (40+)
|
|||||||
make test-errors # golden Rust-style diagnostic output
|
make test-errors # golden Rust-style diagnostic output
|
||||||
make test-stdlib # stdlib golden packages
|
make test-stdlib # stdlib golden packages
|
||||||
make test-registry # package registry (local + HTTP index)
|
make test-registry # package registry (local + HTTP index)
|
||||||
make test-apps # showcase apps build + simpledb/jwt CLI smoke
|
make test-apps # showcase apps build + simpledb/jwt CLI smoke (in `make test`)
|
||||||
make test-dwarf # #line maps + .debug_info + --release (E.4)
|
make test-dwarf # #line maps + .debug_info + --release (in `make test`)
|
||||||
|
make test-registry # package registry local + HTTP (in `make test`)
|
||||||
|
make test-lsp # hover + references/rename smokes
|
||||||
make bench # micro-benchmarks (Bux + C/Nim/Zig twins)
|
make bench # micro-benchmarks (Bux + C/Nim/Zig twins)
|
||||||
make bench-nexus # wrk throughput vs apps/nexus /api/health
|
make bench-nexus # wrk throughput vs apps/nexus /api/health
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 2026-07-19
|
||||||
> **Текущо:** v0.5.x — registry HTTP, apps smoke, E.5 benches, **E.4 DWARF `#line` + gdb**
|
> **Текущо:** v0.5.x — E.4 DWARF, E.5 benches, **LSP 0.5 references/rename**, CI apps/dwarf/registry
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+287
-2
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# Hover uses real bootstrap sema types when possible (globals + stdlib).
|
# Hover uses real bootstrap sema types when possible (globals + stdlib).
|
||||||
# Locals are position-sensitive (scoped) and include inferred `let` types (v0.4.0).
|
# Locals are position-sensitive (scoped) and include inferred `let` types (v0.4.0).
|
||||||
|
# v0.5.0: textDocument/references + rename (scoped locals + workspace globals).
|
||||||
|
|
||||||
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
|
||||||
@@ -1239,6 +1240,279 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
|||||||
"end": {"line": lineNum, "character": endC}
|
"end": {"line": lineNum, "character": endC}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Identifier occurrences (references / rename) — E.1 tooling polish / session 34
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
IdentHit = object
|
||||||
|
line: int ## 0-based
|
||||||
|
col: int ## 0-based start of name
|
||||||
|
len: int
|
||||||
|
|
||||||
|
proc collectIdentHits(content, name: string): seq[IdentHit] =
|
||||||
|
## Textual identifier occurrences of `name`, skipping strings/comments.
|
||||||
|
result = @[]
|
||||||
|
if name.len == 0 or content.len == 0:
|
||||||
|
return
|
||||||
|
var i = 0
|
||||||
|
var inLineComment = false
|
||||||
|
var inBlockComment = false
|
||||||
|
var inString = false
|
||||||
|
var stringDelim = '\0'
|
||||||
|
var escape = false
|
||||||
|
while i < content.len:
|
||||||
|
let c = content[i]
|
||||||
|
if inLineComment:
|
||||||
|
if c == '\n': inLineComment = false
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if inBlockComment:
|
||||||
|
if c == '*' and i + 1 < content.len and content[i + 1] == '/':
|
||||||
|
inBlockComment = false
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if inString:
|
||||||
|
if escape: escape = false
|
||||||
|
elif c == '\\': escape = true
|
||||||
|
elif c == stringDelim: inString = false
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if c == '/' and i + 1 < content.len and content[i + 1] == '/':
|
||||||
|
inLineComment = true
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if c == '/' and i + 1 < content.len and content[i + 1] == '*':
|
||||||
|
inBlockComment = true
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
if c in {'"', '`'} or (c == 'f' and i + 1 < content.len and content[i + 1] == '"'):
|
||||||
|
inString = true
|
||||||
|
if c == 'f':
|
||||||
|
stringDelim = '"'
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
stringDelim = c
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isIdentStart(c):
|
||||||
|
let start = i
|
||||||
|
var j = i + 1
|
||||||
|
while j < content.len and isIdentChar(content[j]):
|
||||||
|
inc j
|
||||||
|
let ident = content[start ..< j]
|
||||||
|
if ident == name:
|
||||||
|
let (line, col) = lineColAt(content, start)
|
||||||
|
result.add(IdentHit(line: line, col: col, len: name.len))
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
inc i
|
||||||
|
|
||||||
|
proc sameLocal*(a, b: LocalBinding): bool =
|
||||||
|
a.name == b.name and a.declLine == b.declLine and a.declCol == b.declCol and
|
||||||
|
a.container == b.container
|
||||||
|
|
||||||
|
proc locationJson(uri: string, line, col, nameLen: int): JsonNode =
|
||||||
|
%*{
|
||||||
|
"uri": uri,
|
||||||
|
"range": {
|
||||||
|
"start": {"line": line, "character": col},
|
||||||
|
"end": {"line": line, "character": col + nameLen}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proc collectReferences(doc: DocumentState, word: string, lineNum: int,
|
||||||
|
includeDecl: bool): seq[JsonNode] =
|
||||||
|
## Collect LSP Location nodes for references at `word` on `lineNum`.
|
||||||
|
result = @[]
|
||||||
|
if word.len == 0: return
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
if doc.locals.len == 0 and doc.content.len > 0:
|
||||||
|
enrichWithSema(doc)
|
||||||
|
|
||||||
|
let (lok, targetLocal) = lookupLocalAt(doc, word, lineNum)
|
||||||
|
let hits = collectIdentHits(doc.content, word)
|
||||||
|
|
||||||
|
if lok:
|
||||||
|
# Scoped local / param: only occurrences that resolve to the same binding
|
||||||
|
for h in hits:
|
||||||
|
let (ok, b) = lookupLocalAt(doc, word, h.line)
|
||||||
|
if not ok or not sameLocal(b, targetLocal):
|
||||||
|
continue
|
||||||
|
if not includeDecl and h.line == targetLocal.declLine and h.col == targetLocal.declCol:
|
||||||
|
continue
|
||||||
|
result.add(locationJson(doc.uri, h.line, h.col, h.len))
|
||||||
|
return
|
||||||
|
|
||||||
|
# File-level or workspace symbol: all textual hits in this document
|
||||||
|
let isFileSym = doc.symbols.hasKey(word) or doc.typeIndex.hasKey(word)
|
||||||
|
let isWsSym = workspaceSymbols.hasKey(word)
|
||||||
|
if not isFileSym and not isWsSym:
|
||||||
|
# Still report textual hits in current file (e.g. undeclared / mid-edit)
|
||||||
|
for h in hits:
|
||||||
|
result.add(locationJson(doc.uri, h.line, h.col, h.len))
|
||||||
|
return
|
||||||
|
|
||||||
|
for h in hits:
|
||||||
|
if not includeDecl and doc.symbols.hasKey(word):
|
||||||
|
let info = doc.symbols[word]
|
||||||
|
if h.line == info.line and h.col == info.col:
|
||||||
|
continue
|
||||||
|
result.add(locationJson(doc.uri, h.line, h.col, h.len))
|
||||||
|
|
||||||
|
# Workspace: other open buffers + on-disk .bux under rootPath
|
||||||
|
if isWsSym or isFileSym:
|
||||||
|
var seenUri = initHashSet[string]()
|
||||||
|
seenUri.incl(doc.uri)
|
||||||
|
|
||||||
|
for u, d in documents.pairs:
|
||||||
|
if d.content.len == 0 or seenUri.contains(u): continue
|
||||||
|
seenUri.incl(u)
|
||||||
|
for h in collectIdentHits(d.content, word):
|
||||||
|
result.add(locationJson(u, h.line, h.col, h.len))
|
||||||
|
|
||||||
|
if rootPath.len > 0 and dirExists(rootPath):
|
||||||
|
var stack: seq[tuple[dir: string, depth: int]] = @[(rootPath, 0)]
|
||||||
|
while stack.len > 0:
|
||||||
|
let (dir, depth) = stack.pop()
|
||||||
|
if depth > 4: continue
|
||||||
|
let base = dir.extractFilename
|
||||||
|
if base in [".git", "build", "examples_pkg", "node_modules", "vendor", "nimcache"]:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
for kind, path in walkDir(dir):
|
||||||
|
if kind == pcDir:
|
||||||
|
stack.add((path, depth + 1))
|
||||||
|
elif kind == pcFile and path.endsWith(".bux"):
|
||||||
|
let u = pathToUri(path.absolutePath)
|
||||||
|
if seenUri.contains(u): continue
|
||||||
|
seenUri.incl(u)
|
||||||
|
try:
|
||||||
|
let text = readFile(path)
|
||||||
|
for h in collectIdentHits(text, word):
|
||||||
|
result.add(locationJson(u, h.line, h.col, h.len))
|
||||||
|
except CatchableError:
|
||||||
|
discard
|
||||||
|
except CatchableError:
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc handleReferences(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
|
let position = paramsNode["position"]
|
||||||
|
let lineNum = position["line"].getInt()
|
||||||
|
let col = position["character"].getInt()
|
||||||
|
var includeDecl = true
|
||||||
|
if paramsNode.hasKey("context") and paramsNode["context"].hasKey("includeDeclaration"):
|
||||||
|
includeDecl = paramsNode["context"]["includeDeclaration"].getBool()
|
||||||
|
|
||||||
|
let doc = getDoc(uri)
|
||||||
|
if doc.content == "":
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
|
||||||
|
let word = findWordAt(doc.content, lineNum, col)
|
||||||
|
if word.len == 0:
|
||||||
|
sendResponse(stream, id, %*[])
|
||||||
|
return
|
||||||
|
|
||||||
|
var arr = newJArray()
|
||||||
|
for loc in collectReferences(doc, word, lineNum, includeDecl):
|
||||||
|
arr.add(loc)
|
||||||
|
sendResponse(stream, id, arr)
|
||||||
|
|
||||||
|
proc isValidIdentName(s: string): bool =
|
||||||
|
if s.len == 0: return false
|
||||||
|
if not isIdentStart(s[0]): return false
|
||||||
|
for i in 1 ..< s.len:
|
||||||
|
if not isIdentChar(s[i]): return false
|
||||||
|
true
|
||||||
|
|
||||||
|
proc handlePrepareRename(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
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, newJNull())
|
||||||
|
return
|
||||||
|
let lines = doc.content.split("\n")
|
||||||
|
if lineNum >= lines.len:
|
||||||
|
sendResponse(stream, id, newJNull())
|
||||||
|
return
|
||||||
|
let l = lines[lineNum]
|
||||||
|
var start = min(col, l.len)
|
||||||
|
var endC = start
|
||||||
|
while start > 0 and l[start - 1] in {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
|
||||||
|
dec start
|
||||||
|
while endC < l.len and l[endC] in {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
|
||||||
|
inc endC
|
||||||
|
if start >= endC:
|
||||||
|
sendResponse(stream, id, newJNull())
|
||||||
|
return
|
||||||
|
let word = l[start ..< endC]
|
||||||
|
# Reject keywords
|
||||||
|
const kws = ["func", "var", "let", "if", "else", "while", "for", "return",
|
||||||
|
"struct", "enum", "true", "false", "null", "self", "match",
|
||||||
|
"import", "module", "type", "const", "pub", "own"]
|
||||||
|
if word in kws:
|
||||||
|
sendResponse(stream, id, newJNull())
|
||||||
|
return
|
||||||
|
sendResponse(stream, id, %*{
|
||||||
|
"range": {
|
||||||
|
"start": {"line": lineNum, "character": start},
|
||||||
|
"end": {"line": lineNum, "character": endC}
|
||||||
|
},
|
||||||
|
"placeholder": word
|
||||||
|
})
|
||||||
|
|
||||||
|
proc handleRename(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||||
|
let uri = paramsNode["textDocument"]["uri"].getStr()
|
||||||
|
let position = paramsNode["position"]
|
||||||
|
let lineNum = position["line"].getInt()
|
||||||
|
let col = position["character"].getInt()
|
||||||
|
let newName = if paramsNode.hasKey("newName"): paramsNode["newName"].getStr() else: ""
|
||||||
|
|
||||||
|
if not isValidIdentName(newName):
|
||||||
|
sendError(stream, id, -32602, "invalid identifier: '" & newName & "'")
|
||||||
|
return
|
||||||
|
|
||||||
|
let doc = getDoc(uri)
|
||||||
|
if doc.content == "":
|
||||||
|
sendResponse(stream, id, %*{"changes": newJObject()})
|
||||||
|
return
|
||||||
|
|
||||||
|
let word = findWordAt(doc.content, lineNum, col)
|
||||||
|
if word.len == 0:
|
||||||
|
sendResponse(stream, id, %*{"changes": newJObject()})
|
||||||
|
return
|
||||||
|
if word == newName:
|
||||||
|
sendResponse(stream, id, %*{"changes": newJObject()})
|
||||||
|
return
|
||||||
|
|
||||||
|
let refs = collectReferences(doc, word, lineNum, includeDecl = true)
|
||||||
|
# Group TextEdits by URI
|
||||||
|
var byUri = initTable[string, JsonNode]()
|
||||||
|
for loc in refs:
|
||||||
|
let u = loc["uri"].getStr()
|
||||||
|
if not byUri.hasKey(u):
|
||||||
|
byUri[u] = newJArray()
|
||||||
|
let r = loc["range"]
|
||||||
|
byUri[u].add(%*{
|
||||||
|
"range": r,
|
||||||
|
"newText": newName
|
||||||
|
})
|
||||||
|
|
||||||
|
var changes = newJObject()
|
||||||
|
for u, edits in byUri.pairs:
|
||||||
|
changes[u] = edits
|
||||||
|
|
||||||
|
sendResponse(stream, id, %*{"changes": changes})
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Document symbols (outline)
|
# Document symbols (outline)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1302,9 +1576,11 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"completionProvider": {"triggerCharacters": [".", ":"]},
|
"completionProvider": {"triggerCharacters": [".", ":"]},
|
||||||
"definitionProvider": true,
|
"definitionProvider": true,
|
||||||
"hoverProvider": true,
|
"hoverProvider": true,
|
||||||
"documentSymbolProvider": true
|
"documentSymbolProvider": true,
|
||||||
|
"referencesProvider": true,
|
||||||
|
"renameProvider": {"prepareProvider": true}
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.4.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.5.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()
|
||||||
@@ -1379,6 +1655,15 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
of "textDocument/documentSymbol":
|
of "textDocument/documentSymbol":
|
||||||
handleDocumentSymbol(stream, id, paramsNode)
|
handleDocumentSymbol(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/references":
|
||||||
|
handleReferences(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/prepareRename":
|
||||||
|
handlePrepareRename(stream, id, paramsNode)
|
||||||
|
|
||||||
|
of "textDocument/rename":
|
||||||
|
handleRename(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
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: textDocument/references + rename (bux-lsp 0.5.0)
|
||||||
|
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
|
||||||
|
echo "building bux-lsp..."
|
||||||
|
(cd "$ROOT" && make lsp >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$TMP/Main.bux" <<'EOF'
|
||||||
|
func Add(a: int, b: int) -> int {
|
||||||
|
let sum = a + b;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
let n = 10;
|
||||||
|
return Add(n, 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"
|
||||||
|
|
||||||
|
{
|
||||||
|
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"'}}}'
|
||||||
|
# references on `sum` (line 1, col 8)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/references","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":8},"context":{"includeDeclaration":true}}}'
|
||||||
|
# prepareRename on sum
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"textDocument/prepareRename","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":8}}}'
|
||||||
|
# rename sum → total
|
||||||
|
rpc '{"jsonrpc":"2.0","id":4,"method":"textDocument/rename","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":1,"character":8},"newName":"total"}}'
|
||||||
|
# references on Add (line 0)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":5,"method":"textDocument/references","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":5},"context":{"includeDeclaration":true}}}'
|
||||||
|
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"
|
||||||
|
|
||||||
|
echo "---- excerpt ----"
|
||||||
|
# Show initialize capabilities mention
|
||||||
|
if ! grep -q 'referencesProvider' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: initialize missing referencesProvider"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q 'renameProvider' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: initialize missing renameProvider"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q '0.5.0' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: version not 0.5.0 in initialize (may be ok)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rename workspace edit should propose "total"
|
||||||
|
if ! grep -q '"newText":"total"' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: rename did not emit newText total"
|
||||||
|
cat "$TMP/out.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Should have at least 2 edits for sum (decl + return)
|
||||||
|
total_edits=$(grep -o '"newText":"total"' "$TMP/out.txt" | wc -l)
|
||||||
|
if [[ "$total_edits" -lt 2 ]]; then
|
||||||
|
echo "FAIL: expected ≥2 renames for sum, got $total_edits"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " rename sum→total edits: $total_edits"
|
||||||
|
|
||||||
|
# References responses are JSON arrays with uri/range — check line numbers for sum
|
||||||
|
if ! grep -q 'Main.bux' "$TMP/out.txt"; then
|
||||||
|
echo "FAIL: no file URI in responses"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PASS: LSP references + rename smoke"
|
||||||
Reference in New Issue
Block a user