From 236eea71886d702857018f724514f5489f22acaa Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Wed, 5 Aug 2026 07:08:11 +1000 Subject: [PATCH] =?UTF-8?q?pi-lsp-extension:=20strip=20dev=20cruft=20(.ral?= =?UTF-8?q?ph/.github/specs/tests)=20=E2=80=94=20keep=20runtime=20files=20?= =?UTF-8?q?only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pi-lsp-extension/.github/CODEOWNERS | 1 - .../.github/workflows/publish.yml | 22 - .../2026-03-16T17-23-27-334Z-feature.json | 57 --- .../2026-03-16T17-56-16-080Z-feature.json | 57 --- .../2026-03-16T18-22-19-450Z-feature.json | 57 --- .../pi-lsp-extension/.ralph/scratchpad.md | 26 -- .../pi-lsp-extension/specs/code-actions.md | 180 -------- .../specs/completions-tool.md | 102 ----- .../specs/custom-tui-rendering.md | 152 ------- .../specs/query-position-resolution.md | 249 ----------- .../pi-lsp-extension/specs/signature-help.md | 126 ------ .../specs/structural-search-rewrite.md | 142 ------- extensions/pi-lsp-extension/specs/tests.md | 286 ------------- .../specs/tree-sitter-integration.md | 82 ---- .../test-structural-search.ts | 296 ------------- .../pi-lsp-extension/test-tree-sitter.ts | 397 ------------------ extensions/pi-lsp-extension/tsconfig.json | 14 - 17 files changed, 2246 deletions(-) delete mode 100644 extensions/pi-lsp-extension/.github/CODEOWNERS delete mode 100644 extensions/pi-lsp-extension/.github/workflows/publish.yml delete mode 100644 extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-23-27-334Z-feature.json delete mode 100644 extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-56-16-080Z-feature.json delete mode 100644 extensions/pi-lsp-extension/.ralph/loops/2026-03-16T18-22-19-450Z-feature.json delete mode 100644 extensions/pi-lsp-extension/.ralph/scratchpad.md delete mode 100644 extensions/pi-lsp-extension/specs/code-actions.md delete mode 100644 extensions/pi-lsp-extension/specs/completions-tool.md delete mode 100644 extensions/pi-lsp-extension/specs/custom-tui-rendering.md delete mode 100644 extensions/pi-lsp-extension/specs/query-position-resolution.md delete mode 100644 extensions/pi-lsp-extension/specs/signature-help.md delete mode 100644 extensions/pi-lsp-extension/specs/structural-search-rewrite.md delete mode 100644 extensions/pi-lsp-extension/specs/tests.md delete mode 100644 extensions/pi-lsp-extension/specs/tree-sitter-integration.md delete mode 100644 extensions/pi-lsp-extension/test-structural-search.ts delete mode 100644 extensions/pi-lsp-extension/test-tree-sitter.ts delete mode 100644 extensions/pi-lsp-extension/tsconfig.json diff --git a/extensions/pi-lsp-extension/.github/CODEOWNERS b/extensions/pi-lsp-extension/.github/CODEOWNERS deleted file mode 100644 index 43fc11f..0000000 --- a/extensions/pi-lsp-extension/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @samfoy \ No newline at end of file diff --git a/extensions/pi-lsp-extension/.github/workflows/publish.yml b/extensions/pi-lsp-extension/.github/workflows/publish.yml deleted file mode 100644 index 535a024..0000000 --- a/extensions/pi-lsp-extension/.github/workflows/publish.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Publish - -on: - release: - types: [published] - -jobs: - publish: - runs-on: ubuntu-latest - environment: npm - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: npm - registry-url: https://registry.npmjs.org - - run: npm ci - - run: npm publish --provenance --access public diff --git a/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-23-27-334Z-feature.json b/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-23-27-334Z-feature.json deleted file mode 100644 index 7694cd3..0000000 --- a/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-23-27-334Z-feature.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "1773681807334-feature", - "presetName": "feature", - "prompt": "implement specs/tree-sitter-integration.md", - "startTime": 1773681807334, - "endTime": 1773683050481, - "outcome": "Task complete βœ“", - "iterations": 4, - "history": [ - { - "hat": "planner", - "event": "plan.start", - "iteration": 1 - }, - { - "hat": "builder", - "event": "tasks.ready", - "iteration": 2 - }, - { - "hat": "reviewer", - "event": "build.done", - "iteration": 3 - }, - { - "hat": "committer", - "event": "review.approved", - "iteration": 4 - } - ], - "iterationLogs": [ - { - "iteration": 1, - "hatKey": "planner", - "hatName": "πŸ“‹ Planner", - "event": "tasks.ready", - "summary": "\n\nPlanning is complete. Here's the summary:\n\nThe spec calls for a tree-sitter layer that provides zero-config code intelligence as a fallback when no LSP server is running. The plan covers:\n\n**New files (5):**\n- `src/tree-sitter/parser-manager.ts` β€” WASM parser loading/caching per language\n- `src/tree-sitter/symbol-extractor.ts` β€” AST walking to extract symbols, find definitions, get node info at positions\n- `src/tree-sitter/workspace-index.ts` β€” project-wide symbol index (capped at 5k files, incremental updates)\n- `src/resolve-provider.ts` β€” routing helper: LSP β†’ tree-sitter β†’ \"no intelligence available\"\n- `src/tools/code-overview.ts` β€” new tool for project structure summaries\n\n**Modified files (6):**\n- `src/tools/symbols.ts`, `definition.ts`, `hover.ts`, `diagnostics.ts` β€” each gets a tree-sitter fallback path\n- `src/index.ts` β€” wire up TreeSitterManager, WorkspaceIndex, register code_overview\n- `src/file-sync.ts` β€” invalidate tree-sitter cache on file changes\n\n**Dependencies:** `web-tree-sitter` + `tree-sitter-wasms` (pre-built grammar WASMs, loaded lazily per language)\n\n**Key design decisions:**\n- Grammars come from `tree-sitter-wasms` npm package (lazy-loaded, no download script)\n- LSP always takes priority; tree-sitter is fallback only\n- Workspace index caps at 5000 files initially, indexes remaining on demand\n- Same output formats as existing LSP tools so the LLM sees consistent results\n\nThe scratchpad has the full step-by-step breakdown. Ready for the Builder hat.", - "timestamp": 1773681927017 - }, - { - "iteration": 2, - "hatKey": "builder", - "hatName": "βš™οΈ Builder", - "event": "build.done", - "summary": "Implementation complete. Here's what I built:\n\n5 new files and 6 modified files implementing the tree-sitter integration spec. The core layer (`parser-manager.ts`, `symbol-extractor.ts`, `workspace-index.ts`) provides WASM-based parsing with per-language symbol extraction for TS/JS, Python, Rust, Go, Java, C/C++, and Ruby. The `resolve-provider.ts` routes each tool call to LSP (if running) or tree-sitter (fallback). All four existing tools (`symbols`, `definition`, `hover`, `diagnostics`) now gracefully degrade to tree-sitter when no LSP server is available. A new `code_overview` tool summarizes project structure using tree-sitter symbol extraction.\n\nKey discovery during implementation: `web-tree-sitter@0.26.x` has ABI incompatibility with `tree-sitter-wasms` pre-built grammars β€” pinned to `0.24.7` which works correctly. All grammars load and parse successfully, and the project compiles cleanly with zero TypeScript errors.", - "timestamp": 1773682806836 - }, - { - "iteration": 3, - "hatKey": "reviewer", - "hatName": "πŸ”Ž Reviewer", - "event": "review.approved", - "summary": "## Review Summary: βœ… Pass (2 fixes applied)\n\n### Bugs Fixed\n\n1. **`export_statement` dead code** (high severity) β€” `TS_JS_SYMBOLS` included an `export_statement` mapping that shadowed the intended unwrapping logic in `extractFromNode`. All exported TS/JS declarations were either incorrectly named or silently dropped. **Fixed** by removing the conflicting mapping entry. Verified all export patterns now extract correctly.\n\n2. **Unused `prefix` parameter** (low) β€” `WorkspaceIndex.addSymbols()` had a dead `prefix?` parameter. Removed.\n\n### Spec Compliance: Complete\nAll requirements met β€” zero-config fallback for 19 languages, graceful LSPβ†’tree-sitter routing, `code_overview` tool, workspace indexing with 5000-file cap, FileSync integration, `[tree-sitter]` annotations, and syntax-only diagnostic disclaimer.\n\n### Compilation: Clean\n`tsc --noEmit` passes. WASM ABI compatibility verified at runtime.", - "timestamp": 1773683001658 - } - ] -} \ No newline at end of file diff --git a/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-56-16-080Z-feature.json b/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-56-16-080Z-feature.json deleted file mode 100644 index cb43d7a..0000000 --- a/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T17-56-16-080Z-feature.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "1773683776080-feature", - "presetName": "feature", - "prompt": "specs/completions-tool.md", - "startTime": 1773683776080, - "endTime": 1773684508903, - "outcome": "Task complete βœ“", - "iterations": 4, - "history": [ - { - "hat": "planner", - "event": "plan.start", - "iteration": 1 - }, - { - "hat": "builder", - "event": "tasks.ready", - "iteration": 2 - }, - { - "hat": "reviewer", - "event": "build.done", - "iteration": 3 - }, - { - "hat": "committer", - "event": "review.approved", - "iteration": 4 - } - ], - "iterationLogs": [ - { - "iteration": 1, - "hatKey": "planner", - "hatName": "πŸ“‹ Planner", - "event": "tasks.ready", - "summary": "Everything is properly wired up.\n\n---\n\n## βœ… Summary\n\n### What was done\n\nImplemented the `lsp_completions` tool per the spec in `specs/completions-tool.md`:\n\n**New file: `src/tools/completions.ts`** (~210 lines)\n- Takes `path`, `line`, `character`, and optional `limit` (default 20) parameters\n- Sends `textDocument/completion` to the LSP server\n- Normalizes response (handles both `CompletionList` and `CompletionItem[]`)\n- Sorts by LSP `sortText` ranking, takes top N items\n- Resolves items in parallel via `completionItem/resolve` (with 2s per-item timeout) for full documentation and signatures\n- Formats output as compact text: kind (padded), label+detail, doc summary (first 2 lines, 120 char cap)\n- Includes `renderCall`/`renderResult` for TUI display\n- Handles: no LSP server, server doesn't support completions, empty results, request failures\n\n**Modified: `src/index.ts`**\n- Added import and registration of `createCompletionsTool`\n- Updated header comment\n\n### Design decisions (from spec open questions)\n- **Skipped synthetic trigger** for v1 β€” inserting/reverting `.` through didChange risks race conditions with diagnostics. LLM can type `obj.` directly.\n- **Resolve all top N items** in parallel with 2s timeout per item β€” uses `Promise.allSettled` so failures fall back gracefully to unresolved items.\n- **No filter parameter** for v1 β€” LSP sort order + limit handles prioritization.\n- **No tree-sitter fallback** β€” completions fundamentally require type info (per spec non-goals).", - "timestamp": 1773683999660 - }, - { - "iteration": 2, - "hatKey": "builder", - "hatName": "βš™οΈ Builder", - "event": "build.done", - "summary": "## Summary\n\nImplemented the **synthetic trigger** feature for `lsp_completions` β€” the main missing piece from the spec. The tool already had solid core functionality (completions, resolve, formatting). Here's what I added:\n\n### Changes to `src/tools/completions.ts`:\n\n1. **`trigger` parameter** β€” `\"auto\"` (default) or `\"none\"`. When auto, the tool detects if the cursor is at the end of an identifier and temporarily inserts a `.` to trigger member completions without requiring the LLM to edit the file.\n\n2. **`shouldSyntheticTrigger()` helper** β€” checks if position is after an identifier char (`\\w`, `)`, `]`, `>`) and there's no dot already present.\n\n3. **`insertDot()` helper** β€” string manipulation to insert `.` at a position.\n\n4. **Modified execute flow** β€” insert dot β†’ `didChange` to LSP β†’ request completions β†’ **always revert** in `finally` block β†’ format results. Falls through gracefully on any failure.\n\n5. **Updated descriptions and render** β€” mentions synthetic trigger in tool description, promptSnippet, and output header.\n\nTypeScript compiles cleanly with `tsc --noEmit`. No changes needed to `index.ts` β€” the tool was already registered.", - "timestamp": 1773684274098 - }, - { - "iteration": 3, - "hatKey": "reviewer", - "hatName": "πŸ”Ž Reviewer", - "event": "review.approved", - "summary": "## πŸ”Ž Review Complete\n\n### Spec Compliance: βœ… All requirements met\n\nThe implementation covers every feature in the spec: tool parameters, synthetic trigger, kind labels, doc summaries, sort/limit, parallel resolve with timeout, registration, and rendering.\n\n### Bug Found: Version Desync πŸ›\n\n**The synthetic trigger sends `didChange` with hardcoded versions 99990/99991 directly to the LSP client, bypassing FileSync's version tracker.** After the revert, FileSync still thinks the document is at a low version (e.g. 3), but the LSP server expects > 99991. The next real edit will send version 4, which monotonic-version-enforcing LSP servers will reject β€” causing stale diagnostics and completions.\n\n**Recommended fix:** Add a `withTemporaryContent(uri, content, fn)` method to LspManager that coordinates version tracking with FileSync, keeping the completions tool clean and making temporary edits reusable.\n\n### Verdict: **β†’ Back to Builder** to fix the version desync before proceeding to commit.", - "timestamp": 1773684389440 - } - ] -} \ No newline at end of file diff --git a/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T18-22-19-450Z-feature.json b/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T18-22-19-450Z-feature.json deleted file mode 100644 index 2bb25b3..0000000 --- a/extensions/pi-lsp-extension/.ralph/loops/2026-03-16T18-22-19-450Z-feature.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "1773685339450-feature", - "presetName": "feature", - "prompt": "specs/structural-search-rewrite.md", - "startTime": 1773685339450, - "endTime": 1773686879376, - "outcome": "Task complete βœ“", - "iterations": 4, - "history": [ - { - "hat": "planner", - "event": "plan.start", - "iteration": 1 - }, - { - "hat": "builder", - "event": "tasks.ready", - "iteration": 2 - }, - { - "hat": "reviewer", - "event": "build.done", - "iteration": 3 - }, - { - "hat": "committer", - "event": "review.approved", - "iteration": 4 - } - ], - "iterationLogs": [ - { - "iteration": 1, - "hatKey": "planner", - "hatName": "πŸ“‹ Planner", - "event": "tasks.ready", - "summary": "---\n\n## Summary\n\n### πŸ“‹ Plan β†’ πŸ”¨ Build β†’ πŸ” Review β€” Complete\n\nImplemented the **Structural Code Search & Rewrite** feature per `specs/structural-search-rewrite.md`.\n\n### New files (5):\n\n| File | Lines | Purpose |\n|------|-------|---------|\n| `src/tree-sitter/pattern-compiler.ts` | ~200 | Parses metavariable patterns (`$NAME`, `$$$NAME`) into matchable `PatternNode` trees. Handles wrapper contexts for expression/statement fragments. |\n| `src/tree-sitter/search-engine.ts` | ~290 | AST matching engine β€” walks target trees depth-first, matches against pattern with field-name-aware child matching. Collects files by language, caps at 5000 files. |\n| `src/tree-sitter/rewrite-engine.ts` | ~100 | Substitutes captured metavariable values into replacement templates. Applies changes bottom-up to preserve byte offsets. |\n| `src/tools/code-search.ts` | ~100 | `code_search` tool β€” pattern, language, path?, max_results?. Returns matches with file:line, matched text, and metavariable bindings. |\n| `src/tools/code-rewrite.ts` | ~140 | `code_rewrite` tool β€” pattern, replacement, language, path?, dry_run?(default true). Previews or applies structural transformations. |\n\n### Modified files (1):\n\n| File | Change |\n|------|--------|\n| `src/index.ts` | Added imports and `pi.registerTool()` calls for both new tools |\n\n### Key design decisions:\n\n1. **Custom AST matcher instead of tree-sitter queries** β€” tree-sitter's S-expression queries match by node type, not text content. A custom matcher lets us match literal text for concrete nodes and wildcard for metavariables, which is what users expect.\n\n2. **Field-name-aware child matching** β€” pattern children with field names (e.g., `function`, `arguments`, `name`) are matched against target children with the same field name. Extra target children (like TypeScript type annotations) are allowed. This prevents false negatives when the target language has more syntax than the pattern mentions.\n\n3. **Wrapper-based pattern parsing** β€” patterns that a", - "timestamp": 1773686181539 - }, - { - "iteration": 2, - "hatKey": "builder", - "hatName": "βš™οΈ Builder", - "event": "build.done", - "summary": "## Summary\n\nThe structural search & rewrite feature is fully implemented and tested. I found and fixed two bugs in the existing code:\n\n### Bug 1: Semicolon stripping in rewrites\n**`src/tree-sitter/rewrite-engine.ts`** β€” Added `preserveTrailingSemicolon()` so that `var x = 10;` β†’ `const x = 10;` (not `const x = 10`). The matched AST node includes the `;` as part of the statement, and the rewrite engine now preserves it when the replacement template doesn't explicitly include one.\n\n### Bug 2: Pattern compilation fails for Python\n**`src/tree-sitter/pattern-compiler.ts`** β€” `$ARG` is not a valid Python identifier. Added **placeholder preprocessing**: before parsing, metavars are replaced with language-safe placeholders (`$NAME` β†’ `__META_NAME__`, `$$$NAME` β†’ `__VMETA_NAME__`), then decoded back after the AST is built. Also added Python-specific wrapper strategies (Python uses `module` as root, not `program`).\n\n### Test results\n- **`test-structural-search.ts`**: 52/52 passing βœ…\n- **`test-tree-sitter.ts`**: 62/62 passing βœ… (no regressions)\n- **`tsc --noEmit`**: clean compile βœ…", - "timestamp": 1773686551683 - }, - { - "iteration": 3, - "hatKey": "reviewer", - "hatName": "πŸ”Ž Reviewer", - "event": "review.approved", - "summary": "**Review complete β€” PASS βœ…**. The implementation is solid and ready for the commit hat. Four minor non-blocking issues noted in the scratchpad for future improvement.", - "timestamp": 1773686779441 - } - ] -} \ No newline at end of file diff --git a/extensions/pi-lsp-extension/.ralph/scratchpad.md b/extensions/pi-lsp-extension/.ralph/scratchpad.md deleted file mode 100644 index c1d62a1..0000000 --- a/extensions/pi-lsp-extension/.ralph/scratchpad.md +++ /dev/null @@ -1,26 +0,0 @@ -# Ralph Scratchpad - -Preset: feature -Task: specs/structural-search-rewrite.md - ---- - -## Committer β€” Iteration 4 - -### Committed: `6fe71f1` - -``` -feat: add structural code search and rewrite tools -``` - -7 files changed, 1414 insertions: -- `src/tree-sitter/pattern-compiler.ts` β€” metavar placeholder encoding, language wrappers -- `src/tree-sitter/search-engine.ts` β€” recursive AST matcher, file collection -- `src/tree-sitter/rewrite-engine.ts` β€” capture substitution, bottom-up application -- `src/tools/code-search.ts` β€” search tool wrapper -- `src/tools/code-rewrite.ts` β€” rewrite tool wrapper with dry-run -- `src/index.ts` β€” tool registration -- `test-structural-search.ts` β€” 52 tests - -### Status: βœ… DONE -All spec requirements met. 52 tests passing, tsc clean. Feature complete. diff --git a/extensions/pi-lsp-extension/specs/code-actions.md b/extensions/pi-lsp-extension/specs/code-actions.md deleted file mode 100644 index 778aed3..0000000 --- a/extensions/pi-lsp-extension/specs/code-actions.md +++ /dev/null @@ -1,180 +0,0 @@ -# Code Actions Tool - -## Summary - -Add an `lsp_code_actions` tool that retrieves available code actions (quick fixes, refactorings, source actions) for a position or range in a file. This lets the LLM discover and apply IDE-level fixes β€” auto-imports, extract method, fix lint errors β€” that would otherwise require manual knowledge of the codebase. - -## Motivation - -When the LLM encounters a diagnostic error (e.g., "Cannot find name 'Foo'"), it currently has to reason about the fix from scratch. But the LSP server often already knows the fix β€” "Add import from './foo'" β€” via code actions. Exposing these saves time and reduces hallucination. - -The `lsp-pi` competitor exposes `codeAction` as one of its actions and it covers: -- Quick fixes (auto-import, remove unused variable, fix typo) -- Refactorings (extract method, extract variable, inline variable) -- Source actions (organize imports, add missing members) - -## Goals - -- **`lsp_code_actions` tool** β€” return available actions for a position or diagnostic range -- **Diagnostic-aware** β€” when called with a position that has a diagnostic, include the diagnostic context so the server returns relevant fixes -- **Actionable output** β€” format actions so the LLM knows what each one does and can apply the edits -- **Preferred actions** β€” highlight preferred/auto-fix actions (the server marks these) - -## Non-goals - -- Executing code actions automatically β€” the tool returns the action list and their edits; the LLM uses `edit`/`write` to apply -- Command-based actions β€” some code actions return a `command` instead of `edit`. We report these but can't execute them (they require IDE integration) -- Resolving lazy actions β€” `codeAction/resolve` for actions that defer their edit computation. This can be a follow-up if needed. - -## Design - -### Tool definition - -**`lsp_code_actions`** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `path` | string | File path | -| `line` | number | Line number (1-indexed) | -| `character` | number | Column number (1-indexed) | -| `endLine` | number? | End line for range selection (1-indexed). Defaults to `line`. | -| `endCharacter` | number? | End column for range selection (1-indexed). Defaults to `character`. | -| `kind` | string? | Filter by action kind (e.g., `"quickfix"`, `"refactor"`, `"source"`) | - -### LSP request - -```typescript -// Collect diagnostics at the position to include as context -const uri = manager.getFileUri(filePath); -const allDiags = client.getDiagnostics(uri); -const rangeDiags = allDiags.filter(d => rangeContainsPosition(d.range, line, character)); - -const result = await client.sendRequest("textDocument/codeAction", { - textDocument: { uri }, - range: { - start: { line: params.line - 1, character: params.character - 1 }, - end: { line: (params.endLine ?? params.line) - 1, character: (params.endCharacter ?? params.character) - 1 }, - }, - context: { - diagnostics: rangeDiags, - only: params.kind ? [params.kind] : undefined, - }, -}); -``` - -Including matching diagnostics in the `context` is critical β€” many servers (jdtls, pyright, typescript-language-server) only return quick fixes when the relevant diagnostic is in the context. - -### Output format - -``` -5 code actions at src/handler.ts:42:10 - - 1. β˜… Add import from './types' [quickfix] - Edits: src/handler.ts:1:1 insert "import { Foo } from './types';\n" - - 2. β˜… Remove unused variable 'x' [quickfix] - Edits: src/handler.ts:42:3-42:15 delete - - 3. Extract to function in module scope [refactor.extract] - Edits: (resolve required) - - 4. Convert to arrow function [refactor.rewrite] - Edits: src/handler.ts:40:1-45:2 replace - - 5. Organize imports [source.organizeImports] - Edits: (resolve required) -``` - -Key formatting: -- **β˜…** marks preferred actions (`isPreferred: true`) β€” these are the auto-fix candidates -- **Kind** shown in brackets β€” helps the LLM filter mentally -- **Edits** shown inline if the action includes a `WorkspaceEdit`. Format matches `lsp_rename` output -- **"resolve required"** for actions that have no edit but have a `data` field (need `codeAction/resolve`) -- Actions are sorted: preferred first, then by kind (quickfix β†’ refactor β†’ source β†’ other) - -### Details object - -```typescript -interface CodeActionsDetails { - count: number; - preferredCount: number; - actions: Array<{ - title: string; - kind?: string; - isPreferred: boolean; - hasEdit: boolean; - hasCommand: boolean; - }>; -} -``` - -### TUI rendering - -**`renderCall`:** -``` -lsp_code_actions src/handler.ts:42:10 -``` - -With kind filter: -``` -lsp_code_actions src/handler.ts:42:10 [quickfix] -``` - -With range: -``` -lsp_code_actions src/handler.ts:42:10-45:2 -``` - -**`renderResult` (collapsed):** -``` -5 actions (2 preferred) -``` - -Or if no actions: -``` -No code actions available -``` - -### Handling edge cases - -- **No actions available** β€” return "No code actions available at this position." -- **Command-only actions** β€” report the title and note that it requires IDE execution: "This action requires IDE execution and cannot be applied via edit/write." -- **Large edit sets** β€” truncate edits to first 20 per action, note remaining count -- **No LSP server** β€” return standard unavailable reason -- **Server doesn't support code actions** β€” check `capabilities.codeActionProvider` and return a clear message - -### File layout - -``` -src/tools/ -└── code-actions.ts -``` - -### Integration - -- Register in `index.ts` alongside existing tools -- Add to system prompt: "When `lsp_diagnostics` shows errors, try `lsp_code_actions` at the error position to find available quick fixes before writing a manual fix." -- The workflow is: `lsp_diagnostics` β†’ see error β†’ `lsp_code_actions` at error position β†’ find auto-import fix β†’ apply with `edit` - -### Utility: `rangeContainsPosition` - -Add a shared helper in `src/shared/format.ts`: - -```typescript -export function rangeContainsPosition( - range: Range, - line: number, // 0-indexed - character: number, // 0-indexed -): boolean { - if (line < range.start.line || line > range.end.line) return false; - if (line === range.start.line && character < range.start.character) return false; - if (line === range.end.line && character > range.end.character) return false; - return true; -} -``` - -## Open questions - -- **`codeAction/resolve`** β€” should we automatically resolve actions that have `data` but no `edit`? This adds a round-trip per action but gives the LLM the actual edits. Could be gated: resolve only preferred actions, or only when the action count is small (< 5). -- **Applying actions** β€” should the tool have an `apply` mode that applies a specific action's edits directly? Pro: simpler workflow. Con: the LLM can already use the edit tool with the provided edit locations. Lean towards keeping it read-only for v1, matching how `lsp_rename` works. -- **Interaction with diagnostics auto-append** β€” when the auto-diagnostics hook fires after a write/edit and shows errors, should it also hint "try lsp_code_actions at line X for quick fixes"? Potentially noisy. diff --git a/extensions/pi-lsp-extension/specs/completions-tool.md b/extensions/pi-lsp-extension/specs/completions-tool.md deleted file mode 100644 index e770964..0000000 --- a/extensions/pi-lsp-extension/specs/completions-tool.md +++ /dev/null @@ -1,102 +0,0 @@ -# Completions Tool - -## Summary - -Add an `lsp_completions` tool that returns completion suggestions at a given position in a file. This lets the LLM discover available methods, properties, and APIs on objects without having to read documentation or source code. - -## Motivation - -When writing code, the LLM often needs to know what methods are available on an object β€” e.g., "what can I call on `s3Client`?" or "what fields does this struct have?". Today it either guesses (risking hallucination) or has to read the source/type definitions manually. - -LSP completion is exactly this: given a cursor position, the language server returns ranked suggestions with type signatures and documentation. Kiro CLI exposes this and it's one of their differentiators. - -## Goals - -- **`lsp_completions` tool** β€” return completion items at a position, with type signatures and docs -- **Smart triggering** β€” the LLM can ask "what methods are available on X" and get actionable results -- **Concise output** β€” return the top N results (default 20) with kind, label, type, and doc summary β€” not the full verbose LSP response - -## Non-goals - -- Snippet expansion or tab-completion UX β€” this is a query tool, not an interactive completion engine -- Commit characters, edit ranges, or other IDE-specific completion metadata -- Tree-sitter fallback β€” completions fundamentally require type information, so this is LSP-only - -## Design - -### Tool definition - -**`lsp_completions`** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `path` | string | File path | -| `line` | number | Line number (1-indexed) | -| `character` | number | Column number (1-indexed) | -| `limit` | number? | Max results to return (default: 20) | - -### Trigger workflow - -The LLM typically uses this in one of two ways: - -1. **Explore an API**: Write `s3Client.` in a file, then call `lsp_completions` at the position after the dot to see available methods. -2. **Verify a method exists**: Before calling `response.bodyAsString()`, check completions on `response.` to confirm the method name. - -In both cases the file needs to be open in the LSP (handled by `FileSync`) and the content needs to reflect the current state. - -### Synthetic trigger - -A common case is "what methods does X have?" where X is already in the code but there's no trailing dot. The tool should support a convenience mode: - -- If the position points to the end of an identifier, the tool temporarily inserts a `.` after it, requests completions, then removes it. This avoids forcing the LLM to edit the file just to explore an API. -- This is optional and gated by a `trigger` parameter (`"auto"` | `"none"`, default `"auto"`). - -### Output format - -``` -20 completions at src/handler.ts:42:15 - -method getObject(params: GetObjectRequest): Promise - Retrieves an object from S3. -method putObject(params: PutObjectRequest): Promise - Uploads an object to S3. -property region: string - The AWS region for this client. -... -``` - -Each item shows: -- **Kind** β€” method, property, function, variable, class, keyword, etc. -- **Label + signature** β€” from `detail` or `labelDetails` in the LSP response -- **Documentation** β€” first 1-2 lines of the doc comment, if available - -Items are sorted by the LSP server's `sortText` ranking (which considers scope, type match, and usage frequency). - -### Resolve for details - -Many LSP servers return minimal items in the initial response and require a `completionItem/resolve` call for documentation and full signatures. The tool should: - -1. Request completions at the position -2. For the top N items, call `completionItem/resolve` in parallel to get full details -3. Merge results and format output - -This adds latency but dramatically improves output quality. Cap resolve calls to the `limit` parameter to bound cost. - -### File layout - -``` -src/tools/ -└── completions.ts -``` - -### Integration - -- Register in `index.ts` alongside existing tools -- Add to the extension's system prompt snippet: mention that `lsp_completions` is available for discovering methods and properties -- No tree-sitter fallback β€” return a clear message if no LSP server is running - -## Open questions - -- **Synthetic trigger safety** β€” temporarily modifying file content to insert a `.` could cause issues if the LSP processes the change and emits diagnostics before we revert. Should we use a separate virtual document, or is the insert-request-revert cycle fast enough to be safe? -- **Performance** β€” `completionItem/resolve` for 20 items could be slow on some servers. Should we resolve lazily (only top 5) or make it configurable? -- **Filter parameter** β€” should the tool accept a `filter` string to pre-filter results (e.g., only methods, only properties)? Or is that over-engineering for v1? diff --git a/extensions/pi-lsp-extension/specs/custom-tui-rendering.md b/extensions/pi-lsp-extension/specs/custom-tui-rendering.md deleted file mode 100644 index eb6df5f..0000000 --- a/extensions/pi-lsp-extension/specs/custom-tui-rendering.md +++ /dev/null @@ -1,152 +0,0 @@ -# Custom TUI Rendering - -## Summary - -Add `renderCall` and `renderResult` methods to the two tools that currently lack them (`code_search` and `code_rewrite`), bringing all tools to parity with the rendering quality of the LSP tools. - -## Motivation - -The `lsp-pi` competitor has polished TUI rendering for its unified `lsp` tool β€” themed headers, collapsible result previews, match counts. Our LSP tools already have good rendering (hover, diagnostics, rename, etc.), but `code_search` and `code_rewrite` fall back to the default pi rendering which dumps raw JSON-ish output. - -These are high-visibility tools β€” structural search results and rewrite previews benefit significantly from custom formatting. - -## Goals - -- **`code_search`**: show a compact one-liner for the call (pattern + language + scope), and a results view with match counts, file grouping, and highlighted captured metavariables -- **`code_rewrite`**: show the call as pattern β†’ replacement, and results as a diff-style before/after preview with file counts -- **Consistent style** β€” match the rendering patterns used by the existing LSP tools (themed labels, accent colors for paths, dim for metadata, success/error for status) - -## Non-goals - -- Interactive expansion/collapse of individual matches β€” pi's TUI doesn't support that level of interactivity in tool results -- Syntax highlighting of matched code β€” theme colors only (muted, accent, etc.), not language-aware highlighting - -## Design - -### `code_search` rendering - -#### `renderCall` - -``` -code_search console.log($ARG) typescript src/ -``` - -Format: `{toolTitle bold} {pattern accent} {language dim} {path dim}` - -If no path is given, omit it (workspace root is the default). - -#### `renderResult` - -**Collapsed (default):** -``` -23 matches in 8 files -``` - -Use `success` color if matches found, `dim` if zero matches. - -**Expanded:** -``` -23 matches in 8 files - -src/handler.ts: - 42:5 console.log(response) $ARG = response - 67:3 console.log("done") $ARG = "done" - -src/utils.ts: - 12:1 console.log(err.message) $ARG = err.message - ... -``` - -Group by file, show line:col, matched text (truncated to ~60 chars), and metavariable bindings. Cap at 20 matches in collapsed view, show all in expanded. - -### `code_rewrite` rendering - -#### `renderCall` - -``` -code_rewrite var $N = $V β†’ const $N = $V javascript [dry-run] -``` - -Format: `{toolTitle bold} {pattern accent} β†’ {replacement accent} {language dim} {mode dim}` - -Show `[dry-run]` or `[apply]` based on the `dry_run` parameter. - -#### `renderResult` - -**Collapsed (default):** -``` -12 replacements in 5 files (dry-run) -``` - -Or after apply: -``` -βœ“ 12 replacements applied in 5 files -``` - -**Expanded:** -``` -12 replacements in 5 files (dry-run) - -src/handler.ts: - 42: - var count = 0 - + const count = 0 - 67: - var name = "foo" - + const name = "foo" - -src/utils.ts: - 12: - var x = arr.length - + const x = arr.length - ... -``` - -Use `error` color for `-` lines, `success` color for `+` lines. Cap preview at 10 replacements collapsed, all in expanded. - -### Implementation - -Both tools already return structured `details` objects β€” the rendering just needs to read from them. - -#### `code_search` details shape (existing) - -```typescript -interface CodeSearchDetails { - matchCount: number; - fileCount: number; - matches: Array<{ - file: string; - line: number; - column: number; - text: string; - captures: Record; - }>; -} -``` - -#### `code_rewrite` details shape (existing) - -```typescript -interface CodeRewriteDetails { - replacementCount: number; - fileCount: number; - dryRun: boolean; - replacements: Array<{ - file: string; - line: number; - before: string; - after: string; - }>; -} -``` - -If the current details shapes don't have all these fields, extend them as needed. - -### File changes - -- `src/tools/code-search.ts` β€” add `renderCall` and `renderResult` methods -- `src/tools/code-rewrite.ts` β€” add `renderCall` and `renderResult` methods - -No new files needed. - -## Open questions - -- **Theme tokens** β€” the existing tools use `toolTitle`, `accent`, `dim`, `muted`, `success`, `error`, `warning`. Are there additional tokens available for diff-style rendering (e.g., `added`, `removed`)? -- **Expanded state** β€” does pi pass `expanded` in the `renderResult` options? Need to verify the `RenderResultOptions` type. The diagnostics tool already checks `options.expanded`. diff --git a/extensions/pi-lsp-extension/specs/query-position-resolution.md b/extensions/pi-lsp-extension/specs/query-position-resolution.md deleted file mode 100644 index 2786ebc..0000000 --- a/extensions/pi-lsp-extension/specs/query-position-resolution.md +++ /dev/null @@ -1,249 +0,0 @@ -# Query-Based Position Resolution - -## Summary - -Add a `query` parameter to position-based tools (`lsp_hover`, `lsp_definition`, `lsp_references`, `lsp_rename`, `lsp_signature_help`, `lsp_code_actions`) that resolves a symbol name to a file position, eliminating the need for the LLM to know exact line/column numbers when it just wants to query a known symbol. - -## Motivation - -Today, to hover over a function named `handleRequest`, the LLM must: - -1. Know or look up the exact line and column where `handleRequest` is defined or used -2. Call `lsp_hover` with `path`, `line`, `character` - -This is friction. The LLM often knows the symbol name but not the exact position. It has to call `lsp_symbols` first to find the position, then call the actual tool β€” two round-trips. - -The `lsp-pi` competitor solves this with a `query` parameter: pass `query: "handleRequest"` and it resolves the position via document symbols before making the LSP request. This is a significant UX improvement. - -## Goals - -- **Optional `query` parameter** on all position-based tools -- **Symbol name resolution** β€” find the symbol's position in the file via document symbols (LSP) or tree-sitter -- **Fallback chain** β€” try LSP document symbols first, then tree-sitter symbol extraction -- **Exact > partial** β€” exact name match wins over substring match -- **Transparent** β€” when `query` is used, include the resolved position in the output so the LLM knows where it landed - -## Non-goals - -- Workspace-wide symbol search via query β€” this is file-scoped resolution. Use `lsp_symbols` with a `query` for workspace search. -- Fuzzy matching β€” exact and substring only. Fuzzy introduces ambiguity. -- Replacing `line`/`character` β€” query is an alternative, not a replacement. When both are provided, `line`/`character` take precedence. - -## Design - -### Parameter changes - -Add to all position-based tool schemas: - -```typescript -query: Type.Optional(Type.String({ - description: "Symbol name to find in the file. Alternative to line/character β€” resolves the symbol's position automatically." -})), -``` - -Tools that gain the `query` parameter: -- `lsp_hover` -- `lsp_definition` -- `lsp_references` -- `lsp_rename` -- `lsp_signature_help` (new tool) -- `lsp_code_actions` (new tool) - -### Resolution logic - -Create a shared resolver in `src/shared/resolve-position.ts`: - -```typescript -export interface ResolvedPosition { - line: number; // 0-indexed (LSP convention) - character: number; // 0-indexed - symbolName: string; // the matched symbol name - source: "lsp" | "tree-sitter"; -} - -export async function resolveSymbolPosition( - filePath: string, - query: string, - manager: LspManager, - treeSitter?: TreeSitterManager | null, -): Promise { - // 1. Try LSP document symbols - const client = manager.getRunningClient(manager.getLanguageId(filePath) ?? ""); - if (client) { - const uri = manager.getFileUri(filePath); - try { - const symbols = await client.sendRequest( - "textDocument/documentSymbol", - { textDocument: { uri } } - ); - const match = findSymbolPosition(symbols, query); - if (match) return { ...match, source: "lsp" }; - } catch { /* fall through */ } - } - - // 2. Try tree-sitter - if (treeSitter) { - const absPath = manager.resolvePath(filePath); - const content = await readFile(absPath, "utf-8"); - const tree = await treeSitter.parse(absPath, content); - if (tree) { - const match = findSymbolInTree(tree, query); - if (match) return { ...match, source: "tree-sitter" }; - } - } - - return null; -} -``` - -### Symbol matching (`findSymbolPosition`) - -Walks the document symbol tree (recursive for children) and matches: - -1. **Exact match** β€” `symbol.name === query` (case-sensitive) -2. **Case-insensitive exact** β€” `symbol.name.toLowerCase() === query.toLowerCase()` -3. **Substring match** β€” `symbol.name.toLowerCase().includes(query.toLowerCase())` - -Returns the first match in priority order. Uses `selectionRange.start` (preferred) or `range.start` for position. - -### Tree-sitter symbol matching (`findSymbolInTree`) - -Uses the existing `extractSymbols` function from `symbol-extractor.ts`: - -1. Extract all symbols from the tree -2. Match using the same priority chain as above -3. Return the symbol's start position - -### Tool integration pattern - -Each tool's `execute` function gains a preamble: - -```typescript -async execute(_toolCallId, params) { - const filePath = params.path.replace(/^@/, ""); - let line = params.line; - let character = params.character; - let resolvedFrom: string | undefined; - - // Resolve position from query if line/character not provided - if ((line === undefined || character === undefined) && params.query) { - const resolved = await resolveSymbolPosition(filePath, params.query, manager, treeSitter); - if (resolved) { - line = resolved.line + 1; // convert to 1-indexed - character = resolved.character + 1; - resolvedFrom = `Resolved "${params.query}" β†’ ${line}:${character} [${resolved.source}]`; - } else { - return { - content: [{ type: "text", text: `Could not find symbol "${params.query}" in ${filePath}` }], - details: { hasResult: false }, - }; - } - } - - if (line === undefined || character === undefined) { - return { - content: [{ type: "text", text: "Either line/character or query is required." }], - details: { hasResult: false }, - }; - } - - // ... existing tool logic using line/character ... - - // Prepend resolved position to output if query was used - if (resolvedFrom) { - const existingText = result.content[0]?.text ?? ""; - result.content[0] = { type: "text", text: `${resolvedFrom}\n\n${existingText}` }; - } - - return result; -} -``` - -### Validation rules - -- If both `line`/`character` and `query` are provided: use `line`/`character`, ignore `query` -- If neither is provided: return error "Either line/character or query is required." -- If `query` is provided but doesn't match any symbol: return error with the file's top-level symbol names as a hint -- `line` and `character` remain individually required when `query` is not used (can't provide just `line` without `character`) - -### Parameter schema update - -The `line` and `character` parameters change from required to optional: - -```typescript -// Before -line: Type.Number({ description: "Line number (1-indexed)" }), -character: Type.Number({ description: "Column number (1-indexed)" }), - -// After -line: Type.Optional(Type.Number({ description: "Line number (1-indexed). Required unless query is provided." })), -character: Type.Optional(Type.Number({ description: "Column number (1-indexed). Required unless query is provided." })), -query: Type.Optional(Type.String({ description: "Symbol name to find in the file. Alternative to line/character." })), -``` - -### `renderCall` updates - -When query is used instead of line/character: - -``` -lsp_hover src/handler.ts query="handleRequest" -``` - -When line/character is used (unchanged): - -``` -lsp_hover src/handler.ts:42:10 -``` - -### System prompt update - -Add a guideline: - -``` -Position-based LSP tools (lsp_hover, lsp_definition, lsp_references, lsp_rename, lsp_signature_help, lsp_code_actions) accept either line/character or a query parameter. Use query when you know the symbol name but not the exact position β€” it resolves the position automatically via document symbols. -``` - -### File layout - -``` -src/shared/ -└── resolve-position.ts # New: shared position resolver - -src/tools/ -β”œβ”€β”€ hover.ts # Modified: add query param -β”œβ”€β”€ definition.ts # Modified: add query param -β”œβ”€β”€ references.ts # Modified: add query param -β”œβ”€β”€ rename.ts # Modified: add query param -β”œβ”€β”€ signature-help.ts # New tool: includes query param -└── code-actions.ts # New tool: includes query param -``` - -## Examples - -### Hover by symbol name -``` -lsp_hover path="src/handler.ts" query="handleRequest" -β†’ Resolved "handleRequest" β†’ 42:10 [lsp] -β†’ function handleRequest(req: Request): Promise -``` - -### Find references by name -``` -lsp_references path="src/types.ts" query="UserConfig" -β†’ Resolved "UserConfig" β†’ 15:14 [tree-sitter] -β†’ 12 references in 5 files -``` - -### Rename by name -``` -lsp_rename path="src/utils.ts" query="formatDate" newName="formatDateTime" -β†’ Resolved "formatDate" β†’ 8:17 [lsp] -β†’ Rename "formatDateTime": 23 edit(s) across 7 file(s) -``` - -## Open questions - -- **Ambiguity** β€” what if a file has multiple symbols with the same name (e.g., overloaded methods, or a variable and a type with the same name)? Current design returns the first exact match. Should we return all matches and let the LLM pick? Or is first-match good enough? -- **Nested symbols** β€” `findSymbolPosition` walks children recursively, so `query="render"` could match a top-level `render` function or a `render` method inside a class. Should class-qualified names be supported (e.g., `query="MyComponent.render"`)? -- **Performance** β€” requesting document symbols adds a round-trip before the actual request. For LSP, this is typically fast (<50ms). For tree-sitter, it's a file parse. Acceptable for the ergonomic benefit. -- **`lsp_completions`** β€” should completions also support query? Less clear β€” completions are about a cursor position in code, not a symbol name. Leaving out for now. diff --git a/extensions/pi-lsp-extension/specs/signature-help.md b/extensions/pi-lsp-extension/specs/signature-help.md deleted file mode 100644 index 263f7ea..0000000 --- a/extensions/pi-lsp-extension/specs/signature-help.md +++ /dev/null @@ -1,126 +0,0 @@ -# Signature Help Tool - -## Summary - -Add an `lsp_signature_help` tool that returns function/method signature information at a call site β€” parameter names, types, documentation, and which parameter is currently active. This complements `lsp_hover` (which shows type info for a symbol) and `lsp_completions` (which lists available members). - -## Motivation - -When the LLM is writing a function call with multiple parameters, it needs to know the parameter order, types, and what each parameter does. Today it either: - -1. Calls `lsp_hover` on the function name β€” gets the full signature but no active-parameter context -2. Reads the source/docs β€” expensive and slow -3. Guesses β€” risks hallucination - -LSP's `textDocument/signatureHelp` is designed exactly for this: given a cursor position inside a call's argument list, it returns the signature with the active parameter highlighted. The `lsp-pi` competitor already exposes this as its `signature` action. - -## Goals - -- **`lsp_signature_help` tool** β€” return the active signature, parameters, and documentation at a call site position -- **Active parameter highlighting** β€” clearly indicate which parameter the cursor is on -- **Multiple overloads** β€” when a function has overloads, show the active one and list alternatives -- **Clean output** β€” format for LLM consumption (not raw LSP JSON) - -## Non-goals - -- Trigger character detection (e.g., auto-invoke on `(` or `,`) β€” this is a query tool, not an IDE feature -- Tree-sitter fallback β€” signature help fundamentally requires type resolution; no useful fallback possible -- Retrigger logic β€” the LSP retrigger context is for interactive editors; we do a single request - -## Design - -### Tool definition - -**`lsp_signature_help`** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `path` | string | File path | -| `line` | number | Line number (1-indexed) | -| `character` | number | Column number (1-indexed) | - -No `query` parameter β€” signature help is inherently position-based (cursor must be inside the argument list). - -### LSP request - -```typescript -const result = await client.sendRequest("textDocument/signatureHelp", { - textDocument: { uri }, - position: { line: params.line - 1, character: params.character - 1 }, -}); -``` - -### Output format - -``` -Signature 1 of 2 (active): - createReadStream(path: PathLike, options?: ReadStreamOptions): ReadStream - - Parameters: - β†’ path: PathLike β€” The path to the file to read - options?: ReadStreamOptions β€” Options for the stream (encoding, start, end, highWaterMark) - - Returns: ReadStream - -Signature 2 of 2: - createReadStream(path: PathLike, encoding: BufferEncoding): ReadStream -``` - -Key formatting rules: -- Show all signatures, mark the active one (`activeSignature` index from LSP response) -- For the active signature, show all parameters with the active one marked with `β†’` -- Include parameter documentation if available (from `SignatureInformation.parameters[].documentation`) -- Include signature-level documentation if available (from `SignatureInformation.documentation`) -- Truncate long documentation to 2 lines per parameter - -### Handling edge cases - -- **No signature help available** β€” return "No signature help available at this position. Cursor must be inside a function call's argument list." -- **Empty parameters** β€” show signature line but skip the parameters section -- **Missing documentation** β€” show parameters with types only, skip doc lines -- **Multiple overloads** β€” show all, sorted by the `activeSignature` first -- **No LSP server running** β€” return the standard unavailable reason message - -### Details object - -```typescript -interface SignatureHelpDetails { - hasResult: boolean; - signatureCount: number; - activeSignature: number; - activeParameter: number; -} -``` - -### TUI rendering - -**`renderCall`:** -``` -lsp_signature_help src/handler.ts:42:15 -``` - -**`renderResult` (collapsed):** -``` -createReadStream(path, options?) β€” param 2 of 2 -``` - -Show the function name and active parameter position. If no result, show "No signature help" in dim. - -### File layout - -``` -src/tools/ -└── signature-help.ts -``` - -### Integration - -- Register in `index.ts` alongside existing tools -- Add to system prompt snippet: "Use `lsp_signature_help` when you need to know the parameter order or types for a function call" -- No tree-sitter fallback β€” return clear message when no LSP is available - -## Open questions - -- **Naming** β€” `lsp_signature_help` vs `lsp_signature` vs `lsp_params`? The LSP protocol calls it "signature help" so `lsp_signature_help` is most precise, but it's verbose. The competitor uses `signature` (as an action on the unified tool). Going with `lsp_signature_help` for consistency with `lsp_diagnostics`, `lsp_definition`, etc. -- **Context parameter** β€” should we accept an optional `context` with `triggerKind` and `triggerCharacter`? Probably not β€” the LLM doesn't know or care about trigger characters. Always send `triggerKind: Invoked`. -- **Interaction with completions** β€” when the LLM is inside a call and wants to know the expected type for the current argument, should it use signature help (to see the parameter type) then completions (to see what values match)? The system prompt should guide this workflow. diff --git a/extensions/pi-lsp-extension/specs/structural-search-rewrite.md b/extensions/pi-lsp-extension/specs/structural-search-rewrite.md deleted file mode 100644 index 64a4f33..0000000 --- a/extensions/pi-lsp-extension/specs/structural-search-rewrite.md +++ /dev/null @@ -1,142 +0,0 @@ -# Structural Code Search & Rewrite - -## Summary - -Add AST-based structural search and rewrite tools powered by tree-sitter's query language. Find and transform code by structure, not text β€” enabling safe, language-aware refactoring that grep and sed can't do. - -## Motivation - -Text-based search (`grep`, `rg`) matches strings, not code structure. Searching for `console.log` catches comments and string literals. Replacing `var` with `const` via regex can break template literals and object keys. - -Kiro CLI offers `pattern_search` and `pattern_rewrite` with metavariable support (`$VAR` matches any single node, `$$$` matches zero-or-more). This is genuinely useful for the LLM β€” it can propose structural transformations and preview them safely. - -## Goals - -- **`code_search` tool** β€” find code matching a structural pattern, returning locations and matched fragments -- **`code_rewrite` tool** β€” transform code matching a pattern into a replacement pattern, with dry-run support -- **Metavariable syntax** β€” `$NAME` for single nodes, `$$$` for variadic sequences, matching Kiro's convention -- **Multi-language** β€” works for any language with a tree-sitter grammar loaded (see tree-sitter-integration spec) - -## Non-goals - -- Type-aware matching (e.g., "find all calls where the argument is a `string`") β€” that needs LSP -- Cross-file rewrite coordination (e.g., updating imports when renaming) β€” use `lsp_rename` for that -- Custom query language exposure β€” we translate metavariable patterns to tree-sitter queries internally - -## Depends on - -- [Tree-sitter Integration](./tree-sitter-integration.md) β€” requires the parser-manager and grammar loading infrastructure - -## Design - -### Pattern syntax - -User-facing patterns use a simplified syntax with metavariables: - -``` -// Single node capture -console.log($ARG) β†’ matches console.log(x), console.log("hello"), etc. - -// Variadic capture -function $NAME($$$PARAMS) { $$$ } β†’ matches any function declaration - -// Literal matching -$OBJ.hasOwnProperty($KEY) β†’ matches foo.hasOwnProperty("bar") -``` - -Internally, patterns are compiled to tree-sitter S-expression queries with `@capture` nodes. - -### Pattern compilation - -1. Parse the pattern string as code in the target language using tree-sitter -2. Walk the resulting AST and replace metavariable identifiers with query captures: - - `$NAME` β†’ `(_) @name` (single node wildcard) - - `$$$PARAMS` β†’ `(_)* @params` (variadic wildcard) - - Literal nodes stay as concrete matchers -3. Produce a tree-sitter query string - -Edge cases: -- Pattern doesn't parse β†’ try wrapping in expression context, then statement context -- Ambiguous parse β†’ return error with suggestion to be more specific - -### Tools - -#### `code_search` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `pattern` | string | Structural pattern with metavariables | -| `language` | string | Target language (typescript, python, rust, etc.) | -| `path` | string? | File or directory to search (default: workspace root) | -| `max_results` | number? | Cap results (default: 50) | - -Returns a list of matches with: -- File path, line number, column -- Matched source text -- Captured metavariable bindings (e.g., `$NAME = "fetchUser"`) - -#### `code_rewrite` - -| Parameter | Type | Description | -|-----------|------|-------------| -| `pattern` | string | Pattern to match | -| `replacement` | string | Replacement pattern using same metavariables | -| `language` | string | Target language | -| `path` | string? | File or directory scope | -| `dry_run` | boolean? | Preview changes without applying (default: true) | - -Returns: -- In dry-run mode: list of planned changes (file, line, before β†’ after) -- In apply mode: summary of changes made, number of files modified - -The tool applies changes via pi's `write` internally (so auto-diagnostics fire and the LLM sees any errors introduced). - -### Matching engine - -For each file in scope: -1. Parse with tree-sitter (reuse cached trees from workspace index) -2. Run the compiled query against the tree -3. Collect matches, extract captured nodes -4. For rewrites: reconstruct the replacement by substituting captured text into the replacement pattern -5. Apply replacements bottom-up (last match first) to preserve byte offsets - -### File layout - -``` -src/ -β”œβ”€β”€ tree-sitter/ -β”‚ β”œβ”€β”€ pattern-compiler.ts # Metavariable pattern β†’ tree-sitter query -β”‚ β”œβ”€β”€ search-engine.ts # Run queries across files, collect matches -β”‚ └── rewrite-engine.ts # Apply replacement patterns -└── tools/ - β”œβ”€β”€ code-search.ts - └── code-rewrite.ts -``` - -## Examples - -### Find all `.unwrap()` calls in Rust -``` -pattern: $E.unwrap() -language: rust -``` - -### Convert `var` to `const` in JavaScript -``` -pattern: var $N = $V -replacement: const $N = $V -language: javascript -``` - -### Find async functions with no await -``` -pattern: async function $NAME($$$) { $$$ } -language: typescript -``` -(Post-filter: check that no captured body node contains `await` β€” this may need a two-pass approach or a `filter` parameter in a future iteration.) - -## Open questions - -- **Pattern ambiguity** β€” some patterns could match at multiple AST levels (expression vs. statement). Should we default to the most specific match, or let the user specify? -- **Replacement formatting** β€” after substitution, should we auto-format the result (e.g., via `prettier` or LSP formatting)? Or leave as-is? -- **Conflict with `lsp_rename`** β€” structural rewrite overlaps with rename for simple cases. The system prompt should guide the LLM: use `lsp_rename` for symbol renames (semantically correct), use `code_rewrite` for structural transformations. diff --git a/extensions/pi-lsp-extension/specs/tests.md b/extensions/pi-lsp-extension/specs/tests.md deleted file mode 100644 index 22f7109..0000000 --- a/extensions/pi-lsp-extension/specs/tests.md +++ /dev/null @@ -1,286 +0,0 @@ -# Test Suite - -## Summary - -Add a comprehensive test suite covering the extension's core modules: LSP client, LSP manager, file sync, tree-sitter engines, tool implementations, and the extension entry point. Tests should run without requiring real LSP servers (except for optional integration tests). - -## Motivation - -The extension has grown to ~6,000 lines across 20+ source files with no automated tests. Both competing LSP extensions have similar gaps β€” `lsp-pi` is the exception with ~1,700 lines of tests. Without tests, refactoring is risky and regressions are invisible until a user hits them. - -## Goals - -- **Unit tests** for all core modules with mocked LSP connections -- **Integration tests** (optional, gated behind `--integration`) that spin up real LSP servers -- **Test runner** that works with `npm test` out of the box -- **CI-friendly** β€” no flaky timeouts, no filesystem side effects, deterministic output - -## Non-goals - -- 100% coverage β€” focus on logic-heavy modules and known edge cases -- Testing the pi extension API itself β€” we trust the framework -- Testing TUI rendering pixel-perfectly β€” just verify the render functions return `Text` nodes - -## Design - -### Test framework - -Use **vitest** β€” it supports TypeScript natively (no build step), has built-in mocking, and is fast. Add to `devDependencies`. - -```json -{ - "devDependencies": { - "vitest": "^3.0.0" - }, - "scripts": { - "test": "vitest run", - "test:watch": "vitest", - "test:integration": "vitest run --project integration" - } -} -``` - -### File layout - -``` -tests/ -β”œβ”€β”€ unit/ -β”‚ β”œβ”€β”€ lsp-client.test.ts -β”‚ β”œβ”€β”€ lsp-manager.test.ts -β”‚ β”œβ”€β”€ file-sync.test.ts -β”‚ β”œβ”€β”€ resolve-provider.test.ts -β”‚ β”œβ”€β”€ tree-sitter/ -β”‚ β”‚ β”œβ”€β”€ parser-manager.test.ts -β”‚ β”‚ β”œβ”€β”€ pattern-compiler.test.ts -β”‚ β”‚ β”œβ”€β”€ search-engine.test.ts -β”‚ β”‚ β”œβ”€β”€ rewrite-engine.test.ts -β”‚ β”‚ β”œβ”€β”€ symbol-extractor.test.ts -β”‚ β”‚ └── workspace-index.test.ts -β”‚ └── tools/ -β”‚ β”œβ”€β”€ diagnostics.test.ts -β”‚ β”œβ”€β”€ hover.test.ts -β”‚ β”œβ”€β”€ definition.test.ts -β”‚ β”œβ”€β”€ references.test.ts -β”‚ β”œβ”€β”€ symbols.test.ts -β”‚ β”œβ”€β”€ rename.test.ts -β”‚ β”œβ”€β”€ completions.test.ts -β”‚ β”œβ”€β”€ code-search.test.ts -β”‚ β”œβ”€β”€ code-rewrite.test.ts -β”‚ β”œβ”€β”€ code-overview.test.ts -β”‚ β”œβ”€β”€ signature-help.test.ts # new tool -β”‚ └── code-actions.test.ts # new tool -β”œβ”€β”€ integration/ -β”‚ β”œβ”€β”€ typescript-server.test.ts -β”‚ └── python-server.test.ts -β”œβ”€β”€ fixtures/ -β”‚ β”œβ”€β”€ sample.ts -β”‚ β”œβ”€β”€ sample.py -β”‚ β”œβ”€β”€ sample.rs -β”‚ └── sample-project/ -β”‚ β”œβ”€β”€ package.json -β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ index.ts -β”‚ β”‚ └── utils.ts -β”‚ └── tsconfig.json -└── helpers/ - β”œβ”€β”€ mock-lsp-client.ts - β”œβ”€β”€ mock-lsp-manager.ts - └── test-utils.ts -``` - -### Mock strategy - -#### `MockLspClient` - -A fake `LspClient` that: -- Records all `sendRequest` calls with method + params -- Returns pre-configured responses per method (set via `mockResponse(method, response)`) -- Stores and returns diagnostics via `getDiagnostics(uri)` / `getAllDiagnostics()` -- Tracks `didOpen` / `didChange` notifications - -```typescript -class MockLspClient { - private responses = new Map(); - private diagnostics = new Map(); - private requests: Array<{ method: string; params: unknown }> = []; - - mockResponse(method: string, response: unknown) { ... } - setDiagnostics(uri: string, diags: Diagnostic[]) { ... } - - async sendRequest(method: string, params: unknown): Promise { - this.requests.push({ method, params }); - return this.responses.get(method) as R; - } - - getRequests(method?: string) { ... } -} -``` - -#### `MockLspManager` - -Wraps `MockLspClient` and provides: -- `getClientForFile(path)` β†’ returns the mock client -- `getFileUri(path)` / `resolvePath(path)` β†’ deterministic paths -- `getLanguageId(path)` β†’ based on extension -- `getRunningClient(languageId)` β†’ returns the mock client -- `getUnavailableReason(path)` β†’ returns a test message - -### Test categories - -#### 1. LSP Client (`lsp-client.test.ts`) - -- JSON-RPC message framing (Content-Length header parsing) -- Request/response correlation (matching IDs) -- Notification handling (`textDocument/publishDiagnostics`) -- Connection error handling and reconnection -- Graceful shutdown sequence -- Timeout behavior for pending requests - -#### 2. LSP Manager (`lsp-manager.test.ts`) - -- Server config resolution (built-in defaults, custom overrides) -- Language detection from file extension -- Lazy server startup (only when `getClientForFile` is called) -- File URI generation (handles spaces, special characters) -- Multiple servers for different languages -- Brazil workspace detection and bemol integration -- Daemon mode (shared server lifecycle) - -#### 3. File Sync (`file-sync.test.ts`) - -- `handleFileRead` β†’ sends `didOpen` with correct URI and content -- `handleFileWrite` β†’ sends `didChange` with incremented version -- Deduplication (multiple reads of the same file don't re-open) -- Version tracking consistency -- Tree-sitter integration (parse/index updates on file change) - -#### 4. Tree-sitter modules - -**parser-manager.test.ts** -- WASM init lifecycle -- Parse TypeScript, Python, Rust, Go, Java source -- Incremental re-parse after edit -- Unknown language handling - -**pattern-compiler.test.ts** -- `$NAME` single-node metavariable compilation -- `$$$NAME` variadic metavariable compilation -- Literal node preservation -- Expression wrapping for partial patterns -- Error cases (unparseable patterns) - -**search-engine.test.ts** -- Single-file pattern matching -- Multi-file directory scanning -- Metavariable capture extraction -- `max_results` limiting -- `.gitignore` respecting - -**rewrite-engine.test.ts** -- Simple substitution (`$A β†’ $A`) -- Multi-capture replacement (`$A.$B β†’ $B.$A`) -- Variadic replacement (`$$$ARGS`) -- Bottom-up application (offset preservation) -- Dry-run vs. apply modes - -**symbol-extractor.test.ts** -- Function/method/class/interface extraction per language -- Nested symbol handling (methods inside classes) -- Signature text extraction -- Syntax error detection - -**workspace-index.test.ts** -- Index build from fixture project -- Symbol search (exact and fuzzy) -- Incremental update on file change -- Skip patterns (`node_modules`, `.git`, etc.) - -#### 5. Tool tests - -Each tool test follows the same pattern: -1. Create a `MockLspManager` with pre-configured responses -2. Call the tool's `execute` function with test params -3. Assert on the returned `content` text and `details` object -4. Verify `renderCall` returns a `Text` node with expected content -5. Verify `renderResult` returns a `Text` node for both partial and complete states - -Example for `hover.test.ts`: - -```typescript -describe("lsp_hover", () => { - it("formats MarkupContent hover response", async () => { - const mockManager = createMockManager(); - mockManager.client.mockResponse("textDocument/hover", { - contents: { kind: "markdown", value: "```ts\nfunction foo(): void\n```" }, - }); - - const tool = createHoverTool(mockManager); - const result = await tool.execute("call-1", { path: "test.ts", line: 5, character: 10 }); - - expect(result.content[0].text).toContain("function foo(): void"); - expect(result.details.hasResult).toBe(true); - }); - - it("falls back to tree-sitter when no LSP", async () => { ... }); - it("returns unavailable message for unknown language", async () => { ... }); - it("handles LSP error gracefully", async () => { ... }); - it("renderCall shows file:line:col", () => { ... }); - it("renderResult shows truncated hover on collapse", () => { ... }); -}); -``` - -#### 6. Integration tests - -Gated behind `--project integration` flag. Require real servers installed: - -**typescript-server.test.ts** -- Start `typescript-language-server` -- Open a TypeScript file -- Get diagnostics, hover, definition, references, completions -- Verify results against known fixture content -- Clean shutdown - -**python-server.test.ts** -- Start `pyright-langserver` -- Same test pattern for Python fixtures - -These tests have a 30-second timeout and skip if the server binary isn't found. - -### Configuration - -`vitest.config.ts`: - -```typescript -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["tests/unit/**/*.test.ts"], - testTimeout: 10000, - projects: [ - { - name: "unit", - include: ["tests/unit/**/*.test.ts"], - }, - { - name: "integration", - include: ["tests/integration/**/*.test.ts"], - testTimeout: 30000, - }, - ], - }, -}); -``` - -### Coverage targets - -- Unit tests: aim for 80%+ line coverage on core modules -- Tools: every tool should have at least 3 tests (happy path, error, edge case) -- Tree-sitter: heavy coverage on pattern-compiler and search-engine (complex logic) -- Integration: smoke-test level β€” verify end-to-end flow works - -## Open questions - -- **Snapshot testing** β€” should `renderCall`/`renderResult` tests use snapshot assertions for the `Text` output? Pros: catches regressions in formatting. Cons: noisy diffs when theme changes. -- **Fixture management** β€” should fixtures be inline strings or separate files? Inline is easier to read in tests; files are better for integration tests that need real project structure. -- **Mock fidelity** β€” how closely should `MockLspClient` mimic real JSON-RPC behavior? A thin mock (just record/playback) is easier to maintain but may miss framing bugs. diff --git a/extensions/pi-lsp-extension/specs/tree-sitter-integration.md b/extensions/pi-lsp-extension/specs/tree-sitter-integration.md deleted file mode 100644 index bd62a71..0000000 --- a/extensions/pi-lsp-extension/specs/tree-sitter-integration.md +++ /dev/null @@ -1,82 +0,0 @@ -# Tree-sitter Integration - -## Summary - -Add a tree-sitter layer that provides code intelligence without requiring an LSP server. This gives the extension useful symbol search, document symbols, and definition lookup out of the box for any supported language β€” no install step needed. - -## Motivation - -Today every tool in the extension requires a running LSP server. If the user hasn't installed `typescript-language-server` or `pyright`, they get nothing. Kiro CLI solves this with a built-in tree-sitter layer that covers 18 languages with zero setup. - -Tree-sitter parsing is fast (incremental), works on syntactically broken files, and can power symbol search, document outlines, and basic go-to-definition without any external process. - -## Goals - -- **Zero-config code intelligence** β€” symbol search, document symbols, and definition lookup work immediately for supported languages -- **Graceful upgrade** β€” when an LSP server _is_ running, LSP results take priority; tree-sitter is the fallback -- **Codebase overview** β€” expose a tool that summarizes project structure (directories, key files, entry points) using tree-sitter symbol extraction -- **Minimal footprint** β€” tree-sitter WASM bindings, no native compilation required at install time - -## Non-goals - -- Replacing LSP β€” tree-sitter cannot do cross-file type resolution, find-references across files, or rename refactoring -- Supporting every language immediately β€” start with the languages we already support via LSP (TypeScript, JavaScript, Python, Rust, Go, Java) and expand later - -## Design - -### Parser management - -- Use `web-tree-sitter` (WASM-based) so the extension works without native build tools -- Bundle or lazy-download `.wasm` grammar files for each supported language -- Cache parsed trees per-file; invalidate on `didChange` events from `FileSync` -- Reuse the `EXT_TO_LANGUAGE` mapping in `lsp-manager.ts` to detect language from file extension - -### Fallback strategy - -Each tool that currently calls the LSP checks whether a server is running for the file's language: - -1. If LSP server is running and healthy β†’ use LSP (current behavior) -2. If no LSP server β†’ fall back to tree-sitter -3. If tree-sitter has no grammar for the language β†’ return "no intelligence available" error - -This logic lives in a shared `resolveProvider(filePath)` helper that tools call instead of going straight to `manager.getClientForFile()`. - -### New tools / tool enhancements - -| Tool | Tree-sitter behavior | -|------|---------------------| -| `lsp_symbols` (file) | Walk the tree-sitter AST for function, class, method, interface, enum, and variable declarations. Same output format as today. | -| `lsp_symbols` (workspace) | Scan project files (respecting `.gitignore`), parse each, and collect top-level symbols. Fuzzy-match against the query. Cache the index and update incrementally. | -| `lsp_definition` | For a symbol at a position, search the current file's tree for a matching definition node. If not found, search the workspace index. Best-effort β€” won't resolve imports through `node_modules` or type aliases. | -| `lsp_hover` | Extract the enclosing node's kind and text (e.g., "function declaration", the signature line). No type info β€” that requires LSP. | -| `lsp_diagnostics` | Tree-sitter can detect parse errors (syntax errors). Report those as a minimal diagnostic set when no LSP is available. | -| `code_overview` | **New tool.** Summarize project structure: directory tree, top-level symbols per file, entry points, dependency manifests. Uses tree-sitter for symbol extraction. | - -### Workspace indexing - -- On first use, walk the project tree (skip `node_modules`, `.git`, `build`, `dist`, `target`, etc.) -- Parse each file with tree-sitter, extract top-level symbols (name, kind, file, line) -- Store in an in-memory index keyed by symbol name -- Re-index individual files when `FileSync` reports changes -- For large repos, cap initial indexing at ~5000 files and index remaining on demand - -### File layout - -``` -src/ -β”œβ”€β”€ tree-sitter/ -β”‚ β”œβ”€β”€ parser-manager.ts # Load/cache WASM parsers per language -β”‚ β”œβ”€β”€ symbol-extractor.ts # AST β†’ symbol list, per-language queries -β”‚ β”œβ”€β”€ workspace-index.ts # Project-wide symbol index -β”‚ └── grammars/ # .wasm files (or download script) -β”œβ”€β”€ resolve-provider.ts # LSP-or-tree-sitter routing -└── tools/ - β”œβ”€β”€ code-overview.ts # New tool - └── ... (existing tools updated to use resolveProvider) -``` - -## Open questions - -- **Grammar distribution** β€” bundle `.wasm` files in the repo (adds ~2-5 MB) or download on first use? Bundling is simpler; downloading keeps the repo small. -- **Incremental indexing performance** β€” is the in-memory index fast enough for monorepos with 10k+ files, or do we need SQLite/a persistent cache? -- **Query language** β€” tree-sitter has a built-in query language for pattern matching. Should we expose that as a user-facing feature here, or save it for the structural search spec? diff --git a/extensions/pi-lsp-extension/test-structural-search.ts b/extensions/pi-lsp-extension/test-structural-search.ts deleted file mode 100644 index 8ae7188..0000000 --- a/extensions/pi-lsp-extension/test-structural-search.ts +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Tests for structural search & rewrite: pattern-compiler, search-engine, rewrite-engine. - * - * Run: npx tsx test-structural-search.ts - */ - -import { TreeSitterManager } from "./src/tree-sitter/parser-manager.js"; -import { compilePattern, type CompiledPattern } from "./src/tree-sitter/pattern-compiler.js"; -import { searchFiles, collectFilesByLanguage } from "./src/tree-sitter/search-engine.js"; -import { computeRewrites, applyRewrites, substituteCaptures } from "./src/tree-sitter/rewrite-engine.js"; -import { writeFileSync, mkdirSync, rmSync, readFileSync } from "node:fs"; -import { resolve } from "node:path"; - -let passed = 0; -let failed = 0; - -function assert(condition: boolean, label: string) { - if (condition) { - console.log(` βœ… ${label}`); - passed++; - } else { - console.log(` ❌ ${label}`); - failed++; - } -} - -const TEST_DIR = resolve("/tmp/structural-search-test"); - -async function main() { - const mgr = new TreeSitterManager(); - await mgr.init(); - - // Set up test fixtures - try { rmSync(TEST_DIR, { recursive: true }); } catch {} - mkdirSync(resolve(TEST_DIR, "src"), { recursive: true }); - - writeFileSync(resolve(TEST_DIR, "src/app.ts"), ` -import { readFile } from "fs"; - -function processData(data: string): string { - console.log("processing", data); - const result = data.trim(); - console.log("done"); - return result; -} - -function helper() { - var x = 10; - var y = "hello"; - const z = 42; -} - -class UserService { - async getUser(id: string) { - const user = await this.db.find(id); - return user; - } - - async deleteUser(id: string) { - console.log("deleting", id); - await this.db.remove(id); - } -} -`); - - writeFileSync(resolve(TEST_DIR, "src/utils.ts"), ` -export function formatDate(d: Date): string { - console.log("formatting date"); - return d.toISOString(); -} - -export function parseDate(s: string): Date { - return new Date(s); -} - -export const doSomething = (a: number, b: number) => { - var result = a + b; - return result; -}; -`); - - // ── 1. Pattern compiler ── - console.log("\nπŸ”§ Pattern Compiler"); - - // Simple identifier pattern - const p1 = await compilePattern("console.log($ARG)", "typescript", mgr); - assert(p1.root.kind === "literal", "console.log($ARG) compiles to literal root"); - assert(p1.metavars.includes("ARG"), "captures $ARG metavar"); - assert(p1.languageId === "typescript", "language is typescript"); - - // Variadic pattern - const p2 = await compilePattern("console.log($$$ARGS)", "typescript", mgr); - assert(p2.metavars.includes("ARGS"), "captures $$$ARGS variadic metavar"); - - // Variable declaration pattern - const p3 = await compilePattern("var $N = $V", "typescript", mgr); - assert(p3.metavars.includes("N"), "captures $N"); - assert(p3.metavars.includes("V"), "captures $V"); - - // Pattern with no metavars - const p4 = await compilePattern("return result", "typescript", mgr); - assert(p4.metavars.length === 0, "no metavars in literal pattern"); - - // Bad pattern should throw - let threw = false; - try { - await compilePattern("{{{{invalid}}}", "typescript", mgr); - } catch { - threw = true; - } - assert(threw, "invalid pattern throws error"); - - // ── 2. Search engine β€” basic matching ── - console.log("\nπŸ” Search Engine β€” basic matching"); - - // Find console.log calls with single arg - const compiled1 = await compilePattern("console.log($ARG)", "typescript", mgr); - const matches1 = await searchFiles(compiled1, TEST_DIR, mgr); - console.log(` Found ${matches1.length} console.log($ARG) matches`); - assert(matches1.length > 0, "found console.log($ARG) matches"); - // Check captures - const firstMatch = matches1[0]; - assert("ARG" in firstMatch.captures, "match has $ARG capture"); - console.log(` First $ARG = "${firstMatch.captures.ARG}"`); - - // Find var declarations - const compiled3 = await compilePattern("var $N = $V", "typescript", mgr); - const matches3 = await searchFiles(compiled3, TEST_DIR, mgr); - console.log(` Found ${matches3.length} var $N = $V matches`); - assert(matches3.length >= 3, "found at least 3 var declarations"); - // Check that captures are different - const varNames = matches3.map(m => m.captures.N); - console.log(` Variable names: ${varNames.join(", ")}`); - assert(varNames.includes("x"), 'found var x'); - assert(varNames.includes("y"), 'found var y'); - - // ── 3. Search engine β€” variadic matching ── - console.log("\nπŸ” Search Engine β€” variadic matching"); - - // Find console.log with any number of args - const compiled2 = await compilePattern("console.log($$$ARGS)", "typescript", mgr); - const matches2 = await searchFiles(compiled2, TEST_DIR, mgr); - console.log(` Found ${matches2.length} console.log($$$ARGS) matches`); - assert(matches2.length >= 4, "found variadic console.log matches (at least 4)"); - - // ── 4. Search engine β€” file scoping ── - console.log("\nπŸ” Search Engine β€” file scoping"); - - const scopedMatches = await searchFiles(compiled1, TEST_DIR, mgr, { - path: "src/utils.ts", - }); - console.log(` Found ${scopedMatches.length} matches in utils.ts only`); - assert(scopedMatches.length >= 1, "found matches in scoped file"); - assert(scopedMatches.every(m => m.file.endsWith("utils.ts")), "all matches in utils.ts"); - - // ── 5. Search engine β€” max results ── - console.log("\nπŸ” Search Engine β€” max results"); - - const limitedMatches = await searchFiles(compiled2, TEST_DIR, mgr, { maxResults: 2 }); - assert(limitedMatches.length <= 2, `max_results respected (got ${limitedMatches.length})`); - - // ── 6. File collection ── - console.log("\nπŸ“‚ File Collection"); - - const tsFiles = await collectFilesByLanguage(TEST_DIR, "typescript", mgr); - assert(tsFiles.length === 2, `found 2 TypeScript files (got ${tsFiles.length})`); - assert(tsFiles.some(f => f.endsWith("app.ts")), "includes app.ts"); - assert(tsFiles.some(f => f.endsWith("utils.ts")), "includes utils.ts"); - - // Skips node_modules - mkdirSync(resolve(TEST_DIR, "node_modules/pkg"), { recursive: true }); - writeFileSync(resolve(TEST_DIR, "node_modules/pkg/index.ts"), "export const x = 1;"); - const tsFiles2 = await collectFilesByLanguage(TEST_DIR, "typescript", mgr); - assert(tsFiles2.length === 2, `still 2 files (node_modules skipped, got ${tsFiles2.length})`); - - // ── 7. Rewrite engine β€” substituteCaptures ── - console.log("\n✏️ Rewrite Engine β€” substituteCaptures"); - - assert( - substituteCaptures("const $N = $V", { N: "x", V: "42" }) === "const x = 42", - "basic substitution works", - ); - assert( - substituteCaptures("$$$ARGS", { ARGS: "a, b, c" }) === "a, b, c", - "variadic substitution works", - ); - assert( - substituteCaptures("$UNKNOWN stays", {}) === "$UNKNOWN stays", - "unknown metavar left as-is", - ); - - // ── 8. Rewrite engine β€” computeRewrites (dry run) ── - console.log("\n✏️ Rewrite Engine β€” computeRewrites (dry run)"); - - const varMatches = await searchFiles(compiled3, TEST_DIR, mgr); - const rewrites = computeRewrites(varMatches, "const $N = $V"); - assert(rewrites.length === varMatches.length, "one rewrite per match"); - for (const r of rewrites) { - assert(r.before.startsWith("var "), `before starts with "var" (got: ${r.before})`); - assert(r.after.startsWith("const "), `after starts with "const" (got: ${r.after})`); - console.log(` ${r.file.split("/").pop()}:${r.line} β€” ${r.before} β†’ ${r.after}`); - } - - // ── 9. Rewrite engine β€” applyRewrites ── - console.log("\n✏️ Rewrite Engine β€” applyRewrites"); - - // Work on a copy to not mess up other tests - const rewriteDir = resolve(TEST_DIR, "rewrite-test"); - mkdirSync(rewriteDir, { recursive: true }); - writeFileSync(resolve(rewriteDir, "test.ts"), ` -function example() { - var a = 1; - var b = "hello"; - const c = true; - var d = []; -} -`); - - const varPattern = await compilePattern("var $N = $V", "typescript", mgr); - const varResults = await searchFiles(varPattern, rewriteDir, mgr); - console.log(` Found ${varResults.length} var declarations to rewrite`); - assert(varResults.length === 3, `found 3 var declarations (got ${varResults.length})`); - - const result = await applyRewrites(varResults, "const $N = $V"); - assert(result.filesModified === 1, `modified 1 file (got ${result.filesModified})`); - assert(result.changes.length === 3, `3 changes (got ${result.changes.length})`); - - // Verify the file was actually rewritten - const rewritten = readFileSync(resolve(rewriteDir, "test.ts"), "utf-8"); - assert(!rewritten.includes("var a"), "var a replaced"); - assert(!rewritten.includes("var b"), "var b replaced"); - assert(!rewritten.includes("var d"), "var d replaced"); - assert(rewritten.includes("const a = 1"), "const a = 1 present"); - assert(rewritten.includes('const b = "hello"'), 'const b = "hello" present'); - assert(rewritten.includes("const d = []"), "const d = [] present"); - assert(rewritten.includes("const c = true"), "original const c preserved"); - console.log(" Rewritten file content:"); - console.log(rewritten.split("\n").map(l => ` ${l}`).join("\n")); - - // ── 10. Rewrite β€” no-op when before === after ── - console.log("\n✏️ Rewrite Engine β€” no-op detection"); - - const constPattern = await compilePattern("const $N = $V", "typescript", mgr); - const constMatches = await searchFiles(constPattern, rewriteDir, mgr); - const noopResult = await applyRewrites(constMatches, "const $N = $V"); - assert(noopResult.filesModified === 0, "no files modified when replacement matches original"); - - // ── 11. Cross-language β€” Python ── - console.log("\n🐍 Cross-language β€” Python"); - - mkdirSync(resolve(TEST_DIR, "py"), { recursive: true }); - writeFileSync(resolve(TEST_DIR, "py/main.py"), ` -def process(data): - print("start") - result = data.strip() - print("done") - return result - -def helper(): - print("helper called") -`); - - const pyPattern = await compilePattern("print($ARG)", "python", mgr); - const pyMatches = await searchFiles(pyPattern, TEST_DIR, mgr, { path: "py" }); - console.log(` Found ${pyMatches.length} print($ARG) matches in Python`); - assert(pyMatches.length >= 3, "found Python print matches"); - assert(pyMatches.every(m => m.file.endsWith(".py")), "all matches are Python files"); - - // ── 12. Search β€” return value structure ── - console.log("\nπŸ“‹ Search β€” result structure"); - - const singleMatch = (await searchFiles(compiled1, TEST_DIR, mgr, { maxResults: 1 }))[0]; - assert(typeof singleMatch.file === "string", "match.file is string"); - assert(typeof singleMatch.line === "number" && singleMatch.line > 0, "match.line is positive number"); - assert(typeof singleMatch.column === "number" && singleMatch.column > 0, "match.column is positive number"); - assert(typeof singleMatch.matchedText === "string", "match.matchedText is string"); - assert(typeof singleMatch.startIndex === "number", "match.startIndex exists"); - assert(typeof singleMatch.endIndex === "number", "match.endIndex exists"); - assert(singleMatch.endIndex > singleMatch.startIndex, "endIndex > startIndex"); - assert(typeof singleMatch.captures === "object", "match.captures is object"); - - // ── Cleanup ── - try { rmSync(TEST_DIR, { recursive: true }); } catch {} - mgr.shutdown(); - - // ── Summary ── - console.log(`\n${"═".repeat(50)}`); - console.log(` ${passed} passed, ${failed} failed`); - console.log(`${"═".repeat(50)}\n`); - process.exit(failed > 0 ? 1 : 0); -} - -main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); -}); diff --git a/extensions/pi-lsp-extension/test-tree-sitter.ts b/extensions/pi-lsp-extension/test-tree-sitter.ts deleted file mode 100644 index 33f78b9..0000000 --- a/extensions/pi-lsp-extension/test-tree-sitter.ts +++ /dev/null @@ -1,397 +0,0 @@ -/** - * Quick smoke test for the tree-sitter integration. - * Exercises: parser-manager, symbol-extractor, workspace-index - * - * Run: npx tsx test-tree-sitter.ts - */ - -import { TreeSitterManager } from "./src/tree-sitter/parser-manager.js"; -import { extractSymbols, getNodeAtPosition, findDefinition, getSyntaxErrors, getSignatureText, getEnclosingDeclaration } from "./src/tree-sitter/symbol-extractor.js"; -import { WorkspaceIndex } from "./src/tree-sitter/workspace-index.js"; -import { writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -let passed = 0; -let failed = 0; - -function assert(condition: boolean, label: string) { - if (condition) { - console.log(` βœ… ${label}`); - passed++; - } else { - console.log(` ❌ ${label}`); - failed++; - } -} - -async function main() { - const mgr = new TreeSitterManager(); - - // ── 1. Init & language detection ── - console.log("\nπŸ”§ Parser Manager β€” init & language detection"); - await mgr.init(); - assert(mgr.getLanguageId("foo.ts") === "typescript", "foo.ts β†’ typescript"); - assert(mgr.getLanguageId("bar.py") === "python", "bar.py β†’ python"); - assert(mgr.getLanguageId("baz.rs") === "rust", "baz.rs β†’ rust"); - assert(mgr.getLanguageId("main.go") === "go", "main.go β†’ go"); - assert(mgr.getLanguageId("App.java") === "java", "App.java β†’ java"); - assert(mgr.getLanguageId("README.md") === undefined, "README.md β†’ undefined"); - assert(mgr.hasGrammar("typescript"), "has grammar: typescript"); - assert(!mgr.hasGrammar("haskell"), "no grammar: haskell"); - assert(mgr.getSupportedLanguages().length >= 19, `supported languages >= 19 (got ${mgr.getSupportedLanguages().length})`); - - // ── 2. Parse TypeScript ── - console.log("\n🌳 Parse TypeScript"); - const tsCode = ` -export function greet(name: string): string { - return \`Hello, \${name}!\`; -} - -export class UserService { - private users: Map = new Map(); - - getUser(id: string): User | undefined { - return this.users.get(id); - } - - addUser(user: User): void { - this.users.set(user.id, user); - } -} - -export interface User { - id: string; - name: string; - email?: string; -} - -export const MAX_USERS = 1000; - -type UserMap = Map; - -export default function main() { - const svc = new UserService(); - svc.addUser({ id: "1", name: "Alice" }); -} -`; - const tsTree = await mgr.parse("/tmp/test.ts", tsCode); - assert(tsTree !== null, "parsed TypeScript successfully"); - - const tsSymbols = extractSymbols(tsTree!, "typescript"); - const names = tsSymbols.map(s => s.name); - console.log(" Symbols found:", names); - assert(names.includes("greet"), "found function greet"); - assert(names.includes("UserService"), "found class UserService"); - assert(names.includes("User"), "found interface User"); - assert(names.includes("MAX_USERS"), "found const MAX_USERS"); - assert(names.includes("main"), "found default export function main"); - assert(names.includes("UserMap"), "found type alias UserMap"); - - // Check nested methods - const userServiceSym = tsSymbols.find(s => s.name === "UserService"); - assert(userServiceSym?.children !== undefined, "UserService has children"); - const methodNames = userServiceSym?.children?.map(c => c.name) ?? []; - assert(methodNames.includes("getUser"), "UserService.getUser found"); - assert(methodNames.includes("addUser"), "UserService.addUser found"); - - // ── 3. Parse Python ── - console.log("\n🐍 Parse Python"); - const pyCode = ` -import os - -class Calculator: - def __init__(self): - self.history = [] - - def add(self, a, b): - result = a + b - self.history.append(result) - return result - - def multiply(self, a, b): - return a * b - -def main(): - calc = Calculator() - print(calc.add(2, 3)) - -MAX_HISTORY = 100 -`; - const pyTree = await mgr.parse("/tmp/test.py", pyCode); - assert(pyTree !== null, "parsed Python successfully"); - const pySymbols = extractSymbols(pyTree!, "python"); - const pyNames = pySymbols.map(s => s.name); - console.log(" Symbols found:", pyNames); - assert(pyNames.includes("Calculator"), "found class Calculator"); - assert(pyNames.includes("main"), "found function main"); - const calcSym = pySymbols.find(s => s.name === "Calculator"); - const calcMethods = calcSym?.children?.map(c => c.name) ?? []; - assert(calcMethods.includes("__init__"), "Calculator.__init__ found"); - assert(calcMethods.includes("add"), "Calculator.add found"); - - // ── 4. Parse Rust ── - console.log("\nπŸ¦€ Parse Rust"); - const rsCode = ` -pub fn process(data: &[u8]) -> Result, Error> { - Ok(data.to_vec()) -} - -pub struct Config { - pub name: String, - pub value: i32, -} - -impl Config { - pub fn new(name: &str) -> Self { - Config { name: name.to_string(), value: 0 } - } -} - -pub trait Processor { - fn process(&self, input: &str) -> String; -} - -pub enum Status { - Active, - Inactive, - Error(String), -} - -const MAX_SIZE: usize = 1024; -`; - const rsTree = await mgr.parse("/tmp/test.rs", rsCode); - assert(rsTree !== null, "parsed Rust successfully"); - const rsSymbols = extractSymbols(rsTree!, "rust"); - const rsNames = rsSymbols.map(s => s.name); - console.log(" Symbols found:", rsNames); - assert(rsNames.includes("process"), "found fn process"); - assert(rsNames.includes("Config"), "found struct Config"); - assert(rsNames.some(n => n.startsWith("impl")), "found impl block"); - assert(rsNames.includes("Processor"), "found trait Processor"); - assert(rsNames.includes("Status"), "found enum Status"); - assert(rsNames.includes("MAX_SIZE"), "found const MAX_SIZE"); - - // ── 5. Parse Go ── - console.log("\nπŸ”΅ Parse Go"); - const goCode = ` -package main - -import "fmt" - -func Add(a, b int) int { - return a + b -} - -type Server struct { - Port int - Host string -} - -func (s *Server) Start() error { - return nil -} - -const MaxRetries = 3 - -var DefaultServer = Server{Port: 8080} -`; - const goTree = await mgr.parse("/tmp/test.go", goCode); - assert(goTree !== null, "parsed Go successfully"); - const goSymbols = extractSymbols(goTree!, "go"); - const goNames = goSymbols.map(s => s.name); - console.log(" Symbols found:", goNames); - assert(goNames.includes("Add"), "found func Add"); - assert(goNames.includes("Server"), "found type Server"); - assert(goNames.includes("Start"), "found method Start"); - assert(goNames.includes("MaxRetries"), "found const MaxRetries"); - - // ── 6. Parse Java ── - console.log("\nβ˜• Parse Java"); - const javaCode = ` -package com.example; - -public class Handler { - private final String name; - - public Handler(String name) { - this.name = name; - } - - public String handle(String input) { - return name + ": " + input; - } -} - -interface Processor { - void process(String data); -} - -enum Status { - OK, ERROR, PENDING -} -`; - const javaTree = await mgr.parse("/tmp/Test.java", javaCode); - assert(javaTree !== null, "parsed Java successfully"); - const javaSymbols = extractSymbols(javaTree!, "java"); - const javaNames = javaSymbols.map(s => s.name); - console.log(" Symbols found:", javaNames); - assert(javaNames.includes("Handler"), "found class Handler"); - assert(javaNames.includes("Processor"), "found interface Processor"); - assert(javaNames.includes("Status"), "found enum Status"); - const handlerSym = javaSymbols.find(s => s.name === "Handler"); - const handlerMethods = handlerSym?.children?.map(c => c.name) ?? []; - assert(handlerMethods.includes("handle"), "Handler.handle found"); - - // ── 7. Node at position & definition finding ── - console.log("\nπŸ“ Node at position & definition lookup"); - // "greet" starts at line 2 col 17 in tsCode (0-indexed: row=1, col=16) - const node = getNodeAtPosition(tsTree!, 1, 17); - assert(node !== null, "found node at position"); - assert(node?.text === "greet", `node text is "greet" (got "${node?.text}")`); - - const defs = findDefinition(tsTree!, "greet", "typescript"); - assert(defs.length > 0, "found definition of greet"); - assert(defs[0].name === "greet", "definition name matches"); - - // ── 8. Syntax errors ── - console.log("\nπŸ”΄ Syntax error detection"); - const badCode = `function foo( { return 42; }`; - const badTree = await mgr.parse("/tmp/bad.ts", badCode); - assert(badTree !== null, "parsed bad code (with errors)"); - const errors = getSyntaxErrors(badTree!); - assert(errors.length > 0, `found ${errors.length} syntax error(s)`); - console.log(" Errors:", errors.map(e => e.message)); - - // ── 9. Enclosing declaration ── - console.log("\nπŸ—οΈ Enclosing declaration"); - // Line 10 (0-indexed: 9) is inside UserService.getUser - const enclosing = getEnclosingDeclaration(tsTree!, 9, 10); - assert(enclosing !== null, "found enclosing declaration"); - if (enclosing) { - const sig = getSignatureText(enclosing); - console.log(` Signature: ${sig}`); - assert(sig.includes("getUser"), `enclosing is getUser (got: ${sig})`); - } - - // ── 10. Workspace index ── - console.log("\nπŸ“š Workspace index"); - const testDir = resolve("/tmp/tree-sitter-test-workspace"); - try { rmSync(testDir, { recursive: true }); } catch {} - mkdirSync(resolve(testDir, "src"), { recursive: true }); - writeFileSync(resolve(testDir, "src/app.ts"), ` -export class AppService { - start(): void {} - stop(): void {} -} - -export function createApp(): AppService { - return new AppService(); -} -`); - writeFileSync(resolve(testDir, "src/utils.ts"), ` -export function formatDate(d: Date): string { - return d.toISOString(); -} - -export function parseDate(s: string): Date { - return new Date(s); -} - -export const VERSION = "1.0.0"; -`); - writeFileSync(resolve(testDir, "src/main.py"), ` -class Database: - def connect(self): - pass - -def run_server(port): - db = Database() - db.connect() -`); - - const wsIndex = new WorkspaceIndex(testDir, mgr); - await wsIndex.build(); - const stats = wsIndex.getStats(); - console.log(` Indexed ${stats.files} files, ${stats.symbols} symbols`); - assert(stats.files === 3, `indexed 3 files (got ${stats.files})`); - assert(stats.symbols > 0, `has symbols (got ${stats.symbols})`); - - // Search - const appResults = wsIndex.search("AppService"); - assert(appResults.length > 0, "found AppService in index"); - assert(appResults[0].name === "AppService", "first result is AppService"); - - const formatResults = wsIndex.search("format"); - assert(formatResults.length > 0, "found format* in index"); - assert(formatResults.some(r => r.name === "formatDate"), "found formatDate"); - - // Cross-language search - const dbResults = wsIndex.search("Database"); - assert(dbResults.length > 0, "found Python class Database in index"); - - // File-specific symbols - const appSyms = wsIndex.getSymbolsForFile(resolve(testDir, "src/app.ts")); - assert(appSyms.length > 0, `app.ts has symbols (got ${appSyms.length})`); - - // Re-index after change - writeFileSync(resolve(testDir, "src/utils.ts"), ` -export function formatDate(d: Date): string { - return d.toISOString(); -} - -export function newHelper(): void {} -`); - await wsIndex.indexFile(resolve(testDir, "src/utils.ts")); - const newResults = wsIndex.search("newHelper"); - assert(newResults.length > 0, "found newHelper after re-index"); - const versionResults = wsIndex.search("VERSION"); - assert(versionResults.length === 0, "VERSION removed after re-index"); - - // ── 11. Tree caching ── - console.log("\nπŸ’Ύ Tree caching"); - const tree1 = await mgr.parse("/tmp/cache-test.ts", "function a() {}"); - const tree2 = await mgr.parse("/tmp/cache-test.ts", "function a() {}"); - assert(tree1 === tree2, "same content returns cached tree"); - const tree3 = await mgr.parse("/tmp/cache-test.ts", "function b() {}"); - assert(tree3 !== tree1, "different content returns new tree"); - mgr.invalidate("/tmp/cache-test.ts"); - assert(mgr.getCachedTree("/tmp/cache-test.ts") === null, "invalidate clears cache"); - - - // ── 12. Windows path regression (fileURLToPath vs .pathname) ── - // On Windows, import.meta.url is 'file:///C:/path/...'. - // Using .pathname gives '/C:/path/...' β€” path.resolve() treats the leading - // slash as 'root of current drive', so if CWD is on E:\ you get E:\C:\... - // Using fileURLToPath() correctly produces 'C:\path\...' on Windows. - console.log("\nπŸͺŸ Windows path regression (fileURLToPath vs .pathname)"); - - const moduleDir = fileURLToPath(new URL(".", import.meta.url)); - const wasmPath = resolve(moduleDir, "node_modules/web-tree-sitter/tree-sitter.wasm"); - - assert(existsSync(wasmPath), `wasm resolves to a real file: ${wasmPath}`); - - // Detect drive-doubling: a sign that .pathname was used on Windows. - // e.g. 'E:\\C:\\Users\\...' when CWD drive != module drive. - const hasDriveDoubling = /[A-Za-z]:\\[A-Za-z]:\\/.test(wasmPath); - assert(!hasDriveDoubling, `path has no doubled drive letter: ${wasmPath}`); - - // Also verify the grammar dir resolves cleanly to real .wasm grammar files. - const grammarPath = resolve(moduleDir, "node_modules/tree-sitter-wasms/out/tree-sitter-typescript.wasm"); - assert(existsSync(grammarPath), `grammar wasm resolves to a real file: ${grammarPath}`); - - // ── Cleanup ── - try { rmSync(testDir, { recursive: true }); } catch {} - mgr.shutdown(); - - // ── Summary ── - console.log(`\n${"═".repeat(50)}`); - console.log(` ${passed} passed, ${failed} failed`); - console.log(`${"═".repeat(50)}\n`); - process.exit(failed > 0 ? 1 : 0); -} - -main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); -}); diff --git a/extensions/pi-lsp-extension/tsconfig.json b/extensions/pi-lsp-extension/tsconfig.json deleted file mode 100644 index 0c61e52..0000000 --- a/extensions/pi-lsp-extension/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "outDir": "dist", - "rootDir": "src", - "declaration": true - }, - "include": ["src/**/*.ts"] -}