Editor Support: the Language Server
jaiscript_lsp speaks the Language Server Protocol over stdin/stdout. It answers from the engine's own lexer,
parser and strict checker, so what the editor underlines is what the engine reports
when the program loads. The VS Code extension in tools/vscode-jaiscript/ launches it for every
.jai file; any other LSP client can run it the same way.
- It never executes scripts. Everything it knows comes from the parse and the checker's types.
- It checks a file as part of its program: the files that import it and everything they import.
- It knows host names from a host profile or from
externdeclarations. - It refuses rather than guesses. A rename it cannot prove complete is not applied.
Quick start
- Build it from the JaiScript folder:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release, thencmake --build build --target jaiscript_lsp. The executable isbuild/bin/jaiscript_lsp(.exeon Windows). - Open
tools/vscode-jaiscriptin VS Code and press F5, or install the extension. - Open a
.jaifile. Diagnostics appear as you type; F12 goes to a definition, Shift+F12 lists references, F2 renames. - For scripts written against your own host, see The host surface.
What it does
Diagnostics on each change, definition and references, hover, completion, signature help, rename, and document and workspace symbols. What each covers: language server features.
What it does not see
- Dynamic values. A member used on a
varhas no definition and no hover, and avarhovers asvar. Declared types,autoand references to them navigate. - Run-time structure. Members a script adds with
extend, names a host registers after load and files reached by a computedimport(expr).externdeclares such names. - Inside a declaration that does not parse. The editor shows the parser's error for it. Findings stay for the code before it and for whole declarations after it; inside it, completion and signature help work from the text before the cursor.
Programs: what is checked together
The checker checks a program as one: a function in one file may use a global or class another file declares, in any import order (Across files). The server gives each file the view it has when its program runs. From the workspace's import graph it walks up from the file to the roots (files nothing imports) and checks the file against every unit those roots reach. A file two programs share sees both; a file nothing imports is its own root.
Paths resolve as the engine resolves them: the importing file's folder, then the configured include paths, then the working directory, which the server sets to the first workspace folder. An open file is read from the editor, unsaved changes included; every other file from disk, re-read when it changes.
The host surface
The checker knows host names only from the engine it checks against. The server builds that engine from a profile:
--host= | registrations |
|---|---|
runner (default) | the jaiscript runner's exact surface: the standard library, file IO, the console and audio functions, NetServer/NetChannel, Download, ARGS |
stdlib | jai::stdlib::register_all alone |
bare | a fresh engine: the language and the core builtins (print, format_value, math::, binary::, extend, same_object, parallel_transform, the file-location functions, reflect::is_enabled), without the standard library's to_string, type_of, to_json, time:: or the rest of reflect:: |
Scripts for another host have two routes: declare the host's names with extern, or build a server that
knows them. The server is six source files over the library; tools/lsp/runner_host.cpp registers the
surface, and a host replaces it with a file that calls its own registration function.
Running the server
The CMake target is jaiscript_lsp (JAISCRIPT_BUILD_LSP: on when JaiScript is the top-level
project, off as a subproject).
jaiscript_lsp --host=runner|stdlib|bare # the host profile (default runner)
--include=<dir> # an include path (repeatable)
--max-message=<bytes> # the largest message body read (default 64 MiB, at least 65536; a larger one is skipped)
--log # each request and its time, on stderrAny LSP client runs it as a stdio process for the jaiscript language and
.jai files, with initialization options that mirror the extension's settings:
{ "host": "runner", "includePaths": ["scripts/shared"] }It reads file changes from workspace/didChangeWatchedFiles for **/*.jai. Its capabilities:
incremental textDocumentSync; hover, definition, references, document highlights; completion on
., :: and ->; signature help on ( and ,; rename with
prepare; document and workspace symbols.
The VS Code extension
The extension folder runs as it is in the repository (out/extension.js is one bundled file). It also
carries syntax highlighting and the debugger attach, which work without the server.
# 1. try it: an Extension Development Host window
code tools/vscode-jaiscript # then press F5 and open a .jai file there
# 2. install it from the repository (a link keeps it current)
# Windows: mklink /J "%USERPROFILE%\.vscode\extensions\snapjaw.jaiscript" tools\vscode-jaiscript
# else: ln -s "$PWD/tools/vscode-jaiscript" ~/.vscode/extensions/snapjaw.jaiscript
# 3. package it
cd tools/vscode-jaiscript
npm install
npx @vscode/vsce package # jaiscript-0.5.2.vsix
code --install-extension jaiscript-0.5.2.vsixThe extension looks for the server in this order, and says jaiscript_lsp was not found when all four
fail:
jaiscript.languageServer.path, when set;server/jaiscript_lspinside the extension folder, where a packaged build carries it;- the JaiScript folder's build trees two levels above the extension folder:
build/bin, thenbuild-debug/bin, then the trees Visual Studio's CMake presets create (Release before Debug); PATH.
The extension's settings are under jaiscript.languageServer., each with its default: extension settings.
A settings change restarts the server, as does JaiScript: Restart Language Server in the command palette.
Rebuilding the extension is npm install and npm run compile.
Rename
A rename edits the declaration and every use in every file of the declaring file's programs. Renaming a method renames it in every class of its hierarchy that declares the name, so an override keeps overriding. It is refused, with the reason, for:
- a member whose name is also used on a receiver of unknown type (a
var): that use might be this member; - a global declared in more than one file: each file sees its own declaration (Modules);
- a new name already declared where the old one is visible;
- a name the host or the language defines.
Performance
An edit re-parses that file and re-checks it against its program. The rest of the program is re-checked, one file at a
time between requests, only when the edit changes a declaration, a signature or a field. On Gator Quest
(36 files, 13,980 lines; Release build, desktop Windows machine, over stdio) an edit inside a function reaches its
diagnostics in 3 to 10 ms depending on the file's size, an edit that changes a declaration re-checks the program in
about 60 ms, and opening the root parses and checks everything in about 210 ms.
python tools/lsp/tests/lsp_test.py --only latency measures it.