@reactivescript/language-server 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # rx-language-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `.rx` language server (spec/11 CP-8e), built on [Volar](https://volarjs.dev).
4
+ A `.rx` file's virtual code is its TSX projection (spec/13 TY-1c), which the
5
+ crate makes with mappings back onto the `.rx` (CP-8c). TypeScript answers hover,
6
+ go to definition, references, rename, completion and type errors on the
7
+ projection, and Volar maps the answers back. The crate's own diagnostics (parse
8
+ errors, else the checker's, each with its rule) are a second service.
9
+
10
+ - `src/rxc.js` asks the crate for a file's analysis, with the editor's unsaved
11
+ text as overlays: its WebAssembly build in process (`src/wasm.rs`; one build
12
+ for every platform, about a millisecond a file once loaded), or, when `RXC`
13
+ names a binary, `rxc serve` as a process per request. It writes the
14
+ projection's prelude to `node_modules/.reactivescript/prelude.d.ts`.
15
+ - `src/index.js` is the language plugin (exact mappings carry every feature;
16
+ ranges only place type errors) and the diagnostics service.
17
+ - `bin/server.js` is the server, over stdio or IPC.
18
+
19
+ The project needs a `tsconfig.json` with Solid's JSX (`"jsx": "preserve"`,
20
+ `"jsxImportSource": "@solidjs/web"`) and Solid in its `node_modules`.
21
+
22
+ ## Running it
23
+
24
+ From npm (published from this folder, `../RELEASING.md`):
25
+
26
+ ```sh
27
+ npx @reactivescript/language-server --stdio
28
+ ```
29
+
30
+ From the repository:
31
+
32
+ ```sh
33
+ cargo build --lib --release --target wasm32-unknown-unknown --manifest-path ../../compiler/Cargo.toml
34
+ npm install
35
+ node bin/server.js --stdio
36
+ ```
37
+
38
+ `initializationOptions: { typescript: { tsdk } }` picks TypeScript's `lib`
39
+ folder; without it, the server uses its own `typescript`.
40
+
41
+ ## Editors
42
+
43
+ **VS Code** (and Cursor, VSCodium): the extension in
44
+ [`../vscode-rx`](../vscode-rx), which bundles this server and the WebAssembly
45
+ build.
46
+
47
+ **Neovim** (0.11+), in `init.lua`:
48
+
49
+ ```lua
50
+ vim.filetype.add({ extension = { rx = "reactivescript" } })
51
+ vim.lsp.config("rx", {
52
+ cmd = { "node", "/path/to/reactivescript/tooling/rx-language-server/bin/server.js", "--stdio" },
53
+ filetypes = { "reactivescript" },
54
+ root_markers = { "tsconfig.json", "package.json" },
55
+ })
56
+ vim.lsp.enable("rx")
57
+ ```
58
+
59
+ Neovim has no `.rx` grammar yet; the server's semantic tokens color names.
60
+
61
+ **Helix**, in `languages.toml`:
62
+
63
+ ```toml
64
+ [language-server.rx]
65
+ command = "node"
66
+ args = ["/path/to/reactivescript/tooling/rx-language-server/bin/server.js", "--stdio"]
67
+
68
+ [[language]]
69
+ name = "reactivescript"
70
+ scope = "source.rx"
71
+ file-types = ["rx"]
72
+ roots = ["tsconfig.json", "package.json"]
73
+ language-servers = ["rx"]
74
+ ```
75
+
76
+ **JetBrains IDEs** (WebStorm, IntelliJ): install the
77
+ [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, add a
78
+ language server with the command
79
+ `node /path/to/reactivescript/tooling/rx-language-server/bin/server.js --stdio`,
80
+ map the file name pattern `*.rx` to it, and add the `tooling/vscode-rx`
81
+ folder under Settings › Editor › TextMate Bundles for highlighting.
82
+
83
+ **Zed** needs an extension of its own (a Zed extension declares the language
84
+ and starts the server); there isn't one yet.
85
+
86
+ The `/path/to/reactivescript` paths are a checkout of this repository; with
87
+ the npm package, the command is `npx @reactivescript/language-server --stdio`
88
+ instead (or `rx-language-server --stdio` once installed).
89
+
90
+ ## Tests
91
+
92
+ ```sh
93
+ cargo build --release --manifest-path ../../compiler/Cargo.toml
94
+ npm install && node test/run.js # VERBOSE=1 prints each answer
95
+ ```
96
+
97
+ `test/run.js` drives the server over LSP on a copy of `examples/shop` (Solid's
98
+ types from `ref/node_modules`, via `ref/scripts/solid-next.sh`): hover, go to
99
+ definition across modules, references, completion while typing (in code and in
100
+ markup), a parse error, a TypeScript type error and a crate diagnostic, each
101
+ placed on the `.rx`.
package/bin/server.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // The .rx language server over stdio (or whatever transport the client asks for): Volar's TypeScript project with the
3
+ // .rx language plugin, TypeScript's services, and the crate's diagnostics.
4
+ const { createConnection, createServer, createTypeScriptProject, loadTsdkByPath } = require("@volar/language-server/node");
5
+ const { create: createTypeScriptServices } = require("volar-service-typescript");
6
+ const { rxLanguagePlugin, rxDiagnostics } = require("../src/index.js");
7
+
8
+ const connection = createConnection();
9
+ const server = createServer(connection);
10
+
11
+ connection.listen();
12
+
13
+ connection.onInitialize(params => {
14
+ const tsdkPath = params.initializationOptions?.typescript?.tsdk ?? require("node:path").dirname(require.resolve("typescript"));
15
+ const tsdk = loadTsdkByPath(tsdkPath, params.locale);
16
+ return server.initialize(
17
+ params,
18
+ createTypeScriptProject(tsdk.typescript, tsdk.diagnosticMessages, () => ({ languagePlugins: [rxLanguagePlugin(tsdk.typescript)] })),
19
+ [...createTypeScriptServices(tsdk.typescript), rxDiagnostics()],
20
+ );
21
+ });
22
+
23
+ connection.onInitialized(server.initialized);
24
+ connection.onShutdown(server.shutdown);
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@reactivescript/language-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The .rx language server (spec/11 CP-8): Volar over the crate's TSX projection, TypeScript's answers mapped back onto .rx, the crate's own diagnostics.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/solidjs/reactivescript.git",
9
+ "directory": "tooling/rx-language-server"
10
+ },
11
+ "type": "commonjs",
12
+ "bin": {
13
+ "rx-language-server": "./bin/server.js"
14
+ },
15
+ "main": "./src/index.js",
16
+ "files": [
17
+ "bin",
18
+ "src"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "scripts": {
24
+ "test": "node test/run.js",
25
+ "prepack": "node scripts/wasm.js"
26
+ },
27
+ "dependencies": {
28
+ "@volar/language-core": "2.4.30",
29
+ "@volar/language-server": "2.4.30",
30
+ "@volar/typescript": "2.4.30",
31
+ "volar-service-typescript": "0.0.71",
32
+ "vscode-uri": "^3.0.8",
33
+ "typescript": "^6.0.3"
34
+ },
35
+ "devDependencies": {
36
+ "@volar/test-utils": "2.4.30"
37
+ }
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,98 @@
1
+ // The .rx language plugin for Volar (spec/11 CP-8). A `.rx` file's virtual code is its TSX projection (spec/13 TY-1c),
2
+ // made by the crate with mappings onto the `.rx`: TypeScript's hover, definition, references, rename, completion and
3
+ // type errors come back through them. The crate's own diagnostics (rule IDs, spec/99) are a service of their own.
4
+ const { analyze, preludeFor } = require("./rxc");
5
+
6
+ /** Every feature, where the projection copies the `.rx`'s text (a name, a member, a literal, a type, a tag). */
7
+ const EXACT = { verification: true, completion: true, semantic: true, navigation: true, structure: true, format: false };
8
+ /** Where it wrote text for a piece of the `.rx` (an expression, a statement): only for placing type errors. */
9
+ const COARSE = { verification: true };
10
+
11
+ /** A module: `.rx`, not a declaration file (`.d.rx`, MO-8/MO-9), which the crate reads beside the modules that import it. */
12
+ const isModule = f => f.endsWith(".rx") && !f.endsWith(".d.rx");
13
+
14
+ const snapshotOf = text => ({ getText: (s, e) => text.slice(s, e), getLength: () => text.length, getChangeRange: () => undefined });
15
+
16
+ /** The last analysis of each file, by its text: the virtual code and the diagnostics share one `rxc` run. */
17
+ const cache = new Map();
18
+ function analysis(file, text) {
19
+ const hit = cache.get(file);
20
+ if (hit && hit.text === text) return hit.result;
21
+ let result;
22
+ try { result = analyze(file, text, preludeFor(file)); } catch (e) {
23
+ console.error(`rx: analyzing ${file} failed: ${e?.stack ?? e}`); // the server's log, else the failure is silent
24
+ result = { code: null, mappings: [], ranges: [], diagnostics: [], error: String(e) };
25
+ }
26
+ cache.set(file, { text, result });
27
+ return result;
28
+ }
29
+
30
+ function virtualCode(file, snapshot) {
31
+ const text = snapshot.getText(0, snapshot.getLength());
32
+ const a = analysis(file, text);
33
+ const code = a.code ?? "";
34
+ const mappings = [
35
+ ...(a.mappings ?? []).map(([g, s, n]) => ({ sourceOffsets: [s], generatedOffsets: [g], lengths: [n], data: EXACT })),
36
+ ...(a.ranges ?? []).map(([g, gn, s, n]) => ({ sourceOffsets: [s], generatedOffsets: [g], lengths: [n], generatedLengths: [gn], data: COARSE })),
37
+ ];
38
+ return {
39
+ id: "root",
40
+ languageId: "reactivescript",
41
+ snapshot,
42
+ mappings: [{ sourceOffsets: [0], generatedOffsets: [0], lengths: [text.length], data: { verification: true, completion: true, semantic: true, navigation: true, structure: true, format: true } }],
43
+ embeddedCodes: [{ id: "tsx", languageId: "typescriptreact", snapshot: snapshotOf(code), mappings }],
44
+ };
45
+ }
46
+
47
+ /** @param {typeof import("typescript")} ts */
48
+ function rxLanguagePlugin(ts, asFileName = uri => uri.fsPath ?? String(uri)) {
49
+ return {
50
+ getLanguageId(id) { return isModule(String(id.path ?? id)) ? "reactivescript" : undefined; },
51
+ createVirtualCode(id, languageId, snapshot) {
52
+ if (languageId !== "reactivescript" || !isModule(asFileName(id))) return undefined;
53
+ return virtualCode(asFileName(id), snapshot);
54
+ },
55
+ typescript: {
56
+ extraFileExtensions: [{ extension: "rx", isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }],
57
+ getServiceScript(root) {
58
+ const tsx = root.embeddedCodes?.find(c => c.id === "tsx");
59
+ return tsx ? { code: tsx, extension: ".tsx", scriptKind: ts.ScriptKind.TSX } : undefined;
60
+ },
61
+ },
62
+ };
63
+ }
64
+
65
+ /** The crate's diagnostics on a `.rx` document: the editor's parse's errors, else the checker's (CP-8d). Volar hands
66
+ * the service the root virtual code's document, whose URI encodes the `.rx` file's. */
67
+ function rxDiagnostics() {
68
+ return {
69
+ name: "reactivescript-diagnostics",
70
+ capabilities: { diagnosticProvider: { interFileDependencies: true, workspaceDiagnostics: false } },
71
+ create(context) {
72
+ return {
73
+ provideDiagnostics(document) {
74
+ if (document.languageId !== "reactivescript") return;
75
+ const { URI } = require("vscode-uri");
76
+ const uri = URI.parse(document.uri);
77
+ const source = context.decodeEmbeddedDocumentUri(uri)?.[0] ?? uri;
78
+ if (source.scheme !== "file") return;
79
+ const file = source.fsPath;
80
+ if (!isModule(file)) return;
81
+ const a = analysis(file, document.getText());
82
+ return (a.diagnostics ?? []).filter(d => d.range).map(d => ({
83
+ range: {
84
+ start: { line: d.range.start.line - 1, character: d.range.start.column },
85
+ end: { line: d.range.end.line - 1, character: d.range.end.column },
86
+ },
87
+ severity: 1,
88
+ source: "rx",
89
+ code: d.code,
90
+ message: `${d.message} (${d.rule})`,
91
+ }));
92
+ },
93
+ };
94
+ },
95
+ };
96
+ }
97
+
98
+ module.exports = { rxLanguagePlugin, rxDiagnostics, analysis };
package/src/rxc.js ADDED
@@ -0,0 +1,94 @@
1
+ // The crate's side of the server: `rxc serve`'s requests (spec/11 CP-8e). Volar asks for a file's virtual code
2
+ // synchronously, so the requests are synchronous calls: into the crate's WebAssembly build, loaded in process (one build
3
+ // for every platform), or, when $RXC names a binary, a short `rxc serve` run per request.
4
+ const { spawnSync } = require("node:child_process");
5
+ const { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } = require("node:fs");
6
+ const path = require("node:path");
7
+
8
+ const repo = path.resolve(__dirname, "../../..");
9
+
10
+ /** The `rxc` binary, when $RXC names one. */
11
+ function rxcPath() {
12
+ return process.env.RXC || path.join(repo, "compiler/target/release/rxc");
13
+ }
14
+
15
+ /** The WebAssembly build: $RX_WASM, else the one bundled beside this file (the packaged extension), else the
16
+ * repository's (`cargo build --lib --release --target wasm32-unknown-unknown`). */
17
+ function wasmPath() {
18
+ if (process.env.RX_WASM) return process.env.RX_WASM;
19
+ for (const f of [path.join(__dirname, "rxc.wasm"), path.join(repo, "compiler/target/wasm32-unknown-unknown/release/reactivescript.wasm")]) if (existsSync(f)) return f;
20
+ return undefined;
21
+ }
22
+
23
+ /** The crate in process, over `src/wasm.rs`'s ABI. */
24
+ function loadWasm(file) {
25
+ let x;
26
+ const enc = new TextEncoder(), dec = new TextDecoder();
27
+ const put = bytes => {
28
+ const p = x.rx_alloc(bytes.length + 4);
29
+ const m = new Uint8Array(x.memory.buffer);
30
+ new DataView(x.memory.buffer).setUint32(p, bytes.length, true);
31
+ m.set(bytes, p + 4);
32
+ return p;
33
+ };
34
+ const env = {
35
+ rx_read(ptr, len) {
36
+ const f = dec.decode(new Uint8Array(x.memory.buffer, ptr, len));
37
+ try { if (!statSync(f).isFile()) return 0; return put(readFileSync(f)); } catch { return 0; }
38
+ },
39
+ };
40
+ x = new WebAssembly.Instance(new WebAssembly.Module(readFileSync(file)), { env }).exports;
41
+ return (method, params) => {
42
+ const req = enc.encode(JSON.stringify({ method, params }));
43
+ const p = x.rx_alloc(req.length);
44
+ new Uint8Array(x.memory.buffer).set(req, p);
45
+ const out = x.rx_call(p, req.length);
46
+ const n = new DataView(x.memory.buffer).getUint32(out, true);
47
+ const text = dec.decode(new Uint8Array(x.memory.buffer, out + 4, n));
48
+ x.rx_free(out, n + 4);
49
+ return JSON.parse(text);
50
+ };
51
+ }
52
+
53
+ function viaProcess(method, params) {
54
+ const r = spawnSync(rxcPath(), ["serve"], { input: JSON.stringify({ id: 1, method, params }) + "\n", maxBuffer: 1 << 28 });
55
+ if (r.error) throw r.error;
56
+ const line = r.stdout.toString().split("\n").find(Boolean);
57
+ if (!line) throw new Error(`rxc serve: no answer (${r.stderr.toString().slice(0, 300)})`);
58
+ return JSON.parse(line).result;
59
+ }
60
+
61
+ let wasm;
62
+ function ask(method, params) {
63
+ if (process.env.RXC || !wasmPath()) return viaProcess(method, params);
64
+ try {
65
+ return (wasm ??= loadWasm(wasmPath()))(method, params);
66
+ } catch (e) {
67
+ wasm = undefined; // a trap (a panic in the crate) leaves the instance unusable: the next request makes a new one
68
+ throw e;
69
+ }
70
+ }
71
+
72
+ /** A file's projection (`code`, `mappings`, `ranges`, UTF-16 offsets) and its diagnostics, as the editor has it. */
73
+ function analyze(file, text, prelude) {
74
+ return ask("analyze", { file, overlays: { [file]: text }, prelude });
75
+ }
76
+
77
+ /** Where a file's projection imports the prelude from: written once beside the nearest `node_modules`, so its own
78
+ * `@solidjs/web` resolves; a `.d.ts`, so the import needs no extension. */
79
+ const preludes = new Map();
80
+ function preludeFor(file) {
81
+ let dir = path.dirname(file);
82
+ while (!existsSync(path.join(dir, "node_modules")) && path.dirname(dir) !== dir) dir = path.dirname(dir);
83
+ const at = path.join(dir, "node_modules", ".reactivescript", "prelude");
84
+ if (!preludes.has(at)) {
85
+ const text = ask("prelude", {});
86
+ mkdirSync(path.dirname(at), { recursive: true });
87
+ const f = at + ".d.ts";
88
+ if (!existsSync(f) || readFileSync(f, "utf8") !== text) writeFileSync(f, text);
89
+ preludes.set(at, true);
90
+ }
91
+ return at;
92
+ }
93
+
94
+ module.exports = { analyze, preludeFor, rxcPath, wasmPath };
package/src/rxc.wasm ADDED
Binary file