@nvl/sveltex-language-server 0.2.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/bin/server.js +10 -0
  4. package/dist/core/config.d.ts +126 -0
  5. package/dist/core/config.js +569 -0
  6. package/dist/core/diagnostics.d.ts +34 -0
  7. package/dist/core/diagnostics.js +67 -0
  8. package/dist/core/frontmatter-data.d.ts +74 -0
  9. package/dist/core/frontmatter-data.js +323 -0
  10. package/dist/core/frontmatter.d.ts +25 -0
  11. package/dist/core/frontmatter.js +348 -0
  12. package/dist/core/lsp-proxy.d.ts +77 -0
  13. package/dist/core/lsp-proxy.js +165 -0
  14. package/dist/core/mapper.d.ts +86 -0
  15. package/dist/core/mapper.js +223 -0
  16. package/dist/core/mapping.d.ts +59 -0
  17. package/dist/core/mapping.js +37 -0
  18. package/dist/core/markdown.d.ts +34 -0
  19. package/dist/core/markdown.js +215 -0
  20. package/dist/core/region-forwarding.d.ts +90 -0
  21. package/dist/core/region-forwarding.js +428 -0
  22. package/dist/core/region-virtual.d.ts +71 -0
  23. package/dist/core/region-virtual.js +131 -0
  24. package/dist/core/regions.d.ts +56 -0
  25. package/dist/core/regions.js +221 -0
  26. package/dist/core/remap.d.ts +84 -0
  27. package/dist/core/remap.js +272 -0
  28. package/dist/core/server-helpers.d.ts +109 -0
  29. package/dist/core/server-helpers.js +182 -0
  30. package/dist/core/server.d.ts +13 -0
  31. package/dist/core/server.js +604 -0
  32. package/dist/core/svelte-proxy.d.ts +100 -0
  33. package/dist/core/svelte-proxy.js +144 -0
  34. package/dist/core/texlab.d.ts +26 -0
  35. package/dist/core/texlab.js +121 -0
  36. package/dist/core/virtual-svelte.d.ts +32 -0
  37. package/dist/core/virtual-svelte.js +67 -0
  38. package/dist/index.d.ts +29 -0
  39. package/dist/index.js +46 -0
  40. package/package.json +73 -0
@@ -0,0 +1,348 @@
1
+ // File description: Hover and completion for a `.sveltex` frontmatter block.
2
+ //
3
+ // A `.sveltex` document may open with a YAML / TOML / JSON frontmatter block.
4
+ // SvelTeX reads it and renders the recognised keys into the document's
5
+ // `<svelte:head>` — `title` becomes `<title>`, `meta` becomes `<meta>` tags,
6
+ // and so on. This module documents that mapping, surfacing it as hover text
7
+ // and as completion suggestions.
8
+ //
9
+ // Both are *context-aware*: which keys are valid depends on the block the
10
+ // caret sits in — the top level, or inside `meta` / `base` / `link` — so e.g.
11
+ // `title` is documented and suggested at the top level but not inside `meta`,
12
+ // where SvelTeX would not render it as a title.
13
+ //
14
+ // Frontmatter is a non-delegated region: the embedded Svelte language server
15
+ // never sees it. Hover and completion here are therefore computed natively.
16
+ // They need no position mapping — a frontmatter region is verbatim `.sveltex`
17
+ // source, so a token's line/character already are its source coordinates —
18
+ // and no real YAML/TOML/JSON parse: the line a key sits on, and which block
19
+ // encloses it, are recognised by small line-shaped patterns that cover all
20
+ // three syntaxes.
21
+ import { CompletionItemKind, MarkupKind, } from 'vscode-languageserver-protocol';
22
+ import { keysForContext, metaHeadEffect, metaRenderedHtml, META_HTTP_EQUIV, META_NAMES, } from './frontmatter-data.js';
23
+ /**
24
+ * Splits a single frontmatter line into its key and first value token.
25
+ *
26
+ * Recognises all three frontmatter syntaxes with one shape: `key: value`
27
+ * (YAML / JSON), `key = value` (TOML), optionally quoted tokens, an optional
28
+ * leading YAML list-item dash, and a TOML `[table]` / `[[array]]` header
29
+ * (which has a key but no value).
30
+ *
31
+ * @param line - A single line of frontmatter text.
32
+ * @returns The key and value tokens found on the line (either may be absent).
33
+ */
34
+ function parseFrontmatterLine(line) {
35
+ // `key: value` / `key = value` / `"key": "value"`, with an optional `- `
36
+ // list-item prefix. The value is the first token after the separator.
37
+ const keyValue = /^(\s*(?:-\s+)?)(['"]?)([A-Za-z_][\w-]*)\2(\s*[:=]\s*)(['"]?)([\w-][\w./-]*)?/u.exec(line);
38
+ if (keyValue) {
39
+ const [, indent = '', kq = '', key = '', sep = '', vq = '', value] = keyValue;
40
+ const keyStart = indent.length + kq.length;
41
+ const key_ = {
42
+ name: key,
43
+ start: keyStart,
44
+ end: keyStart + key.length,
45
+ };
46
+ if (value === undefined || value.length === 0)
47
+ return { key: key_ };
48
+ // The closing key quote is `\2`, i.e. another `kq`.
49
+ const valueStart = indent.length + kq.length * 2 + key.length + sep.length + vq.length;
50
+ return {
51
+ key: key_,
52
+ value: {
53
+ name: value,
54
+ start: valueStart,
55
+ end: valueStart + value.length,
56
+ },
57
+ };
58
+ }
59
+ // TOML table header: `[base]`, `[[meta]]` — a key with no value.
60
+ const table = /^(\s*\[+\s*)([A-Za-z_][\w-]*)/u.exec(line);
61
+ if (table) {
62
+ const [, prefix = '', name = ''] = table;
63
+ return {
64
+ key: {
65
+ name,
66
+ start: prefix.length,
67
+ end: prefix.length + name.length,
68
+ },
69
+ };
70
+ }
71
+ return {};
72
+ }
73
+ /** Whether the caret column falls on (or just past the end of) `token`. */
74
+ function caretOn(caret, token) {
75
+ return caret >= token.start && caret <= token.end;
76
+ }
77
+ /**
78
+ * Finds which block a caret line sits inside — `meta`, `base` or `link` — by
79
+ * walking up to the nearest enclosing key: a less-indented YAML/JSON key, or
80
+ * a TOML `[table]` header. `undefined` means the frontmatter top level.
81
+ *
82
+ * @param lines - The `.sveltex` document split into lines.
83
+ * @param lineIndex - The caret's line index.
84
+ */
85
+ function frontmatterContext(lines, lineIndex) {
86
+ const indentOf = (s) => (/^\s*/u.exec(s)?.[0] ?? '').length;
87
+ let minIndent = indentOf(lines[lineIndex] ?? '');
88
+ for (let i = lineIndex - 1; i >= 0; i -= 1) {
89
+ const raw = lines[i] ?? '';
90
+ const trimmed = raw.trim();
91
+ if (trimmed === '' || trimmed.startsWith('#'))
92
+ continue;
93
+ // The frontmatter fence — the top of the block has been reached.
94
+ if (/^[-+]{3,}$/u.test(trimmed))
95
+ break;
96
+ // A TOML `[table]` / `[[table]]` header is the enclosing table.
97
+ const toml = /^\[+\s*([A-Za-z_][\w-]*)/u.exec(trimmed);
98
+ if (toml) {
99
+ const name = toml[1];
100
+ return name === 'meta' || name === 'base' || name === 'link'
101
+ ? name
102
+ : undefined;
103
+ }
104
+ // YAML / JSON: an ancestor is any line indented less than the caret.
105
+ const indent = indentOf(raw);
106
+ if (indent < minIndent) {
107
+ minIndent = indent;
108
+ const keyName = parseFrontmatterLine(raw).key?.name;
109
+ if (keyName === 'meta' ||
110
+ keyName === 'base' ||
111
+ keyName === 'link') {
112
+ return keyName;
113
+ }
114
+ }
115
+ }
116
+ return undefined;
117
+ }
118
+ /** Top-level keys whose value SvelTeX expects to be an object or array. */
119
+ const STRUCTURED_VALUE_KEYS = new Set([
120
+ 'base',
121
+ 'meta',
122
+ 'link',
123
+ 'imports',
124
+ ]);
125
+ /**
126
+ * The shape of a JavaScript identifier — used to decide whether a key
127
+ * needs quoting when written as an object-literal key in the rendered
128
+ * `metadata` example.
129
+ */
130
+ const identifierRegExp = /^[A-Za-z_$][\w$]*$/u;
131
+ /**
132
+ * Build the per-effect sections appended to the hover of a top-level
133
+ * frontmatter key — one per frontmatter-processing step the key takes
134
+ * part in. Each section names the step's `frontmatter: { … }` toggle so
135
+ * the reader learns how to switch it off.
136
+ *
137
+ * @param key - The bare key text — used to format the `metadata` object
138
+ * key in the rendered example.
139
+ * @param doc - The key's entry doc; `headEffect` / `element` decide the
140
+ * head section's sentence.
141
+ */
142
+ function effectSections(key, doc) {
143
+ const sections = [];
144
+ const placeholder = STRUCTURED_VALUE_KEYS.has(key)
145
+ ? '〈value〉'
146
+ : '"〈value〉"';
147
+ const disableHint = (toggle) => `To turn this off, set \`frontmatter: { ${toggle}: false }\` in ` +
148
+ 'your SvelTeX configuration.';
149
+ // <svelte:head> — structural keys supply `headEffect` explicitly,
150
+ // `<meta>` / `<meta http-equiv>` / `<meta charset>` keys derive it.
151
+ const head = doc.headEffect ??
152
+ (doc.element !== undefined ? metaHeadEffect(doc.element) : undefined);
153
+ if (head !== undefined) {
154
+ sections.push('---', '', head, '', disableHint('head'), '');
155
+ }
156
+ // `import` statements — only for the special `imports` key.
157
+ if (key === 'imports') {
158
+ sections.push('---', '', "Adds an `import` statement to the page's `<script>` for " +
159
+ 'each entry — each key is the module path, each value ' +
160
+ 'the binding(s) to import.', '', disableHint('imports'), '');
161
+ }
162
+ // `export const metadata` module-script export — the original key is
163
+ // preserved, quoted when it isn't a valid identifier.
164
+ const metaKey = identifierRegExp.test(key) ? key : JSON.stringify(key);
165
+ sections.push('---', '', `Adds \`${metaKey}: ${placeholder}\` to the page's \`metadata\` ` +
166
+ 'export.', '', disableHint('metadata'), '');
167
+ return sections;
168
+ }
169
+ /**
170
+ * Builds the Markdown body shown when hovering a frontmatter key or value.
171
+ *
172
+ * @param name - The bare token text.
173
+ * @param doc - The token's {@link FrontmatterEntryDoc}.
174
+ * @param topLevelKey - The bare key text when the hover is over a top-level
175
+ * frontmatter key (not a nested-block key, not a value). When supplied,
176
+ * per-effect sections are appended describing what the key inserts into
177
+ * the page's `<svelte:head>` / `<script>` / `metadata` export, each with
178
+ * the `frontmatter: { … }` toggle that switches that step off.
179
+ */
180
+ function entryHoverMarkdown(name, doc, topLevelKey) {
181
+ const rendered = doc.rendersAs ??
182
+ (doc.element !== undefined ? metaRenderedHtml(doc.element) : undefined);
183
+ const heading = rendered
184
+ ? `**\`${name}\`** — renders \`${rendered}\``
185
+ : `**\`${name}\`**`;
186
+ const linkLabel = doc.element
187
+ ? `\`${doc.element}\` on MDN`
188
+ : 'SvelTeX documentation';
189
+ const parts = [
190
+ heading,
191
+ '',
192
+ doc.summary,
193
+ '',
194
+ `[${linkLabel}](${doc.docUrl})`,
195
+ '',
196
+ ];
197
+ if (topLevelKey !== undefined) {
198
+ parts.push(...effectSections(topLevelKey, doc));
199
+ }
200
+ parts.push('_SvelTeX frontmatter_');
201
+ return parts.join('\n');
202
+ }
203
+ /**
204
+ * Wraps an entry doc and its token into an LSP {@link Hover}.
205
+ *
206
+ * @param topLevelKey - Forwarded to {@link entryHoverMarkdown}; supplied
207
+ * for hovers over a top-level frontmatter key so the markdown body is
208
+ * followed by per-effect sections.
209
+ */
210
+ function entryHover(token, doc, line, topLevelKey) {
211
+ return {
212
+ contents: {
213
+ kind: MarkupKind.Markdown,
214
+ value: entryHoverMarkdown(token.name, doc, topLevelKey),
215
+ },
216
+ range: {
217
+ start: { line, character: token.start },
218
+ end: { line, character: token.end },
219
+ },
220
+ };
221
+ }
222
+ /**
223
+ * Computes the hover for a caret inside a `.sveltex` frontmatter region.
224
+ *
225
+ * @param source - Full text of the `.sveltex` document.
226
+ * @param position - The caret position, in `.sveltex` coordinates. The caller
227
+ * guarantees it falls inside a `frontmatter` region.
228
+ * @returns A {@link Hover} describing the frontmatter key — or, on a `name:` /
229
+ * `http-equiv:` line, the standard `<meta>` value — under the caret, or `null`
230
+ * when the caret is not on a token recognised in that block.
231
+ */
232
+ export function computeFrontmatterHover(source, position) {
233
+ const lines = source.split(/\r\n?|\n/u);
234
+ const line = lines[position.line];
235
+ if (line === undefined)
236
+ return null;
237
+ const { key, value } = parseFrontmatterLine(line);
238
+ const caret = position.character;
239
+ // Caret on the key — describe it, but only if the key is valid in the
240
+ // block the caret sits in. `title` inside `meta`, say, is left
241
+ // undocumented because SvelTeX would not render it as the page title
242
+ // there. For top-level keys the body is followed by per-effect
243
+ // sections naming each frontmatter-processing step the key takes part
244
+ // in and the `frontmatter: { … }` toggle that switches it off.
245
+ if (key && caretOn(caret, key)) {
246
+ const context = frontmatterContext(lines, position.line);
247
+ const doc = keysForContext(context)[key.name];
248
+ if (doc === undefined)
249
+ return null;
250
+ const topLevelKey = context === undefined ? key.name : undefined;
251
+ return entryHover(key, doc, position.line, topLevelKey);
252
+ }
253
+ // Caret on the value of a `name:` / `http-equiv:` entry — describe the
254
+ // standard metadata name or pragma directive it selects.
255
+ if (key && value && caretOn(caret, value)) {
256
+ const schema = key.name === 'name'
257
+ ? META_NAMES
258
+ : key.name === 'http-equiv'
259
+ ? META_HTTP_EQUIV
260
+ : undefined;
261
+ const doc = schema?.[value.name];
262
+ if (doc)
263
+ return entryHover(value, doc, position.line);
264
+ }
265
+ return null;
266
+ }
267
+ /**
268
+ * Classifies a caret on a frontmatter line as completing a key or a value.
269
+ *
270
+ * The caret is in value position once it is past the `:` / `=` separator of a
271
+ * `key:` / `key =` pair; everywhere else a key is being typed.
272
+ *
273
+ * @param line - The frontmatter line the caret is on.
274
+ * @param caret - The caret's character offset within the line.
275
+ */
276
+ function completionContext(line, caret) {
277
+ const pair = /^(\s*(?:-\s+)?)(['"]?)([A-Za-z_][\w-]*)\2(\s*)([:=])/u.exec(line);
278
+ if (pair) {
279
+ const [match = '', , , key = ''] = pair;
280
+ if (caret > match.length - 1)
281
+ return { kind: 'value', ofKey: key };
282
+ }
283
+ return { kind: 'key' };
284
+ }
285
+ /**
286
+ * Computes the completion list for a caret inside a `.sveltex` frontmatter
287
+ * region: the keys valid in the enclosing block when a key is being typed, or
288
+ * the standard `<meta>` `name` / `http-equiv` values when the caret is on the
289
+ * value of such an entry.
290
+ *
291
+ * @param source - Full text of the `.sveltex` document.
292
+ * @param position - The caret position, in `.sveltex` coordinates. The caller
293
+ * guarantees it falls inside a `frontmatter` region.
294
+ * @returns A {@link CompletionList} — empty (but never `null`) when nothing
295
+ * sensible can be suggested.
296
+ */
297
+ export function computeFrontmatterCompletion(source, position) {
298
+ const empty = { isIncomplete: false, items: [] };
299
+ const lines = source.split(/\r\n?|\n/u);
300
+ const line = lines[position.line];
301
+ if (line === undefined)
302
+ return empty;
303
+ const context = completionContext(line, position.character);
304
+ let entries;
305
+ let kind;
306
+ if (context.kind === 'key') {
307
+ // Offer only the keys valid in the block the caret sits in — so e.g.
308
+ // `title` is not suggested inside a `meta` block.
309
+ entries = keysForContext(frontmatterContext(lines, position.line));
310
+ kind = CompletionItemKind.Property;
311
+ }
312
+ else if (context.ofKey === 'name') {
313
+ entries = META_NAMES;
314
+ kind = CompletionItemKind.EnumMember;
315
+ }
316
+ else if (context.ofKey === 'http-equiv') {
317
+ entries = META_HTTP_EQUIV;
318
+ kind = CompletionItemKind.EnumMember;
319
+ }
320
+ else {
321
+ // The value of any other key is free-form — nothing to suggest.
322
+ return empty;
323
+ }
324
+ // The inserted text replaces the partial identifier already typed.
325
+ const typed = /[A-Za-z0-9_-]*$/u.exec(line.slice(0, position.character));
326
+ const range = {
327
+ start: {
328
+ line: position.line,
329
+ character: position.character - (typed?.[0] ?? '').length,
330
+ },
331
+ end: { line: position.line, character: position.character },
332
+ };
333
+ const items = Object.entries(entries).map(([name, doc]) => {
334
+ const item = {
335
+ label: name,
336
+ kind,
337
+ documentation: {
338
+ kind: MarkupKind.Markdown,
339
+ value: doc.summary,
340
+ },
341
+ textEdit: { range, newText: name },
342
+ };
343
+ if (doc.element)
344
+ item.detail = doc.element;
345
+ return item;
346
+ });
347
+ return { isIncomplete: false, items };
348
+ }
@@ -0,0 +1,77 @@
1
+ import { type InitializeParams, type InitializeResult } from 'vscode-languageserver-protocol';
2
+ /**
3
+ * How a child language server is launched.
4
+ *
5
+ * - `fork`: run a Node module (`module`) with `child_process.fork` and a
6
+ * `--stdio` argument — used for the bundled math language server.
7
+ * - `spawn`: run a native executable (`command`) with `child_process.spawn` —
8
+ * used for the TexLab binary.
9
+ */
10
+ export type LspSpawnSpec = {
11
+ kind: 'fork';
12
+ module: string;
13
+ args?: readonly string[];
14
+ } | {
15
+ kind: 'spawn';
16
+ command: string;
17
+ args?: readonly string[];
18
+ };
19
+ /**
20
+ * Handlers for messages a child sends back unprompted (diagnostics, log
21
+ * messages, server-to-client requests). Both are optional: a proxy whose child
22
+ * never pushes notifications needs neither.
23
+ */
24
+ export interface LspProxyHandlers {
25
+ /** Invoked for every notification the child sends. */
26
+ onNotification?: (method: string, params: unknown) => void;
27
+ /** Invoked for every server-to-client request the child sends. */
28
+ onRequest?: (method: string, params: unknown) => Promise<unknown>;
29
+ }
30
+ /**
31
+ * A live connection to a child language server.
32
+ *
33
+ * Lifecycle: {@link LspProxy.start} launches the child and performs the LSP
34
+ * `initialize` handshake; {@link LspProxy.sendRequest} /
35
+ * {@link LspProxy.sendNotification} forward messages; {@link LspProxy.stop}
36
+ * shuts it down gracefully.
37
+ */
38
+ export declare class LspProxy {
39
+ #private;
40
+ /**
41
+ * @param spec - How to launch the child language server.
42
+ * @param label - A short name for the server, used to tag its stderr.
43
+ * @param handlers - Optional handlers for child-originated traffic.
44
+ */
45
+ constructor(spec: LspSpawnSpec, label: string, handlers?: LspProxyHandlers);
46
+ /** The `InitializeResult` the child returned (available after `start`). */
47
+ get initializeResult(): InitializeResult | undefined;
48
+ /** Whether the child process is running and initialized. */
49
+ get isRunning(): boolean;
50
+ /**
51
+ * Launches the child language server and completes the `initialize`
52
+ * handshake.
53
+ *
54
+ * @param initializeParams - The `initialize` params to send the child.
55
+ * @returns The child's `InitializeResult`.
56
+ * @throws If the child cannot be spawned or its stdio is unavailable.
57
+ */
58
+ start(initializeParams: InitializeParams): Promise<InitializeResult>;
59
+ /**
60
+ * Forwards a request to the child and resolves with its response.
61
+ *
62
+ * @throws If the proxy is not running.
63
+ */
64
+ sendRequest<R = unknown>(method: string, params: unknown): Promise<R>;
65
+ /**
66
+ * Forwards a notification to the child.
67
+ *
68
+ * No-op if the proxy is not running — notifications are fire-and-forget and
69
+ * a not-yet-started child must not crash the host.
70
+ */
71
+ sendNotification(method: string, params: unknown): Promise<void>;
72
+ /**
73
+ * Gracefully shuts the child down: LSP `shutdown` + `exit`, then disposes
74
+ * the connection and kills the process if it has not exited.
75
+ */
76
+ stop(): Promise<void>;
77
+ }
@@ -0,0 +1,165 @@
1
+ // File description: `LspProxy` — a generic JSON-RPC client to a child language
2
+ // server, spawned over stdio.
3
+ //
4
+ // `svelte-proxy.ts` already proxies the embedded `svelte-language-server`, but
5
+ // it is specific to that one server (it resolves its `bin/server.js`). The math
6
+ // language server (`@nvl/sveltex-math-language-server`) and TexLab need the same
7
+ // "spawn a child, speak LSP over stdio" treatment, so this module factors that
8
+ // out behind a small, server-agnostic API.
9
+ //
10
+ // Two flavours of child are supported:
11
+ //
12
+ // - a Node module run with `child_process.fork` (`--stdio`), for the bundled
13
+ // math language server, and
14
+ // - a native executable run with `child_process.spawn`, for the TexLab
15
+ // binary found on `PATH`.
16
+ //
17
+ // Both end up as a `ChildProcessWithoutNullStreams`-like object exposing
18
+ // `stdin`/`stdout`/`stderr`; the proxy speaks LSP over those pipes.
19
+ import { fork, spawn } from 'node:child_process';
20
+ // `vscode-languageserver-protocol`'s `./node` subpath alias is not resolvable
21
+ // under `Node16` resolution (the package predates the npm `exports` field), so
22
+ // the Node entry point is imported via its concrete file path — the same
23
+ // approach `svelte-proxy.ts` uses.
24
+ import { StreamMessageReader, StreamMessageWriter, createProtocolConnection, } from 'vscode-languageserver-protocol/lib/node/main.js';
25
+ import { ExitNotification, InitializedNotification, InitializeRequest, ShutdownRequest, } from 'vscode-languageserver-protocol';
26
+ /**
27
+ * A live connection to a child language server.
28
+ *
29
+ * Lifecycle: {@link LspProxy.start} launches the child and performs the LSP
30
+ * `initialize` handshake; {@link LspProxy.sendRequest} /
31
+ * {@link LspProxy.sendNotification} forward messages; {@link LspProxy.stop}
32
+ * shuts it down gracefully.
33
+ */
34
+ export class LspProxy {
35
+ #child;
36
+ #connection;
37
+ #initializeResult;
38
+ #spec;
39
+ #handlers;
40
+ /** A short label used to prefix the child's stderr in host logs. */
41
+ #label;
42
+ /**
43
+ * @param spec - How to launch the child language server.
44
+ * @param label - A short name for the server, used to tag its stderr.
45
+ * @param handlers - Optional handlers for child-originated traffic.
46
+ */
47
+ constructor(spec, label, handlers = {}) {
48
+ this.#spec = spec;
49
+ this.#label = label;
50
+ this.#handlers = handlers;
51
+ }
52
+ /** The `InitializeResult` the child returned (available after `start`). */
53
+ get initializeResult() {
54
+ return this.#initializeResult;
55
+ }
56
+ /** Whether the child process is running and initialized. */
57
+ get isRunning() {
58
+ return this.#connection !== undefined;
59
+ }
60
+ /**
61
+ * Launches the child language server and completes the `initialize`
62
+ * handshake.
63
+ *
64
+ * @param initializeParams - The `initialize` params to send the child.
65
+ * @returns The child's `InitializeResult`.
66
+ * @throws If the child cannot be spawned or its stdio is unavailable.
67
+ */
68
+ async start(initializeParams) {
69
+ const child = this.#spawnChild();
70
+ this.#child = child;
71
+ if (!child.stdout || !child.stdin) {
72
+ throw new Error(`Failed to obtain stdio streams for ${this.#label} child process.`);
73
+ }
74
+ // Surface the child's stderr for debugging without crashing the host.
75
+ child.stderr?.on('data', (chunk) => {
76
+ process.stderr.write(`[${this.#label}] ${chunk.toString()}`);
77
+ });
78
+ const connection = createProtocolConnection(new StreamMessageReader(child.stdout), new StreamMessageWriter(child.stdin));
79
+ this.#connection = connection;
80
+ // Route child-originated traffic to the host's handlers. The catch-all
81
+ // `onNotification`/`onRequest` overloads live on `MessageConnection`
82
+ // but are not surfaced by the narrower `ProtocolConnection` type; the
83
+ // runtime object is a `MessageConnection`, so this view is sound.
84
+ const starConnection = connection;
85
+ const onChildNotification = (method, params) => {
86
+ this.#handlers.onNotification?.(method, params);
87
+ };
88
+ const onChildRequest = async (method, params) => {
89
+ if (this.#handlers.onRequest) {
90
+ return this.#handlers.onRequest(method, params);
91
+ }
92
+ return null;
93
+ };
94
+ starConnection.onNotification(onChildNotification);
95
+ starConnection.onRequest(onChildRequest);
96
+ connection.onClose(() => {
97
+ this.#connection = undefined;
98
+ });
99
+ connection.onError(() => {
100
+ // Errors are logged by the reader; nothing actionable here.
101
+ });
102
+ connection.listen();
103
+ const result = await connection.sendRequest(InitializeRequest.type, initializeParams);
104
+ this.#initializeResult = result;
105
+ await connection.sendNotification(InitializedNotification.type, {});
106
+ return result;
107
+ }
108
+ /** Spawns the child process per {@link LspSpawnSpec}. */
109
+ #spawnChild() {
110
+ if (this.#spec.kind === 'fork') {
111
+ return fork(this.#spec.module, [...(this.#spec.args ?? [])], {
112
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
113
+ execArgv: [],
114
+ });
115
+ }
116
+ return spawn(this.#spec.command, [...(this.#spec.args ?? [])], {
117
+ stdio: ['pipe', 'pipe', 'pipe'],
118
+ });
119
+ }
120
+ /**
121
+ * Forwards a request to the child and resolves with its response.
122
+ *
123
+ * @throws If the proxy is not running.
124
+ */
125
+ async sendRequest(method, params) {
126
+ if (!this.#connection) {
127
+ throw new Error(`${this.#label} proxy is not running.`);
128
+ }
129
+ return this.#connection.sendRequest(method, params);
130
+ }
131
+ /**
132
+ * Forwards a notification to the child.
133
+ *
134
+ * No-op if the proxy is not running — notifications are fire-and-forget and
135
+ * a not-yet-started child must not crash the host.
136
+ */
137
+ async sendNotification(method, params) {
138
+ if (!this.#connection)
139
+ return;
140
+ await this.#connection.sendNotification(method, params);
141
+ }
142
+ /**
143
+ * Gracefully shuts the child down: LSP `shutdown` + `exit`, then disposes
144
+ * the connection and kills the process if it has not exited.
145
+ */
146
+ async stop() {
147
+ const connection = this.#connection;
148
+ const child = this.#child;
149
+ this.#connection = undefined;
150
+ this.#child = undefined;
151
+ if (connection) {
152
+ try {
153
+ await connection.sendRequest(ShutdownRequest.method);
154
+ await connection.sendNotification(ExitNotification.method);
155
+ }
156
+ catch {
157
+ // The child may already be gone; fall through to kill it.
158
+ }
159
+ connection.dispose();
160
+ }
161
+ if (child && child.exitCode === null) {
162
+ child.kill();
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,86 @@
1
+ import { type Position, type Range } from 'vscode-languageserver-textdocument';
2
+ import type { Mapping, MappingFeatures } from './mapping.js';
3
+ /** A which-way-round selector for {@link SourceMap}'s feature queries. */
4
+ export type MapDirection = 'toGenerated' | 'toSource';
5
+ /**
6
+ * Bidirectional mapper between a source document and its generated counterpart.
7
+ *
8
+ * Construct one per open `.sveltex` file (rebuilt on every debounced re-parse).
9
+ * The mappings must be sorted by `sourceOffset`; {@link SourceMap.create}
10
+ * enforces this so callers need not.
11
+ */
12
+ export declare class SourceMap {
13
+ #private;
14
+ private constructor();
15
+ /**
16
+ * Builds a {@link SourceMap}.
17
+ *
18
+ * @param mappings - The span pairs linking the two documents. Order does
19
+ * not matter; they are sorted internally.
20
+ * @param sourceText - Full text of the source `.sveltex` document.
21
+ * @param generatedText - Full text of the generated `.svelte` document.
22
+ */
23
+ static create(mappings: Mapping[], sourceText: string, generatedText: string): SourceMap;
24
+ /** The generated virtual document's full text. */
25
+ get generatedText(): string;
26
+ /** The source document's full text. */
27
+ get sourceText(): string;
28
+ /**
29
+ * Translates a source-document offset to a generated-document offset.
30
+ *
31
+ * @param sourceOffset - Offset in the `.sveltex` document.
32
+ * @returns The corresponding generated offset, or `undefined` if the offset
33
+ * falls outside every mapped span (i.e. inside a non-delegated region).
34
+ */
35
+ sourceOffsetToGenerated(sourceOffset: number): number | undefined;
36
+ /**
37
+ * Translates a generated-document offset to a source-document offset.
38
+ *
39
+ * @param generatedOffset - Offset in the virtual `.svelte` document.
40
+ * @returns The corresponding source offset, or `undefined` if the offset
41
+ * falls outside every mapped span.
42
+ */
43
+ generatedOffsetToSource(generatedOffset: number): number | undefined;
44
+ /**
45
+ * Translates a source-document {@link Position} to a generated-document
46
+ * position.
47
+ *
48
+ * @returns The mapped position, or `undefined` if it lies in an unmapped
49
+ * region.
50
+ */
51
+ sourcePositionToGenerated(position: Position): Position | undefined;
52
+ /**
53
+ * Translates a generated-document {@link Position} to a source-document
54
+ * position.
55
+ *
56
+ * @returns The mapped position, or `undefined` if it lies in an unmapped
57
+ * region.
58
+ */
59
+ generatedPositionToSource(position: Position): Position | undefined;
60
+ /**
61
+ * Translates a source-document {@link Range} to a generated-document range.
62
+ *
63
+ * @returns The mapped range, or `undefined` if either endpoint is unmapped.
64
+ *
65
+ * @remarks
66
+ * A range is only translated when _both_ endpoints land in mapped spans.
67
+ * This is the conservative choice: a half-mapped range would produce a
68
+ * nonsensical generated range and confuse the embedded server.
69
+ */
70
+ sourceRangeToGenerated(range: Range): Range | undefined;
71
+ /**
72
+ * Translates a generated-document {@link Range} to a source-document range.
73
+ *
74
+ * @returns The mapped range, or `undefined` if either endpoint is unmapped.
75
+ */
76
+ generatedRangeToSource(range: Range): Range | undefined;
77
+ /**
78
+ * Returns the {@link MappingFeatures} governing a given offset.
79
+ *
80
+ * @param offset - The offset to query.
81
+ * @param direction - `'toGenerated'` to interpret `offset` as a source
82
+ * offset, `'toSource'` to interpret it as a generated offset.
83
+ * @returns The feature flags, or `undefined` if the offset is unmapped.
84
+ */
85
+ featuresAt(offset: number, direction: MapDirection): MappingFeatures | undefined;
86
+ }