diff --git a/Makefile b/Makefile index cf14208..e72dfac 100644 --- a/Makefile +++ b/Makefile @@ -188,6 +188,9 @@ test-lsp: lsp @echo "==> LSP implementation smoke" @chmod +x tools/smoke_lsp_implementation.sh @tools/smoke_lsp_implementation.sh + @echo "==> LSP workspace import index smoke" + @chmod +x tools/smoke_lsp_workspace_imports.sh + @tools/smoke_lsp_workspace_imports.sh .PHONY: test-registry test-registry: build diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 510c3f6..266dd11 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 — **LSP 0.13 implementation**, HirNode sourceFile, optional selfhost-loop CI +> **Текущо:** v0.5.x — **LSP 0.14 workspace imports**, implementation, selfhost-loop CI > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -786,9 +786,22 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 50 (LSP 0.14 workspace import path index) + +1. **`workspaceImportPaths`**: URI → full import paths (`["Std","Io"]`) +2. **`registerWorkspaceImports`** at end of `analyzeFile` (scan + open/edit) +3. **`isKnownImportPathPrefix`** uses workspace index first — no open doc required +4. Path segment snapshot fix (no shared seq mutation across segments) +5. Smoke: `tools/smoke_lsp_workspace_imports.sh` + - only Main opened; Util closed on disk via `rootUri` scan + - Io→Net ≥2 edits including `Util.bux` in WorkspaceEdit +6. Version **bux-lsp 0.14.0**; `make test-lsp` + +--- + ## Следващи стъпки -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 +1. Expr/Stmt-level sourceFile if macros / cross-file inlining land +2. Fix selfhost C backend so buxc2→buxc3 fixed-point is green +3. Main PR CI workflow (`make test`) beyond optional selfhost-loop +4. LSP type hierarchy / prepareTypeHierarchy (optional) diff --git a/tools/lsp_server.nim b/tools/lsp_server.nim index 4307bd5..f84fddf 100644 --- a/tools/lsp_server.nim +++ b/tools/lsp_server.nim @@ -15,6 +15,7 @@ # 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). +# v0.14.0: workspace-wide import path index (no open-doc required). import std/[json, os, strutils, streams, tables, osproc, sequtils, sets] import lexer, parser, ast, sema, types, scope, source_location @@ -149,10 +150,25 @@ var workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]() ## Cross-file: "Iface.Method" → list of implementors workspaceImpls = initTable[string, seq[tuple[uri, typeName, meth: string]]]() + ## Import paths by file URI (from scanWorkspace + open docs) — v0.14 + ## Each entry is a full path like @["Std", "Io"] (not open-doc dependent). + workspaceImportPaths = initTable[string, seq[seq[string]]]() cachedStdlibDir = "" cachedStdlibDecls: seq[Decl] = @[] stdlibLoaded = false +proc registerWorkspaceImports(uri: string, segs: seq[PathSegInfo]) = + ## Index unique full import paths for this URI (replaces prior entry). + var paths: seq[seq[string]] = @[] + var seen = initHashSet[string]() + for s in segs: + if s.path.len == 0: continue + let key = s.path.join("::") + if seen.contains(key): continue + seen.incl(key) + paths.add(s.path) + workspaceImportPaths[uri] = paths + proc getDoc(uri: string): DocumentState = if not documents.hasKey(uri): documents[uri] = DocumentState(uri: uri) @@ -453,8 +469,11 @@ proc analyzeFile(path: string, content: string): DocumentState = break let (line, col) = lineColAt(content, nameStart) path.add(name) + # Snapshot path so later segments do not mutate earlier PathSegInfo + var pathSnap: seq[string] = @[] + for p in path: pathSnap.add(p) result.importPaths.add(PathSegInfo( - name: name, line: line, col: col, path: path, index: path.len - 1)) + name: name, line: line, col: col, path: pathSnap, index: pathSnap.len - 1)) skipWs(content, i) if i + 1 < content.len and content[i] == ':' and content[i + 1] == ':': i += 2 @@ -608,6 +627,9 @@ proc analyzeFile(path: string, content: string): DocumentState = if not matchedTypeKw: inc i + # Always refresh workspace import index for this URI (empty clears stale paths) + registerWorkspaceImports(result.uri, result.importPaths) + # --------------------------------------------------------------------------- # Real sema types for hover # --------------------------------------------------------------------------- @@ -1751,12 +1773,14 @@ proc pathStartsWith(path, prefix: seq[string]): bool = true proc isKnownImportPathPrefix(prefix: seq[string], name: string): bool = - ## True if some open document's import path starts with prefix ++ name. + ## True if any workspace (or open-doc) import path starts with prefix ++ name. + ## Uses workspaceImportPaths from scanWorkspace — no open document required. let full = prefix & name + for _, paths in workspaceImportPaths.pairs: + for p in paths: + if pathStartsWith(p, full): + return true for _, d in documents.pairs: - if d.importPaths.len == 0 and d.content.len > 0: - # lazy: may not have been copied yet - discard for seg in d.importPaths: if pathStartsWith(seg.path, full): return true @@ -2914,7 +2938,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = "callHierarchyProvider": true, "implementationProvider": true }, - "serverInfo": {"name": "bux-lsp", "version": "0.13.0"} + "serverInfo": {"name": "bux-lsp", "version": "0.14.0"} }) if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull: rootPath = paramsNode["rootPath"].getStr() diff --git a/tools/smoke_lsp_workspace_imports.sh b/tools/smoke_lsp_workspace_imports.sh new file mode 100755 index 0000000..785def5 --- /dev/null +++ b/tools/smoke_lsp_workspace_imports.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Smoke: workspace-wide import path index (bux-lsp 0.14) +# Util.bux is never opened — only indexed via scanWorkspace(rootUri). +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 + +# Only Main is opened. Util lives on disk and must still be renamed via +# workspace import index + path-seg disk scan. +cat > "$TMP/Main.bux" <<'EOF' +import Std::Io::{PrintLine}; +func Main() -> int { + PrintLine("hi"); + return 0; +} +EOF + +cat > "$TMP/Util.bux" <<'EOF' +import Std::Io::PrintLine; +func Util() -> int { + return 0; +} +EOF + +# Extra module path only in closed file — classification of use sites can still +# match workspace-known prefixes after scan. +cat > "$TMP/Closed.bux" <<'EOF' +import Vendor::Crypto::Hash; +func Closed() -> 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" + +# Open ONLY Main — Util/Closed stay closed (indexed by initialized → scanWorkspace) +{ + 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"'}}}' + # rename Io under Std (Main open; Util closed) → Net + rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/rename","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":12},"newName":"Net"}}' + rpc '{"jsonrpc":"2.0","id":3,"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.14.0' "$TMP/out.txt"; then + echo "WARN: version not 0.14.0" +fi + +# Io→Net must hit Main + closed Util (≥2). URI for Util should appear in changes. +net_edits=$(grep -o '"newText":"Net"' "$TMP/out.txt" | wc -l) +if [[ "$net_edits" -lt 2 ]]; then + echo "FAIL: Io→Net expected ≥2 edits with Util closed, got $net_edits" + cat "$TMP/out.txt" + exit 1 +fi +echo " path Io→Net edits (Main open, Util closed): $net_edits" + +# Workspace edit should mention Util.bux +if ! grep -q 'Util.bux' "$TMP/out.txt"; then + echo "FAIL: rename did not include closed Util.bux in WorkspaceEdit" + cat "$TMP/out.txt" + exit 1 +fi +echo " closed Util.bux included in WorkspaceEdit" + +echo "PASS: LSP workspace import path index (0.14)"