CrateStack Editor Tooling
This document records the current state of CrateStack editor support, how to use it locally, and the most useful follow-up work.
Scope
CrateStack has two editor surfaces:
- Rust files that consume one of the role-specific schema macros —
cratestack::include_server_schema!(...),cratestack::include_embedded_schema!(...), orcratestack::include_client_schema!(...) .cstackschema files authored directly
Those surfaces have different constraints.
Rust support depends on a real Cargo workspace because the schema macros are proc-macros that expand relative to a real schema path.
.cstack support is intentionally split out into a standalone language server so basic schema authoring does not require a full host project checkout.
Current State
Implemented in this repo today:
crates/cratestack-lspprovides a standalone language server for.cstackfiles (built on the actively-maintainedtower-lsp-server0.23 fork as of 0.3.0; the previoustower-lsp0.20 dep had been unmaintained since 2024)packages/cratestack-vscodeprovides the VS Code extension wrapper that launchescratestack-lspcratestack-cli check --format jsonprovides machine-readable diagnostics for CI or editor fallback integrations- parser and semantic structures now preserve schema docs and source spans needed for editor features and generated Rust docs
- the schema macros now emit Rust
#[doc = "..."]attributes from schema-authored comments
Implemented .cstack editor features:
- diagnostics — all independent errors at once, not one per save
- hover, including an enum's variants and, for an ordinal enum, each variant's number (since 0.17.0)
- completion
- go-to-definition, including enums and mixins
- find-all-references (
textDocument/references) - rename (F2), with
prepareRenameso the editor can refuse an invalid rename before showing the input box - semantic tokens (
textDocument/semanticTokens/full) - document symbols
- document highlight
- basic syntax highlighting through the bundled TextMate grammar
- relation-aware definition lookup inside
@relation(fields:[...],references:[...]) - narrower relation diagnostics that point at the bad relation token instead of only the whole declaration line
Semantic tokens
The TextMate grammar cannot tell String (a builtin scalar), User (a model), Role (an enum) and Timestamps (a mixin) apart — to a regex they are four bare capitalised words. The server re-colours identifiers by what they actually resolve to:
| Schema construct | Token type |
|---|---|
| model | struct |
| enum | enum |
| enum variant | enumMember |
| mixin | interface |
| builtin scalar | type |
field, @relation column | property |
| procedure | function |
| procedure argument | parameter |
Tokens supplement the grammar rather than replace it. VS Code has no tree-sitter API for third-party languages, so the grammar keeps handling keywords, strings and comments — available instantly, before the server starts — and the server layers resolved identifier colouring on top. Only an attribute's @name head is a decorator, so columns named inside @relation(fields: [...], references: [...]) keep colouring as the properties they are.
Features survive a syntax error
A failed parse used to drop the schema entirely, so every feature needing one flickered off keystroke by keystroke while typing — a file spends most of its editing life invalid. The server now retains the last schema that parsed together with the exact text it was parsed from, because spans index into the text that produced them: resolving a retained span against the current buffer would land in the wrong place while still looking like working navigation.
Two limits are deliberate. Diagnostics always describe the current text, so a retained schema never suppresses a live error. And a document that has never parsed keeps nothing. Because a result can now legitimately predate what is on screen, hover marks a stale popup with a one-line note rather than presenting it as current.
Implemented Rust-side editor improvements:
- schema
///docs now flow into generated Rust docs and rust-analyzer hovers when proc-macro expansion is enabled - procedure
/// @param name ...docs now flow into generated procedure argument types
Current limitations:
- the parser still validates an initial schema subset rather than the full target grammar described across the broader docs
- Rust-side support is still project-dependent and requires real Cargo context
- the LSP does not yet implement formatting or code actions
- parsing has no error recovery, so a syntax error still yields exactly one diagnostic — the multi-error reporting above applies to semantic validation, where declarations are independent
- the VS Code extension prefers a bundled server binary when one is staged, but it does not yet auto-download release binaries
Rust Setup In VS Code
For Rust consumers of the CrateStack schema macros, use rust-analyzer and point it at the workspace or workspaces that actually build the schema consumer.
Recommended workspace settings for this repo:
{
"rust-analyzer.linkedProjects": [
"cratestack/Cargo.toml",
"your-backend/Cargo.toml"
],
"rust-analyzer.procMacro.enable": true,
"rust-analyzer.cargo.buildScripts.enable": true,
"rust-analyzer.checkOnSave": true,
"rust-analyzer.check.allTargets": true
}Why this is required:
- this repo root is not a single Cargo workspace
- generated Rust APIs come from proc-macro expansion
- the generated
cratestack_schemamodule only exists when rust-analyzer can build the real consumer crate
.cstack Setup In VS Code
The intended path for .cstack files is the cratestack-vscode extension plus cratestack-lsp.
Local development flow:
- From
cratestack/, build the language server withcargo build -p cratestack-lsp. - From
cratestack/packages/cratestack-vscode, runpnpm installif needed. - Install or run the extension.
- If the server binary is not on
PATHand not bundled into the extension package, setcratestack.lsp.pathto the built binary.
Supported extension settings:
cratestack.lsp.path: path to thecratestack-lspbinarycratestack.lsp.args: extra args passed through to the server
The extension resolves the server in this order:
- configured
cratestack.lsp.path, if set to something other than the defaultcratestack-lsp - bundled binary under
server/<platform>/cratestack-lsp cratestack-lsponPATH
CLI Fallback And CI
For machine-readable schema validation outside the editor:
cargo run -p cratestack-cli -- check --schema path/to/schema.cstack --format jsonThis is useful for:
- CI validation
- fallback editor integrations outside VS Code
- smoke-testing parser and semantic diagnostics without starting the LSP
Schema Docs And Generated Rust Docs
Schema-authored comments now serve both schema authors and Rust consumers.
Supported today:
- leading
///comments on declarations and fields /// @param name ...docs for procedure arguments- proc-macro emission of Rust
#[doc = "..."]attributes for generated models, fields, inputs, and procedure modules
This keeps one documentation source for:
.cstackauthors reading schemas- Rust users reading generated API docs and hovers
- future richer hover content in the
.cstacklanguage server
Packaging And Release Flow
The current extension packaging model is intentionally thin.
cratestack-vscode contributes the language registration and launches cratestack-lsp; the heavy logic stays in the Rust binary.
Current release flow:
- Build the release server with
cargo build --release -p cratestack-lsp. - Stage the binary into
packages/cratestack-vscode/server/<platform>/withpnpm run stage-server. - Package the extension with
pnpm run package:vsix.
The VSIX packaging step uses vsce --no-dependencies because the extension ships a small JavaScript wrapper plus the staged server binary rather than relying on npm dependency scanning to decide runtime contents.
Listing metadata is in place: alongside .vscodeignore, license, and repository, package.json declares an icon (packages/cratestack-vscode/icon.png, a 256×256 PNG) and a matching galleryBanner. Neither the Marketplace nor Open VSX requires an icon to accept a publish, so without one both listings — and the in-editor Extensions sidebar after a manual VSIX install — fall back to a generic placeholder. The field is platform-independent, so every per-target VSIX carries it without extra work.
Marketplace And Open VSX
The published extension's identity, for anyone searching for it or scripting an install:
| Extension ID | cratestack.cratestack-vscode-plugin |
| Display name | CrateStack Schema |
| Registries | Visual Studio Marketplace (VS Code) and Open VSX (VSCodium, Cursor, Windsurf) |
The display name is CrateStack Schema, not CrateStack. The Marketplace rejects the shorter name as already taken — independently of the extension ID, which was itself accepted. Nothing public holds the shorter name: an extensionquery for CrateStack across the entire gallery, not just VS Code extensions, returns zero results, so whatever reserves it is unlisted, removed, or internal. Gallery search is therefore not a valid way to check a candidate display name in advance; only a real publish attempt answers the question.
Open VSX has no such constraint and accepted CrateStack at v0.10.1, so the two registries genuinely disagree about this name's availability — v0.10.1 is live on Open VSX under the old display name.
Verifying a publish
The Marketplace item page lags a successful publish. After a publish succeeds, the listing page can return 404 for several minutes — in both publisher casings — while the extension is already fully published with every target platform. That is the same write-path/read-path split that makes npm view unreliable immediately after publishing, and it reads exactly like a failed publish.
Query the gallery API instead. It is consistent with the write path, and it returns targetPlatform per version, so it also confirms every per-platform vsix landed — which the item page does not show directly:
curl -s -X POST 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json;api-version=3.0-preview.1' \
-d '{"filters":[{"criteria":[{"filterType":7,"value":"cratestack.cratestack-vscode-plugin"}],"pageSize":5,"pageNumber":1}],"flags":914}'itemName is case-insensitive — both cratestack. and Cratestack. resolve. Worth knowing, because the gallery API reports the publisher as Cratestack while package.json declares cratestack, which is enough of a mismatch to look like a problem when it isn't.
Probing a publish without cutting a release
release-vscode.yml normally runs on a vX.Y.Z tag push. It also accepts a manual workflow_dispatch, which builds every target and publishes to the Marketplace only — the attach-github-release and publish-openvsx jobs are gated if: github.event_name == 'push', so a manual run can neither create a GitHub Release nor reach Open VSX.
That is a deliberate exception to the tag-push-only rule, narrowed so the rule still holds absolutely for Open VSX (where a publish cannot be cleanly deleted and retried). It exists because several Marketplace rejections are discoverable only at publish time, and some cost a version number to retry — a displayName collision worst of all, since the field is baked into the vsix at package time and a failed release is bumped past rather than re-run. A failed Marketplace publish consumes nothing: the version is taken only on success, so probing the same version repeatedly is safe.
The trade is that a successful probe is a real publish, and can put a version on the Marketplace whose artifact differs from the one attached to that tag's GitHub Release — which is exactly what happens when the probe is what fixed the artifact. Cosmetic, and resolved at the next real tag.
Verification In Repo
Covered today:
- parser tests for docs, spans, and related regressions
- LSP tests for hover, definitions, symbols, and relation diagnostics
- extension package tests for server path resolution
- VS Code extension-host smoke tests for activation and bundled server launch
- Rust workspace tests for the underlying crates
This gives reasonable confidence that the current editor stack works end to end, including packaged extension behavior.
Future Improvements
Highest-value follow-up work:
- Add code actions for common relation mistakes, especially missing
fields/referencestargets and simple typo recovery. - Add formatting (
textDocument/formatting) for.cstackfiles. - Add stronger extension-host end-to-end tests that assert definition, hover, and diagnostics through the actual VS Code APIs.
Semantic tokens, rename and find-references were on this list and have since shipped — see Current State. So has the marketplace icon — see Packaging And Release Flow.
Likely medium-term work:
- Extend the parser and semantic model toward the full target grammar described in the broader PRD and ADR docs.
- Add richer relation-aware validation so relation diagnostics can reason about more mismatches before code generation.
- Expose more stable editor-oriented library surfaces from parser and semantic crates instead of keeping some logic narrowly embedded in the current LSP layer.
- Improve multi-platform release packaging so extension artifacts can be produced and verified more systematically across supported targets.
- Add non-VS-Code editor integration paths using the standalone
cratestack-lspbinary.
Deferred or optional follow-ups:
- Formatting support for
.cstackonce the schema grammar and style expectations stabilize. - Auto-download or release-channel discovery for
cratestack-lspbinaries instead of requiring either a bundled server or manual path setup. - More workspace-aware Rust and schema cross-navigation if future architecture needs symbol links between generated Rust surfaces and original schema declarations.