Skip to content

feat: LSP transport skeleton + coordinate layer (#222 Phase C)#718

Merged
aallan merged 3 commits into
mainfrom
feat/222-phase-c-lsp-transport
Jun 10, 2026
Merged

feat: LSP transport skeleton + coordinate layer (#222 Phase C)#718
aallan merged 3 commits into
mainfrom
feat/222-phase-c-lsp-transport

Conversation

@aallan

@aallan aallan commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Phase C of the #222 plan: the LSP transport skeletonvera lsp serves 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 at initialize never promises something unimplemented. This PR does not finish #222 (Phases D–E follow; the issue stays open).

What's in it

New vera/lsp/ package

  • convert.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 LSP Position (0-based line, UTF-16 code units). LineIndex transcodes 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-keyed DocumentStore, full-text sync, version tracking, lazy LineIndex invalidated 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: VeraLanguageServer subclassing the typed pygls.lsp.server.LanguageServer, didOpen/didChange/didClose wired to the store, create_server() factory for test isolation.

CLI: vera lsp dispatches before the path-argument guard (same shape as version); 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.x pygls.server import path no longer exists) + lsprotocol>=2023.0, mirrored into [dev] so CI runs the tests. Both pure-Python; the wheel-availability gate reads project.dependencies only and is unaffected. mypy strict overrides: pygls.*/lsprotocol.* missing-imports + an untyped-decorator exemption for vera.lsp.server (pygls' @server.feature is untyped), matching the established vera.transform narrow-exception shape.

Verification

  • Live transport proof: one end-to-end test drives the real vera lsp subprocess over raw JSON-RPC stdio framing — initialize → didOpen → shutdown → exit — and pins serverInfo, textDocumentSync, and exit code 0.
  • Coordinate goldens (the substance, 30 tests total): parametrized code-point↔UTF-16 tables including astral fixtures (ab🎉cd) and surrogate-pair snapping; Span vs SourceLocation conversions pinning the differing column bases; BMP-multibyte (é, →) confirmed as single UTF-16 units; point→token widening over slot tokens, punctuation, and EOL; DocumentStore semantics 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.lock regenerated 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

    • LSP served over stdio with full-text document sync and a coordinate-conversion layer handling UTF-16 code-units
    • New CLI command to run the language server (with guidance if optional LSP extras are missing)
  • Tests

    • Comprehensive LSP test suite including coordinate-mapping, document lifecycle and end-to-end stdio handshake
  • Chores

    • Version bumped to v0.0.163; added optional [lsp] dependency extra
  • Documentation

    • Updated changelog, history, README, roadmap and testing totals

aallan and others added 2 commits June 10, 2026 19:12
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>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f640f420-2a92-4cb9-ac77-c1352875c700

📥 Commits

Reviewing files that changed from the base of the PR and between 94acbaa and 52cbb0a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (4)
  • TESTING.md
  • pyproject.toml
  • tests/test_lsp.py
  • vera/cli.py

📝 Walkthrough

Walkthrough

Implements 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.

Changes

Vera LSP Phase C: Stdio Transport, Document Sync, and Coordinate Conversion

Layer / File(s) Summary
Version bump and documentation updates
vera/__init__.py, pyproject.toml, CHANGELOG.md, HISTORY.md, README.md, ROADMAP.md, TESTING.md
Bump release to v0.0.163 and update changelog/HISTORY/README/ROADMAP/TESTING counts and compare links; add TESTING entry for new tests/test_lsp.py.
Dependency configuration and mypy overrides
pyproject.toml
Add project.optional-dependencies.lsp (pygls>=2.0, lsprotocol>=2023.0), include vera[lsp] in dev, and add mypy overrides for pygls.*/lsprotocol.* and vera.lsp.server.
Coordinate conversion layer
vera/lsp/__init__.py, vera/lsp/convert.py
Add LineIndex (per-line code-point↔UTF‑16 mapping with surrogate snapping) and converters: span_to_range, location_to_position, location_to_range (token widening), and position_to_cp.
In-memory document store
vera/lsp/documents.py
Add Document dataclass with lazy LineIndex caching and DocumentStore with open, change, close, get, and index invalidation on full-text change.
LSP server skeleton
vera/lsp/server.py
Add VeraLanguageServer (pygls LanguageServer) with Full text-document sync, per-instance DocumentStore, handlers for did_open/did_change/did_close, create_server() and main() to run over stdio.
CLI entry point
vera/cli.py
Add lsp command help text; cmd_lsp() imports/runs vera.lsp.server.main and reports actionable install message for missing optional deps; main() dispatches to cmd_lsp.
Comprehensive test suite
tests/test_lsp.py
Add layered tests: LineIndex (UTF‑16/astral/surrogate cases), Span/Location ↔ LSP mappings with token-widening, DocumentStore lifecycle and invalidation, in-process handler tests, and stdio JSON-RPC end-to-end handshake round-trip (_lsp_msg helper).

Sequence Diagram

sequenceDiagram
  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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

compiler, tests, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing LSP transport skeleton and coordinate conversion layer (Phase C), directly matching the primary deliverables in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/222-phase-c-lsp-transport

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.83051% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.95%. Comparing base (19abc7e) to head (52cbb0a).

Files with missing lines Patch % Lines
vera/cli.py 15.38% 11 Missing ⚠️
vera/lsp/server.py 96.42% 1 Missing ⚠️
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              
Flag Coverage Δ
javascript 61.40% <ø> (ø)
python 94.67% <89.83%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 19abc7e and 94acbaa.

⛔ Files ignored due to path filters (5)
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (13)
  • CHANGELOG.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • pyproject.toml
  • tests/test_lsp.py
  • vera/__init__.py
  • vera/cli.py
  • vera/lsp/__init__.py
  • vera/lsp/convert.py
  • vera/lsp/documents.py
  • vera/lsp/server.py

Comment thread pyproject.toml Outdated
Comment thread tests/test_lsp.py Outdated
Comment thread tests/test_lsp.py
Comment thread vera/cli.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>
@aallan aallan merged commit 0509ec5 into main Jun 10, 2026
27 checks passed
@aallan aallan deleted the feat/222-phase-c-lsp-transport branch June 10, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant