Skip to content

Port LSP client extension from TypeScript to Rust#1

Open
SSFSKIM wants to merge 7 commits into
mainfrom
claude/start-4MfIl
Open

Port LSP client extension from TypeScript to Rust#1
SSFSKIM wants to merge 7 commits into
mainfrom
claude/start-4MfIl

Conversation

@SSFSKIM

@SSFSKIM SSFSKIM commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

This PR ports Claude Code's Language Server Protocol (LSP) client stack from TypeScript to Rust, enabling Codex to drive language servers for code intelligence features (go-to-definition, find-references, hover, diagnostics, etc.).

The implementation mirrors the three-layer architecture from Claude Code Src/:

  1. Transport layer (transport.rs): Async JSON-RPC framer over child process stdio with LSP Content-Length framing
  2. Instance layer (instance.rs): Per-server lifecycle management (spawn → initialize → request/notify with transient-error retry)
  3. Manager layer (manager.rs): Routing by file extension, document sync, and diagnostic registry
  4. Tool layer (tool.rs): Model-facing lsp/query tool exposing nine operations (go-to-definition, find-references, hover, etc.)

Key Changes

New Crates & Modules

  • codex-lsp crate: Complete LSP client implementation with ~1,500 lines of core logic
    • transport.rs (403 lines): JSON-RPC framer with request/response routing and notification dispatch
    • instance.rs (305 lines): Server lifecycle with initialize handshake and transient-error retry (ContentModified backoff)
    • manager.rs (363 lines): Extension-based routing, document sync, diagnostic batching
    • tool.rs (394 lines): Nine LSP operations exposed as a single namespaced tool
    • format.rs (289 lines): Human-readable formatting of LSP results (locations, diagnostics, symbols)
    • diagnostics.rs (219 lines): Passive registry with deduplication, severity sorting, and volume capping
    • config.rs (88 lines): Validated runtime view of server configs
    • schema.rs (37 lines): JSON schema generation for tool input
    • extension.rs (118 lines): Extension wiring (thread-scoped manager, tool registration, diagnostic injection)

Configuration

  • codex-config::LspServerConfig (config/src/lsp_config.rs): New TOML config struct for [lsp_servers.<name>] table
    • Mirrors [mcp_servers] shape: command, args, env, extension-to-language mapping, workspace folder, initialization options, timeouts, restart limits
    • Generated JSON schema in core/config.schema.json

Integration Points

  • ApplyPatchDocSync trait (extension-api/src/apply_patch_doc_sync.rs): Seam between apply-patch runtime and LSP manager for document sync without creating a dependency cycle
  • lsp_sync.rs in codex-core: Forwards committed file changes to the LSP manager via the trait object
  • Extension registration in app-server/src/extensions.rs: Installs the LSP extension alongside other extensions

Testing

  • Unit tests for all layers:
    • transport_tests.rs: JSON-RPC framer over tokio::io::duplex
    • instance_tests.rs: Server lifecycle and handshake
    • manager_tests.rs: Extension routing and document sync
    • tool_tests.rs: Tool dispatch and operation formatting
    • diagnostics_tests.rs: Registry deduplication and volume capping
    • config_tests.rs: Config validation and normalization
    • format_tests.rs: Location and diagnostic formatting
    • extension_tests.rs: Extension wiring
  • Smoke test (smoke_tests.rs): Real rust-analyzer integration (ignored by default, requires rust-analyzer on PATH)
  • Test support (test_support.rs): Shared helpers for framing and mock server scripting

Design Notes

  • Lazy server start: Servers are not spawned until first use; process exit is observed implicitly (stream closure fails in-flight requests with JsonRpcError::Closed)
  • Position encoding: Wire uses UTF-16 (declared in initialize), tool input/output uses

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT

claude added 7 commits May 31, 2026 16:48
…rkspace

- Add §0 errata table to docs/proposals/03-lsp-crate.md reconciling the
  spec against the real codex-rs APIs (8 points: 4 confirmed, 4 corrected).
- Fix inline code that would not compile: ApplyPatchDocSyncHandle newtype
  for the doc-sync hook (ExtensionData::get needs a concrete Sized type),
  real ToolSpec::Namespace builder for LspTool::spec(), exact runtime
  Config location for the lsp_servers field.
- Register ext/lsp-client in the workspace members and add the
  codex-lsp path dependency (crate scaffold to follow).

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
…ig wiring

Build sequence steps 1-7 of the codex-lsp port (docs/proposals/03-lsp-crate.md):
- Crate scaffold ext/lsp-client (package codex-lsp / lib codex_lsp) + workspace
  member, codex-lsp path dep, and lsp-types workspace dep.
- config: LspServerConfig type, [lsp_servers] on ConfigToml, lsp_servers on the
  runtime Config (core).
- extension-api: ApplyPatchDocSync trait + ApplyPatchDocSyncHandle + FileChange
  doc-sync seam (no codex-lsp <- codex-core cycle).
- transport: hand-rolled Content-Length JSON-RPC framer over child stdio,
  testable via tokio::io::duplex.
- instance: lifecycle state machine, initialize handshake (utf-16, didSave, no
  workspace/configuration), ContentModified(-32801) retry with 500/1000/2000ms
  backoff, workspace/configuration -> null[] responder.
- diagnostics: severity map, within-batch + cross-turn LRU dedup, severity sort,
  caps 10/file + 30 total.
- manager: first-wins extension routing, lazy start, full-content didChange doc
  sync, publishDiagnostics wiring, ManagerDocSync ApplyPatchDocSync impl.

22 unit/integration tests pass (cargo test -p codex-lsp). Tool, extension wiring,
and the core apply-patch hook follow in subsequent commits.

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
- tool.rs: single namespaced lsp/query tool, 9 operations, 1->0-based position
  conversion, lazy didOpen with 10MB guard, two-step call hierarchy, git
  check-ignore (batch 50) location filtering; ToolExposure::Direct, parallel-safe.
- format.rs: tolerant formatting of hover/locations/symbols/call-hierarchy from
  generic JSON (Location vs LocationLink, DocumentSymbol vs SymbolInformation).
- schema.rs: schemars input-schema helper mirroring ext/memories.

30 unit tests pass (cargo test -p codex-lsp).

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
- extension.rs: LspExtension implements ThreadLifecycle (build LspManager from
  config.lsp_servers, skip Exec sessions; store manager + ApplyPatchDocSyncHandle;
  shutdown on stop), ToolContributor (lsp/query), ContextContributor (inject
  pushed diagnostics as a developer-policy fragment each turn). install() registers
  all three.
- core/lsp_sync.rs: notify_if_present reads ApplyPatchDocSyncHandle from the thread
  store and maps AppliedPatchFileChange -> FileChange (no-op when no LSP extension).
- apply_patch.rs: rename _ctx -> ctx, forward committed changes to the doc-sync seam.
- app-server: register codex_lsp::install + add codex-lsp dependency.

32 codex-lsp tests pass; app-server builds clean. config.schema.json regen + core
test run follow.

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
Adds LspServerConfig definition and the lsp_servers property to the generated
config schema (just write-config-schema).
- Replace std Mutex .lock().unwrap() with .unwrap_or_else(PoisonError::into_inner)
  across transport/instance/diagnostics (unwrap_used is denied workspace-wide).
- Fix two genuine await_holding_lock bugs: hoist the tokio MutexGuard .take() out
  of the if-let in transport::shutdown and instance::stop so no guard is held
  across .await.
- diagnostics: sort_by_key for severity; NonZeroUsize without expect.
- schema/tool: panic! instead of expect on infallible schema generation.
- manager::request_value -> pub(crate) (no longer leaks the pub(crate) LspError).
- transport: while-let reader loop; ok_or_else for piped child stdio; map(Vec::len).

clippy -p codex-lsp --tests is clean; 32 tests still pass.

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
Adds src/smoke_tests.rs with two #[ignore] tests run on demand:
- tokio_spawn_rust_analyzer_responds: drives tokio::process + manual framing
  against a real rust-analyzer, asserting a Content-Length response.
- real_rust_analyzer_hover: spins up rust-analyzer over the full
  LspServerInstance/LspTransport stack against a tiny fixture crate, didOpens
  the file, and asserts a genuine hover ('pub fn add(a: i32, b: i32) -> i32'),
  exercising the initialize handshake and ContentModified retry during indexing.

The fixture sets [lsp_servers].env RUSTUP_TOOLCHAIN so the spawned server uses a
toolchain that has the rust-analyzer component (the repo pins one via
rust-toolchain.toml that may not), which also demonstrates the env config field.

Run: cargo test -p codex-lsp -- --ignored smoke
Both pass; normal suite stays at 32 passed / 2 ignored; clippy clean.

https://claude.ai/code/session_01HHRZTCpEbspDRhizowVDFT
SSFSKIM pushed a commit that referenced this pull request Jun 20, 2026
## Stack

1. [1 of 3] Support long raw TUI goal objectives - openai#27508
2. [2 of 3] Support long pasted text in TUI goals - openai#27509
3. **[3 of 3] Support images in TUI goals** - this PR

## Why

The first two PRs make goal definitions resilient to long text, but
`/goal` still dropped image inputs from the composer. That meant a user
could attach images while defining a goal and the resulting goal
continuation would not have any useful reference to those images.

Goal state still persists only objective text, so image inputs need to
become paths or URLs that the agent can read later.

## What Changed

- Extends TUI `GoalDraft` with local image attachments and remote image
URLs.
- Copies local goal images through the app-server filesystem layer into
the managed goal attachment directory, then rewrites active image
placeholders to file references.
- Appends unplaced local images and remote image URLs to the objective
as referenced image files or URLs.
- Preserves goal image metadata through live `/goal` submission and
queued `/goal` dispatch.

## Verification

- Added goal materialization coverage for local image files and remote
image URLs.
- Added/updated TUI slash-command coverage showing `/goal` drafts
include attached images instead of dropping them.

## Manual Testing

- Attached an image by bracketed-pasting its local path into a live
`/goal` composer. The `[Image #1]` placeholder became a server-host
`image-1.png` reference, copied bytes matched exactly, and no attachment
was written under the TUI's local home.
- Deleted an image placeholder before submitting a small goal and
verified no image was copied.
- Attached PNG and JPEG files to the same goal. Placeholder order was
preserved as `image-1.png` and `image-2.jpg`, and both remote copies
matched their source bytes.
- Tried extensionless, malformed-extension, and
extension/content-mismatched paths; the composer rejected them as image
attachments before goal dispatch rather than creating misleading managed
image files.
- Combined a local image, a large pasted block, and enough raw text to
exceed 4,000 characters. The remote attachment directory contained the
image, paste sidecar, and `goal-objective.md`; all embedded references
used server-host paths and both payloads matched their sources.
- Submitted an image replacement while a goal was active, verified no
image was copied before confirmation, then canceled and confirmed the
attachment count was unchanged.
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.

2 participants