Evan Reichard 77876264ee feat(watcher): forward FS events as workspace/didChangeWatchedFiles
LSP servers maintain their own workspace index built at initialize time
and rely on the client to push file-system events. Previously the daemon
only synced the single file being queried, so externally created/changed
files (codegen, build scripts, git checkout, the agent's own writes from
the perspective of other open files) left the server's index stale until
manual /lsp-destroy.

Each ClientEntry now lazily owns a WorkspaceWatcher (chokidar + picomatch)
that translates FS events into workspace/didChangeWatchedFiles batches.
Patterns come from the server via client/registerCapability (no
speculative watching). Ignores layer a tiny baseline (.git, .DS_Store)
over the repo's root .gitignore, with a fallback list for non-git
workspaces. Events debounce 50ms quiet / 500ms max wait.

Notable: gopls registers absolute-path globs (/abs/root/**/*.go) rather
than relative ones, so compileWatchers() matches each event against both
relative and absolute path forms. Caught by the integration test; unit
regression test added.

Rollback: PI_LSP_DISABLE_WATCHERS=1 disables all watcher creation.

- src/client.ts: honor register/unregisterCapability for
  workspace/didChangeWatchedFiles; advertise dynamicRegistration;
  expose getFileWatchers/onWatchersChanged/sendNotification
- src/watcher.ts: new WorkspaceWatcher with layered ignores,
  debounce+batch, Created+Deleted coalescing, dual-form glob matching
- src/daemon.ts: per-entry watcher lifecycle, PI_LSP_DISABLE_WATCHERS,
  LSP_DEBUG-gated pattern/event logging
- test/unit/watcher.test.ts: 11 tests against real chokidar + temp dir
- test/integration/watcher-gopls.test.ts: end-to-end against gopls
- AGENTS.md: new "Workspace File Watching" section
- flake.nix: add go (required by gopls integration test)
2026-05-19 23:43: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%