@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,604 @@
1
+ // File description: `createServer` — the transport-agnostic heart of the
2
+ // SvelTeX language server.
3
+ //
4
+ // `createServer` takes an already-constructed LSP `Connection` (created over
5
+ // stdio by `index.ts`, or over IPC by the VS Code extension) and wires it up.
6
+ // It deliberately does NOT import `vscode`, `process.stdin`, or any transport:
7
+ // that separation is what lets the same core back both the VS Code extension
8
+ // and the planned Zed extension. The Zed extension only needs to launch
9
+ // `bin/server.js`; everything below is shared.
10
+ //
11
+ // Request flow:
12
+ // editor --(.sveltex coords)--> createServer --(map src->gen)--> SvelteProxy
13
+ // <--(.sveltex coords)-- createServer <--(map gen->src)-- child server
14
+ //
15
+ // Non-delegated regions (verbatim/code/math/frontmatter) are blanked out of the
16
+ // virtual document, so the embedded Svelte server never sees them; requests and
17
+ // responses that land there are dropped.
18
+ import { DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, PublishDiagnosticsNotification, TextDocumentSyncKind, } from 'vscode-languageserver-protocol';
19
+ import { TextDocument } from 'vscode-languageserver-textdocument';
20
+ import { SvelteProxy } from './svelte-proxy.js';
21
+ import { computeRegions } from './regions.js';
22
+ import { buildVirtualSvelte } from './virtual-svelte.js';
23
+ import { RegionForwarder, isLatexVerbatimRegion } from './region-forwarding.js';
24
+ import { defaultConfigSnapshot, loadConfigSnapshot, } from './config.js';
25
+ import { computeDocumentSymbols, computeFoldingRanges, computeSelectionRanges, } from './markdown.js';
26
+ import { computeFrontmatterCompletion, computeFrontmatterHover, } from './frontmatter.js';
27
+ import { mapProxiedDiagnostics, mergeDiagnostics } from './diagnostics.js';
28
+ import { remapCodeActions, remapCompletion, remapDefinition, remapDocumentLinks, remapHighlights, remapHover, remapReferences, remapSignatureHelp, remapWorkspaceEdit, toSourceUri, toVirtualUri, } from './remap.js';
29
+ import { DEFAULT_SVELTEX_EXTENSION, isNativeCompletionItem, markNativeCompletion, pickDefined, readServerPaths, withoutPullDiagnostics, workspaceRootOf, } from './server-helpers.js';
30
+ /**
31
+ * Wires a SvelTeX language server onto the given connection.
32
+ *
33
+ * @param connection - An LSP {@link Connection}, already created for whatever
34
+ * transport the host uses. This function never calls `listen()` — the caller
35
+ * (`startServer` in `index.ts`, or the VS Code client) owns the lifecycle.
36
+ *
37
+ * @remarks
38
+ * Transport-agnostic by construction: no `vscode` import, no direct stdio
39
+ * access. This is the contract the future Zed extension relies on.
40
+ */
41
+ export function createServer(connection) {
42
+ /** Open `.sveltex` documents, keyed by URI. */
43
+ const documents = new Map();
44
+ /** Resolved SvelTeX config; replaced once `initialize` locates a config. */
45
+ let config = defaultConfigSnapshot();
46
+ /**
47
+ * Workspace root, captured at `initialize`. Kept so the watched-file
48
+ * handler can reload the config later without re-deriving it.
49
+ */
50
+ let workspaceRoot;
51
+ /**
52
+ * Live config-reload bookkeeping (see {@link scheduleConfigReload}). A
53
+ * burst of `svelte.config.*` watch events is debounced into one reload,
54
+ * and `configReloadInFlight`/`configReloadQueued` single-flight it —
55
+ * together they cap the config loader at a single child process at a time.
56
+ */
57
+ let configReloadTimer;
58
+ let configReloadInFlight = false;
59
+ let configReloadQueued = false;
60
+ /**
61
+ * The embedded Svelte language server. Notifications it emits for a virtual
62
+ * `.svelte` URI are translated and re-emitted by the host on the
63
+ * corresponding `.sveltex` URI.
64
+ */
65
+ const proxy = new SvelteProxy({
66
+ onNotification: (method, params) => {
67
+ handleProxyNotification(method, params);
68
+ },
69
+ onRequest: async (method, params) => {
70
+ // Server-to-client requests from the child (e.g.
71
+ // `client/registerCapability`, `workspace/configuration`) are
72
+ // forwarded to the real editor and the response relayed back.
73
+ return connection.sendRequest(method, params);
74
+ },
75
+ });
76
+ /**
77
+ * Logs one operational line to the editor's "SvelTeX Language Server"
78
+ * output channel, tagged `[sveltex]`. Carries child-server lifecycle
79
+ * messages and config-load outcomes — the things that, when they go
80
+ * wrong, would otherwise fail silently.
81
+ */
82
+ const logInfo = (message) => {
83
+ connection.console.info(`[sveltex] ${message}`);
84
+ };
85
+ /**
86
+ * Forwards hover/completion in non-delegated regions to dedicated child
87
+ * servers: the math language server for `math` regions, TexLab for LaTeX
88
+ * `verbatim` regions. Spawns its children lazily on first use.
89
+ *
90
+ * Its lifecycle log lines (TexLab / math server found, started, failed)
91
+ * are routed to the editor's output channel so a missing or crashing
92
+ * child is visible rather than a silent loss of language features.
93
+ */
94
+ const regionForwarder = new RegionForwarder(config, logInfo);
95
+ /**
96
+ * Returns whether a URI denotes a SvelTeX document, based on the live
97
+ * config's `extensions` list (defaults to `['.sveltex']`). A user who
98
+ * sets `extensions: ['.svtx']` in their SvelTeX config has the LSP open
99
+ * `.svtx` files; the default still applies if the config hasn't
100
+ * resolved yet.
101
+ */
102
+ function isSveltexUri(uri) {
103
+ const exts = config.extensions.length
104
+ ? config.extensions
105
+ : [DEFAULT_SVELTEX_EXTENSION];
106
+ return exts.some((ext) => uri.endsWith(ext));
107
+ }
108
+ /** Builds a {@link RemapContext} for an open document. */
109
+ function remapContext(doc) {
110
+ return {
111
+ sourceUri: doc.uri,
112
+ virtualUri: toVirtualUri(doc.uri),
113
+ sourceMap: doc.virtual.sourceMap,
114
+ };
115
+ }
116
+ /**
117
+ * Returns the region of `doc` that contains `position`.
118
+ *
119
+ * @param doc - The open document.
120
+ * @param position - A caret position in `.sveltex` coordinates.
121
+ * @returns The containing {@link Region}, or `undefined` if the position is
122
+ * out of range.
123
+ *
124
+ * @remarks
125
+ * Regions tile the document gap-free, so the position lands in exactly one
126
+ * — except a caret exactly on an interior boundary, which is resolved to
127
+ * the region the boundary _opens_ (so a caret right after a `$…$` is
128
+ * treated as the following region, not the math one).
129
+ */
130
+ function regionAt(doc, position) {
131
+ const textDoc = TextDocument.create('mem://sveltex', 'sveltex', doc.version, doc.text);
132
+ const offset = textDoc.offsetAt(position);
133
+ if (offset < 0 || offset > doc.text.length)
134
+ return undefined;
135
+ for (const region of doc.regions) {
136
+ if (offset >= region.sourceStart && offset < region.sourceEnd) {
137
+ return region;
138
+ }
139
+ }
140
+ // A caret at the very end of the document belongs to the last region.
141
+ return doc.regions.at(-1);
142
+ }
143
+ /**
144
+ * Whether a request landing in `region` should be forwarded to a dedicated
145
+ * child server (rather than the Svelte proxy).
146
+ *
147
+ * `true` for a `math` region (forwarded to the math language server) and
148
+ * for a `verbatim` region whose tag is a configured LaTeX environment
149
+ * (forwarded to TexLab). `RegionForwarder` makes the final call about
150
+ * whether a child is actually available; this is just the fast gate.
151
+ */
152
+ function isForwardableRegion(doc, region) {
153
+ if (region.kind === 'math')
154
+ return true;
155
+ return isLatexVerbatimRegion(doc.text, region, config.latexTags);
156
+ }
157
+ /**
158
+ * Rebuilds the regions, virtual document and source map for `text` and
159
+ * stores them against `uri`.
160
+ */
161
+ function rebuild(uri, text, version) {
162
+ const regions = computeRegions(text, config);
163
+ const virtual = buildVirtualSvelte(text, regions);
164
+ const doc = { uri, text, version, regions, virtual };
165
+ documents.set(uri, doc);
166
+ return doc;
167
+ }
168
+ /** Sends the virtual document to the child via `textDocument/didOpen`. */
169
+ async function proxyDidOpen(doc) {
170
+ await proxy.sendNotification(DidOpenTextDocumentNotification.method, {
171
+ textDocument: {
172
+ uri: toVirtualUri(doc.uri),
173
+ languageId: 'svelte',
174
+ version: doc.version,
175
+ text: doc.virtual.text,
176
+ },
177
+ });
178
+ }
179
+ /**
180
+ * Sends a full-text update of the virtual document to the child via
181
+ * `textDocument/didChange`.
182
+ */
183
+ async function proxyDidChange(doc) {
184
+ await proxy.sendNotification(DidChangeTextDocumentNotification.method, {
185
+ textDocument: { uri: toVirtualUri(doc.uri), version: doc.version },
186
+ contentChanges: [{ text: doc.virtual.text }],
187
+ });
188
+ }
189
+ /** Tells the child to close the virtual document. */
190
+ async function proxyDidClose(uri) {
191
+ await proxy.sendNotification(DidCloseTextDocumentNotification.method, {
192
+ textDocument: { uri: toVirtualUri(uri) },
193
+ });
194
+ }
195
+ /**
196
+ * Routes a notification originating in the child server back to the editor.
197
+ *
198
+ * The only notification needing translation is `publishDiagnostics`: its
199
+ * ranges are in virtual-document coordinates and must be mapped back, and
200
+ * its URI must be rewritten from the virtual `.svelte` URI to the
201
+ * `.sveltex` URI. All other notifications (log messages, telemetry, ...)
202
+ * are forwarded verbatim.
203
+ */
204
+ function handleProxyNotification(method, params) {
205
+ if (method === PublishDiagnosticsNotification.method) {
206
+ const diagnosticsParams = params;
207
+ const sourceUri = toSourceUri(diagnosticsParams.uri);
208
+ const doc = documents.get(sourceUri);
209
+ if (!doc)
210
+ return;
211
+ const ctx = remapContext(doc);
212
+ const proxied = mapProxiedDiagnostics(diagnosticsParams.diagnostics, ctx.sourceMap);
213
+ // Native LaTeX/math diagnostics are stubbed for v1.
214
+ // TODO: produce native diagnostics for verbatim/math regions.
215
+ const merged = mergeDiagnostics(proxied, []);
216
+ void connection.sendDiagnostics({
217
+ uri: sourceUri,
218
+ ...(diagnosticsParams.version !== undefined
219
+ ? { version: diagnosticsParams.version }
220
+ : {}),
221
+ diagnostics: merged,
222
+ });
223
+ return;
224
+ }
225
+ // Pass through everything else (window/logMessage, $/progress, ...).
226
+ void connection.sendNotification(method, params);
227
+ }
228
+ /**
229
+ * Maps a request's `{ textDocument, position }` from source to generated
230
+ * coordinates and forwards the request to the child.
231
+ *
232
+ * @typeParam R - The expected response shape. The LSP wire protocol is
233
+ * untyped JSON, so `R` is a caller-supplied assertion about what the child
234
+ * returns — hence it appears only in the return position.
235
+ * @returns The raw child response, or `null` if the document is unknown,
236
+ * the proxy is down, or the position falls in a non-delegated region.
237
+ */
238
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
239
+ async function proxyPositionRequest(method, params) {
240
+ const doc = documents.get(params.textDocument.uri);
241
+ if (!doc || !proxy.isRunning)
242
+ return null;
243
+ const ctx = remapContext(doc);
244
+ const position = ctx.sourceMap.sourcePositionToGenerated(params.position);
245
+ if (!position)
246
+ return null;
247
+ const generatedParams = {
248
+ ...params,
249
+ textDocument: { uri: ctx.virtualUri },
250
+ position,
251
+ };
252
+ const result = await proxy.sendRequest(method, generatedParams);
253
+ return { result, ctx };
254
+ }
255
+ // ----- lifecycle ---------------------------------------------------------
256
+ connection.onInitialize(async (params) => {
257
+ // Locate and load the SvelTeX config (from `svelte.config.*` at
258
+ // the workspace root).
259
+ workspaceRoot = workspaceRootOf(params);
260
+ if (workspaceRoot) {
261
+ config = await loadConfigSnapshot(workspaceRoot, logInfo);
262
+ }
263
+ // The forwarder needs the resolved config (math backend, LaTeX
264
+ // tags) before any request can be routed.
265
+ regionForwarder.updateConfig(config);
266
+ // A host that has bundled the child servers (the VS Code
267
+ // extension) cannot rely on `node_modules` existing next to this
268
+ // server, so it passes the bundled servers' absolute paths in
269
+ // `initializationOptions`. When the field is absent — standalone
270
+ // use, the Zed extension — the proxies fall back to resolving the
271
+ // children from `node_modules` exactly as before.
272
+ const serverPaths = readServerPaths(params.initializationOptions);
273
+ proxy.setServerPath(serverPaths.svelteLanguageServer);
274
+ regionForwarder.setMathServerPath(serverPaths.mathLanguageServer);
275
+ // Start the embedded Svelte server with the host's own initialize
276
+ // params (so its TypeScript service resolves the real project) —
277
+ // but with the pull-diagnostics capability stripped. With it,
278
+ // `svelte-language-server` answers diagnostics only on demand via
279
+ // `textDocument/diagnostic` and stops *pushing* `publishDiagnostics`
280
+ // notifications; this server forwards only pushed diagnostics (it
281
+ // advertises no `diagnosticProvider`, so the editor never pulls),
282
+ // so without the strip diagnostics would silently never appear.
283
+ let childCapabilities;
284
+ try {
285
+ const childResult = await proxy.start({
286
+ ...params,
287
+ capabilities: withoutPullDiagnostics(params.capabilities),
288
+ });
289
+ childCapabilities = childResult.capabilities;
290
+ }
291
+ catch (error) {
292
+ connection.console.error(`Failed to start svelte-language-server: ${String(error)}`);
293
+ }
294
+ // Advertise ONLY what this server actually answers, in two groups:
295
+ //
296
+ // - Native — handled here directly (`textDocumentSync`, the
297
+ // Markdown document-symbol / folding / selection features) or by
298
+ // forwarding to the math / TexLab children (`hover`,
299
+ // `completion`). Always advertised: they work even when the
300
+ // Svelte child is unavailable.
301
+ // - Proxied — forwarded verbatim to `svelte-language-server` with
302
+ // no local fallback. Advertised only if that child advertises
303
+ // them; otherwise the editor fires requests this server can
304
+ // answer only with `-32601`. `svelte-language-server` has no
305
+ // `textDocument/documentLink` handler, for one — advertising it
306
+ // unconditionally flooded the editor log with failures.
307
+ //
308
+ // Spreading the child's *whole* capability set is wrong too: it
309
+ // pulls in pull diagnostics, semantic tokens, inlay hints, … which
310
+ // have no handler here at all.
311
+ //
312
+ // `textDocumentSync` is `Full` because the virtual document is
313
+ // rebuilt wholesale. The completion trigger characters are extended
314
+ // with `\` and `{`: the editor only re-requests completion on a
315
+ // trigger character it was told about, and those two open a TeX
316
+ // command / a `\begin{...}` environment name inside a forwarded
317
+ // math or LaTeX region.
318
+ const childCompletion = childCapabilities?.completionProvider;
319
+ const triggerCharacters = [
320
+ ...new Set([
321
+ ...(childCompletion?.triggerCharacters ?? []),
322
+ '\\',
323
+ '{',
324
+ ]),
325
+ ];
326
+ return {
327
+ capabilities: {
328
+ // Native — handled here, or via the math / TexLab
329
+ // children; advertised unconditionally.
330
+ textDocumentSync: TextDocumentSyncKind.Full,
331
+ hoverProvider: true,
332
+ completionProvider: {
333
+ ...(childCompletion ?? {}),
334
+ triggerCharacters,
335
+ },
336
+ documentSymbolProvider: true,
337
+ foldingRangeProvider: true,
338
+ selectionRangeProvider: true,
339
+ // Proxied — forwarded to `svelte-language-server`; each is
340
+ // advertised only if that child advertises it.
341
+ ...pickDefined(childCapabilities, [
342
+ 'definitionProvider',
343
+ 'referencesProvider',
344
+ 'documentHighlightProvider',
345
+ 'signatureHelpProvider',
346
+ 'renameProvider',
347
+ 'codeActionProvider',
348
+ 'documentLinkProvider',
349
+ ]),
350
+ },
351
+ serverInfo: {
352
+ name: 'sveltex-language-server',
353
+ },
354
+ };
355
+ });
356
+ /**
357
+ * The config-reload pump: drains pending reload requests one at a time,
358
+ * re-pointing the region forwarder at each fresh snapshot.
359
+ *
360
+ * {@link loadConfigSnapshot} spawns a child process, so reloads are run
361
+ * strictly sequentially — `configReloadInFlight` keeps only one pump (and
362
+ * thus one child process) alive at a time, and requests that arrive
363
+ * mid-reload are coalesced into a single trailing pass via
364
+ * `configReloadQueued`.
365
+ */
366
+ async function runConfigReloadPump() {
367
+ try {
368
+ while (configReloadQueued) {
369
+ configReloadQueued = false;
370
+ const root = workspaceRoot;
371
+ if (!root)
372
+ break;
373
+ config = await loadConfigSnapshot(root, logInfo);
374
+ regionForwarder.updateConfig(config);
375
+ }
376
+ }
377
+ finally {
378
+ configReloadInFlight = false;
379
+ }
380
+ }
381
+ /**
382
+ * Debounced entry point for a config reload: a single editor save can emit
383
+ * several watch events in quick succession (atomic write-and-rename, …),
384
+ * and they collapse into one reload once the file stops changing. The
385
+ * debounced callback then kicks {@link runConfigReloadPump}, unless one is
386
+ * already running.
387
+ */
388
+ function scheduleConfigReload() {
389
+ if (configReloadTimer)
390
+ clearTimeout(configReloadTimer);
391
+ // 200 ms: long enough to absorb a save's burst of events, short
392
+ // enough to still feel immediate.
393
+ configReloadTimer = setTimeout(() => {
394
+ configReloadTimer = undefined;
395
+ configReloadQueued = true;
396
+ if (configReloadInFlight)
397
+ return;
398
+ configReloadInFlight = true;
399
+ void runConfigReloadPump();
400
+ }, 200);
401
+ // The debounce timer must not by itself keep the process alive.
402
+ configReloadTimer.unref();
403
+ }
404
+ // A watched `svelte.config.*` (or a `sveltex.config.*` it imports)
405
+ // changed: schedule a debounced reload so region detection and TexLab
406
+ // forwarding pick the new settings up without an LSP restart.
407
+ connection.onDidChangeWatchedFiles(() => {
408
+ if (!workspaceRoot)
409
+ return;
410
+ scheduleConfigReload();
411
+ });
412
+ connection.onShutdown(async () => {
413
+ if (configReloadTimer)
414
+ clearTimeout(configReloadTimer);
415
+ await Promise.all([proxy.stop(), regionForwarder.stop()]);
416
+ });
417
+ // ----- document synchronization -----------------------------------------
418
+ connection.onDidOpenTextDocument((params) => {
419
+ const { uri, version, text } = params.textDocument;
420
+ if (!isSveltexUri(uri))
421
+ return;
422
+ const doc = rebuild(uri, text, version);
423
+ void proxyDidOpen(doc);
424
+ });
425
+ connection.onDidChangeTextDocument((params) => {
426
+ const uri = params.textDocument.uri;
427
+ if (!isSveltexUri(uri))
428
+ return;
429
+ if (!documents.get(uri))
430
+ return;
431
+ // The client uses Full sync (we advertised it), so the last change
432
+ // entry holds the complete new text.
433
+ const last = params.contentChanges.at(-1);
434
+ if (!last || !('text' in last))
435
+ return;
436
+ // Re-parse synchronously. The editor requests completion on the
437
+ // very `\` (or `{`) it just inserted — i.e. immediately after this
438
+ // `didChange`. Debouncing the re-parse would leave that request
439
+ // working off stale regions and a stale source map while the caret
440
+ // is already past them, so the position mis-maps and the forwarded
441
+ // request silently returns nothing. `computeRegions` is cheap
442
+ // enough to run per keystroke, and `svelte-language-server`
443
+ // debounces its own (heavier) analysis downstream.
444
+ const doc = rebuild(uri, last.text, params.textDocument.version);
445
+ void proxyDidChange(doc);
446
+ });
447
+ connection.onDidCloseTextDocument((params) => {
448
+ const uri = params.textDocument.uri;
449
+ if (!isSveltexUri(uri))
450
+ return;
451
+ documents.delete(uri);
452
+ void proxyDidClose(uri);
453
+ });
454
+ // ----- proxied, position-mapped language features -----------------------
455
+ connection.onHover(async (params) => {
456
+ // A hover inside a non-delegated region (math, LaTeX verbatim) is
457
+ // handled by a dedicated child server, not the Svelte proxy.
458
+ const doc = documents.get(params.textDocument.uri);
459
+ if (doc) {
460
+ const region = regionAt(doc, params.position);
461
+ if (region && isForwardableRegion(doc, region)) {
462
+ return regionForwarder.forwardHover(doc.text, doc.uri, region, params.position);
463
+ }
464
+ // Frontmatter is non-delegated — the Svelte child never sees it —
465
+ // so its keys are documented natively.
466
+ if (region?.kind === 'frontmatter') {
467
+ return computeFrontmatterHover(doc.text, params.position);
468
+ }
469
+ }
470
+ const proxied = await proxyPositionRequest('textDocument/hover', params);
471
+ if (!proxied)
472
+ return null;
473
+ return remapHover(proxied.result, proxied.ctx);
474
+ });
475
+ connection.onCompletion(async (params) => {
476
+ const doc = documents.get(params.textDocument.uri);
477
+ if (doc) {
478
+ const region = regionAt(doc, params.position);
479
+ if (region && isForwardableRegion(doc, region)) {
480
+ return markNativeCompletion(await regionForwarder.forwardCompletion(doc.text, doc.uri, region, params.position));
481
+ }
482
+ // Frontmatter is non-delegated — suggest its keys/values natively.
483
+ if (region?.kind === 'frontmatter') {
484
+ return markNativeCompletion(computeFrontmatterCompletion(doc.text, params.position));
485
+ }
486
+ }
487
+ const proxied = await proxyPositionRequest('textDocument/completion', params);
488
+ if (!proxied)
489
+ return null;
490
+ return remapCompletion(proxied.result, proxied.ctx);
491
+ });
492
+ // A `completionItem/resolve` goes back to whichever server produced the
493
+ // item. Items this server makes itself — region forwards (TexLab, the
494
+ // math server) and native frontmatter completion — are already complete,
495
+ // so they are returned unchanged; only genuine Svelte-proxy items are
496
+ // resolved by the embedded child. Forwarding a foreign item to the Svelte
497
+ // server instead makes it error on a document it never opened.
498
+ connection.onCompletionResolve(async (item) => {
499
+ if (isNativeCompletionItem(item))
500
+ return item;
501
+ if (!proxy.isRunning)
502
+ return item;
503
+ return proxy.sendRequest('completionItem/resolve', item);
504
+ });
505
+ connection.onDefinition(async (params) => {
506
+ const proxied = await proxyPositionRequest('textDocument/definition', params);
507
+ if (!proxied)
508
+ return null;
509
+ return remapDefinition(proxied.result, proxied.ctx);
510
+ });
511
+ connection.onReferences(async (params) => {
512
+ const proxied = await proxyPositionRequest('textDocument/references', params);
513
+ if (!proxied)
514
+ return null;
515
+ return remapReferences(proxied.result, proxied.ctx);
516
+ });
517
+ connection.onDocumentHighlight(async (params) => {
518
+ const proxied = await proxyPositionRequest('textDocument/documentHighlight', params);
519
+ if (!proxied)
520
+ return null;
521
+ return remapHighlights(proxied.result, proxied.ctx);
522
+ });
523
+ connection.onSignatureHelp(async (params) => {
524
+ const proxied = await proxyPositionRequest('textDocument/signatureHelp', params);
525
+ if (!proxied)
526
+ return null;
527
+ return remapSignatureHelp(proxied.result);
528
+ });
529
+ connection.onRenameRequest(async (params) => {
530
+ const proxied = await proxyPositionRequest('textDocument/rename', params);
531
+ if (!proxied)
532
+ return null;
533
+ return remapWorkspaceEdit(proxied.result, proxied.ctx);
534
+ });
535
+ connection.onPrepareRename(async (params) => {
536
+ const proxied = await proxyPositionRequest('textDocument/prepareRename', params);
537
+ if (!proxied || !proxied.result)
538
+ return null;
539
+ // `prepareRename` may return a bare `Range` or a `{ range, placeholder }`
540
+ // object; only the `Range` case needs mapping. The bare-range case is
541
+ // the one `svelte-language-server` returns.
542
+ return (proxied.ctx.sourceMap.generatedRangeToSource(proxied.result) ?? null);
543
+ });
544
+ connection.onCodeAction(async (params) => {
545
+ const doc = documents.get(params.textDocument.uri);
546
+ if (!doc || !proxy.isRunning)
547
+ return null;
548
+ const ctx = remapContext(doc);
549
+ const range = ctx.sourceMap.sourceRangeToGenerated(params.range);
550
+ // A code action requested over a non-delegated region has nothing the
551
+ // embedded server can offer.
552
+ if (!range)
553
+ return null;
554
+ const generatedParams = {
555
+ ...params,
556
+ textDocument: { uri: ctx.virtualUri },
557
+ range,
558
+ context: {
559
+ ...params.context,
560
+ // Diagnostics in the request context are in source
561
+ // coordinates; map the ones that fall in delegated regions.
562
+ diagnostics: params.context.diagnostics
563
+ .map((diag) => {
564
+ const mapped = ctx.sourceMap.sourceRangeToGenerated(diag.range);
565
+ if (!mapped)
566
+ return undefined;
567
+ return { ...diag, range: mapped };
568
+ })
569
+ .filter((d) => Boolean(d)),
570
+ },
571
+ };
572
+ const result = await proxy.sendRequest('textDocument/codeAction', generatedParams);
573
+ return remapCodeActions(result, ctx);
574
+ });
575
+ connection.onDocumentLinks(async (params) => {
576
+ const doc = documents.get(params.textDocument.uri);
577
+ if (!doc || !proxy.isRunning)
578
+ return null;
579
+ const ctx = remapContext(doc);
580
+ const result = await proxy.sendRequest('textDocument/documentLink', {
581
+ textDocument: { uri: ctx.virtualUri },
582
+ });
583
+ return remapDocumentLinks(result, ctx);
584
+ });
585
+ // ----- native Markdown features (no proxy, no mapping) ------------------
586
+ connection.onDocumentSymbol((params) => {
587
+ const doc = documents.get(params.textDocument.uri);
588
+ if (!doc)
589
+ return null;
590
+ return computeDocumentSymbols(doc.text, config);
591
+ });
592
+ connection.onFoldingRanges((params) => {
593
+ const doc = documents.get(params.textDocument.uri);
594
+ if (!doc)
595
+ return null;
596
+ return computeFoldingRanges(doc.text, config);
597
+ });
598
+ connection.onSelectionRanges((params) => {
599
+ const doc = documents.get(params.textDocument.uri);
600
+ if (!doc)
601
+ return null;
602
+ return computeSelectionRanges(doc.text, params.positions, config);
603
+ });
604
+ }