Skip to content

Add Rustanka language server#59

Draft
iainlane wants to merge 210 commits intografana:masterfrom
iainlane:iainlane/rustanka-language-server
Draft

Add Rustanka language server#59
iainlane wants to merge 210 commits intografana:masterfrom
iainlane:iainlane/rustanka-language-server

Conversation

@iainlane
Copy link
Member

@iainlane iainlane commented Feb 27, 2026

This PR adds a language server to Rustanka. It supports all the things: typing, formatting, diagnostics, code actions, hovering, renaming, references, all the stuff.

image

I would say it is currently a little bit buggy. But do have a play with it:

cargo build --release --bin jrsonnet-lsp

and read the README for info on the config options. 😁

This adds the first full LSP implementation for jrsonnet. It introduces
a new `jrsonnet-lsp` binary and a dedicated crate stack for document
management, scope resolution, import graphing, type inference,
diagnostics/checking, request handlers, and server orchestration.

Implemented features include incremental sync, definition, hover,
completion, references, rename, inlay hints, code lens, semantic tokens,
formatting, workspace symbols, async request execution, and async
diagnostics with dependency-aware cache invalidation.

Type inference now includes flow typing driven by stdlib predicates.
Flow typing means we narrow a variable's type inside control-flow
branches based on checks such as `std.isString(x)` and
`std.isNumber(x)`.

Example:

```jsonnet
if std.isString(x) then x + "!" else x
```

In the `then` branch, `x` is narrowed to `string`.

Example:

```jsonnet
if std.isNumber(x) then x + 1 else x
```

In the `then` branch, `x` is narrowed to `number`. Predicates marked
partial (for example `std.isInteger`) only narrow the `true` branch; the
`false` branch is not treated as the strict complement.

There were some changes to other crates required:

- The AST now models field/index/slice/call as explicit expression nodes
  (`ExprField`, `ExprIndex`, `ExprSlice`, `ExprCall`), giving stable
  node boundaries and ranges used by hover, go-to-definition,
  completion, and semantic token classification.

- The Rowan parser now exposes the Rowan green-tree types and richer
  syntax error types, allowing parse results/errors to be reused safely
  across async diagnostics and structural tests.

- `jrsonnet-fmt` is refactored into context/macros/printable modules,
  with explicit style options and snapshot coverage, so
  `textDocument/formatting`, which calls this, can be deterministic and
  regression-tested.

- Evaluator/Tanka/import resolution plumbing is updated so eval diagnostics
  and execute-command evaluation follow the same path-resolution behavior as
  the language server.

- Workspace dependency and compatibility updates (`lsp-types` bump,
  lockfile refresh, and required clippy/FFI cleanups in touched crates)
  keep the expanded workspace compiling cleanly under the stricter lint
  profile used for this work.

LSP documentation is added under `docs/lsp/`.`
Add textDocument/didSave handling in the server and advertise save support in textDocumentSync options. On save, refresh document state (when text is provided), invalidate dependent type cache entries, update import graph state, and reschedule diagnostics for the saved file and open importers.

Add integration coverage for save-triggered diagnostics refresh and update architecture docs to describe the didSave path.
Wire  into async command execution with argument parsing for URI,
position, and optional includeDeclaration. Reuse the existing references
path and return LSP Location arrays to command callers.

Add integration coverage for command behavior (with and without
declarations) and update LSP docs to mark the command as implemented.
Advertise range support in semantic token capabilities, route
textDocument/semanticTokens/range requests in the server, and add a
range-aware token generation path in handlers.

Add unit and integration tests for range filtering and update LSP docs
to reflect full + range semantic token support.
- finalize each TypeAnalysis by merging its LocalTyStore into the shared
  GlobalTyStore and remapping all recorded expression/document types
  before caching or reuse. - introduce GlobalTy in jrsonnet-lsp-types
  and switch type-cache/import-resolver boundaries to use it, so
  cross-file APIs cannot accept local Ty IDs at compile time. - harden
  TySubst::merge for cyclic local types by lowering unresolved local
  refs to Any during merge, preventing invalid global entries. - update
  cross-file cache tests/bench wiring for GlobalTy and add cycle
  regression coverage in subst tests.
- advertise and route textDocument/implementation in the server
  capability and request dispatcher. - implement async
  go-to-implementation resolution for local bindings and import-backed
  fields, with separate declaration vs implementation ranges when
  available. - load unopened imported files on demand so implementation
  lookup can traverse import chains without requiring all files to be
  open. - add integration coverage for local and import-field
  implementation lookups, plus regression coverage for import
  diagnostics + definition stability. - document implementation-provider
  behavior and handler flow in docs/lsp architecture/handlers docs.
- make URI handling fallible and propagate/skip invalid paths in request
  and diagnostics flows

- replace panic-prone indexing/slicing with checked access across
  handlers, inference, check, scope, import, and type-store code

- remove remaining panic-style server dispatch fallthroughs and simplify
  error handling paths

- tighten type-store mutation paths (object merge/narrow, union/sum
  simplification, safe deref fallbacks)

- keep behavior stable with targeted test runs across lsp-document,
  lsp-types, lsp-import, lsp-scope, lsp-inference, lsp-check, and
  lsp-handlers
- route definition/declaration/implementation through explicit
  target-based resolution

- keep declaration semantics aligned with definition for declaration
  sites while preserving distinct implementation jumps

- strengthen integration coverage with structural assertions and
  declaration checks for import-field navigation

- update handler docs to describe dedicated declaration dispatch
Introduce distinct goto paths so declaration returns the nearest lexical
binder while definition follows local alias chains to canonical origins.

- add handler-level declaration vs definition modes and alias-chain
  resolution for local bindings/import aliases - route server
  declaration/implementation through lexical declaration handling, while
  keeping implementation expression jumps intact - add structural
  integration coverage for divergence cases (local alias and
  import-field alias)
Rewrite declaration/definition/implementation guidance in clearer
language with practical intent and worked examples.

- explain when each request should be used - add concrete alias/import
  examples with expected jump targets - align architecture summary
  wording with handler-level behavior
Cross-file reference search no longer requires the cursor to sit on the
top-level definition token.

When the cursor is on a local reference, we now resolve its definition
first and treat it as cross-file-exportable if that definition is
file-scope. This preserves existing behavior for definitions while
making references from local usage sites return importer hits as
expected.
Fix the failing type-at-position test for computed object field names
by addressing the inference gap instead of weakening the assertion.

Object inference previously skipped dynamic field-name expressions
entirely, so no type information was recorded for tokens inside
`[expr]` field keys. This caused `type_at_position` to fall back to
less-specific ancestor types.

The object inference passes now:
- mark objects with dynamic fields as open (`has_unknown = true`)
- infer dynamic key expressions in pass 2 so their expression ranges
  are recorded in analysis
- preserve unknown-field state when merging super objects

Also adds a regression test that dynamic-field objects are open.
Together with the existing query test, this verifies the root cause
and the behavior end-to-end.
Split expression inference implementation out of `expr/mod.rs` so the
module file only declares submodules and re-exports public/internal
entry points.

The implementation previously lived directly in `mod.rs`, which made it
harder to navigate and expanded diff noise for unrelated changes. This
moves the core inference logic into `expr/core.rs` and leaves `mod.rs`
as import/export wiring only.

No behavior changes are intended. The refactor keeps existing visibility
boundaries by re-exporting crate-internal items used by sibling modules
and preserving external APIs.

Also updates one test import in `expr/advanced.rs` to match how the test
module references `Document` after the split.
Extend inlay hint rendering so comprehension bindings can emit per-name
type hints when the binding uses destructuring.

Previously, the comprehension hint path only handled simple bindings
(`for x in ...`). Destructured forms like `for [a, b] in ...` produced no
binding hints even when the inferred element type was known.

The comprehension pipeline now reuses the existing destructuring hint
walker with the inferred iterator element type. This keeps behavior
consistent with local/object-local destructuring hints and supports
nested array/object patterns.

This remains non-intrusive by default. Destructured comprehension hints
appear only when `inlayHints.comprehensions = "all"` and
`inlayHints.destructuring = "all"` are both enabled.

Also adds unit coverage for both the enabled case and the config-gated
disabled case, and documents the category interaction in
`docs/lsp/README.md`.
Add focused end-to-end scenarios for inlay hints so configuration and
category interactions are validated through the scenario runner, not
only unit-level handlers.

New coverage includes:
- local/object-local mode filters (`all`, `variables`, `functions`)
- comprehension + destructuring gating for destructured `for` bindings
- function-parameter hints combined with call-argument hints,
  including named-argument skip behavior

Each fixture keeps one concern per scenario so failures are easy to
triage and configuration regressions are easier to localize.
Make formatting request options first-class across formatter, LSP, and
scenario harness paths.

What changed:
- map request options for indentation (`insertSpaces`/`tabSize`) and
  trimming flags through the LSP formatting request path
- move trailing-whitespace and final-newline trimming behavior into
  `jrsonnet-fmt` options so normalization lives in the formatter
- keep `insertFinalNewline` handling at the LSP layer as a final
  request-specific post-step

Also expands test coverage:
- async formatting option unit tests in LSP
- integration tests for optional formatting request flags
- scenario runner support for formatting request option fields
- new scenario fixtures covering option combinations, files with
  trailing input newlines, and tabs/spaces/tabSize behavior

README formatting docs now describe these request option effects.
Drop README language that promised how the server applies
`FormattingOptions` on formatting requests.

This was over-specific for user-facing docs and implied a behavioral
contract users should not need spelled out. The section now keeps the
configuration field reference and removes per-request option mapping
details.
Add end-to-end range formatting support across formatter, handlers,
server wiring, and scenario/integration test harnesses.

Core behavior:
- add `jrsonnet-fmt::format_code_range` and `ByteRangeEdit` so range
  edit computation lives in the formatter crate
- compute a minimal changed byte span from full formatted output and
  return edits only when changes are fully contained in the requested
  range
- keep full-document formatting behavior unchanged

LSP wiring:
- advertise `documentRangeFormattingProvider`
- route `textDocument/rangeFormatting` through request dispatch and
  async handler plumbing
- apply the same request option merge path used by document formatting
  (`insertSpaces`/`tabSize`, trimming options)

Scenario and test coverage:
- extend scenario DSL with `requestRangeFormatting` and
  `expectRangeFormatting`
- add focused runner fixtures for:
  - edits contained in requested range
  - no-op when formatter changes escape requested range
  - request option behavior for tabs/spaces and tab size
- add integration tests for range formatting responses and lifecycle
  capability assertions
- update missing-step coverage scenario to include range formatting

Docs:
- update architecture/handlers docs to include range formatting request
  routing and capability coverage
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