@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,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
|
+
}
|