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 extern declarations.
  • It refuses rather than guesses. A rename it cannot prove complete is not applied.

Quick start​

  1. Build it from the JaiScript folder: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release, then cmake --build build --target jaiscript_lsp. The executable is build/bin/jaiscript_lsp (.exe on Windows).
  2. Open tools/vscode-jaiscript in VS Code and press F5, or install the extension.
  3. Open a .jai file. Diagnostics appear as you type; F12 goes to a definition, Shift+F12 lists references, F2 renames.
  4. 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 var has no definition and no hover, and a var hovers as var. Declared types, auto and references to them navigate.
  • Run-time structure. Members a script adds with extend, names a host registers after load and files reached by a computed import(expr). extern declares 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
stdlibjai::stdlib::register_all alone
barea 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).

shell
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 stderr

Any LSP client runs it as a stdio process for the jaiscript language and .jai files, with initialization options that mirror the extension's settings:

initializationOptions
{ "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.

shell
# 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.vsix

The extension looks for the server in this order, and says jaiscript_lsp was not found when all four fail:

  1. jaiscript.languageServer.path, when set;
  2. server/jaiscript_lsp inside the extension folder, where a packaged build carries it;
  3. the JaiScript folder's build trees two levels above the extension folder: build/bin, then build-debug/bin, then the trees Visual Studio's CMake presets create (Release before Debug);
  4. 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.