Add galvan-lsp language server and galvan-format formatter#53
Open
antoniusnaumann wants to merge 33 commits into
Open
Add galvan-lsp language server and galvan-format formatter#53antoniusnaumann wants to merge 33 commits into
antoniusnaumann wants to merge 33 commits into
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 inSTYLE.md. Usable as a library (the LSP's formatting backend) and as a stdin/stdout + in-place +--checkCLI (e.g. Helixformatter = { command = "galvan-format" }). Operator spelling (unicode/logical) is configurable.Rust interop in the LSP
The LSP analyzes crates with the rustdoc-JSON interop:
Crate::analyzebuilds aRustInteropfrom the crate'susedeclarations (resolving dependencies against the nearestCargo.tomlvia the newRustInterop::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: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.RustSourceSpan, resolved against the documented dependency's source root) — jumping from a Galvan call lands in the.rsfile, including registry checkouts under~/.cargo.Verified against
example-projects/serde-jsonwith real rustdoc data: hover showsfn to_string(value: T) -> Result<String, Error>,serde_json::yields 78 completions, and goto-definition lands inserde_json-1.0.150/src/ser.rs.Grammar + compiler: async parses, TODO diagnostics
tree-sitter-galvan (
48ef48e) now parsesasync fn(modifier after visibility), multi-segment expression namespaces (tokio::net::TcpListener.bind(addr)),//inside string literals, and closure-parameter modifiers (|mut ticket|). The compiler carriesis_asynconFnSignature/MainDecland namespaces on associated calls, and emitsunimplementeddiagnostics 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
typecheck_with_interopis infallible, returning aTypecheckedstruct (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 thegalvan-lspe2e feature tests (with an injected-interop test module),galvan-formatidempotency/canonical-style tests, andgalvan-rustdoclift tests.python3 galvan-lsp/tests/stdio_smoke.py— full protocol session against the real binary: passes.//in strings and modifier closure params.Follow-ups (tracked in
galvan-lsp/TODO.md)workspace/didChangeWatchedFilesto pick up dependency changes without a server restart.header.is_asyncfrom rustdoc so awaited Rust futures can be typechecked once async lands.🤖 Generated with Claude Code