Evan Reichard 0aa44bedc4 fix(watcher): coalesce edge cases, drain on unregister, expose ready()
Addresses review feedback on 7787626:

1. Deleted->Created at the same path is a file replacement, not a no-op.
   Previous coalescing dropped both Created->Deleted and Deleted->Created;
   the latter left the server with no signal to re-read replaced content.
   Now: Deleted->Created collapses to Changed, Created->Changed keeps
   Created (server didn't know the file at all). Extracted coalesce() so
   the matrix is reviewable in one place.

2. setPatterns([]) (server unregistered all watchers) stopped chokidar
   but left pending events + timers intact, so a queued batch could
   still fire after the server stopped caring. Now drains via
   cancelPending() before stopping chokidar.

3. Added ready() returning a promise resolved by chokidar's initial-scan
   'ready' event. Production daemon doesn't need to await it (LSP
   handshake gives chokidar ample wall-time), but tests now use it
   instead of fixed 200ms sleeps - deflakes the suite on slower
   filesystems and addresses the (narrow) startup race where a file
   created during chokidar's initial crawl could be missed.

4. Unit tests replace 11 hardcoded sleeps with watcher.ready(), and add
   coverage for the two coalesce fixes plus the unregister-drains case.
2026-05-19 23:51:32 -04:00
2026-04-25 21:06:15 -04:00

evan/pi-lsp

LSP extension for pi coding agent. Provides LSP tools that the LLM can use to query language servers, plus automatic diagnostics after edit/write operations.

Features

LSP Tools (callable by LLM)

  • lsp_hover - Get hover documentation for a symbol
  • lsp_definition - Find the definition of a symbol
  • lsp_references - Find all references to a symbol
  • lsp_completion - Get completion suggestions
  • lsp_documentSymbol - Get the symbol outline of a file
  • lsp_diagnostics - Get lint/type-check diagnostics

Auto-Check

Automatically runs LSP diagnostics after edit or write tool calls. If issues are found, sends a message with the diagnostics to the LLM.

Enable/disable:

pi --lsp-auto-check=false  # Disable auto-check
pi --lsp-auto-check=true   # Enable (default)

Manual Check Command

Run diagnostics manually on specific files:

/lsp-check main.go utils.go

Server Control Commands

Disable a server so this pi instance won't use it (the shared daemon and other instances are unaffected). When all servers are disabled, LSP tools are removed from the active tool set.

Command Args Behavior
/lsp-servers none List running servers and disabled state
/lsp-disable [<id>] Disable all (no arg) or specific server. Bare command disables all.
/lsp-enable [<id>] Enable all (no arg) or specific server. Restores tools when any is enabled.
/lsp-destroy [<id>] Kill running daemon entries for all (no arg) or specific server. Explicitly destructive.
/lsp-disable gopls          # Disable just gopls; other LSP tools still work
/lsp-disable                # Disable all — removes LSP tools from active set
/lsp-enable gopls           # Re-enable gopls; restores tools
/lsp-enable                 # Re-enable all
/lsp-destroy gopls          # Kill running gopls process(es) in the daemon
/lsp-destroy                 # Kill all running server processes

Install

cd ~/.pi/extensions/lsp
npm install

CLI Usage (for development/testing)

tsx ./cli.ts <file> <lsp_command> <req_data_json> [--no-daemon]
tsx ./cli.ts daemon <status|stop>

Requests use a long-lived background daemon by default. The daemon is autospawned on first use, keeps one language server alive per (server.id, project root), and evicts idle servers after ServerConfig.idleTtlMs (default: 5 minutes). Pass --no-daemon to use the legacy one-shot path for debugging.

req_data_json is the raw LSP params for the command, minus textDocument.uri (we inject that from <file>).

Commands

  • hover
  • definition
  • references
  • completion
  • documentSymbol
  • diagnostics (waits briefly for the first publishDiagnostics)

Examples

# Hover at line 224, col 23 (LSP is 0-indexed, so subtract 1)
npm run lsp -- backend/api/server.go hover \
  '{"position":{"line":223,"character":22}}'

# Go to definition
npm run lsp -- backend/api/server.go definition \
  '{"position":{"line":223,"character":22}}'

# Document symbols (no params needed)
npm run lsp -- backend/api/server.go documentSymbol '{}'

# Diagnostics
npm run lsp -- backend/api/server.go diagnostics '{}'

# Inspect/stop the background daemon
npm run lsp -- daemon status
npm run lsp -- daemon stop

Set LSP_DEBUG=1 to forward server stderr to the daemon log. The daemon socket is $XDG_RUNTIME_DIR/pi-lsp-$UID.sock (tmpdir fallback); logs are in /tmp/pi-lsp-daemon.log.

Adding A Server

Edit server.ts:

{
  id: "rust-analyzer",
  match: ["rs"],
  command: "rust-analyzer",
  args: [],
  rootMarkers: ["Cargo.toml"],
  languageId: "rust",
}

Per-Repo Config (.pi-lsp.json)

Drop a .pi-lsp.json at any ancestor of your working files to add or override servers for that repo. The nearest config (walking upward) wins.

{
  "servers": [
    {
      "id": "rust-analyzer",
      "match": ["rs"],
      "command": "rust-analyzer",
      "args": [],
      "rootMarkers": ["Cargo.toml"],
      "languageId": "rust"
    },
    { "id": "gopls", "args": ["-remote=auto", "-vv"] }
  ],
  "disable": ["oxlint"]
}

Merge rules:

  • Entry with a built-in id → fields shallow-merge over the built-in (user wins).
  • Entry with a new id → appended; must include match, command, args, rootMarkers.
  • disable → filters out matching ids (built-in or user-defined).

Reloading after edits: Config is re-read on mtime change, so new lookups pick up changes automatically. However, already-running language servers in the daemon keep their original spawn args. If you change command, args, or rootMarkers, run /lsp-destroy (or pi-lsp daemon stop) so they respawn with the new config.

Security note: .pi-lsp.json controls what binary pi-lsp spawns. Treat it like .vscode/settings.json — don't accept untrusted configs from arbitrary repos.

Adding A Command

  1. Add to the LspCommand union in src/types.ts.
  2. Add a handler in src/commands.ts.

Future

  • Daemon hardening - persistent metrics, health checks, and richer status output.
  • Build output - ship compiled JS entrypoints instead of relying on tsx for development.
Description
No description provided
Readme 490 KiB
Languages
TypeScript 99.6%
Nix 0.4%