@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,428 @@
1
+ // File description: Forwards language requests that land inside a non-delegated
2
+ // region — TeX math, or a LaTeX verbatim environment — to a dedicated child
3
+ // language server, and maps the results back to `.sveltex` coordinates.
4
+ //
5
+ // `svelte-language-server` never sees these regions (they are blanked out of
6
+ // the virtual `.svelte` document). To still offer hover/completion in them:
7
+ //
8
+ // - a `math` region is forwarded to a spawned `@nvl/sveltex-math-language-server`
9
+ // child, started with `initializationOptions.backend` set from the
10
+ // SvelTeX config's `mathBackend` (only `mathjax` / `katex` are forwarded —
11
+ // `custom` / `none` mean no math server exists, so the region is skipped);
12
+ // - a `verbatim` region whose tag is a LaTeX/TeX environment (one of the
13
+ // configured `latexTags`) is forwarded to a spawned `texlab` child, if a
14
+ // `texlab` binary is found on `PATH`; if it is not, forwarding is skipped
15
+ // silently.
16
+ //
17
+ // Each region is handed to its child as its own small, standalone virtual
18
+ // document (see `region-virtual.ts`): bare TeX for the math server, bare LaTeX
19
+ // for TexLab. Requests are mapped `.sveltex` → region before forwarding;
20
+ // responses are mapped region → `.sveltex` afterwards.
21
+ import { createRequire } from 'node:module';
22
+ import { CompletionItemKind, } from 'vscode-languageserver-protocol';
23
+ import { LspProxy } from './lsp-proxy.js';
24
+ import { findTexlab } from './texlab.js';
25
+ import { buildLatexScaffold, buildRegionVirtualDocument, } from './region-virtual.js';
26
+ import { remapCompletion, remapHover } from './remap.js';
27
+ /** The math backends that have a corresponding math language server. */
28
+ const FORWARDABLE_MATH_BACKENDS = new Set(['mathjax', 'katex']);
29
+ /**
30
+ * Resolves the absolute path of the math language server's `bin/server.js`.
31
+ *
32
+ * `@nvl/sveltex-math-language-server` is a regular dependency of this package,
33
+ * so a plain module resolution finds it — the standalone and Zed scenarios.
34
+ *
35
+ * A host that has bundled the math server to a sibling file (the VS Code
36
+ * extension) cannot rely on `node_modules` and passes the bundled file's
37
+ * absolute path explicitly via `override`.
38
+ *
39
+ * @param override - An explicit absolute path to use instead of resolving from
40
+ * `node_modules`. When given, it is returned verbatim.
41
+ */
42
+ function resolveMathServerPath(override) {
43
+ if (override)
44
+ return override;
45
+ const require = createRequire(import.meta.url);
46
+ return require.resolve('@nvl/sveltex-math-language-server/bin/server.js');
47
+ }
48
+ /**
49
+ * Whether `region` carries a LaTeX/TeX environment, i.e. its opening tag is one
50
+ * of the configured `latexTags` (case-insensitive, as SvelTeX tags are).
51
+ *
52
+ * @param source - Full text of the `.sveltex` document.
53
+ * @param region - The region to test (only `verbatim` regions can match).
54
+ * @param latexTags - The configured LaTeX verbatim tags.
55
+ */
56
+ export function isLatexVerbatimRegion(source, region, latexTags) {
57
+ if (region.kind !== 'verbatim')
58
+ return false;
59
+ const slice = source.slice(region.sourceStart, region.sourceEnd);
60
+ const tagMatch = /^<\s*([a-zA-Z][-.:0-9_a-zA-Z]*)/u.exec(slice);
61
+ if (!tagMatch)
62
+ return false;
63
+ const tag = (tagMatch[1] ?? '').toLowerCase();
64
+ return latexTags.some((t) => t.toLowerCase() === tag);
65
+ }
66
+ /** A {@link ForwarderLog} that discards every message. */
67
+ const noopLog = () => undefined;
68
+ /**
69
+ * A short, human-readable description of a forwarded request's raw result —
70
+ * used to log what a child server (notably TexLab) actually returned, so an
71
+ * empty `<tex>` completion can be told apart from a routing failure.
72
+ */
73
+ function describeResult(result) {
74
+ if (result === null || result === undefined)
75
+ return 'nothing';
76
+ if (Array.isArray(result))
77
+ return `${String(result.length)} item(s)`;
78
+ if (typeof result === 'object') {
79
+ if ('items' in result) {
80
+ const items = result.items;
81
+ if (Array.isArray(items)) {
82
+ return `${String(items.length)} item(s)`;
83
+ }
84
+ }
85
+ if ('contents' in result)
86
+ return 'a hover';
87
+ }
88
+ return 'a result';
89
+ }
90
+ /**
91
+ * Relabels `Text`-kind completion items as `Function`.
92
+ *
93
+ * TexLab tags every command completion `CompletionItemKind.Text` — the generic
94
+ * "a word that occurs in the document" kind. VS Code routes `Text` items
95
+ * through the `editor.suggest.showWords` toggle and folds them in with
96
+ * buffer-word suggestions, so a user who has turned word suggestions off (a
97
+ * common preference) sees *no* `<tex>` completions at all. A LaTeX control
98
+ * sequence is a function-like macro, not a stray word, so it is presented as
99
+ * `Function`: both more accurate and immune to the `showWords` toggle.
100
+ *
101
+ * @param result - A completion result already remapped to `.sveltex` coords.
102
+ * @returns The same result with every `Text`-kind item relabelled `Function`.
103
+ */
104
+ export function withFunctionCompletionKind(result) {
105
+ if (!result)
106
+ return result;
107
+ const toFunctionKind = (item) => item.kind === CompletionItemKind.Text
108
+ ? { ...item, kind: CompletionItemKind.Function }
109
+ : item;
110
+ return Array.isArray(result)
111
+ ? result.map(toFunctionKind)
112
+ : { ...result, items: result.items.map(toFunctionKind) };
113
+ }
114
+ /**
115
+ * Manages the child language servers that back non-delegated regions of one
116
+ * SvelTeX workspace, and forwards hover/completion requests to them.
117
+ *
118
+ * One instance is created per `createServer` call. The children are spawned
119
+ * lazily on first use and reused for the lifetime of the server.
120
+ */
121
+ export class RegionForwarder {
122
+ /**
123
+ * The math language server child, spawned lazily on the first math
124
+ * request. This holds the in-flight spawn *promise*, not the resolved
125
+ * proxy: a `<tex>`/`$…$` document typically triggers several region
126
+ * requests at once, and they must all await the one spawn — caching only a
127
+ * "spawn started" flag would let every request after the first see no
128
+ * proxy yet and give up.
129
+ */
130
+ #mathProxyPromise;
131
+ /**
132
+ * The TexLab child, spawned lazily on the first LaTeX-verbatim request.
133
+ * Holds the in-flight spawn promise — see {@link RegionForwarder.#mathProxyPromise}.
134
+ */
135
+ #texlabProxyPromise;
136
+ /** The resolved SvelTeX config snapshot (backend, latex tags). */
137
+ #config;
138
+ /** Monotonic counter making each forwarded virtual document URI unique. */
139
+ #virtualDocCounter = 0;
140
+ /**
141
+ * An explicit `@nvl/sveltex-math-language-server` `bin/server.js` path, or
142
+ * `undefined` to resolve it from `node_modules`. Set via
143
+ * {@link setMathServerPath} before the math child is first spawned. See
144
+ * {@link resolveMathServerPath}.
145
+ */
146
+ #mathServerPathOverride;
147
+ /**
148
+ * Sink for child-server lifecycle log lines, wired by the host to the
149
+ * editor's output channel. Defaults to discarding them.
150
+ */
151
+ #log;
152
+ /**
153
+ * @param config - The resolved SvelTeX config snapshot. Replaceable via
154
+ * {@link updateConfig} when the workspace config is (re)loaded.
155
+ * @param log - Optional sink for child-server lifecycle log lines (TexLab /
156
+ * the math server found, started, or failed). Defaults to discarding them.
157
+ */
158
+ constructor(config, log = noopLog) {
159
+ this.#config = config;
160
+ this.#log = log;
161
+ }
162
+ /** Replaces the config snapshot (e.g. after the config file is loaded). */
163
+ updateConfig(config) {
164
+ this.#config = config;
165
+ }
166
+ /**
167
+ * Overrides the location of the math language server child.
168
+ *
169
+ * Standalone use needs no override — the server is resolved from
170
+ * `node_modules`. A host that has bundled the server to a sibling file (the
171
+ * VS Code extension) calls this with that file's absolute path before any
172
+ * math region is forwarded, since `node_modules` will not exist at runtime.
173
+ *
174
+ * @param serverPath - Absolute path of the math server's `bin/server.js`,
175
+ * or `undefined` to keep resolving from `node_modules`.
176
+ */
177
+ setMathServerPath(serverPath) {
178
+ this.#mathServerPathOverride = serverPath;
179
+ }
180
+ /**
181
+ * Forwards a hover request that lands in `region` to the appropriate child
182
+ * server.
183
+ *
184
+ * @param source - Full text of the `.sveltex` document.
185
+ * @param sourceUri - The `.sveltex` document URI.
186
+ * @param region - The region the request position falls in.
187
+ * @param position - The request position, in `.sveltex` coordinates.
188
+ * @returns The hover, mapped back to `.sveltex` coordinates, or `null` if
189
+ * the region is not forwardable or the child has nothing to offer.
190
+ */
191
+ async forwardHover(source, sourceUri, region, position) {
192
+ const forwarded = await this.#forward('textDocument/hover', source, sourceUri, region, position);
193
+ if (!forwarded)
194
+ return null;
195
+ return remapHover(forwarded.result, forwarded.ctx);
196
+ }
197
+ /**
198
+ * Forwards a completion request that lands in `region` to the appropriate
199
+ * child server.
200
+ *
201
+ * @returns The completion result, mapped back to `.sveltex` coordinates,
202
+ * or `null` if the region is not forwardable.
203
+ */
204
+ async forwardCompletion(source, sourceUri, region, position) {
205
+ const forwarded = await this.#forward('textDocument/completion', source, sourceUri, region, position);
206
+ if (!forwarded)
207
+ return null;
208
+ const remapped = remapCompletion(forwarded.result, forwarded.ctx);
209
+ // TexLab tags every command completion `kind: Text` — the generic
210
+ // "a word from the document" kind, which editors fold in with (and
211
+ // can suppress alongside) word-based suggestions. A LaTeX control
212
+ // sequence is a function-like macro, so present it as one.
213
+ return region.kind === 'verbatim'
214
+ ? withFunctionCompletionKind(remapped)
215
+ : remapped;
216
+ }
217
+ /** Shuts every spawned child server down. */
218
+ async stop() {
219
+ // Await any in-flight spawn first so a child still starting up is not
220
+ // leaked past `stop()`.
221
+ const [mathProxy, texlabProxy] = await Promise.all([
222
+ this.#mathProxyPromise,
223
+ this.#texlabProxyPromise,
224
+ ]);
225
+ this.#mathProxyPromise = undefined;
226
+ this.#texlabProxyPromise = undefined;
227
+ await Promise.all([mathProxy?.stop(), texlabProxy?.stop()]);
228
+ }
229
+ /**
230
+ * The core forward: pick the child for `region`, build a standalone
231
+ * virtual document for the region, sync it to the child, map the request
232
+ * position into it, send the request, and return the raw result plus the
233
+ * `RemapContext` needed to map the response back.
234
+ *
235
+ * @typeParam R - The expected response shape. The LSP wire protocol is
236
+ * untyped JSON, so `R` is a caller-supplied assertion about what the child
237
+ * returns — hence it appears only in the return position.
238
+ */
239
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
240
+ async #forward(method, source, sourceUri, region, position) {
241
+ const target = await this.#proxyForRegion(source, region);
242
+ if (!target)
243
+ return null;
244
+ const latexScaffold = this.#latexScaffoldFor(source, region);
245
+ const virtual = buildRegionVirtualDocument(source, region, latexScaffold);
246
+ const generatedPosition = virtual.sourceMap.sourcePositionToGenerated(position);
247
+ // The position can fall on a stripped delimiter/tag — unmapped. Nothing
248
+ // to forward in that case.
249
+ if (!generatedPosition) {
250
+ this.#log(`${method}: caret is on a ${region.kind} delimiter/tag, ` +
251
+ 'not inside the region — not forwarded.');
252
+ return null;
253
+ }
254
+ // A fresh URI per call keeps the child's document state from going
255
+ // stale as region boundaries shift between edits; the child treats
256
+ // each as an independent open/close.
257
+ this.#virtualDocCounter += 1;
258
+ const virtualUri = `${sourceUri}.region${String(this.#virtualDocCounter)}${target.extension}`;
259
+ await target.proxy.sendNotification('textDocument/didOpen', {
260
+ textDocument: {
261
+ uri: virtualUri,
262
+ languageId: target.languageId,
263
+ version: 1,
264
+ text: virtual.text,
265
+ },
266
+ });
267
+ try {
268
+ const result = await target.proxy.sendRequest(method, {
269
+ textDocument: { uri: virtualUri },
270
+ position: generatedPosition,
271
+ });
272
+ if (region.kind === 'verbatim') {
273
+ const preamble = latexScaffold
274
+ ? "the project's preamble"
275
+ : 'the built-in fallback preamble';
276
+ this.#log(`${method} forwarded to TexLab with ${preamble} → ` +
277
+ describeResult(result));
278
+ }
279
+ const ctx = {
280
+ sourceUri,
281
+ virtualUri,
282
+ sourceMap: virtual.sourceMap,
283
+ };
284
+ return { result, ctx };
285
+ }
286
+ finally {
287
+ // Always release the child's copy of the document.
288
+ await target.proxy.sendNotification('textDocument/didClose', {
289
+ textDocument: { uri: virtualUri },
290
+ });
291
+ }
292
+ }
293
+ /**
294
+ * Returns the {@link LatexScaffold} for a LaTeX verbatim region — the
295
+ * project's own, when `sveltex.config.*` declares the region's tag — or
296
+ * `undefined` for any other region (and for an unconfigured tag), in which
297
+ * case `buildRegionVirtualDocument` falls back to its built-in scaffold.
298
+ *
299
+ * Feeding TexLab the project's real `\documentclass` + preamble means its
300
+ * completion and hover see exactly the packages and preamble macros the
301
+ * block will actually be compiled with.
302
+ */
303
+ #latexScaffoldFor(source, region) {
304
+ if (region.kind !== 'verbatim')
305
+ return undefined;
306
+ const slice = source.slice(region.sourceStart, region.sourceEnd);
307
+ const tag = /^<\s*([a-zA-Z][-.:0-9_a-zA-Z]*)/u.exec(slice)?.[1];
308
+ if (!tag)
309
+ return undefined;
310
+ const scaffold = this.#config.texScaffolds[tag.toLowerCase()];
311
+ if (!scaffold)
312
+ return undefined;
313
+ return buildLatexScaffold(scaffold.documentClass, scaffold.preamble);
314
+ }
315
+ /**
316
+ * Selects (lazily spawning if needed) the child server for a region, along
317
+ * with the `languageId` / URI extension that child expects.
318
+ *
319
+ * @returns The child and its document metadata, or `undefined` if the
320
+ * region is not forwardable (non-math/verbatim region, a `custom`/`none`
321
+ * math backend, a non-LaTeX verbatim tag, or TexLab not installed).
322
+ */
323
+ async #proxyForRegion(source, region) {
324
+ if (region.kind === 'math') {
325
+ const proxy = await this.#ensureMathProxy();
326
+ if (!proxy)
327
+ return undefined;
328
+ return { proxy, languageId: 'latex', extension: '.tex' };
329
+ }
330
+ if (region.kind === 'verbatim' &&
331
+ isLatexVerbatimRegion(source, region, this.#config.latexTags)) {
332
+ const proxy = await this.#ensureTexlabProxy();
333
+ if (!proxy)
334
+ return undefined;
335
+ return { proxy, languageId: 'latex', extension: '.tex' };
336
+ }
337
+ return undefined;
338
+ }
339
+ /**
340
+ * Returns the math language server proxy, spawning it on first use.
341
+ *
342
+ * Concurrent first-callers all await the one spawn (see
343
+ * {@link RegionForwarder.#mathProxyPromise}).
344
+ *
345
+ * @returns The running proxy, or `undefined` if the configured math
346
+ * backend has no language server (`custom` / `none`) or the spawn failed.
347
+ */
348
+ async #ensureMathProxy() {
349
+ // The backend check is per-call, not cached: `updateConfig` may switch
350
+ // a `none`/`custom` project to `mathjax`/`katex` after construction.
351
+ if (!FORWARDABLE_MATH_BACKENDS.has(this.#config.mathBackend)) {
352
+ return undefined;
353
+ }
354
+ this.#mathProxyPromise ??= this.#startMathProxy();
355
+ const proxy = await this.#mathProxyPromise;
356
+ return proxy?.isRunning ? proxy : undefined;
357
+ }
358
+ /** Spawns and initializes the math language server child. */
359
+ async #startMathProxy() {
360
+ try {
361
+ const proxy = new LspProxy({
362
+ kind: 'fork',
363
+ module: resolveMathServerPath(this.#mathServerPathOverride),
364
+ args: ['--stdio'],
365
+ }, 'sveltex-math-language-server');
366
+ await proxy.start({
367
+ processId: process.pid,
368
+ rootUri: null,
369
+ capabilities: {},
370
+ // The backend is `mathjax` or `katex` here — `#ensureMathProxy`
371
+ // already excluded the other values.
372
+ initializationOptions: {
373
+ backend: this.#config.mathBackend,
374
+ },
375
+ });
376
+ this.#log(`Math language server started (${this.#config.mathBackend}).`);
377
+ return proxy;
378
+ }
379
+ catch (error) {
380
+ // A failed spawn must not break the rest of the language server.
381
+ this.#log('Math language server failed to start: ' +
382
+ (error instanceof Error ? error.message : String(error)));
383
+ return undefined;
384
+ }
385
+ }
386
+ /**
387
+ * Returns the TexLab proxy, spawning it on first use. TexLab support is
388
+ * best-effort — a missing or unstartable `texlab` yields `undefined` — but
389
+ * no longer silent: each outcome is logged via {@link RegionForwarder.#log}.
390
+ *
391
+ * Concurrent first-callers all await the one spawn (see
392
+ * {@link RegionForwarder.#mathProxyPromise}).
393
+ *
394
+ * @returns The running proxy, or `undefined` if `texlab` is not on `PATH`
395
+ * or the spawn failed.
396
+ */
397
+ async #ensureTexlabProxy() {
398
+ this.#texlabProxyPromise ??= this.#startTexlabProxy();
399
+ const proxy = await this.#texlabProxyPromise;
400
+ return proxy?.isRunning ? proxy : undefined;
401
+ }
402
+ /** Locates, spawns, and initializes the TexLab child. */
403
+ async #startTexlabProxy() {
404
+ const texlabPath = findTexlab();
405
+ if (!texlabPath) {
406
+ this.#log('TexLab not found on PATH — hover/completion in LaTeX ' +
407
+ 'verbatim regions is disabled. Install `texlab` and make ' +
408
+ 'sure it is on the PATH the editor process sees.');
409
+ return undefined;
410
+ }
411
+ try {
412
+ this.#log(`TexLab found at ${texlabPath}; starting…`);
413
+ const proxy = new LspProxy({ kind: 'spawn', command: texlabPath }, 'texlab');
414
+ await proxy.start({
415
+ processId: process.pid,
416
+ rootUri: null,
417
+ capabilities: {},
418
+ });
419
+ this.#log('TexLab started.');
420
+ return proxy;
421
+ }
422
+ catch (error) {
423
+ this.#log('TexLab failed to start: ' +
424
+ (error instanceof Error ? error.message : String(error)));
425
+ return undefined;
426
+ }
427
+ }
428
+ }
@@ -0,0 +1,71 @@
1
+ import { SourceMap } from './mapper.js';
2
+ import type { Region } from './regions.js';
3
+ /** A standalone virtual document for one region. */
4
+ export interface RegionVirtualDocument {
5
+ /**
6
+ * The virtual document text handed to the child server. For a math region
7
+ * this is the bare inner text (delimiters stripped); for a LaTeX verbatim
8
+ * region it is that inner text embedded in {@link latexRegionScaffold}.
9
+ */
10
+ text: string;
11
+ /**
12
+ * Bidirectional mapper between the `.sveltex` source and {@link text}.
13
+ * Built over a single identity mapping covering the region's inner span.
14
+ */
15
+ sourceMap: SourceMap;
16
+ /** Offset in the `.sveltex` source where the inner text begins. */
17
+ innerStart: number;
18
+ /** Offset one past the end of the inner text in the `.sveltex` source. */
19
+ innerEnd: number;
20
+ }
21
+ /**
22
+ * The strings a verbatim (LaTeX) region's content is wrapped in before being
23
+ * handed to TexLab: a `prefix` (document class + preamble + `\begin{document}`)
24
+ * and a `suffix` (`\end{document}`).
25
+ */
26
+ export interface LatexScaffold {
27
+ /** Text inserted before the region content. */
28
+ prefix: string;
29
+ /** Text inserted after the region content. */
30
+ suffix: string;
31
+ }
32
+ /**
33
+ * Builds a {@link LatexScaffold} from a `\documentclass` line and a preamble.
34
+ *
35
+ * The region content ends up wrapped as
36
+ * `<documentClass>\n<preamble>\n\begin{document}\n…\n\end{document}\n` — the
37
+ * same shape SvelTeX's preprocessor compiles a TeX verbatim block into, which
38
+ * is what lets the LSP feed TexLab the project's _real_ packages and macros.
39
+ */
40
+ export declare function buildLatexScaffold(documentClass: string, preamble: string): LatexScaffold;
41
+ /**
42
+ * The fallback scaffold, used for a `<tex>` region whose project declares no
43
+ * readable `sveltex.config.*` TeX environment.
44
+ *
45
+ * TexLab's completion and hover are context-sensitive: a bare command fragment
46
+ * is not treated as document-body content and yields next to nothing. Wrapping
47
+ * it in `\begin{document}…\end{document}` unlocks command completion; the
48
+ * preamble loads `amsmath` and `tikz`, the packages a `<tex>` block most often
49
+ * relies on. When the project's config _is_ readable the LSP instead uses its
50
+ * real document class + preamble — see `config.ts`'s `TexScaffold`.
51
+ */
52
+ export declare const latexRegionScaffold: LatexScaffold;
53
+ /**
54
+ * Builds a {@link RegionVirtualDocument} for a math or verbatim region.
55
+ *
56
+ * @param source - Full text of the `.sveltex` document.
57
+ * @param region - The region to extract. Its `kind` must be `math` or
58
+ * `verbatim`; for any other kind the whole slice is treated as inner content.
59
+ * @param latexScaffold - The scaffold to wrap a `verbatim` region in; defaults
60
+ * to {@link latexRegionScaffold}. Ignored for `math` regions.
61
+ * @returns The standalone virtual document and its source map.
62
+ *
63
+ * @remarks
64
+ * A `math` region's virtual document is the bare inner text. A `verbatim`
65
+ * region's is that inner text wrapped in `latexScaffold`. Either way the
66
+ * `SourceMap` holds one identity mapping covering the inner span —
67
+ * `[innerStart … innerEnd) ↔ [prefixLength … prefixLength + innerLength)` — so
68
+ * a caret in the inner text maps straight back to the right `.sveltex` offset,
69
+ * while the synthetic scaffold lines are covered by no mapping at all.
70
+ */
71
+ export declare function buildRegionVirtualDocument(source: string, region: Region, latexScaffold?: LatexScaffold): RegionVirtualDocument;
@@ -0,0 +1,131 @@
1
+ // File description: Builds a per-region virtual document for a non-delegated
2
+ // region (a math region, or a LaTeX verbatim region) plus the `SourceMap` that
3
+ // ties it back to the `.sveltex` source.
4
+ //
5
+ // `virtual-svelte.ts` blanks these regions out of the ONE virtual `.svelte`
6
+ // document handed to `svelte-language-server`. To get language assistance for
7
+ // them instead, the SvelTeX server forwards requests to a dedicated child (the
8
+ // math language server, or TexLab). The region's syntactic wrapper — the
9
+ // `$`/`$$`/`\(`/`\[` math delimiters, or the `<tex>…</tex>` verbatim tags — is
10
+ // stripped, and positions are mapped across that strip.
11
+ //
12
+ // The math server wants bare TeX. TexLab, a full LaTeX language server, does
13
+ // not: its completion and hover are context-sensitive, and a bare fragment
14
+ // (`\alp`) yields almost nothing — TexLab does not see it as document-body
15
+ // content. So a LaTeX (verbatim) region's content is embedded in a minimal
16
+ // `\documentclass…\begin{document}…\end{document}` scaffold. The scaffold lines
17
+ // are synthetic — no `Mapping` covers them — so they never appear in a
18
+ // forwarded result. The same `SourceMap` / `Mapping` model the Svelte side
19
+ // uses is reused throughout.
20
+ import { identityMapping } from './mapping.js';
21
+ import { SourceMap } from './mapper.js';
22
+ /**
23
+ * Builds a {@link LatexScaffold} from a `\documentclass` line and a preamble.
24
+ *
25
+ * The region content ends up wrapped as
26
+ * `<documentClass>\n<preamble>\n\begin{document}\n…\n\end{document}\n` — the
27
+ * same shape SvelTeX's preprocessor compiles a TeX verbatim block into, which
28
+ * is what lets the LSP feed TexLab the project's _real_ packages and macros.
29
+ */
30
+ export function buildLatexScaffold(documentClass, preamble) {
31
+ return {
32
+ prefix: `${documentClass}\n${preamble}\n\\begin{document}\n`,
33
+ suffix: '\n\\end{document}\n',
34
+ };
35
+ }
36
+ /**
37
+ * The fallback scaffold, used for a `<tex>` region whose project declares no
38
+ * readable `sveltex.config.*` TeX environment.
39
+ *
40
+ * TexLab's completion and hover are context-sensitive: a bare command fragment
41
+ * is not treated as document-body content and yields next to nothing. Wrapping
42
+ * it in `\begin{document}…\end{document}` unlocks command completion; the
43
+ * preamble loads `amsmath` and `tikz`, the packages a `<tex>` block most often
44
+ * relies on. When the project's config _is_ readable the LSP instead uses its
45
+ * real document class + preamble — see `config.ts`'s `TexScaffold`.
46
+ */
47
+ export const latexRegionScaffold = buildLatexScaffold('\\documentclass{article}', '\\usepackage{amsmath}\n\\usepackage{tikz}');
48
+ /**
49
+ * Determines the math delimiter wrapper of a math-region slice.
50
+ *
51
+ * SvelTeX math regions are delimited by `$$…$$`, `$…$`, `\[…\]` or `\(…\)`.
52
+ * The slice always _includes_ the delimiters (verified against
53
+ * `computeRegions`' output).
54
+ *
55
+ * @param slice - The full text of the math region.
56
+ * @returns The prefix/suffix lengths to strip. An unrecognised slice yields
57
+ * `[0, 0]` (nothing stripped — better to over-include than to mis-map).
58
+ */
59
+ function mathWrapper(slice) {
60
+ if (slice.startsWith('$$') && slice.endsWith('$$') && slice.length >= 4) {
61
+ return [2, 2];
62
+ }
63
+ if (slice.startsWith('$') && slice.endsWith('$') && slice.length >= 2) {
64
+ return [1, 1];
65
+ }
66
+ if (slice.startsWith('\\[') && slice.endsWith('\\]'))
67
+ return [2, 2];
68
+ if (slice.startsWith('\\(') && slice.endsWith('\\)'))
69
+ return [2, 2];
70
+ return [0, 0];
71
+ }
72
+ /**
73
+ * Determines the verbatim-tag wrapper of a verbatim-region slice.
74
+ *
75
+ * A SvelTeX verbatim region is an HTML-like element, `<tag …>…</tag>` or the
76
+ * self-closing `<tag …/>`. For a self-closing element there is no inner
77
+ * content, so the whole slice is "wrapper".
78
+ *
79
+ * @param slice - The full text of the verbatim region.
80
+ * @returns The prefix/suffix lengths to strip.
81
+ */
82
+ function verbatimWrapper(slice) {
83
+ // Self-closing `<tag … />`: no inner content.
84
+ if (/\/\s*>\s*$/u.test(slice) && !/<\/\s*[a-zA-Z]/u.test(slice)) {
85
+ return [slice.length, 0];
86
+ }
87
+ // Opening tag: `<tag …>` — match up to the first unescaped `>`.
88
+ const open = /^<[a-zA-Z][^>]*>/u.exec(slice);
89
+ // Closing tag: `</tag …>` at the very end.
90
+ const close = /<\/\s*[a-zA-Z][^>]*>\s*$/u.exec(slice);
91
+ if (!open || !close)
92
+ return [0, 0];
93
+ return [open[0].length, close[0].length];
94
+ }
95
+ /**
96
+ * Builds a {@link RegionVirtualDocument} for a math or verbatim region.
97
+ *
98
+ * @param source - Full text of the `.sveltex` document.
99
+ * @param region - The region to extract. Its `kind` must be `math` or
100
+ * `verbatim`; for any other kind the whole slice is treated as inner content.
101
+ * @param latexScaffold - The scaffold to wrap a `verbatim` region in; defaults
102
+ * to {@link latexRegionScaffold}. Ignored for `math` regions.
103
+ * @returns The standalone virtual document and its source map.
104
+ *
105
+ * @remarks
106
+ * A `math` region's virtual document is the bare inner text. A `verbatim`
107
+ * region's is that inner text wrapped in `latexScaffold`. Either way the
108
+ * `SourceMap` holds one identity mapping covering the inner span —
109
+ * `[innerStart … innerEnd) ↔ [prefixLength … prefixLength + innerLength)` — so
110
+ * a caret in the inner text maps straight back to the right `.sveltex` offset,
111
+ * while the synthetic scaffold lines are covered by no mapping at all.
112
+ */
113
+ export function buildRegionVirtualDocument(source, region, latexScaffold = latexRegionScaffold) {
114
+ const slice = source.slice(region.sourceStart, region.sourceEnd);
115
+ const [prefix, suffix] = region.kind === 'math'
116
+ ? mathWrapper(slice)
117
+ : region.kind === 'verbatim'
118
+ ? verbatimWrapper(slice)
119
+ : [0, 0];
120
+ const innerStart = region.sourceStart + prefix;
121
+ const innerEnd = Math.max(innerStart, region.sourceEnd - suffix);
122
+ const innerText = source.slice(innerStart, innerEnd);
123
+ // A `verbatim` region is forwarded to TexLab, which needs document-body
124
+ // context; a `math` region goes to the bare-TeX math server, which does
125
+ // not. Embed the former in the LaTeX scaffold (the project's, or the
126
+ // built-in default).
127
+ const scaffold = region.kind === 'verbatim' ? latexScaffold : { prefix: '', suffix: '' };
128
+ const text = `${scaffold.prefix}${innerText}${scaffold.suffix}`;
129
+ const sourceMap = SourceMap.create([identityMapping(innerStart, scaffold.prefix.length, innerText.length)], source, text);
130
+ return { text, sourceMap, innerStart, innerEnd };
131
+ }