Learn/Rune LSP

Rune LSP

eustress-lsp is the language server for Rune scripts in Eustress. It runs the same analyzer as Studio's Problems panel and speaks the Language Server Protocol over stdio or TCP, so any editor with an LSP client gets diagnostics, hover, completion, navigation and rename.

Time10 min readLevelIntermediateUpdatedUpdated Sep 2026

01Overview

One Analyzer, Every Editor

eustress-lsp is a small program that exposes the Rune analyzer built into Eustress Engine as a standard language server. Studio's script editor, its Problems panel and eustress-lsp all call the same analyzer functions, so a script shows the same errors, in the same words, wherever you open it.

The server itself only translates. It turns editor positions into the analyzer's line and column numbers, calls the analyzer, and turns the results back into protocol messages, using the tower-lsp library. Improving the analyzer improves every editor at once.

Diagnostics

Diagnostics

Parse, compile and dry-run errors, pushed on every edit.

Hover

Hover

Signatures, descriptions and examples for the Eustress API.

Navigation

Navigation

Definitions, references and an outline, across the Universe.

Editing

Editing

Completion, parameter hints, rename and a quick fix.

Getting the Binary

eustress-lsp is built from the engine package and carries the engine's version. The Windows installer places eustress-lsp.exe beside eustress-engine.exe, where Studio finds it. The Windows zip, the macOS disk image and the Linux archive do not include it yet; on those, build the server from the Eustress source:

Terminal
# In the eustress/ folder of the repository
cargo build --release -p eustress-engine --bin eustress-lsp

# The binary lands in target/release/ (eustress-lsp.exe on Windows)

The lsp feature that enables it is on by default. To see which version you have:

Terminal
$ eustress-lsp --version
eustress-lsp 0.3.6
Usage: eustress-lsp [--tcp [--port <n>] [--port-file <path>]]

02Running It

Command Line

FlagEffect
(none)Serve one editor over stdin and stdout
--tcpServe over TCP on 127.0.0.1 instead, for any number of editors
--port <n>With --tcp, listen on port n. The default, 0, lets the operating system pick a free port
--port-file <path>With --tcp, write the port number to this file, creating its folders
-h, --help, --versionPrint the version and usage, then exit

Any other argument is reported on stderr and ignored, and a --port value that is not a number counts as 0.

stdio

With no flags, eustress-lsp reads protocol messages on stdin and writes replies on stdout. This is how editors usually run a language server: the editor starts the process, owns it, and is its only client.

TCP

With --tcp, the server listens on the loopback address only, so it accepts connections from your own machine. Once it is listening, it prints port=<n> on stdout and writes the same number to the --port-file path, if you gave one. Each connection gets its own session, with its own open documents and Universe index, so several editors can share one server.

Ctrl+C stops the server and deletes the port file. Status lines, such as the listening address and each new connection, go to stderr.

Terminal
# Let the operating system pick a port, and record it where editors look
eustress-lsp --tcp --port-file MyUniverse/.eustress/lsp.port

# Or listen on a port of your choosing
eustress-lsp --tcp --port 7000
Info
Same code either way

A server you start yourself and the one Studio starts run the same code and read the same files, so their features are identical. TCP only changes who starts the process and how many editors share it.

03Inside Studio

How Studio Starts It

Studio starts eustress-lsp for you, so editors can connect without launching their own. As soon as the open Space resolves to a Universe (the nearest parent folder that contains Spaces/), Studio runs:

Launched by Studio
eustress-lsp --tcp --port-file <Universe>/.eustress/lsp.port

It uses the first copy of the program it finds:

  1. The file named by the EUSTRESS_LSP_BIN environment variable
  2. eustress-lsp beside the Studio executable, where the Windows installer puts it
  3. The target/release and target/debug folders of the source tree Studio was built from

If there is none, Studio logs one message and stops looking until it restarts. Editors can still start their own server over stdio.

Restarts and Logs

  • One server per Universe. Moving to another Space in the same Universe keeps the server. Moving to a Space in a different Universe stops it, deletes its port file and starts a new one there.
  • Clean exit. Closing Studio stops the server and deletes .eustress/lsp.port.
  • Crash safety on Windows. The server runs without a console window and is tied to Studio by a job object, so Windows ends it even if Studio crashes or is force-closed.
  • Logs. The server's stderr goes to eustress-lsp.log, and the launcher's latest status to eustress-lsp-launcher.log, both in the system temp folder (%TEMP% on Windows).
Info
A port file left behind

After a crash, the port file can outlive the server, since only a normal exit deletes it. Studio overwrites the file the next time it starts the server, and the VS Code extension abandons a dead port after 1.5 seconds.

04Diagnostics

Three Passes

Each analysis runs up to three passes over the text of one file:

errors found: skip the dry runParseCompileDry runDiagnosticsRune parserfunctions indexedengine modulesnames resolvedlifecycle functionscalled onceeditor, Problemspanel, Output
Parse and compile always run. The dry run happens only when neither found an error, so a file that does not compile is never executed.
  1. Parse. Rune's own parser reads the file. A syntax error becomes a diagnostic, and each top-level function is recorded as a symbol for navigation.
  2. Compile. The file is compiled against Rune's standard modules plus the engine's Rune modules (the eustress module, event_bus and the realism laws), the same set Play mode compiles against. This catches unknown names and bad imports that parsing cannot.
  3. Dry run. If the first two passes found no errors, the analyzer calls each lifecycle function the script defines, once: on_init, on_ready, on_update and on_tick with a time step of 0.016 s, and on_button_click with the button name TestButton. A failure is reported at the function's name, since it would fail the same way in Play. on_exit is never called.
Advanced
The dry run executes your code

The dry run calls your functions for real, in Studio and in every running eustress-lsp, each time a file is analyzed: on every edit in an external editor, and for every script in the Universe when an editor connects. The HTTP functions in the Eustress API send real requests when called this way, so keep network calls out of on_init and on_update, or guard them.

Sources and Timing

Each diagnostic names the pass that produced it:

SourceSeverityMeaning
runeErrorA syntax or compile error
rune-warningWarningA compiler warning
rune-linkErrorA link error, shown at the start of the file because the compiler gives it no location
rune-runtimeErrorA lifecycle function failed during the dry run

eustress-lsp analyzes a file when it opens, on every change and on every save, and pushes the results with textDocument/publishDiagnostics. Document sync is Full: your editor sends the whole file with each change, and the server analyzes it right away.

Inside Studio, the script editor waits until 80 ms after your last keystroke, then feeds the same diagnostics to the squiggles, the Problems panel and the Output panel.

05Language Features

Capabilities

The server's initialize response advertises these capabilities and names the server eustress-lsp, with the engine's version:

CapabilitySettingWhat you get
textDocumentSyncFullThe editor sends the whole file on every change
hoverProviderOnAPI documentation, functions in the file, diagnostics
completionProviderAlso opens on a period or a double quoteKeywords, functions, API names, simulation keys
signatureHelpProviderOpens on an opening parenthesis, advances on a commaParameter hints for Eustress API calls
semanticTokensProviderWhole documentColors for keywords, API functions and types, your functions, strings, numbers and comments
definitionProviderOnThe file, then the Universe, then an API page
referencesProviderOnDeclarations that share the name
documentSymbolProviderOnThe file's top-level functions
renameProviderOnEdits in the open file and in files that declare the name
codeActionProviderOnInsert missing semicolon

Anything not in the table, such as formatting, workspace symbols or inlay hints, is not implemented. After the handshake, the server logs eustress-lsp ready to the editor.

Hover and Navigation

Take a small script that reads and writes simulation values:

Rune
use eustress::{get_sim_value, log_info, set_sim_value};

pub fn on_init() {
    log_info("cycle_life ready");
}

pub fn on_update(dt) {
    let soc = get_sim_value("battery.soc");
    set_sim_value("battery.soc_safe", clamp(soc, 0.0, 1.0));
}

fn clamp(value, lo, hi) {
    if value < lo { lo } else if value > hi { hi } else { value }
}
  • Hover on get_sim_value shows its entry in the Eustress API catalog, which is built from the engine's own Rune module source: category, signature, description, and an example when there is one. On clamp it shows a function defined in this file and the line it starts on. On a name inside an error's range, it shows the error.
  • Go to definition on clamp jumps to it in this file. A name the file does not declare resolves to functions with that name in other .rune files of the Universe. On get_sim_value it opens a generated Markdown page for the API entry, written to eustress-lsp-api in the temp folder and deleted when the server shuts down.
  • Find references on clamp lists the declarations named clamp in this file and across the Universe. Call sites are not listed yet.
  • Outline shows on_init, on_update and clamp.

Completion and Editing

  • Completion lists Rune keywords first, then functions from the open file, then Eustress API functions and types with their signatures, up to 50 items.
  • Simulation keys. Inside the quoted key of a get_sim_value or set_sim_value call, completion lists the keys in .eustress/runtime-snapshot.json, which Studio rewrites 4 times a second while it runs.
  • Parameter hints appear for Eustress API calls and highlight the parameter you are typing.
  • Rename replaces every occurrence of the name outside comments and strings, in the open file and in each other file that declares a function with that name. The new name must be a valid Rune identifier.
  • Quick fix. When a diagnostic says a semicolon is expected, Insert missing semicolon adds it.
  • Suggestions. Lines that call get_sim_value, set_sim_value, http_request or datastore_get also offer actions titled Eustress: …. They run a command no editor implements yet, so choosing one leaves the file unchanged.
Advanced
Rename matches names, not scripts

Each Rune script compiles on its own, yet a rename also edits every other script in the Universe that declares a function with the same name, including all uses of that name inside it. Before renaming a common name such as clamp, review the other files the rename changed before you save them.

The Universe Index

When an editor connects, the server takes the editor's workspace folder (or its root path), walks up as many as 16 levels to the first folder that contains Spaces/, and indexes every .rune file below it, up to 12 folders deep. Folders whose names start with a dot, target and node_modules are skipped.

A file watcher with a 150 ms debounce keeps the index current when files change on disk, including edits from other tools and a git checkout, and every save re-indexes the saved file. With no Universe found, navigation stays within the open file.

06Editor Setup

VS Code and Its Forks

Use the Eustress Rune LSP extension. It connects to the server Studio starts and falls back to launching eustress-lsp itself. IDE Integration covers installing and configuring it.

Neovim

Neovim 0.10 and newer can start the server from init.lua without plugins:

init.lua
-- Recognize .rune files, then start eustress-lsp for them.
vim.filetype.add({ extension = { rune = 'rune' } })

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'rune',
  callback = function(args)
    vim.lsp.start({
      name = 'eustress-lsp',
      cmd = { 'eustress-lsp' },
      -- The Universe is the folder that contains Spaces/.
      root_dir = vim.fs.root(args.buf, 'Spaces'),
    })
  end,
})

Helix

Add the server and a Rune language entry to languages.toml in your Helix configuration folder:

languages.toml
[language-server.eustress-lsp]
command = "eustress-lsp"

[[language]]
name = "rune"
scope = "source.rune"
file-types = ["rune"]
roots = ["Spaces"]
language-servers = ["eustress-lsp"]

In both editors, use the program's full path if its folder is not on PATH. The Windows installer does not add its folder to PATH.

07What's Next

Richer Symbols

The analyzer indexes functions today. Next it will record use declarations, structs, enums, constants, impl blocks and modules, so the outline, go to definition and rename cover them too, and a period after a value will complete its members.

The Server in Every Package

The Windows zip, the macOS disk image and the Linux archive will ship eustress-lsp beside the engine, as the Windows installer does, so Studio starts the server on every platform without a source build.

One analyzer. Every editor you like.

Loading Eustress Engine...