feat: LSP transport skeleton + coordinate layer (#222 Phase C)#718
Conversation
vera lsp now serves LSP 3.17 over stdio: handshake, serverInfo, full-text document sync, and an in-memory document store. Deliberately featureless — Phase D wires the obligation core in behind this transport, so the capability surface advertised at initialize never promises something unimplemented. New vera/lsp/ package: - convert.py — the load-bearing coordinate layer. Three systems meet at the LSP boundary and every feature must convert through here: ast.Span (raw Lark: 1-based line, 1-based code-point column, exclusive end), errors.SourceLocation (1-based line but 0-BASED column — the bases differ, which is why every converter takes its source type explicitly), and LSP Position (0-based line, UTF-16 code units). LineIndex does per-line code-point<->UTF-16 transcoding (astral-plane chars occupy two LSP columns); UTF-16 offsets landing inside a surrogate pair snap to the character start per the LSP spec's invalid-position guidance. Includes point->token-range widening for diagnostics (which carry a point, not a span). - documents.py — URI-keyed DocumentStore, full-text sync model, version tracking, lazy LineIndex invalidated on change. The store is the source of truth for open files; features never read disk. - server.py — pygls 2.x skeleton (VeraLanguageServer subclassing the typed pygls.lsp.server.LanguageServer — note the 1.x pygls.server path no longer exists), didOpen/didChange/didClose wired to the store, create_server() factory for test isolation. vera/cli.py: cmd_lsp + dispatch special-case before the path-argument length guard (the same shape as version), with an actionable install-the-extra message when pygls is missing. USAGE line added. pyproject.toml: new optional [lsp] extra (pygls>=2.0 — pinned to the 2.x API line the import path is written against; lsprotocol>=2023.0), mirrored into [dev] so CI's pip install -e .[dev] can run the tests; both pure-Python so the wheel-availability gate (which reads project.dependencies only) is unaffected. mypy overrides: pygls.*/ lsprotocol.* ignore_missing_imports, plus untyped-decorator exemption for vera.lsp.server (pygls' @server.feature is untyped) matching the established vera.transform narrow-exception shape. tests/test_lsp.py (30 tests): parametrized cp<->UTF-16 goldens with astral fixtures + surrogate-pair snapping, Span/SourceLocation conversion goldens pinning the differing column bases, point->token widening edge cases (slot token, punctuation, EOL), DocumentStore semantics, an in-process handler-drive test, and one stdio end-to-end round-trip against the real vera lsp subprocess (initialize -> didOpen -> shutdown -> exit, exit code 0, serverInfo + textDocumentSync pinned). Validation: mypy strict clean (67 files), ruff S clean, obligation + verifier suites green (496), doc counts consistent (4,250 tests / 34 files). Part of #222 (Phase C). Release prep follows before the PR. Co-Authored-By: Claude <noreply@anthropic.invalid>
Version bump across all tracked sites, CHANGELOG cut for 0.0.163, HISTORY.md single-sentence row, footer count, uv.lock regenerated for the new [lsp] extra, site assets. Skip-changelog: the CHANGELOG cut for 0.0.163 IS this commit's changelog change. Co-Authored-By: Claude <noreply@anthropic.invalid>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughImplements Vera LSP Phase C: stdio-based LSP server with serverInfo handshake, in-memory full-text DocumentStore, UTF-16-aware coordinate conversion (Span/SourceLocation ↔ LSP Position/Range), CLI entrypoint, optional LSP extras and mypy overrides, plus comprehensive tests. ChangesVera LSP Phase C: Stdio Transport, Document Sync, and Coordinate Conversion
Sequence DiagramsequenceDiagram
participant Client
participant VeraLanguageServer
participant DocumentStore
Client->>VeraLanguageServer: initialize (stdio)
VeraLanguageServer-->>Client: serverInfo + capabilities
Client->>VeraLanguageServer: textDocument/didOpen(uri,text,version)
VeraLanguageServer->>DocumentStore: open(uri,text,version)
Client->>VeraLanguageServer: textDocument/didChange(uri,contentChanges,version)
VeraLanguageServer->>DocumentStore: change(uri,new_text,version)
Client->>VeraLanguageServer: textDocument/didClose(uri)
VeraLanguageServer->>DocumentStore: close(uri)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #718 +/- ##
==========================================
- Coverage 90.96% 90.95% -0.01%
==========================================
Files 64 67 +3
Lines 24318 24434 +116
Branches 292 292
==========================================
+ Hits 22121 22225 +104
- Misses 2190 2202 +12
Partials 7 7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pyproject.toml`:
- Around line 64-67: The duplicated dependency pins for the [dev] extra (the
"pygls>=2.0" and "lsprotocol>=2023.0" entries) create a sync burden with the
[lsp] extra; either remove the duplicates and update CI to install both extras
(e.g., ensure CI runs pip install -e ".[dev,lsp]") or keep the duplication but
add a sanity check to CI or a pre-commit/lint job that compares the [dev] and
[lsp] lists and fails if they diverge; update CI configuration or add a small
check script invoked by the lint job that inspects pyproject.toml and verifies
the pygls and lsprotocol pins are identical between [dev] and [lsp].
In `@tests/test_lsp.py`:
- Around line 170-171: The test currently only ensures no exception is raised;
modify test_close_unknown_uri_is_noop to create a DocumentStore instance into a
variable (e.g., store), call store.close("file:///never-opened.vera"), and then
assert an observable postcondition such as len(store) == 0 to verify the store
remains empty; reference the DocumentStore class and the
test_close_unknown_uri_is_noop function when making this change.
- Around line 183-214: The test can leak a child process if
proc.communicate(requests, timeout=60) raises subprocess.TimeoutExpired; wrap
the communicate call in try/except/finally, catch subprocess.TimeoutExpired,
call proc.kill() (or proc.terminate() then proc.kill() if needed) and then call
proc.communicate() again to reap pipes, and ensure this cleanup runs in a
finally block so the vera lsp subprocess (proc) is always terminated; reference
the proc variable, the communicate call and subprocess.TimeoutExpired in
tests/test_lsp.py to locate where to add the handling.
In `@vera/cli.py`:
- Around line 1153-1168: cmd_lsp() currently catches any ImportError from "from
vera.lsp.server import main" and misattributes unrelated import failures to the
missing [lsp] extra; change the handler to only treat missing optional deps as
that error by catching ModuleNotFoundError (e.g. "except ModuleNotFoundError as
e:") and inspect e.name to ensure it's one of the optional dependencies ("pygls"
or "lsprotocol") before printing the "[lsp] extra" install message; if e.name is
different (or if a plain ImportError is raised), re-raise the exception so real
import errors in vera.lsp.server propagate instead of being masked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c9d7b95b-8535-44a8-8950-aae82de1c29a
⛔ Files ignored due to path filters (5)
docs/index.htmlis excluded by!docs/**docs/index.mdis excluded by!docs/**docs/llms-full.txtis excluded by!docs/**docs/llms.txtis excluded by!docs/**uv.lockis excluded by!**/*.lock,!uv.lock
📒 Files selected for processing (13)
CHANGELOG.mdHISTORY.mdREADME.mdROADMAP.mdTESTING.mdpyproject.tomltests/test_lsp.pyvera/__init__.pyvera/cli.pyvera/lsp/__init__.pyvera/lsp/convert.pyvera/lsp/documents.pyvera/lsp/server.py
All four findings valid: - pyproject: the [dev] extra now pulls [lsp] via the self-referential 'vera[lsp]' requirement (PEP 508 extras self-reference; verified against both pip and uv lock locally) instead of mirroring the pins — no second list to keep in sync, no CI install-command changes. - cmd_lsp: the import guard narrows to ModuleNotFoundError for pygls/lsprotocol specifically; any other import failure inside vera.lsp surfaces as its real traceback instead of being misreported as a missing extra. - test_lsp e2e: communicate() timeout now kills the subprocess and fails with captured output, so a hung server can't orphan a child process and destabilise later tests. - test_close_unknown_uri: asserts the observable postcondition (unrelated documents untouched, store size unchanged) instead of only no-exception. Co-Authored-By: Claude <noreply@anthropic.invalid>
Summary
Phase C of the #222 plan: the LSP transport skeleton —
vera lspserves LSP 3.17 over stdio with handshake, full-text document sync, and the coordinate-conversion layer every later feature converts through. Deliberately featureless: Phase D wires the obligation core (diagnostics, hover, slot goto, typed-hole completion) onto this transport, so the capability surface advertised atinitializenever promises something unimplemented. This PR does not finish #222 (Phases D–E follow; the issue stays open).What's in it
New
vera/lsp/packageconvert.py— the load-bearing piece. Three coordinate systems meet at the LSP boundary:ast.Span(raw Lark — 1-based line, 1-based code-point column, exclusive end),errors.SourceLocation(1-based line but 0-based column — the bases differ, so every converter takes its source type explicitly rather than guessing), and LSPPosition(0-based line, UTF-16 code units).LineIndextranscodes code-point ↔ UTF-16 per line (astral-plane chars occupy two LSP columns); offsets landing inside a surrogate pair snap to the character start per the spec's invalid-position guidance. Includes point→token-range widening for diagnostics, which carry a point rather than a span.documents.py— URI-keyedDocumentStore, full-text sync, version tracking, lazyLineIndexinvalidated on change. The store is the source of truth for open files; features never read disk for open documents.server.py— pygls 2.x skeleton:VeraLanguageServersubclassing the typedpygls.lsp.server.LanguageServer, didOpen/didChange/didClose wired to the store,create_server()factory for test isolation.CLI:
vera lspdispatches before the path-argument guard (same shape asversion); a missing[lsp]extra prints an actionable install message instead of a traceback. USAGE updated.Packaging: new optional
[lsp]extra —pygls>=2.0(pinned to the 2.x API line: the 1.xpygls.serverimport path no longer exists) +lsprotocol>=2023.0, mirrored into[dev]so CI runs the tests. Both pure-Python; the wheel-availability gate readsproject.dependenciesonly and is unaffected. mypy strict overrides:pygls.*/lsprotocol.*missing-imports + an untyped-decorator exemption forvera.lsp.server(pygls'@server.featureis untyped), matching the establishedvera.transformnarrow-exception shape.Verification
vera lspsubprocess over raw JSON-RPC stdio framing — initialize → didOpen → shutdown → exit — and pinsserverInfo,textDocumentSync, and exit code 0.ab🎉cd) and surrogate-pair snapping;SpanvsSourceLocationconversions pinning the differing column bases; BMP-multibyte (é, →) confirmed as single UTF-16 units; point→token widening over slot tokens, punctuation, and EOL;DocumentStoresemantics including index invalidation; an in-process handler-drive test.mypy vera/strict clean (67 files); ruff S clean; obligation + verifier suites green (496 — Phases A/B untouched); full non-stress suite green via pre-commit on both commits; doc counts consistent (4,250 tests / 34 files).Release prep (in this PR per workflow)
v0.0.163 across all tracked sites, CHANGELOG cut, HISTORY.md single-sentence row,
uv.lockregenerated for the new extra, site assets, tag on branch.Part of #222 (Phase C of the phased plan — D–E follow in their own PRs).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Chores
Documentation