Spice Language Server
Ownership:
spice lspand its compiler-service implementation live inspice-framework/toolchain. This document records the ecosystem contract retained by core.
spice lsp serves editor-neutral Language Server Protocol 3.x features over
standard JSON-RPC on stdin/stdout. Stdout contains protocol frames only; command
or transport failures go to stderr after the connection ends.
Editor command
Configure an editor LSP client to launch the repository-built or installed Spice executable with one argument:
go tool github.com/spice-framework/toolchain/cmd/spice lspThe client should identify Go documents and pass a local workspace folder URI.
If it does not, opening a file makes Spice walk upward to the nearest go.mod.
Remote document and workspace URIs are rejected.
Spice advertises UTF-16 positions and full-document synchronization. Open,
change, save, and close notifications maintain bounded defensive overlay
copies. Files on disk remain ordinary Go; annotation completion always inserts
or preserves the // @... representation.
Analysis
Each workspace owns an isolated compiler/service.Service. After a 150 ms
debounce, the server submits all open document versions as one overlay request.
A newer version cancels older work, advances the service sequence, and prevents
the older result from being published. Analysis is read-only: it does not
generate files or update ownership manifests.
The server publishes the same stable diagnostic codes and physical source locations used by the CLI for annotation syntax and validation, exact provider wiring, lifecycle hooks, application features, configuration declarations, and module ownership, boundary, dependency, and cycle failures. LSP ranges are converted from compiler byte columns to zero-based UTF-16 positions. A diagnostic publication includes the analyzed document version so an editor can also reject an obsolete message.
Application-global failures that do not have a source location use
window/showMessage; they are never attached to an arbitrary file.
Language features
Completion and navigation are derived from the shared compiler service:
- statically decoded descriptors selected by explicit annotation imports;
- exported descriptor candidates discovered from source already present in the target module graph, workspace modules, vendor tree, local replacements, and module cache;
- descriptor package paths and symbols inside
@importdeclarations; - annotation arguments and required-argument snippets;
- bootstrap allowed values such as management endpoint names;
- exact module IDs and named-interface identities;
- generated configuration property keys.
The earlier @spice.import spelling is a hard error, not an alias. The
diagnostic is anchored to the retired token and carries a version-checked
workspace edit that replaces only it with @import.
Typing @ on an otherwise empty declaration line may complete to a valid
comment and add the corresponding explicit import as a versioned additional
text edit:
// @management.Enable(expose=["health"])Existing named aliases and namespace imports are preserved. Completion detail
identifies the descriptor package, selected module version or replacement,
implementation tool, and whether the target go.mod authorizes that exact
tool, so the inserted source has inspectable provenance. Catalog discovery
forces GOPROXY=off, never executes a descriptor or tool, never changes module
files, and keeps fully typed imported descriptors authoritative over lexical
pre-import candidates. Completion is refused when @ appears in an unrelated
Go expression.
Hover renders the descriptor summary and GoDoc, typed arguments, descriptions, defaults and allowed values, targets, examples, compatibility, resolved module provenance, tool, handler, protocol, and implementation symbol. Signature help uses the same argument model and tracks the active annotation argument. Configuration hover still omits secret defaults.
Definition and document-link requests return the exact one-file Go descriptor
function selected by the current file’s named, aliased, or namespace import.
textDocument/implementation returns the real Go handler source symbol declared
by that descriptor. The compiler resolves both locations offline through the
same target module graph used for analysis, including vendor and local
replacement source. Unknown annotations and @ text in strings or ordinary
comments never become links. Unimported annotations fail closed and do not
receive synthetic documentation or navigation targets.
For an SDK argument with ValueDomainGoInterface, completion and navigation
use the same loaded Go type universe as dependency validation. An interface
from any package in the module graph receives a namespace @import; definition
and hover resolve the operand to the real Go interface declaration and method
set. This path does not depend on the editor’s Go index, and it never inserts an
ordinary import used only by an annotation or a handwritten assertion.
Code actions come from compiler/diagnostic.SuggestedFix. The server returns an
action only when every edit names an open document, carries the exact current
document version, and intersects the requested range. The first available fix
converts an accidental raw annotation line:
@Applicationto:
// @ApplicationThe edit is a precise prefix insertion rather than a file rewrite.
An explicitly imported descriptor whose tool is absent from the target
application’s own go.mod also receives a command-backed quick fix:
- Preview runs the exact displayed
go get -tool package@versionagainst a temporary sibling modfile and shows the completego.mod/go.sumdiff; the application files remain unchanged. - Apply previewed is offered only after that preview and acts as the explicit confirmation. The server checks the content-derived token and the original hashes of both module files before staged replacement, and rolls back on a write failure.
The preview is bounded, tied to an unauthorized descriptor from the shared
offline catalog, and replaced when the user previews again. Execute commands
run as tracked asynchronous LSP requests so the server continues reading
$/cancelRequest; cancellation terminates the go get subprocess and returns
the standard LSP request-cancelled error. The language server advertises only
these two exact execute-command IDs. It does not accept an arbitrary shell
command, package path, workspace, or unpreviewed mutation.
The server also provides full-document semantic tokens. It reports
@qualified.Annotation as decorator, argument names as parameter, quoted
values as string, integers as number, booleans and unquoted values as
keyword, and annotation delimiters as operator. The // prefix remains
ordinary Go comment syntax. Editors choose whether and how to combine semantic
tokens with their native Go grammar.
Workspace settings
Clients may select a target and bounded package patterns during initialization:
{ "initializationOptions": { "target": "Commerce", "patterns": ["./..."] }}The same values may be refreshed without restarting through
workspace/didChangeConfiguration:
{ "settings": { "spice": { "target": "Commerce", "patterns": ["./..."] } }}Omitting patterns uses the compiler service default ./.... Values must be
trimmed; invalid settings fail closed without replacing the last complete
metadata result.
Protocol and resource boundaries
The stdlib-only transport bounds a message at 16 MiB, a header line at 8 KiB,
and a header block at 64 lines. Duplicate or invalid Content-Length framing
is fatal because the stream cannot be safely resynchronized. A length-delimited
invalid JSON body receives the standard JSON-RPC parse error and the session
continues.
The server supports initialize, initialized, shutdown, exit, cancellation, document open/change/save/close, workspace-folder changes, configuration refresh, diagnostics, completion, signature help, hover, definition, implementation and document-link navigation, quick fixes, and full semantic tokens. Its execute-command provider is limited to confirmed annotation-tool preview/apply. A clean first analysis still publishes an empty diagnostic set for each open document, allowing clients to finish synchronization without a sentinel error. Shutdown cancels active analyses and command requests. Caller context cancellation interrupts a blocked closable input stream. Multiple workspaces never share services, overlays, results, caches, or installation previews.
The first-party Zed adapter and its setup/fixture are documented in
zed.md.