@zubra/language-server 5.2.0 → 5.4.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.
Files changed (3) hide show
  1. package/backend.js +16 -1
  2. package/package.json +1 -1
  3. package/server.js +32 -3
package/backend.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // How the language server reaches the compiler: the WebAssembly build (@zubra/compiler) when it's
2
- // installed in the workspace (or beside the server), otherwise the `zubra` CLI (or the one in `compiler`). Both give the same diagnostics.
2
+ // installed in the workspace (or beside the server), otherwise the `zubra` CLI (or the one in `compiler`). Both give the same diagnostics,
3
+ // and both format (`format` answers { ok, output } or { ok: false, error }).
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import { existsSync, readFileSync } from 'node:fs';
5
6
  import { dirname, join, resolve } from 'node:path';
@@ -49,6 +50,11 @@ function wasmBackend(wasm) {
49
50
  const response = wasm.compileFile(path, { emit: 'manifest' });
50
51
  return response.ok ? JSON.parse(response.output) : null;
51
52
  },
53
+ format(path, text, options) {
54
+ const absolute = resolve(path);
55
+ const response = wasm.request({ command: 'format', path: absolute, files: { [absolute]: text }, ...options });
56
+ return response.ok ? { ok: true, output: response.output } : { ok: false, error: response.error ?? '' };
57
+ },
52
58
  };
53
59
  }
54
60
 
@@ -66,6 +72,15 @@ function cliBackend(compiler) {
66
72
  if (result.status !== 0) return null;
67
73
  try { return JSON.parse(result.stdout); } catch { return null; }
68
74
  },
75
+ format(path, text, { indentWidth, useTabs, printWidth } = {}) {
76
+ const args = ['fmt', '--stdin', path];
77
+ if (useTabs) args.push('--tabs');
78
+ else if (indentWidth) args.push('--indent', String(indentWidth));
79
+ if (printWidth) args.push('--width', String(printWidth));
80
+ const result = run(args, text);
81
+ if (result.error) return { ok: false, error: result.error.message };
82
+ return result.status === 0 ? { ok: true, output: result.stdout } : { ok: false, error: result.stderr };
83
+ },
69
84
  };
70
85
  }
71
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zubra/language-server",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "Language server for .zb components: diagnostics, completion, hover and go to definition",
5
5
  "license": "ISC",
6
6
  "repository": {
package/server.js CHANGED
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // zubra language server: diagnostics from the compiler as you type, completion of directives,
3
3
  // imported components, their props and events, hover documentation from their manifests, and
4
- // go to definition for imported components. Speaks LSP over stdio (`--stdio`).
4
+ // go to definition for imported components, and formatting (`zubra fmt`). Speaks LSP over stdio
5
+ // (`--stdio`).
5
6
  //
6
- // initializationOptions: { compiler?: "/path/to/zubra" } — otherwise @zubra/compiler (WebAssembly)
7
- // when installed, else `zubra` on PATH.
7
+ // initializationOptions: { compiler?: "/path/to/zubra", printWidth?: number } — otherwise
8
+ // @zubra/compiler (WebAssembly) when installed, else `zubra` on PATH. printWidth is the width a
9
+ // tag is fitted into when formatting (default 100); indentation comes from the editor.
8
10
  import { createConnection, ProposedFeatures, TextDocuments, TextDocumentSyncKind, DiagnosticSeverity, CompletionItemKind, InsertTextFormat, MarkupKind } from 'vscode-languageserver/node';
9
11
  import { TextDocument } from 'vscode-languageserver-textdocument';
10
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -16,17 +18,21 @@ import { completions, documentation, interfaceOf, offsetAt, positionAt, tagAt }
16
18
  const connection = createConnection(ProposedFeatures.all);
17
19
  const documents = new TextDocuments(TextDocument);
18
20
  let backend;
21
+ let printWidth;
22
+ let toldAboutVersion = false;
19
23
  const manifests = new Map(); // path → { mtime, component }
20
24
 
21
25
  connection.onInitialize(async (params) => {
22
26
  const folder = params.workspaceFolders?.[0]?.uri ?? params.rootUri;
23
27
  backend = await createBackend({ ...params.initializationOptions, root: folder ? fileURLToPath(folder) : undefined });
28
+ printWidth = Number(params.initializationOptions?.printWidth) || undefined;
24
29
  return {
25
30
  capabilities: {
26
31
  textDocumentSync: TextDocumentSyncKind.Incremental,
27
32
  completionProvider: { triggerCharacters: ['<', ' ', '{'] },
28
33
  hoverProvider: true,
29
34
  definitionProvider: true,
35
+ documentFormattingProvider: true,
30
36
  },
31
37
  serverInfo: { name: 'zubra-language-server', version: '1.2.0' },
32
38
  };
@@ -119,5 +125,28 @@ connection.onDefinition(({ textDocument, position }) => {
119
125
  return { uri: pathToFileURL(component.path).href, range: { start, end: { line: start.line, character: start.character + component.tag.length + 1 } } };
120
126
  });
121
127
 
128
+ // Format Document (and format on save): the whole file, as `zubra fmt` lays it out, with the
129
+ // editor's indentation. A file that doesn't parse isn't formatted: its diagnostics say why.
130
+ connection.onDocumentFormatting(({ textDocument, options }) => {
131
+ const document = documents.get(textDocument.uri);
132
+ if (!document) return [];
133
+ const text = document.getText();
134
+ const result = backend.format(pathOf(textDocument.uri), text, {
135
+ indentWidth: options.tabSize,
136
+ useTabs: options.insertSpaces === false,
137
+ printWidth,
138
+ });
139
+ if (!result.ok) {
140
+ // A compiler from before `zubra fmt` (5.3 and older) doesn't know the command.
141
+ if (/unknown command|usage: zubra/.test(result.error) && !toldAboutVersion) {
142
+ toldAboutVersion = true;
143
+ connection.window.showInformationMessage('Formatting .zb files needs Zubra 5.4 or later: update @zubra/compiler (or the zubra CLI).');
144
+ }
145
+ return [];
146
+ }
147
+ if (result.output === text) return [];
148
+ return [{ range: { start: { line: 0, character: 0 }, end: positionAt(text, text.length) }, newText: result.output }];
149
+ });
150
+
122
151
  documents.listen(connection);
123
152
  connection.listen();