Skip to content

Add galvan-lsp language server and galvan-format formatter#53

Open
antoniusnaumann wants to merge 33 commits into
mainfrom
feature/language-server
Open

Add galvan-lsp language server and galvan-format formatter#53
antoniusnaumann wants to merge 33 commits into
mainfrom
feature/language-server

Conversation

@antoniusnaumann

@antoniusnaumann antoniusnaumann commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Adds two new tooling crates and the compiler-side infrastructure they build on:

  • galvan-lsp — a language server speaking LSP over stdio, built on the real parser + typechecker. Features: diagnostics (with stable codes and machine-applicable fixes), goto definition/type definition across files, references, rename, hover, context-aware completion (including enum paths and foreign-keyword spellings with quickfix explanations), signature help with overloads, inlay type hints, document/workspace symbols, semantic tokens, code actions (write inferred type annotations, foreign-keyword quickfixes), document highlight, folding, selection ranges, and full/range/on-type formatting with incremental sync.
  • galvan-format — the canonical source formatter: a Wadler-style pretty-printer over the tree-sitter CST with the style contract documented in STYLE.md. Usable as a library (the LSP's formatting backend) and as a stdin/stdout + in-place + --check CLI (e.g. Helix formatter = { command = "galvan-format" }). Operator spelling (unicode/logical) is configurable.

Rust interop in the LSP

The LSP analyzes crates with the rustdoc-JSON interop: Crate::analyze builds a RustInterop from the crate's use declarations (resolving dependencies against the nearest Cargo.toml via the new RustInterop::from_uses_in) and caches it process-wide per (project, imported crates) pair. The build runs on a background thread: analysis never blocks on cargo/rustdoc, and when the build lands the server re-publishes diagnostics for the project's open documents. Lifted items flow through the typechecker's symbol index (DefinitionKind::RustItem), which gives:

  • Completion: serde_json:: completes the crate's lifted functions/types/constants, Type. completes associated functions and constants of imported Rust types, and imported types appear in type/value positions.
  • Hover: interop symbols show a rendered Galvan signature plus their Rust path and crate.
  • Go-to-definition into Rust sources: rustdoc item spans are lifted (RustSourceSpan, resolved against the documented dependency's source root) — jumping from a Galvan call lands in the .rs file, including registry checkouts under ~/.cargo.

Verified against example-projects/serde-json with real rustdoc data: hover shows fn to_string(value: T) -> Result<String, Error>, serde_json:: yields 78 completions, and goto-definition lands in serde_json-1.0.150/src/ser.rs.

Grammar + compiler: async parses, TODO diagnostics

tree-sitter-galvan (48ef48e) now parses async fn (modifier after visibility), multi-segment expression namespaces (tokio::net::TcpListener.bind(addr)), // inside string literals, and closure-parameter modifiers (|mut ticket|). The compiler carries is_async on FnSignature/MainDecl and namespaces on associated calls, and emits unimplemented diagnostics for async functions and closure-parameter modifiers instead of a whole-file parse error. The axum-api example now parses end to end and is in canonical format.

Compiler-side changes supporting the LSP

  • The typechecker builds a symbol index during checking and spans all diagnostics; identifiers carry source spans.
  • typecheck_with_interop is infallible, returning a Typechecked struct (module + errors + index): duplicate definitions become diagnostics instead of hard errors, so tooling always gets a usable result for broken files.

Merge state

main (including the Rust-interop work from #14) is already merged into this branch (ef74737), so this PR merges cleanly.

Testing

  • cargo test --workspace — all 36 suites pass, including the galvan-lsp e2e feature tests (with an injected-interop test module), galvan-format idempotency/canonical-style tests, and galvan-rustdoc lift tests.
  • python3 galvan-lsp/tests/stdio_smoke.py — full protocol session against the real binary: passes.
  • 109 tree-sitter corpus tests pass, including new cases for async fns, namespaced associated items, // in strings and modifier closure params.

Follow-ups (tracked in galvan-lsp/TODO.md)

  • workspace/didChangeWatchedFiles to pick up dependency changes without a server restart.
  • Lift header.is_async from rustdoc so awaited Rust futures can be typechecked once async lands.

🤖 Generated with Claude Code

antoniusnaumann and others added 30 commits July 3, 2026 18:39
Introduce a new galvan-lsp crate implementing a Language Server Protocol
server for Galvan. It reuses the compiler crates as libraries
(galvan-files, galvan-parse, galvan-into-ast, galvan-ast, galvan-resolver)
rather than duplicating parsing or name resolution.

Features:
- Hover: shows signature/declaration and doc comment of the function or
  type under the cursor
- Go to definition: jumps to top-level function and type declarations
- Completion: top-level functions, types, and language keywords
- Syntax diagnostics from tree-sitter error/missing nodes

Because AST identifiers carry no spans, the server uses the tree-sitter
parse tree (re-exported by galvan-parse) to map cursor positions to tokens
and resolves them by name via galvan-resolver's LookupContext. Compiler
limitations that block richer features (locals, methods, cross-file
resolution, semantic diagnostics) are documented in
galvan-lsp/compiler-features.md, with graceful degradation instead of
workarounds in the other crates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a crate-wide semantic index (src/workspace.rs) so hover,
go-to-definition, and completion work across all files of a crate, not
just the current buffer.

A Crate loads every .galvan file under the crate's source root (the
nearest ancestor `src` directory, matching the compiler) using
galvan_files::read_sources, and aggregates them into a single
LookupContext via LookupContext::add_from — the same mechanism the
compiler uses for a whole crate. Files open in the editor contribute
their in-memory (unsaved) contents; the rest are read from disk. Each
resolved declaration carries its originating Source, which is turned into
a cross-file Location.

Document is slimmed to the purely syntactic view of one buffer (text +
parse tree); all semantic resolution now goes through Crate. compiler-
features.md is updated: same-crate resolution is now supported; only
cross-*crate* `use` imports remain blocked on the resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnostic spans only carried byte offsets, not the file they originate
from, so when several files are checked together (a whole crate) their
diagnostics could not be routed back to the right source.

Add ErrorCollector::set_current_file; the typechecker sets it before
lowering each top-level item, and the file is stamped onto any span whose
file is still empty. This also improves the compiler's own multi-file
error messages. Behaviour for single-file callers is unchanged (the file
defaults to empty).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run galvan_hir::typecheck over the whole crate on each change and publish
the resulting type errors alongside the existing tree-sitter syntax
diagnostics. Crate::diagnostics builds a combined SegmentedAsts for the
crate, typechecks it (guarded by catch_unwind so the server survives
pathological input), and returns span-carrying compiler diagnostics; the
diagnostics feature keeps those belonging to the refreshed document and
maps their byte spans to LSP ranges.

Relies on the new ErrorCollector::set_current_file in galvan-hir to route
each diagnostic to the correct file. compiler-features.md item #5 is
updated from a stub to implemented, with the remaining caveats (span-less
diagnostics, empty RustInterop, crate-level LookupErrors) documented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ident and TypeIdent now carry the span of their token, populated when
reading the tree-sitter parse tree. Spans are not part of an
identifier's identity: equality and hashing still use only the name, so
identifiers remain usable as lookup keys. Also adds TypeDecl::span and
a visit_type_idents walker over TypeElement for tools that need to
enumerate type references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The typechecker now records a SymbolIndex as a by-product of lowering:
definitions (functions, types, struct fields, enum variants, parameters
and locals with their scopes) and every reference it resolves (variable
reads, calls, method calls, type annotations, constructor calls, field
accesses, enum accesses and match patterns), each keyed by file and
identifier span. typecheck returns Typechecked { module, index, errors }
and no longer aborts: duplicate top-level declarations keep the first
declaration and are reported as spanned DuplicateDeclaration diagnostics
(LookupContext::add_from returns conflicts instead of Err).

galvan_hir::query::expression_at maps a position to the innermost HIR
expression, exposing the inferred type at that position.

All remaining span-less diagnostics in the typechecker and codegen now
carry spans pointing at the offending token or expression.

Includes tests asserting that error-producing Galvan code reports the
expected message, suggestion and span, and that the symbol index
resolves locals, methods, types and fields by position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hover, go-to-definition, the new find-references capability and
completion all resolve through the SymbolIndex produced by typechecking
the crate, so locals, parameters, methods, fields, enum variants and
types follow one resolution path. Completion is scope-aware
(visible_locals) and offers the receiver type's fields and methods after
a dot, using the HIR expression-at-position query for the receiver type;
when the buffer does not parse mid-keystroke, the analysis runs on a
probe with a placeholder identifier inserted after the dot. Name-based
lookup remains only as the degradation path when the crate fails to
parse.

compiler-features.md now records shortcomings 1, 2, 3 and 5 as resolved
and lists what is still missing (cross-crate imports, cached Rust
interop, UFCS members in completion).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completion now classifies the syntactic context at the cursor and only
offers what can occur there: enum cases after `Enum::` (and nothing
after unknown qualifiers such as packages), type names after `->` and
annotating colons, nothing where a new name is introduced, and ranked
locals/functions/types/keywords elsewhere. Keywords are filtered by
position (top level vs statement start vs expression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…caching

- Point queries (definition_at/reference_at) are end-inclusive and pick the
  innermost span, so hover/goto/references work with the cursor right after
  an identifier.
- Hover/goto fall back to name-based resolution and member completion probes
  when the current file has a parse error but the rest of the crate analyzes.
- Diagnostics are published for every open document of the crate, cleared on
  close, and re-computed on save (which also re-reads the crate from disk).
- Crate analysis is memoized per Crate and the Crate is cached per root keyed
  on open-document versions; read_sources/typechecker failures are logged.
- crate_root no longer falls back to the server's CWD; LSP positions past the
  end of a line clamp before the newline; completion probes guard char
  boundaries.

Progress is tracked in galvan-lsp/TODO.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keyword groups now mirror tree-sitter-galvan/grammar/keywords.js: async,
const, main, struct and enum are no longer offered; move, none, loop, try
and throw are; the built-in statement functions print/println/assert/panic
are offered as functions. Shadowed locals collapse to the innermost binding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three are read off the existing typechecker symbol index. Rename edits
the definition token and every recorded reference across the crate;
document symbols nest fields, enum cases and methods under their type;
inlay hints show the inferred type of unannotated local bindings. Location
conversion now builds one LineIndex per source file instead of one per
result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drives the built binary through initialize, didOpen, completion, hover,
documentSymbol, rename, inlayHint and didClose — the layer the Rust e2e
tests do not cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Call sites are detected by a forward scan of the document text (tracking
strings, interpolations, char literals, comments and bracket nesting), so
help keeps working while the argument list is unclosed and the file does
not parse. Signatures are rendered from the declaration's own source with
precise UTF-16 parameter offsets; labelled arguments select parameters by
name so reordered constructor arguments highlight correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
Syntax-level tokens (comments, literals, keywords) come from the
tree-sitter tree; identifiers are classified through the typechecker's
symbol index with a declaration modifier on defining occurrences.
String interpolations are carved out of string tokens so the embedded
expressions highlight as code, contextual control words and builtins
get keyword / defaultLibrary fallbacks, and multi-line tokens are split
per line for client compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
The "Add type annotation" refactor action inserts the typechecker's
inferred type after an unannotated local binding. The inference is
shared with inlay hints through inlay_hints::unannotated_locals, and
each inlay hint now carries the same insertion as its text edit so
clients can apply it from the hint directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
Normalizes leading indentation (bracket depth, leading-closer dedent,
member-chain continuation indent) and trailing whitespace without ever
reflowing tokens, so it cannot change program meaning. Multi-line
string content is protected via the parse tree and unparseable files
are refused. A conformance test pins zero edits on the hand-formatted
example projects; idempotence is tested as well.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
The smoke test now asserts and exercises signatureHelp, semanticTokens,
codeAction and formatting over the real protocol. The TODO document
records all audit items as done and lists the follow-ups (quickfixes
blocked on structured diagnostics, builtin signatures, semantic-token
range/delta, AST reuse in analyze(), a future galvan-format crate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
Diagnostic gains `code` (stable snake-case identifier per TranspilerError
variant) and `fix` (span + replacement). The Levenshtein did-you-mean
helpers now attach the matched candidate as a fix over the unknown
name's span, so tooling can apply the suggestion mechanically instead
of parsing free text. ErrorCollector stamps the current file onto fix
spans the same way it does for diagnostic spans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
New features/foreign_syntax.rs maps well-known keywords from other
languages to their Galvan equivalent (func/def -> fn, switch -> match,
class/struct -> type, var -> mut, null -> none, import -> use, ...) and
detects them on three paths, since the parser treats them differently:

- inside tree-sitter error regions (`func greet()` breaks the parse);
  comments and strings are skipped, and the generic "Syntax error" is
  replaced by a diagnostic narrowed to the keyword
- as unresolved callees in code that parses (`switch color { }` is a
  trailing-closure call, `var x = 1` a free-function call; the
  typechecker lowers unknown callees silently, so the parse tree and
  symbol index are consulted directly)
- as unknown identifiers/types from the typechecker (`let x = null`),
  rewriting the generic message

Identifiers that merely share a name with a foreign keyword stay legal:
only error regions and unresolved names are checked.

Diagnostics now carry the compiler's stable code and machine-applicable
fix in Diagnostic.code/data. Code actions decode that data back from
context.diagnostics, so every fix-carrying diagnostic — foreign keywords
and the compiler's did-you-mean suggestions alike — becomes a preferred
one-click quickfix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
Keyword completion items gain familiarity aliases from the foreign-
keyword table: typing `switch` matches an item that shows and inserts
`match` (filter_text carries the foreign word, the label the Galvan
one, the label description explains the mapping). Aliases follow the
keyword sets they belong to — `func`/`class`/`import` only at the top
level, `var`/`switch` in statement position — and sort after the real
keywords so an unfiltered list is unchanged up front.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
…ype formatting

Five features on existing infrastructure, each a pure module:

- documentHighlight: the symbol's spans from the index, current file
  only; the binding is a write, uses are reads.
- foldingRange: multi-line bracket pairs fold from the opener's line to
  the line before the closer; runs of consecutive `//` lines fold as
  comments, runs of `use` declarations as imports.
- selectionRange: the parse-tree ancestor chain, deduplicated by range,
  for stepwise expand-selection.
- typeDefinition: variable/parameter/field to its type declaration,
  enum case to its enum.
- rangeFormatting and onTypeFormatting (trigger `}`): the whitespace
  formatter's edits filtered to the requested lines; depth is always
  computed from the top of the file so partial formats never disagree
  with a full format.

The stdio smoke test now covers the six new capabilities plus a full
foreign-keyword quickfix round trip over didChange.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
…ke test

- The thirteen semantic handlers shared a get-document / crate_for /
  to_file_path preamble; it now lives in Backend::request_context and a
  RequestContext struct. Syntax-only handlers (formatting, folding,
  selection) keep the plain document lookup so they never load a crate.
- Text sync is INCREMENTAL: did_change applies ranged edits in order
  against the current text, still accepting whole-document changes.
  The smoke test applies a quickfix through a ranged didChange and
  asserts the diagnostics clear.
- The smoke test resolves the server binary relative to itself
  (override with GALVAN_LSP_BIN) instead of a hard-coded absolute path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
All roadmap items are done; "Possible next steps" now lists the newly
found grammar gap (`use foo::Bar` does not parse although the spec and
the axum example use it), further compiler-fix candidates for the
quickfix pipeline, codeLens, call hierarchy, pull diagnostics and
watched-files cache eviction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
A Wadler-style pretty-printer over the tree-sitter parse tree: normalizes
spacing and indentation, reflows bracketed lists and member chains at a
100-column width, preserves comments, blank-line paragraphs and exact
token spellings, and refuses to format sources with syntax errors.

The galvan-format binary reads stdin/writes stdout (the contract Helix
expects), formats files in place, and offers --check, --line-width,
--indent-width and --use-tabs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
textDocument/formatting now runs the real formatter and returns minimal
per-line TextEdits from an LCS line diff (re-indented lines become
whitespace-only edits, so cursors stay put); range and on-type formatting
keep filtering those edits by line. The example projects and test
fixtures are updated to the canonical style (trailing commas in broken
lists, collapsed empty bodies).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
STYLE.md records every formatting decision with its default and the
precedent it follows (rustfmt/Prettier/gofmt), plus the invariants and
the deliberately-untouched territory (token spellings, string contents,
use order).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
…control flow

Two new FormatOptions dimensions, both defaulting to untouched:
unicode_operators normalizes symbol pairs (≠/!=, ≥/>=, →/->, ±/+-, ...)
to Unicode or ASCII, and logical_operators normalizes and/&&, or/||,
not/!, in/∈ to words or symbols (CLI: --operators, --logical).

Per the updated STYLE.md, trailing closures and else expressions used as
values now collapse to one line when every body holds a single statement
and the whole expression fits (let color = if dark { blue } else { red });
in statement position bodies still always break, and when such an
expression breaks, all of its bodies break together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPHpRVsLL5MjMBXiRpUY9N
Reconciles the LSP branch with the Rust-interop work from main:

- Keep main's transpile_segmented_asts structure while adopting the
  branch's infallible typecheck_with_interop returning Typechecked
  (module + errors + symbol index).
- Record symbol-index member references only for named constructor
  arguments, now that constructor args carry field_name: Option<Ident>
  for positional construction.
- Handle the new HirCollection::Tuple variant in the HIR query visitor.
- Adopt main's string-based typecheck tests (lower_with_interop) and
  drop .expect() at call sites of the now-infallible APIs.
- Re-format the serde-json example into canonical galvan-format style
  after taking main's content changes.

Full workspace tests and the LSP stdio smoke test pass on the merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Grammar (tree-sitter-galvan 48ef48e): async fn modifier, multi-segment
namespaces on function calls and associated items, string contents no
longer lex `//` as a comment, closure parameters accept modifiers.

Compiler: FnSignature/MainDecl carry is_async, AssociatedFunctionCall/
AssociatedConstant carry Option<UsePath> namespace, ClosureParameter
carries Option<DeclModifier>. The typechecker emits `unimplemented`
diagnostics for async fns and closure-parameter modifiers instead of
the previous whole-file parse error.

The axum-api example now parses end to end and is formatted in
canonical style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
antoniusnaumann and others added 3 commits July 13, 2026 19:49
galvan-rustdoc: lifted items now carry their rustdoc source span
(RustSourceSpan; relative filenames resolved against the documented
dependency's recorded source root), functions expose their associated
receiver, and RustInterop::from_uses_in resolves dependencies against an
explicit consumer project directory instead of CARGO_MANIFEST_DIR/cwd.

galvan-hir: the typechecker registers every lifted interop item in the
symbol index as DefinitionKind::RustItem (rendered Galvan signature,
namespace, Rust path, source location) and records references where
interop calls, constants and receivers resolve, so all position-based
tooling picks them up.

galvan-lsp: Crate::analyze builds the interop from the crate's `use`
declarations and caches it process-wide per (project, imported crates);
completion offers crate items after `crate::`, associated items after
`Type.` and imported types in type/value positions; hover renders the
lifted signature with its crate origin; go-to-definition jumps into the
Rust sources (registry or path dependencies). Covered by injected-interop
e2e tests; verified against example-projects/serde-json with real rustdoc
data (hover, 78 completions after serde_json::, goto-def into the
registry sources).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An interop cache miss no longer blocks analyze() on cargo metadata and
rustdoc: the slot is marked Building, a named background thread builds
the interop, and analysis proceeds without it. On completion the thread
publishes the consumer project directory through
workspace::interop_ready_events; the server's watcher task (spawned on
`initialized`) evicts the project's cached crates — their analyses ran
without interop — and re-publishes diagnostics for its open documents,
so editors pick up the interop without waiting for the next edit.

The server's documents/crates state moves behind an Arc so the watcher
can share it; refresh becomes a free function used by both the request
handlers and the watcher. ToplevelItem is now Clone (for handing the
use declarations to the build thread).

Covered by the background_interop_build e2e test (real Cargo project,
soft-missing dependency) and verified against example-projects/serde-json:
hover starts empty and flips to the lifted signature once the build
lands; the stdio smoke test still passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anics

Requests timed out because the server process died: the axum example
(parsing since the grammar fixes) reaches AST-conversion code that was
previously unreachable, and the first todo!() panic on the main thread
killed the whole server.

- Implement TupleTypeItem::read_cursor (`-> (StatusCode, Json<T>)`).
- Fix ParametricTypeItem's argument loop to match the actual `,` node
  kind instead of "_comma", so multi-argument generics
  (`Result<Json<T>, E>`) convert instead of panicking.
- Treat any remaining AST-conversion panic like a parse failure in the
  LSP (catch_unwind in Crate::from_sources/run_analysis with a stderr
  note): a language server must survive whatever is typed.

Hardening found while investigating:

- Request contexts and the diagnostics refresh now snapshot the document
  instead of holding a DashMap reference across crate loading and
  analysis (a held reference can deadlock with concurrent didChange
  writes once a writer queues on the same shard).
- Interop builds run on a single worker with a supersede check instead
  of one thread per use-set: typing a `use` line no longer spawns a
  cargo/rustdoc invocation per keystroke, and stale queued builds are
  skipped (their Building marker is dropped so an undo re-queues them).

Verified: latency probe on axum-api (previously: server dead, all
requests time out) now 82 requests, worst 18ms with the interop build
running; concurrency stress probe 3x clean; full workspace tests, stdio
smoke test and the serde-json interop probe all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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