6.3 KiB
PgTidy LSP Status & Roadmap
This document describes the current state of the PgTidy LSP server (pkg/lsp,
cmd/pgtidy/lsp.go), what it actually provides today, and concrete next steps.
It was written as part of issue #3 ("See what we can provide for LSP").
Note: The V3 milestone is already implemented and shipping (
docs/todo.mdmarks V3 done), so this is an inventory + gap analysis, not a greenfield proposal.
What the server provides today
Verified by hand against the built binary (pgtidy lsp, JSON-RPC 2.0 over stdio,
Content-Length framing) — no external LSP library, all wire types hand-rolled.
Advertised capabilities (initialize → capabilities)
| Capability | Value | Where |
|---|---|---|
textDocumentSync |
1 (full sync) |
serverCaps |
documentFormattingProvider |
true |
handle("initialize") |
documentRangeFormattingProvider |
true |
handle("initialize") |
codeActionProvider |
true |
handle("initialize") |
Supported methods
| Method | Direction | Behavior |
|---|---|---|
initialize / shutdown / exit / initialized |
req/resp + notif | Lifecycle. exit/shutdown acknowledged with null result. |
textDocument/didOpen |
notif | Stores document text; triggers publishDiagnostics. |
textDocument/didChange |
notif | Stores latest content version; triggers publishDiagnostics. |
textDocument/didClose |
notif | Drops text + fix cache; clears diagnostics with empty list. |
textDocument/formatting |
req/resp | Full-doc format via pkg/format; returns one fullReplace TextEdit. |
textDocument/rangeFormatting |
req/resp | Formats doc, returns minimal edit over the selected line range. |
textDocument/codeAction |
req/resp | Returns quick-fix WorkspaceEdits for fixable diagnostics overlapping the range. |
textDocument/publishDiagnostics |
notif | Sent on every open/change; diagnostic code = RuleID, source = pgtidy. |
$/cancelRequest |
req | Ignored (per LSP, no response). |
| unknown | req/resp | -32601 method not found (when the request has an id). |
Verified at runtime (e2e smoke test)
initializereturns the capability block above.didOpenonselect * from t;→publishDiagnosticswithCOR001("SELECT * is fragile…", severity 4 = hint, codeCOR001).textDocument/formattingon that input → edit replacing withSELECT *\nFROM t;\n(keyword casing + clause-per-line applied).textDocument/hover→-32601 method not found(not implemented — correct).
What the server does NOT provide (gaps)
These are the most useful, well-scoped gaps to fill next. None are blockers for the current shipping state.
- No
hover.textDocument/hoveris unimplemented and returns-32601. A natural first add: return theRuleID+ a short explanation for diagnostics on the hovered range, or a keyword/type doc forhoveron SQL identifiers. - No
documentSymbol/documentLink. No outline/symbol tree. For a formatter that already parsesCREATE FUNCTION/PROCEDUREheaders into a CST, a symbol provider listing functions/procedures would be low-cost and high-value in large schema files. hover-style diagnostics shape. Diagnostics currently use aRangewhoseend.characterisstart.character + 1(a 1-char caret), not the actual offending span. A real highlight range would improve editor UX.textDocument/willSave/willSaveWaitUntil/didSave. No save hooks — "format-on-save" must currently be driven by the client bindingtextDocument/formattingto the editor's save event. AwillSaveWaitUntilhandler would let the server own format-on-save.- No
completion.textDocument/completionis not implemented. Not urgent for a formatter/linter, but relevant if PL/pgSQL autocompletion (keywords, types) is ever in scope. - No diagnostics debounce/coalescing beyond full-sync. Every
didChangere-runs the full lint engine. Fine for now; a debounce + incremental re-check becomes relevant on large files. initializationOptions/ workspace config.initializeparams are parsed nowhere — no way to pass style overrides or a config path over the protocol.- No
textDocument/prepareRename,rename,references,foldingRange. Low priority; would be natural extensions once symbol info exists.
Conventions to keep consistent
- One core, many frontends. The LSP reuses
pkg/diagnostics.Diagnosticandpkg/lintdirectly — no parallel diagnostic model. New LSP features should reuse these, not fork them. - Safety gate is non-negotiable. Both
formattingandrangeFormattingcallformat.SemanticallyEqual(src, out)before returning edits; on failure they return an empty edit (keep original). Any new code path that formats must honor this invariant (invariant #5 inAGENTS.md). - No new external deps. The wire layer is intentionally dependency-free. New protocol types should be added as local structs, not pulled in from an LSP library.
Concrete next steps (recommended, smallest-first)
Ranked by effort/value for the smallest useful delta:
- Add a real diagnostic highlight range (swap the 1-char caret for the actual
offending span) — ~1 file, no new method, immediate UX win. Reuses existing
RuleID/severity data. - Add
textDocument/hoverreturning the rule explanation for the hovered diagnostic, or a keyword/type glossary. Reusespkg/lintrule metadata. - Add
documentSymbollistingCREATE FUNCTION/PROCEDUREsignatures. Reuses the existing CST header parse inpkg/format. - Add
willSaveWaitUntilto own format-on-save instead of relying on client binding.
Do NOT: expand the LSP surface into a broad design (workspace features, incremental parsing, custom
textDocument/*extensions) as part of this issue. Keep any change scoped to the above and evidence-backed by apkg/lsptest (seeserver_test.gofor the framed-request/response harness).
Verification
go build ./cmd/pgtidysucceeds.go test ./...passes (LSP unit tests inpkg/lsp/server_test.goexerciseinitialize, formatting, range formatting,didClosediagnostics clearing).- Runtime e2e smoke test (framed JSON-RPC over stdio) confirmed
initializecapabilities,COR001diagnostics, and a formatting edit;hovercorrectly returns-32601.