@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,56 @@
1
+ import type { SveltexConfigSnapshot } from './config.js';
2
+ /**
3
+ * The kind of a {@link Region}.
4
+ *
5
+ * - `markdown`: Plain Markdown / HTML. Delegated to the Svelte language server
6
+ * (which understands the HTML subset) and additionally analyzed natively for
7
+ * Markdown-specific features (folding, symbols, selection ranges).
8
+ * - `svelte`: Svelte markup that is _not_ a mustache tag — `<script>`,
9
+ * `<style>`, `<svelte:*>` elements, logic blocks (`{#if}`/`{/if}`/...) and
10
+ * special tags (`{@const}`, `{@debug}`, ...). Delegated verbatim.
11
+ * - `mustacheTag`: A Svelte mustache tag, e.g. `{name}`. Delegated verbatim.
12
+ * - `code`: A fenced code block or inline code span. _Not_ delegated.
13
+ * - `math`: Inline or display math (`$...$`, `$$...$$`, `\(...\)`, `\[...\]`).
14
+ * _Not_ delegated.
15
+ * - `verbatim`: A SvelTeX verbatim environment (`<tex>`, `<verbatim>`, ...).
16
+ * _Not_ delegated.
17
+ * - `frontmatter`: A YAML / TOML / JSON frontmatter block. _Not_ delegated.
18
+ */
19
+ export type RegionKind = 'markdown' | 'svelte' | 'mustacheTag' | 'code' | 'math' | 'verbatim' | 'frontmatter';
20
+ /**
21
+ * A contiguous slice of a `.sveltex` document, classified by {@link RegionKind}.
22
+ *
23
+ * `Region`s returned by {@link computeRegions} tile the entire document
24
+ * gap-free: `regions[i].sourceEnd === regions[i + 1].sourceStart`, the first
25
+ * region starts at offset `0`, and the last ends at `document.length`.
26
+ */
27
+ export interface Region {
28
+ /** What kind of content this region holds. */
29
+ kind: RegionKind;
30
+ /** Offset of the first character of the region (inclusive). */
31
+ sourceStart: number;
32
+ /** Offset one past the last character of the region (exclusive). */
33
+ sourceEnd: number;
34
+ }
35
+ /**
36
+ * Returns whether a region of the given kind should be delegated to the
37
+ * embedded Svelte language server.
38
+ */
39
+ export declare function isDelegated(kind: RegionKind): boolean;
40
+ /**
41
+ * Parses a `.sveltex` document and returns a gap-free, sorted list of
42
+ * {@link Region}s.
43
+ *
44
+ * @param document - The full text of the `.sveltex` file.
45
+ * @param config - A snapshot of the resolved `sveltex.config.*` (verbatim tags,
46
+ * math delimiters, directive settings).
47
+ * @returns The document's regions, tiling `[0, document.length)` with no gaps
48
+ * and no overlaps.
49
+ *
50
+ * @remarks
51
+ * The heavy lifting is done by SvelTeX's own exported detectors
52
+ * (`parseToMdast`, `getMdastES`, `getSvelteES`, `getMathInSpecialDelimsES`,
53
+ * `getColonES`) plus `outermostRanges` to discard nested matches. Anything not
54
+ * covered by a detected snippet is emitted as a `markdown` region.
55
+ */
56
+ export declare function computeRegions(document: string, config: SveltexConfigSnapshot): Region[];
@@ -0,0 +1,221 @@
1
+ // File description: Splits a `.sveltex` document into a flat, gap-free array of
2
+ // `Region`s by reusing SvelTeX's own region-detection building blocks.
3
+ //
4
+ // SvelTeX's preprocessor produces no usable source map for markup (see the
5
+ // package README), so the LSP must reconstruct the region structure itself.
6
+ // Fortunately `@nvl/sveltex` exports the precise, offset-bearing detectors it
7
+ // uses internally. We call those and classify each detected snippet as either
8
+ // "delegated" (handed to the embedded Svelte language server) or "non-delegated"
9
+ // (verbatim / code / math / frontmatter — blanked out before delegation).
10
+ //
11
+ // TODO: upstream a public `getRegions()` into `@nvl/sveltex` so that this file
12
+ // can stop importing from the `dist/` deep path below.
13
+ import { getColonES, getMathInSpecialDelimsES, getMdastES, getSvelteES, outermostRanges, parseToMdast, } from '@nvl/sveltex/dist/utils/escape.js';
14
+ /**
15
+ * `RegionKind`s whose contents are forwarded verbatim to the embedded Svelte
16
+ * language server. Everything else is blanked out in the virtual document.
17
+ */
18
+ const DELEGATED_KINDS = new Set([
19
+ 'markdown',
20
+ 'svelte',
21
+ 'mustacheTag',
22
+ ]);
23
+ /**
24
+ * Returns whether a region of the given kind should be delegated to the
25
+ * embedded Svelte language server.
26
+ */
27
+ export function isDelegated(kind) {
28
+ return DELEGATED_KINDS.has(kind);
29
+ }
30
+ /**
31
+ * Maps a SvelTeX snippet `type` to the LSP's {@link RegionKind}.
32
+ *
33
+ * SvelTeX snippet types are `code | math | svelte | mustacheTag | verbatim |
34
+ * frontmatter`, which line up one-to-one with our region kinds (none of them is
35
+ * `markdown` — plain Markdown is whatever is left over).
36
+ */
37
+ function snippetTypeToRegionKind(type) {
38
+ switch (type) {
39
+ case 'code':
40
+ case 'math':
41
+ case 'svelte':
42
+ case 'mustacheTag':
43
+ case 'verbatim':
44
+ case 'frontmatter':
45
+ return type;
46
+ default:
47
+ // Defensive: an unknown snippet type is treated as opaque verbatim
48
+ // so that it is never mistakenly delegated.
49
+ return 'verbatim';
50
+ }
51
+ }
52
+ /**
53
+ * Detects SvelTeX verbatim environments (`<tex>...</tex>`, `<verbatim/>`, ...).
54
+ *
55
+ * SvelTeX's `getVerbatimES` is _not_ part of the package's public surface, so
56
+ * we re-derive the (simple) detection here: a verbatim environment is just an
57
+ * HTML-like element whose tag name is one of the configured verbatim tags. This
58
+ * intentionally mirrors the regexes in `@nvl/sveltex`'s `escape.ts`.
59
+ *
60
+ * TODO: replace with `getVerbatimES` once `@nvl/sveltex` exports it.
61
+ */
62
+ function detectVerbatimRanges(document, verbatimTags) {
63
+ const ranges = [];
64
+ if (verbatimTags.length === 0)
65
+ return ranges;
66
+ const alternation = verbatimTags
67
+ .map((t) => t.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'))
68
+ .join('|');
69
+ // Matches `<tag ...>...</tag>` (with lazy inner content) or `<tag ... />`.
70
+ // `s` flag: `.` spans newlines. `u` flag is required by the repo's lint.
71
+ const re = new RegExp(`<(${alternation})(?:\\s[^>]*?)?(?:/>|>.*?</\\s*\\1\\s*>)`, 'gisu');
72
+ for (const match of document.matchAll(re)) {
73
+ const start = match.index;
74
+ ranges.push({
75
+ kind: 'verbatim',
76
+ start,
77
+ end: start + match[0].length,
78
+ });
79
+ }
80
+ return ranges;
81
+ }
82
+ /**
83
+ * Returns a copy of `document` with every character inside one of `ranges`
84
+ * replaced by a space; newline characters are kept, so offsets _and_ line
85
+ * numbers are preserved.
86
+ *
87
+ * It is used to hide already-classified snippets from the verbatim-tag scan:
88
+ * a `<tex>` written inside an inline-code span (`` `<tex>` ``), a string in a
89
+ * `<script>`, etc. must not be mistaken for the opening of a verbatim
90
+ * environment — otherwise its match runs greedily to the next, unrelated
91
+ * `</tex>` and swallows the real block in between.
92
+ */
93
+ function maskRanges(document, ranges) {
94
+ if (ranges.length === 0)
95
+ return document;
96
+ const chars = document.split('');
97
+ for (const range of ranges) {
98
+ const end = Math.min(range.end, chars.length);
99
+ for (let i = Math.max(0, range.start); i < end; i += 1) {
100
+ const ch = chars[i];
101
+ if (ch !== '\n' && ch !== '\r')
102
+ chars[i] = ' ';
103
+ }
104
+ }
105
+ return chars.join('');
106
+ }
107
+ /**
108
+ * Parses a `.sveltex` document and returns a gap-free, sorted list of
109
+ * {@link Region}s.
110
+ *
111
+ * @param document - The full text of the `.sveltex` file.
112
+ * @param config - A snapshot of the resolved `sveltex.config.*` (verbatim tags,
113
+ * math delimiters, directive settings).
114
+ * @returns The document's regions, tiling `[0, document.length)` with no gaps
115
+ * and no overlaps.
116
+ *
117
+ * @remarks
118
+ * The heavy lifting is done by SvelTeX's own exported detectors
119
+ * (`parseToMdast`, `getMdastES`, `getSvelteES`, `getMathInSpecialDelimsES`,
120
+ * `getColonES`) plus `outermostRanges` to discard nested matches. Anything not
121
+ * covered by a detected snippet is emitted as a `markdown` region.
122
+ */
123
+ export function computeRegions(document, config) {
124
+ const tagged = [];
125
+ try {
126
+ const verbatimTags = config.verbatimTags;
127
+ const mdastTags = [...verbatimTags, 'script', 'style'];
128
+ const ast = parseToMdast(document, mdastTags, config.mathDelims, config.directives);
129
+ const lines = document.split(/\r\n?|\n/u);
130
+ const snippets = [
131
+ ...getMdastES({
132
+ ast,
133
+ document,
134
+ lines,
135
+ texSettings: config.mathDelims,
136
+ directiveSettings: config.directives,
137
+ }),
138
+ ...getSvelteES(document),
139
+ ...getMathInSpecialDelimsES(document, config.mathDelims),
140
+ ];
141
+ // `getColonES` reports the individual `:` characters inside special
142
+ // Svelte elements (`<svelte:head>` etc.). Those colons live inside
143
+ // `svelte`/`markdown` regions already, so they need no separate region;
144
+ // we only consult `getColonES` to stay forward-compatible and ignore
145
+ // its output here.
146
+ void getColonES;
147
+ for (const snippet of outermostRanges([...snippets], 'original.loc')) {
148
+ tagged.push({
149
+ kind: snippetTypeToRegionKind(snippet.type),
150
+ start: snippet.original.loc.start,
151
+ end: snippet.original.loc.end,
152
+ });
153
+ }
154
+ // Scan for verbatim environments over a copy of the document with
155
+ // every snippet found above blanked out. A bare regex scan of the raw
156
+ // document would anchor on a `<tex>` that is really inside an
157
+ // inline-code span and pair it with a *later*, unrelated `</tex>` —
158
+ // swallowing the genuine verbatim block in between.
159
+ tagged.push(...detectVerbatimRanges(maskRanges(document, tagged), verbatimTags));
160
+ }
161
+ catch {
162
+ // If SvelTeX's parser throws (malformed input mid-edit is common), fall
163
+ // back to treating the whole document as delegated Markdown. The Svelte
164
+ // language server is itself resilient to partial input.
165
+ return [
166
+ { kind: 'markdown', sourceStart: 0, sourceEnd: document.length },
167
+ ];
168
+ }
169
+ return fillGaps(tagged, document.length);
170
+ }
171
+ /**
172
+ * Sorts tagged ranges, drops overlaps, and fills the holes between them with
173
+ * `markdown` regions so that the result tiles `[0, length)` exactly.
174
+ */
175
+ function fillGaps(tagged, length) {
176
+ // `outermostRanges` already removed nesting among the mdast/svelte snippets,
177
+ // but verbatim ranges were appended afterwards and could overlap a snippet
178
+ // (e.g. a verbatim tag that also looks like an HTML element). Re-run the
179
+ // outermost filter over the combined set, sorted by start offset.
180
+ const sorted = [...tagged].sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - a.end);
181
+ const regions = [];
182
+ let cursor = 0;
183
+ for (const range of sorted) {
184
+ // Skip ranges that overlap something we already emitted.
185
+ if (range.start < cursor)
186
+ continue;
187
+ if (range.start >= length)
188
+ break;
189
+ const end = Math.min(range.end, length);
190
+ if (end <= range.start)
191
+ continue;
192
+ // Fill the gap before this range with plain Markdown.
193
+ if (range.start > cursor) {
194
+ regions.push({
195
+ kind: 'markdown',
196
+ sourceStart: cursor,
197
+ sourceEnd: range.start,
198
+ });
199
+ }
200
+ regions.push({
201
+ kind: range.kind,
202
+ sourceStart: range.start,
203
+ sourceEnd: end,
204
+ });
205
+ cursor = end;
206
+ }
207
+ // Trailing Markdown.
208
+ if (cursor < length) {
209
+ regions.push({
210
+ kind: 'markdown',
211
+ sourceStart: cursor,
212
+ sourceEnd: length,
213
+ });
214
+ }
215
+ // An empty document still yields one (empty) Markdown region so that
216
+ // downstream consumers never have to special-case `regions.length === 0`.
217
+ if (regions.length === 0) {
218
+ regions.push({ kind: 'markdown', sourceStart: 0, sourceEnd: length });
219
+ }
220
+ return regions;
221
+ }
@@ -0,0 +1,84 @@
1
+ import type { CodeAction, Command, CompletionItem, CompletionList, Definition, DefinitionLink, DocumentHighlight, DocumentLink, Hover, Location, Range, SignatureHelp, WorkspaceEdit } from 'vscode-languageserver-protocol';
2
+ import type { SourceMap } from './mapper.js';
3
+ /**
4
+ * Bundles the two URIs and the source map for one open `.sveltex` file, so the
5
+ * remapping helpers have everything they need in a single argument.
6
+ */
7
+ export interface RemapContext {
8
+ /** The real `.sveltex` document URI as seen by the editor. */
9
+ sourceUri: string;
10
+ /** The synthetic `<source>.svelte` URI handed to the embedded server. */
11
+ virtualUri: string;
12
+ /** Bidirectional mapper between the two documents. */
13
+ sourceMap: SourceMap;
14
+ }
15
+ /**
16
+ * Derives the virtual `.svelte` URI for a given `.sveltex` source URI.
17
+ *
18
+ * Appending `.svelte` (rather than replacing the extension) keeps the original
19
+ * name recoverable and makes the embedded TypeScript service treat the file as
20
+ * Svelte.
21
+ */
22
+ export declare function toVirtualUri(sourceUri: string): string;
23
+ /** Inverse of {@link toVirtualUri}. */
24
+ export declare function toSourceUri(virtualUri: string): string;
25
+ /**
26
+ * Remaps the result of a definition / declaration / type-definition /
27
+ * implementation request.
28
+ */
29
+ export declare function remapDefinition(result: Definition | DefinitionLink[] | null | undefined, ctx: RemapContext): Definition | DefinitionLink[] | null;
30
+ /**
31
+ * Remaps the result of a find-references request.
32
+ */
33
+ export declare function remapReferences(result: Location[] | null | undefined, ctx: RemapContext): Location[] | null;
34
+ /**
35
+ * Remaps the result of a document-highlight request. Highlights always refer to
36
+ * the requested document, so any highlight that fails to map is dropped.
37
+ */
38
+ export declare function remapHighlights(result: DocumentHighlight[] | null | undefined, ctx: RemapContext): DocumentHighlight[] | null;
39
+ /**
40
+ * Remaps a hover result. The optional `range` is mapped; if it fails to map the
41
+ * hover is dropped entirely (its contents describe code that, from the user's
42
+ * point of view, is not at the hovered spot).
43
+ */
44
+ export declare function remapHover(result: Hover | null | undefined, ctx: RemapContext): Hover | null;
45
+ /**
46
+ * Remaps a completion result (either a bare item array or a `CompletionList`).
47
+ */
48
+ export declare function remapCompletion(result: CompletionItem[] | CompletionList | null | undefined, ctx: RemapContext): CompletionItem[] | CompletionList | null;
49
+ /**
50
+ * Remaps a `WorkspaceEdit` (the result of a rename). Only the `changes` keyed
51
+ * by the virtual URI are rewritten; edits to other files pass through.
52
+ *
53
+ * @remarks
54
+ * `documentChanges` (the versioned form) is intentionally _not_ remapped in
55
+ * v1: `svelte-language-server`'s rename returns the `changes` form, and
56
+ * versioned edits would also require tracking the source document's version.
57
+ *
58
+ * TODO: support `documentChanges` once incremental virtual updates land.
59
+ */
60
+ export declare function remapWorkspaceEdit(result: WorkspaceEdit | null | undefined, ctx: RemapContext): WorkspaceEdit | null;
61
+ /**
62
+ * Remaps a code-action result. Each action may carry an inline `WorkspaceEdit`
63
+ * and a list of `diagnostics`; both are rewritten. Bare `Command`s have no
64
+ * positions and pass through unchanged.
65
+ */
66
+ export declare function remapCodeActions(result: (Command | CodeAction)[] | null | undefined, ctx: RemapContext): (Command | CodeAction)[] | null;
67
+ /**
68
+ * Remaps a signature-help result. `SignatureHelp` carries no document ranges,
69
+ * so it is returned unchanged; the function exists to keep the proxy call sites
70
+ * uniform and to provide an obvious hook should a future LSP version add
71
+ * positional data.
72
+ */
73
+ export declare function remapSignatureHelp(result: SignatureHelp | null | undefined): SignatureHelp | null;
74
+ /**
75
+ * Remaps document links: each link's `range` is in generated coordinates.
76
+ */
77
+ export declare function remapDocumentLinks(result: DocumentLink[] | null | undefined, ctx: RemapContext): DocumentLink[] | null;
78
+ /**
79
+ * Maps a `Range` from source to generated coordinates, for request payloads
80
+ * (e.g. the `range` of a code-action request).
81
+ *
82
+ * @returns The generated range, or `undefined` if it is not fully mapped.
83
+ */
84
+ export declare function sourceRangeToGenerated(range: Range, ctx: RemapContext): Range | undefined;
@@ -0,0 +1,272 @@
1
+ // File description: Pure helpers that rewrite the position-bearing parts of LSP
2
+ // payloads between source (`.sveltex`) and generated (`.svelte`) coordinates.
3
+ //
4
+ // The host server holds, per open file, exactly one virtual `.svelte` document
5
+ // (see `virtual-svelte.ts`). Translating a request therefore means: rewrite the
6
+ // source URI to the virtual URI and map the request position source -> generated;
7
+ // translating a response means the reverse, plus dropping anything that maps
8
+ // into a non-delegated (unmapped) region.
9
+ /**
10
+ * Derives the virtual `.svelte` URI for a given `.sveltex` source URI.
11
+ *
12
+ * Appending `.svelte` (rather than replacing the extension) keeps the original
13
+ * name recoverable and makes the embedded TypeScript service treat the file as
14
+ * Svelte.
15
+ */
16
+ export function toVirtualUri(sourceUri) {
17
+ return `${sourceUri}.svelte`;
18
+ }
19
+ /** Inverse of {@link toVirtualUri}. */
20
+ export function toSourceUri(virtualUri) {
21
+ return virtualUri.endsWith('.svelte')
22
+ ? virtualUri.slice(0, -'.svelte'.length)
23
+ : virtualUri;
24
+ }
25
+ /**
26
+ * Rewrites a `Location` from generated to source coordinates.
27
+ *
28
+ * @returns The remapped location, or `undefined` if it does not belong to this
29
+ * document's virtual file or maps into an unmapped region.
30
+ */
31
+ function remapLocation(location, ctx) {
32
+ if (location.uri !== ctx.virtualUri) {
33
+ // A location in some other file (e.g. a `node_modules` `.d.ts`) needs
34
+ // no mapping — pass it through untouched.
35
+ return location;
36
+ }
37
+ const range = ctx.sourceMap.generatedRangeToSource(location.range);
38
+ if (!range)
39
+ return undefined;
40
+ return { uri: ctx.sourceUri, range };
41
+ }
42
+ /**
43
+ * Rewrites a `LocationLink` from generated to source coordinates.
44
+ */
45
+ function remapLocationLink(link, ctx) {
46
+ if (link.targetUri !== ctx.virtualUri)
47
+ return link;
48
+ const targetRange = ctx.sourceMap.generatedRangeToSource(link.targetRange);
49
+ const targetSelectionRange = ctx.sourceMap.generatedRangeToSource(link.targetSelectionRange);
50
+ if (!targetRange || !targetSelectionRange)
51
+ return undefined;
52
+ const originSelectionRange = link.originSelectionRange
53
+ ? ctx.sourceMap.generatedRangeToSource(link.originSelectionRange)
54
+ : undefined;
55
+ return {
56
+ ...link,
57
+ targetUri: ctx.sourceUri,
58
+ targetRange,
59
+ targetSelectionRange,
60
+ ...(originSelectionRange ? { originSelectionRange } : {}),
61
+ };
62
+ }
63
+ /**
64
+ * Remaps the result of a definition / declaration / type-definition /
65
+ * implementation request.
66
+ */
67
+ export function remapDefinition(result, ctx) {
68
+ if (!result)
69
+ return null;
70
+ if (Array.isArray(result)) {
71
+ // Either `Location[]` or `LocationLink[]`; both are handled per-item.
72
+ const out = result
73
+ .map((item) => 'targetUri' in item
74
+ ? remapLocationLink(item, ctx)
75
+ : remapLocation(item, ctx))
76
+ .filter((item) => Boolean(item));
77
+ return out;
78
+ }
79
+ return remapLocation(result, ctx) ?? null;
80
+ }
81
+ /**
82
+ * Remaps the result of a find-references request.
83
+ */
84
+ export function remapReferences(result, ctx) {
85
+ if (!result)
86
+ return null;
87
+ return result
88
+ .map((loc) => remapLocation(loc, ctx))
89
+ .filter((loc) => Boolean(loc));
90
+ }
91
+ /**
92
+ * Remaps the result of a document-highlight request. Highlights always refer to
93
+ * the requested document, so any highlight that fails to map is dropped.
94
+ */
95
+ export function remapHighlights(result, ctx) {
96
+ if (!result)
97
+ return null;
98
+ return result
99
+ .map((highlight) => {
100
+ const range = ctx.sourceMap.generatedRangeToSource(highlight.range);
101
+ if (!range)
102
+ return undefined;
103
+ return { ...highlight, range };
104
+ })
105
+ .filter((h) => Boolean(h));
106
+ }
107
+ /**
108
+ * Remaps a hover result. The optional `range` is mapped; if it fails to map the
109
+ * hover is dropped entirely (its contents describe code that, from the user's
110
+ * point of view, is not at the hovered spot).
111
+ */
112
+ export function remapHover(result, ctx) {
113
+ if (!result)
114
+ return null;
115
+ if (!result.range)
116
+ return result;
117
+ const range = ctx.sourceMap.generatedRangeToSource(result.range);
118
+ if (!range)
119
+ return null;
120
+ return { ...result, range };
121
+ }
122
+ /**
123
+ * Remaps an array of `TextEdit`s, dropping edits whose range does not map.
124
+ */
125
+ function remapTextEdits(edits, ctx) {
126
+ return edits
127
+ .map((edit) => {
128
+ const range = ctx.sourceMap.generatedRangeToSource(edit.range);
129
+ if (!range)
130
+ return undefined;
131
+ return { ...edit, range };
132
+ })
133
+ .filter((edit) => Boolean(edit));
134
+ }
135
+ /**
136
+ * Remaps a single completion item: its `textEdit` and `additionalTextEdits`
137
+ * carry ranges in generated coordinates.
138
+ *
139
+ * @returns The remapped item, or `undefined` if its primary `textEdit` range
140
+ * cannot be mapped (applying it would corrupt the source document).
141
+ */
142
+ function remapCompletionItem(item, ctx) {
143
+ const next = { ...item };
144
+ if (item.textEdit) {
145
+ if ('range' in item.textEdit) {
146
+ const range = ctx.sourceMap.generatedRangeToSource(item.textEdit.range);
147
+ if (!range)
148
+ return undefined;
149
+ next.textEdit = { ...item.textEdit, range };
150
+ }
151
+ else {
152
+ // `InsertReplaceEdit`: map both ranges.
153
+ const insert = ctx.sourceMap.generatedRangeToSource(item.textEdit.insert);
154
+ const replace = ctx.sourceMap.generatedRangeToSource(item.textEdit.replace);
155
+ if (!insert || !replace)
156
+ return undefined;
157
+ next.textEdit = { ...item.textEdit, insert, replace };
158
+ }
159
+ }
160
+ if (item.additionalTextEdits) {
161
+ next.additionalTextEdits = remapTextEdits(item.additionalTextEdits, ctx);
162
+ }
163
+ return next;
164
+ }
165
+ /**
166
+ * Remaps a completion result (either a bare item array or a `CompletionList`).
167
+ */
168
+ export function remapCompletion(result, ctx) {
169
+ if (!result)
170
+ return null;
171
+ const items = Array.isArray(result) ? result : result.items;
172
+ const remapped = items
173
+ .map((item) => remapCompletionItem(item, ctx))
174
+ .filter((item) => Boolean(item));
175
+ if (Array.isArray(result))
176
+ return remapped;
177
+ return { ...result, items: remapped };
178
+ }
179
+ /**
180
+ * Remaps a `WorkspaceEdit` (the result of a rename). Only the `changes` keyed
181
+ * by the virtual URI are rewritten; edits to other files pass through.
182
+ *
183
+ * @remarks
184
+ * `documentChanges` (the versioned form) is intentionally _not_ remapped in
185
+ * v1: `svelte-language-server`'s rename returns the `changes` form, and
186
+ * versioned edits would also require tracking the source document's version.
187
+ *
188
+ * TODO: support `documentChanges` once incremental virtual updates land.
189
+ */
190
+ export function remapWorkspaceEdit(result, ctx) {
191
+ if (!result)
192
+ return null;
193
+ if (!result.changes)
194
+ return result;
195
+ const changes = {};
196
+ for (const [uri, edits] of Object.entries(result.changes)) {
197
+ if (uri === ctx.virtualUri) {
198
+ changes[ctx.sourceUri] = remapTextEdits(edits, ctx);
199
+ }
200
+ else {
201
+ changes[uri] = edits;
202
+ }
203
+ }
204
+ return { ...result, changes };
205
+ }
206
+ /**
207
+ * Remaps a code-action result. Each action may carry an inline `WorkspaceEdit`
208
+ * and a list of `diagnostics`; both are rewritten. Bare `Command`s have no
209
+ * positions and pass through unchanged.
210
+ */
211
+ export function remapCodeActions(result, ctx) {
212
+ if (!result)
213
+ return null;
214
+ return result.map((entry) => {
215
+ // `CodeAction` carries `edit` / `diagnostics`; a bare `Command` has
216
+ // neither. Probing for those keys narrows `entry` to `CodeAction`
217
+ // without a cast (a `Command` is returned unchanged — it has no
218
+ // positions).
219
+ if (!('edit' in entry) && !('diagnostics' in entry)) {
220
+ return entry;
221
+ }
222
+ const action = entry;
223
+ const next = { ...action };
224
+ if (action.edit) {
225
+ next.edit = remapWorkspaceEdit(action.edit, ctx) ?? action.edit;
226
+ }
227
+ if (action.diagnostics) {
228
+ next.diagnostics = action.diagnostics
229
+ .map((diag) => {
230
+ const range = ctx.sourceMap.generatedRangeToSource(diag.range);
231
+ if (!range)
232
+ return undefined;
233
+ return { ...diag, range };
234
+ })
235
+ .filter((d) => Boolean(d));
236
+ }
237
+ return next;
238
+ });
239
+ }
240
+ /**
241
+ * Remaps a signature-help result. `SignatureHelp` carries no document ranges,
242
+ * so it is returned unchanged; the function exists to keep the proxy call sites
243
+ * uniform and to provide an obvious hook should a future LSP version add
244
+ * positional data.
245
+ */
246
+ export function remapSignatureHelp(result) {
247
+ return result ?? null;
248
+ }
249
+ /**
250
+ * Remaps document links: each link's `range` is in generated coordinates.
251
+ */
252
+ export function remapDocumentLinks(result, ctx) {
253
+ if (!result)
254
+ return null;
255
+ return result
256
+ .map((link) => {
257
+ const range = ctx.sourceMap.generatedRangeToSource(link.range);
258
+ if (!range)
259
+ return undefined;
260
+ return { ...link, range };
261
+ })
262
+ .filter((link) => Boolean(link));
263
+ }
264
+ /**
265
+ * Maps a `Range` from source to generated coordinates, for request payloads
266
+ * (e.g. the `range` of a code-action request).
267
+ *
268
+ * @returns The generated range, or `undefined` if it is not fully mapped.
269
+ */
270
+ export function sourceRangeToGenerated(range, ctx) {
271
+ return ctx.sourceMap.sourceRangeToGenerated(range);
272
+ }