@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.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/bin/server.js +10 -0
- package/dist/core/config.d.ts +126 -0
- package/dist/core/config.js +569 -0
- package/dist/core/diagnostics.d.ts +34 -0
- package/dist/core/diagnostics.js +67 -0
- package/dist/core/frontmatter-data.d.ts +74 -0
- package/dist/core/frontmatter-data.js +323 -0
- package/dist/core/frontmatter.d.ts +25 -0
- package/dist/core/frontmatter.js +348 -0
- package/dist/core/lsp-proxy.d.ts +77 -0
- package/dist/core/lsp-proxy.js +165 -0
- package/dist/core/mapper.d.ts +86 -0
- package/dist/core/mapper.js +223 -0
- package/dist/core/mapping.d.ts +59 -0
- package/dist/core/mapping.js +37 -0
- package/dist/core/markdown.d.ts +34 -0
- package/dist/core/markdown.js +215 -0
- package/dist/core/region-forwarding.d.ts +90 -0
- package/dist/core/region-forwarding.js +428 -0
- package/dist/core/region-virtual.d.ts +71 -0
- package/dist/core/region-virtual.js +131 -0
- package/dist/core/regions.d.ts +56 -0
- package/dist/core/regions.js +221 -0
- package/dist/core/remap.d.ts +84 -0
- package/dist/core/remap.js +272 -0
- package/dist/core/server-helpers.d.ts +109 -0
- package/dist/core/server-helpers.js +182 -0
- package/dist/core/server.d.ts +13 -0
- package/dist/core/server.js +604 -0
- package/dist/core/svelte-proxy.d.ts +100 -0
- package/dist/core/svelte-proxy.js +144 -0
- package/dist/core/texlab.d.ts +26 -0
- package/dist/core/texlab.js +121 -0
- package/dist/core/virtual-svelte.d.ts +32 -0
- package/dist/core/virtual-svelte.js +67 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +46 -0
- 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
|
+
}
|