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.
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
Parse, compile and dry-run errors, pushed on every edit.
Hover
Signatures, descriptions and examples for the Eustress API.
Navigation
Definitions, references and an outline, across the Universe.
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:
# 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:
$ eustress-lsp --version
eustress-lsp 0.3.6
Usage: eustress-lsp [--tcp [--port <n>] [--port-file <path>]]02Running It
Command Line
| Flag | Effect |
|---|---|
| (none) | Serve one editor over stdin and stdout |
--tcp | Serve 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, --version | Print 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.
# 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 7000A 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:
eustress-lsp --tcp --port-file <Universe>/.eustress/lsp.portIt uses the first copy of the program it finds:
- The file named by the
EUSTRESS_LSP_BINenvironment variable eustress-lspbeside the Studio executable, where the Windows installer puts it- The
target/releaseandtarget/debugfolders 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 toeustress-lsp-launcher.log, both in the system temp folder (%TEMP%on Windows).
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:
- 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.
- Compile. The file is compiled against Rune's standard modules plus the engine's Rune modules (the
eustressmodule,event_busand the realism laws), the same set Play mode compiles against. This catches unknown names and bad imports that parsing cannot. - 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_updateandon_tickwith a time step of 0.016 s, andon_button_clickwith the button nameTestButton. A failure is reported at the function's name, since it would fail the same way in Play.on_exitis never called.
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:
| Source | Severity | Meaning |
|---|---|---|
rune | Error | A syntax or compile error |
rune-warning | Warning | A compiler warning |
rune-link | Error | A link error, shown at the start of the file because the compiler gives it no location |
rune-runtime | Error | A 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:
| Capability | Setting | What you get |
|---|---|---|
textDocumentSync | Full | The editor sends the whole file on every change |
hoverProvider | On | API documentation, functions in the file, diagnostics |
completionProvider | Also opens on a period or a double quote | Keywords, functions, API names, simulation keys |
signatureHelpProvider | Opens on an opening parenthesis, advances on a comma | Parameter hints for Eustress API calls |
semanticTokensProvider | Whole document | Colors for keywords, API functions and types, your functions, strings, numbers and comments |
definitionProvider | On | The file, then the Universe, then an API page |
referencesProvider | On | Declarations that share the name |
documentSymbolProvider | On | The file's top-level functions |
renameProvider | On | Edits in the open file and in files that declare the name |
codeActionProvider | On | Insert 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.
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_valueorset_sim_valuecall, 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_requestordatastore_getalso offer actions titled Eustress: …. They run a command no editor implements yet, so choosing one leaves the file unchanged.
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:
-- 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:
[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.