@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,223 @@
|
|
|
1
|
+
// File description: `SourceMap` — a bidirectional, offset-based mapper between
|
|
2
|
+
// the source `.sveltex` document and the generated virtual `.svelte` document.
|
|
3
|
+
//
|
|
4
|
+
// Geometry is expressed with `Mapping` offset-triples (see `mapping.ts`). LSP
|
|
5
|
+
// requests, however, speak in line/character `Position`s, so the mapper also
|
|
6
|
+
// holds a `TextDocument` view of each side purely to convert offsets <-> LSP
|
|
7
|
+
// positions. The two concerns are kept separate: all mapping math is done on
|
|
8
|
+
// integer offsets, and line/character conversion is a thin shell on top.
|
|
9
|
+
import { TextDocument, } from 'vscode-languageserver-textdocument';
|
|
10
|
+
/**
|
|
11
|
+
* Bidirectional mapper between a source document and its generated counterpart.
|
|
12
|
+
*
|
|
13
|
+
* Construct one per open `.sveltex` file (rebuilt on every debounced re-parse).
|
|
14
|
+
* The mappings must be sorted by `sourceOffset`; {@link SourceMap.create}
|
|
15
|
+
* enforces this so callers need not.
|
|
16
|
+
*/
|
|
17
|
+
export class SourceMap {
|
|
18
|
+
/** Mappings sorted ascending by `sourceOffset`. */
|
|
19
|
+
#bySource;
|
|
20
|
+
/** The same mappings sorted ascending by `generatedOffset`. */
|
|
21
|
+
#byGenerated;
|
|
22
|
+
/** Text-document view of the source `.sveltex` file. */
|
|
23
|
+
#sourceDoc;
|
|
24
|
+
/** Text-document view of the generated virtual `.svelte` file. */
|
|
25
|
+
#generatedDoc;
|
|
26
|
+
constructor(mappings, sourceText, generatedText) {
|
|
27
|
+
this.#bySource = [...mappings].sort((a, b) => a.sourceOffset - b.sourceOffset);
|
|
28
|
+
this.#byGenerated = [...mappings].sort((a, b) => a.generatedOffset - b.generatedOffset);
|
|
29
|
+
// The language id is irrelevant; these documents are used only for
|
|
30
|
+
// offset <-> position arithmetic.
|
|
31
|
+
this.#sourceDoc = TextDocument.create('mem://source', 'sveltex', 0, sourceText);
|
|
32
|
+
this.#generatedDoc = TextDocument.create('mem://generated', 'svelte', 0, generatedText);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Builds a {@link SourceMap}.
|
|
36
|
+
*
|
|
37
|
+
* @param mappings - The span pairs linking the two documents. Order does
|
|
38
|
+
* not matter; they are sorted internally.
|
|
39
|
+
* @param sourceText - Full text of the source `.sveltex` document.
|
|
40
|
+
* @param generatedText - Full text of the generated `.svelte` document.
|
|
41
|
+
*/
|
|
42
|
+
static create(mappings, sourceText, generatedText) {
|
|
43
|
+
return new SourceMap(mappings, sourceText, generatedText);
|
|
44
|
+
}
|
|
45
|
+
/** The generated virtual document's full text. */
|
|
46
|
+
get generatedText() {
|
|
47
|
+
return this.#generatedDoc.getText();
|
|
48
|
+
}
|
|
49
|
+
/** The source document's full text. */
|
|
50
|
+
get sourceText() {
|
|
51
|
+
return this.#sourceDoc.getText();
|
|
52
|
+
}
|
|
53
|
+
// ----- offset translation ------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Translates a source-document offset to a generated-document offset.
|
|
56
|
+
*
|
|
57
|
+
* @param sourceOffset - Offset in the `.sveltex` document.
|
|
58
|
+
* @returns The corresponding generated offset, or `undefined` if the offset
|
|
59
|
+
* falls outside every mapped span (i.e. inside a non-delegated region).
|
|
60
|
+
*/
|
|
61
|
+
sourceOffsetToGenerated(sourceOffset) {
|
|
62
|
+
const m = findContaining(this.#bySource, sourceOffset, (x) => x.sourceOffset, (x) => x.sourceLength);
|
|
63
|
+
if (!m)
|
|
64
|
+
return undefined;
|
|
65
|
+
return translate(sourceOffset, m.sourceOffset, m.sourceLength, m.generatedOffset, m.generatedLength);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Translates a generated-document offset to a source-document offset.
|
|
69
|
+
*
|
|
70
|
+
* @param generatedOffset - Offset in the virtual `.svelte` document.
|
|
71
|
+
* @returns The corresponding source offset, or `undefined` if the offset
|
|
72
|
+
* falls outside every mapped span.
|
|
73
|
+
*/
|
|
74
|
+
generatedOffsetToSource(generatedOffset) {
|
|
75
|
+
const m = findContaining(this.#byGenerated, generatedOffset, (x) => x.generatedOffset, (x) => x.generatedLength);
|
|
76
|
+
if (!m)
|
|
77
|
+
return undefined;
|
|
78
|
+
return translate(generatedOffset, m.generatedOffset, m.generatedLength, m.sourceOffset, m.sourceLength);
|
|
79
|
+
}
|
|
80
|
+
// ----- position translation ---------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* Translates a source-document {@link Position} to a generated-document
|
|
83
|
+
* position.
|
|
84
|
+
*
|
|
85
|
+
* @returns The mapped position, or `undefined` if it lies in an unmapped
|
|
86
|
+
* region.
|
|
87
|
+
*/
|
|
88
|
+
sourcePositionToGenerated(position) {
|
|
89
|
+
const offset = this.#sourceDoc.offsetAt(position);
|
|
90
|
+
const generated = this.sourceOffsetToGenerated(offset);
|
|
91
|
+
if (generated === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
return this.#generatedDoc.positionAt(generated);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Translates a generated-document {@link Position} to a source-document
|
|
97
|
+
* position.
|
|
98
|
+
*
|
|
99
|
+
* @returns The mapped position, or `undefined` if it lies in an unmapped
|
|
100
|
+
* region.
|
|
101
|
+
*/
|
|
102
|
+
generatedPositionToSource(position) {
|
|
103
|
+
const offset = this.#generatedDoc.offsetAt(position);
|
|
104
|
+
const source = this.generatedOffsetToSource(offset);
|
|
105
|
+
if (source === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
return this.#sourceDoc.positionAt(source);
|
|
108
|
+
}
|
|
109
|
+
// ----- range translation -------------------------------------------------
|
|
110
|
+
/**
|
|
111
|
+
* Translates a source-document {@link Range} to a generated-document range.
|
|
112
|
+
*
|
|
113
|
+
* @returns The mapped range, or `undefined` if either endpoint is unmapped.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* A range is only translated when _both_ endpoints land in mapped spans.
|
|
117
|
+
* This is the conservative choice: a half-mapped range would produce a
|
|
118
|
+
* nonsensical generated range and confuse the embedded server.
|
|
119
|
+
*/
|
|
120
|
+
sourceRangeToGenerated(range) {
|
|
121
|
+
const start = this.sourcePositionToGenerated(range.start);
|
|
122
|
+
const end = this.sourcePositionToGenerated(range.end);
|
|
123
|
+
if (!start || !end)
|
|
124
|
+
return undefined;
|
|
125
|
+
return { start, end };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Translates a generated-document {@link Range} to a source-document range.
|
|
129
|
+
*
|
|
130
|
+
* @returns The mapped range, or `undefined` if either endpoint is unmapped.
|
|
131
|
+
*/
|
|
132
|
+
generatedRangeToSource(range) {
|
|
133
|
+
const start = this.generatedPositionToSource(range.start);
|
|
134
|
+
const end = this.generatedPositionToSource(range.end);
|
|
135
|
+
if (!start || !end)
|
|
136
|
+
return undefined;
|
|
137
|
+
return { start, end };
|
|
138
|
+
}
|
|
139
|
+
// ----- feature flags -----------------------------------------------------
|
|
140
|
+
/**
|
|
141
|
+
* Returns the {@link MappingFeatures} governing a given offset.
|
|
142
|
+
*
|
|
143
|
+
* @param offset - The offset to query.
|
|
144
|
+
* @param direction - `'toGenerated'` to interpret `offset` as a source
|
|
145
|
+
* offset, `'toSource'` to interpret it as a generated offset.
|
|
146
|
+
* @returns The feature flags, or `undefined` if the offset is unmapped.
|
|
147
|
+
*/
|
|
148
|
+
featuresAt(offset, direction) {
|
|
149
|
+
const m = direction === 'toGenerated'
|
|
150
|
+
? findContaining(this.#bySource, offset, (x) => x.sourceOffset, (x) => x.sourceLength)
|
|
151
|
+
: findContaining(this.#byGenerated, offset, (x) => x.generatedOffset, (x) => x.generatedLength);
|
|
152
|
+
return m?.features;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Maps a position within `[fromOffset, fromOffset + fromLength]` onto the
|
|
157
|
+
* corresponding position within `[toOffset, toOffset + toLength]`.
|
|
158
|
+
*
|
|
159
|
+
* For the v1 identity case (`fromLength === toLength`) this is simply
|
|
160
|
+
* `toOffset + (offset - fromOffset)`. When the spans differ in length the
|
|
161
|
+
* offset is clamped to the destination span — a defensive measure for future
|
|
162
|
+
* non-affine mappings; it never triggers in v1.
|
|
163
|
+
*/
|
|
164
|
+
function translate(offset, fromOffset, fromLength, toOffset, toLength) {
|
|
165
|
+
const delta = offset - fromOffset;
|
|
166
|
+
if (delta <= 0)
|
|
167
|
+
return toOffset;
|
|
168
|
+
if (delta >= fromLength)
|
|
169
|
+
return toOffset + toLength;
|
|
170
|
+
if (fromLength === toLength)
|
|
171
|
+
return toOffset + delta;
|
|
172
|
+
return toOffset + Math.min(delta, toLength);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Binary-searches a list of mappings (sorted by the offset accessor) for the
|
|
176
|
+
* mapping whose span contains `offset`.
|
|
177
|
+
*
|
|
178
|
+
* The span is treated as the half-open interval `[start, start + length)`, with
|
|
179
|
+
* one exception: a zero-length probe at `start + length` (the very end of a
|
|
180
|
+
* span) also matches, so that a caret positioned immediately after the last
|
|
181
|
+
* character of a mapped region still maps. This matches how editors place the
|
|
182
|
+
* cursor at end-of-token.
|
|
183
|
+
*
|
|
184
|
+
* @param sorted - Mappings sorted ascending by `getStart`.
|
|
185
|
+
* @param offset - The offset to locate.
|
|
186
|
+
* @param getStart - Accessor for a mapping's span start.
|
|
187
|
+
* @param getLength - Accessor for a mapping's span length.
|
|
188
|
+
* @returns The containing mapping, or `undefined`.
|
|
189
|
+
*/
|
|
190
|
+
function findContaining(sorted, offset, getStart, getLength) {
|
|
191
|
+
let lo = 0;
|
|
192
|
+
let hi = sorted.length - 1;
|
|
193
|
+
while (lo <= hi) {
|
|
194
|
+
const mid = (lo + hi) >> 1;
|
|
195
|
+
const m = sorted[mid];
|
|
196
|
+
if (!m)
|
|
197
|
+
break;
|
|
198
|
+
const start = getStart(m);
|
|
199
|
+
const end = start + getLength(m);
|
|
200
|
+
if (offset < start) {
|
|
201
|
+
hi = mid - 1;
|
|
202
|
+
}
|
|
203
|
+
else if (offset > end) {
|
|
204
|
+
lo = mid + 1;
|
|
205
|
+
}
|
|
206
|
+
else if (offset === end && offset !== start) {
|
|
207
|
+
// `offset` is exactly at this span's end. Prefer the next span if
|
|
208
|
+
// it starts here (so an interior boundary maps into the right-hand
|
|
209
|
+
// region); otherwise accept this span (true end of document).
|
|
210
|
+
const next = sorted[mid + 1];
|
|
211
|
+
if (next && getStart(next) === offset) {
|
|
212
|
+
lo = mid + 1;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
return m;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
return m;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-mapping feature flags, mirroring Volar's `CodeInformation`.
|
|
3
|
+
*
|
|
4
|
+
* Every flag defaults to `true` when the mapping is created via
|
|
5
|
+
* {@link identityMapping}. They exist so that later phases can, for example,
|
|
6
|
+
* keep a region navigable (go-to-definition) while suppressing diagnostics in
|
|
7
|
+
* it, without changing the mapping geometry.
|
|
8
|
+
*/
|
|
9
|
+
export interface MappingFeatures {
|
|
10
|
+
/** Whether diagnostics inside this mapping should surface to the user. */
|
|
11
|
+
diagnostics: boolean;
|
|
12
|
+
/** Whether hover / signature-help requests are answered here. */
|
|
13
|
+
verification: boolean;
|
|
14
|
+
/** Whether completion is offered here. */
|
|
15
|
+
completion: boolean;
|
|
16
|
+
/** Whether semantic-token / highlight requests are answered here. */
|
|
17
|
+
semantic: boolean;
|
|
18
|
+
/** Whether go-to-definition / references / rename are answered here. */
|
|
19
|
+
navigation: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Links one contiguous span of the source document to one contiguous span of
|
|
23
|
+
* the generated document.
|
|
24
|
+
*
|
|
25
|
+
* Unlike Volar's `CodeMapping` (which packs several spans into parallel
|
|
26
|
+
* arrays), each `Mapping` here describes exactly one span pair. The arrays live
|
|
27
|
+
* one level up, in the {@link SourceMap}. Keeping one span per object makes the
|
|
28
|
+
* binary searches in {@link SourceMap} trivial to reason about and test.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* In v1 every delegated region is copied byte-for-byte, so `sourceLength` and
|
|
32
|
+
* `generatedLength` are always equal and the mapping is affine with slope 1.
|
|
33
|
+
* The fields are kept independent regardless, so that a future phase which
|
|
34
|
+
* expands Markdown to HTML (changing lengths) needs no API change here.
|
|
35
|
+
*/
|
|
36
|
+
export interface Mapping {
|
|
37
|
+
/** Offset of the span in the source `.sveltex` document. */
|
|
38
|
+
sourceOffset: number;
|
|
39
|
+
/** Length of the span in the source document. */
|
|
40
|
+
sourceLength: number;
|
|
41
|
+
/** Offset of the span in the generated `.svelte` document. */
|
|
42
|
+
generatedOffset: number;
|
|
43
|
+
/** Length of the span in the generated document. */
|
|
44
|
+
generatedLength: number;
|
|
45
|
+
/** Feature flags for content inside this span. */
|
|
46
|
+
features: MappingFeatures;
|
|
47
|
+
}
|
|
48
|
+
/** Returns a {@link MappingFeatures} object with every feature enabled. */
|
|
49
|
+
export declare function allFeatures(): MappingFeatures;
|
|
50
|
+
/**
|
|
51
|
+
* Builds an identity {@link Mapping}: a span that occupies the same length in
|
|
52
|
+
* both documents (the v1 case for every delegated region).
|
|
53
|
+
*
|
|
54
|
+
* @param sourceOffset - Start of the span in the source document.
|
|
55
|
+
* @param generatedOffset - Start of the span in the generated document.
|
|
56
|
+
* @param length - Length of the span (identical in both documents).
|
|
57
|
+
* @param features - Feature flags; defaults to all-enabled.
|
|
58
|
+
*/
|
|
59
|
+
export declare function identityMapping(sourceOffset: number, generatedOffset: number, length: number, features?: MappingFeatures): Mapping;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// File description: The `Mapping` data model — a single offset-triple linking a
|
|
2
|
+
// span of the source `.sveltex` document to a span of the generated virtual
|
|
3
|
+
// `.svelte` document.
|
|
4
|
+
//
|
|
5
|
+
// This is a deliberate, trimmed-down adaptation of Volar's `CodeMapping`
|
|
6
|
+
// representation (`sourceOffsets[]` / `generatedOffsets[]` / `lengths[]` plus
|
|
7
|
+
// per-region `CodeInformation` feature flags). We do NOT depend on Volar — the
|
|
8
|
+
// architecture brief is explicit that Volar cannot host the real
|
|
9
|
+
// `svelte-language-server` — we only borrow its proven data shape.
|
|
10
|
+
/** Returns a {@link MappingFeatures} object with every feature enabled. */
|
|
11
|
+
export function allFeatures() {
|
|
12
|
+
return {
|
|
13
|
+
diagnostics: true,
|
|
14
|
+
verification: true,
|
|
15
|
+
completion: true,
|
|
16
|
+
semantic: true,
|
|
17
|
+
navigation: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds an identity {@link Mapping}: a span that occupies the same length in
|
|
22
|
+
* both documents (the v1 case for every delegated region).
|
|
23
|
+
*
|
|
24
|
+
* @param sourceOffset - Start of the span in the source document.
|
|
25
|
+
* @param generatedOffset - Start of the span in the generated document.
|
|
26
|
+
* @param length - Length of the span (identical in both documents).
|
|
27
|
+
* @param features - Feature flags; defaults to all-enabled.
|
|
28
|
+
*/
|
|
29
|
+
export function identityMapping(sourceOffset, generatedOffset, length, features = allFeatures()) {
|
|
30
|
+
return {
|
|
31
|
+
sourceOffset,
|
|
32
|
+
sourceLength: length,
|
|
33
|
+
generatedOffset,
|
|
34
|
+
generatedLength: length,
|
|
35
|
+
features,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type DocumentSymbol, type FoldingRange, type Position, type SelectionRange } from 'vscode-languageserver-protocol';
|
|
2
|
+
import type { SveltexConfigSnapshot } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Computes document symbols for a `.sveltex` file: a nested outline built from
|
|
5
|
+
* its Markdown headings.
|
|
6
|
+
*
|
|
7
|
+
* @param document - Full text of the `.sveltex` document.
|
|
8
|
+
* @param config - Resolved SvelTeX config snapshot.
|
|
9
|
+
* @returns A hierarchical list of {@link DocumentSymbol}s. Headings nest by
|
|
10
|
+
* level (an `##` becomes a child of the preceding `#`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function computeDocumentSymbols(document: string, config: SveltexConfigSnapshot): DocumentSymbol[];
|
|
13
|
+
/**
|
|
14
|
+
* Computes folding ranges for a `.sveltex` file.
|
|
15
|
+
*
|
|
16
|
+
* @param document - Full text of the `.sveltex` document.
|
|
17
|
+
* @param config - Resolved SvelTeX config snapshot.
|
|
18
|
+
* @returns Folding ranges for multi-line block constructs: headings (folding
|
|
19
|
+
* down to the next same-or-higher heading), fenced code blocks, lists and
|
|
20
|
+
* blockquotes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function computeFoldingRanges(document: string, config: SveltexConfigSnapshot): FoldingRange[];
|
|
23
|
+
/**
|
|
24
|
+
* Computes selection ranges for a `.sveltex` file: for each requested
|
|
25
|
+
* {@link Position}, the chain of progressively larger mdast nodes that contain
|
|
26
|
+
* it.
|
|
27
|
+
*
|
|
28
|
+
* @param document - Full text of the `.sveltex` document.
|
|
29
|
+
* @param positions - The caret positions to expand.
|
|
30
|
+
* @param config - Resolved SvelTeX config snapshot.
|
|
31
|
+
* @returns One {@link SelectionRange} per input position (index-aligned). A
|
|
32
|
+
* position with no enclosing node yields a degenerate empty range.
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeSelectionRanges(document: string, positions: Position[], config: SveltexConfigSnapshot): SelectionRange[];
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// File description: Native Markdown language features derived from the mdast.
|
|
2
|
+
//
|
|
3
|
+
// The embedded Svelte language server understands the HTML subset of a
|
|
4
|
+
// `.sveltex` file but nothing of its Markdown structure. This module fills that
|
|
5
|
+
// gap by parsing the document with SvelTeX's own `parseToMdast` and walking the
|
|
6
|
+
// resulting tree to produce document symbols, folding ranges and selection
|
|
7
|
+
// ranges. These features need no position mapping: the mdast carries offsets on
|
|
8
|
+
// the original `.sveltex` source directly.
|
|
9
|
+
import { FoldingRangeKind, SymbolKind, } from 'vscode-languageserver-protocol';
|
|
10
|
+
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
11
|
+
import { parseToMdast } from '@nvl/sveltex/dist/utils/escape.js';
|
|
12
|
+
/**
|
|
13
|
+
* Parses a `.sveltex` document into an mdast root.
|
|
14
|
+
*
|
|
15
|
+
* @returns The mdast root, or `undefined` if parsing throws.
|
|
16
|
+
*/
|
|
17
|
+
function parse(document, config) {
|
|
18
|
+
try {
|
|
19
|
+
return parseToMdast(document, [...config.verbatimTags, 'script', 'style'], config.mathDelims, config.directives);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Depth-first pre-order walk over an mdast tree. */
|
|
26
|
+
function walk(node, visit) {
|
|
27
|
+
visit(node);
|
|
28
|
+
for (const child of node.children ?? []) {
|
|
29
|
+
walk(child, visit);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Extracts the concatenated plain text of a node's literal descendants. */
|
|
33
|
+
function textOf(node) {
|
|
34
|
+
let text = '';
|
|
35
|
+
walk(node, (n) => {
|
|
36
|
+
if (typeof n.value === 'string')
|
|
37
|
+
text += n.value;
|
|
38
|
+
});
|
|
39
|
+
return text.trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Computes document symbols for a `.sveltex` file: a nested outline built from
|
|
43
|
+
* its Markdown headings.
|
|
44
|
+
*
|
|
45
|
+
* @param document - Full text of the `.sveltex` document.
|
|
46
|
+
* @param config - Resolved SvelTeX config snapshot.
|
|
47
|
+
* @returns A hierarchical list of {@link DocumentSymbol}s. Headings nest by
|
|
48
|
+
* level (an `##` becomes a child of the preceding `#`).
|
|
49
|
+
*/
|
|
50
|
+
export function computeDocumentSymbols(document, config) {
|
|
51
|
+
const root = parse(document, config);
|
|
52
|
+
if (!root)
|
|
53
|
+
return [];
|
|
54
|
+
const doc = TextDocument.create('mem://md', 'sveltex', 0, document);
|
|
55
|
+
const roots = [];
|
|
56
|
+
const stack = [];
|
|
57
|
+
walk(root, (node) => {
|
|
58
|
+
if (node.type !== 'heading' || !node.position)
|
|
59
|
+
return;
|
|
60
|
+
const depth = node.depth ?? 1;
|
|
61
|
+
const range = {
|
|
62
|
+
start: doc.positionAt(node.position.start.offset),
|
|
63
|
+
end: doc.positionAt(node.position.end.offset),
|
|
64
|
+
};
|
|
65
|
+
const symbol = {
|
|
66
|
+
name: textOf(node) || `H${String(depth)}`,
|
|
67
|
+
kind: SymbolKind.String,
|
|
68
|
+
range,
|
|
69
|
+
selectionRange: range,
|
|
70
|
+
children: [],
|
|
71
|
+
};
|
|
72
|
+
// Pop headings of equal or deeper level; the remaining top of stack,
|
|
73
|
+
// if any, is this heading's parent.
|
|
74
|
+
while (stack.length > 0) {
|
|
75
|
+
const top = stack[stack.length - 1];
|
|
76
|
+
if (top && top.depth >= depth) {
|
|
77
|
+
stack.pop();
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const parent = stack[stack.length - 1];
|
|
84
|
+
if (parent) {
|
|
85
|
+
parent.symbol.children?.push(symbol);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
roots.push(symbol);
|
|
89
|
+
}
|
|
90
|
+
stack.push({ depth, symbol });
|
|
91
|
+
});
|
|
92
|
+
return roots;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Computes folding ranges for a `.sveltex` file.
|
|
96
|
+
*
|
|
97
|
+
* @param document - Full text of the `.sveltex` document.
|
|
98
|
+
* @param config - Resolved SvelTeX config snapshot.
|
|
99
|
+
* @returns Folding ranges for multi-line block constructs: headings (folding
|
|
100
|
+
* down to the next same-or-higher heading), fenced code blocks, lists and
|
|
101
|
+
* blockquotes.
|
|
102
|
+
*/
|
|
103
|
+
export function computeFoldingRanges(document, config) {
|
|
104
|
+
const root = parse(document, config);
|
|
105
|
+
if (!root)
|
|
106
|
+
return [];
|
|
107
|
+
const doc = TextDocument.create('mem://md', 'sveltex', 0, document);
|
|
108
|
+
const ranges = [];
|
|
109
|
+
/** Adds a folding range spanning `[startOffset, endOffset)` if multi-line. */
|
|
110
|
+
const add = (startOffset, endOffset, kind) => {
|
|
111
|
+
const startLine = doc.positionAt(startOffset).line;
|
|
112
|
+
const endLine = doc.positionAt(endOffset).line;
|
|
113
|
+
if (endLine > startLine) {
|
|
114
|
+
ranges.push(kind === undefined
|
|
115
|
+
? { startLine, endLine }
|
|
116
|
+
: { startLine, endLine, kind });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
// Block-level constructs fold as a whole.
|
|
120
|
+
walk(root, (node) => {
|
|
121
|
+
if (!node.position)
|
|
122
|
+
return;
|
|
123
|
+
switch (node.type) {
|
|
124
|
+
case 'code':
|
|
125
|
+
add(node.position.start.offset, node.position.end.offset, FoldingRangeKind.Region);
|
|
126
|
+
break;
|
|
127
|
+
case 'list':
|
|
128
|
+
case 'blockquote':
|
|
129
|
+
case 'table':
|
|
130
|
+
add(node.position.start.offset, node.position.end.offset);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const headings = [];
|
|
137
|
+
walk(root, (node) => {
|
|
138
|
+
if (node.type === 'heading' && node.position) {
|
|
139
|
+
headings.push({
|
|
140
|
+
depth: node.depth ?? 1,
|
|
141
|
+
startOffset: node.position.start.offset,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
for (let i = 0; i < headings.length; i++) {
|
|
146
|
+
const current = headings[i];
|
|
147
|
+
if (!current)
|
|
148
|
+
continue;
|
|
149
|
+
let endOffset = document.length;
|
|
150
|
+
for (let j = i + 1; j < headings.length; j++) {
|
|
151
|
+
const next = headings[j];
|
|
152
|
+
if (next && next.depth <= current.depth) {
|
|
153
|
+
endOffset = next.startOffset;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// End the fold at the end of the previous line, not the next heading.
|
|
158
|
+
const endLine = Math.max(doc.positionAt(current.startOffset).line, doc.positionAt(endOffset).line - 1);
|
|
159
|
+
const startLine = doc.positionAt(current.startOffset).line;
|
|
160
|
+
if (endLine > startLine) {
|
|
161
|
+
ranges.push({ startLine, endLine });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return ranges;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Computes selection ranges for a `.sveltex` file: for each requested
|
|
168
|
+
* {@link Position}, the chain of progressively larger mdast nodes that contain
|
|
169
|
+
* it.
|
|
170
|
+
*
|
|
171
|
+
* @param document - Full text of the `.sveltex` document.
|
|
172
|
+
* @param positions - The caret positions to expand.
|
|
173
|
+
* @param config - Resolved SvelTeX config snapshot.
|
|
174
|
+
* @returns One {@link SelectionRange} per input position (index-aligned). A
|
|
175
|
+
* position with no enclosing node yields a degenerate empty range.
|
|
176
|
+
*/
|
|
177
|
+
export function computeSelectionRanges(document, positions, config) {
|
|
178
|
+
const root = parse(document, config);
|
|
179
|
+
const doc = TextDocument.create('mem://md', 'sveltex', 0, document);
|
|
180
|
+
return positions.map((position) => {
|
|
181
|
+
const offset = doc.offsetAt(position);
|
|
182
|
+
// Collect every node whose span contains `offset`, smallest last.
|
|
183
|
+
const containing = [];
|
|
184
|
+
if (root) {
|
|
185
|
+
walk(root, (node) => {
|
|
186
|
+
if (!node.position)
|
|
187
|
+
return;
|
|
188
|
+
const start = node.position.start.offset;
|
|
189
|
+
const end = node.position.end.offset;
|
|
190
|
+
if (offset >= start && offset <= end) {
|
|
191
|
+
containing.push(node);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
containing.sort((a, b) => {
|
|
196
|
+
const aLen = (a.position?.end.offset ?? 0) - (a.position?.start.offset ?? 0);
|
|
197
|
+
const bLen = (b.position?.end.offset ?? 0) - (b.position?.start.offset ?? 0);
|
|
198
|
+
return bLen - aLen; // widest first
|
|
199
|
+
});
|
|
200
|
+
// Build the nested chain from widest to narrowest.
|
|
201
|
+
let selectionRange;
|
|
202
|
+
for (const node of containing) {
|
|
203
|
+
if (!node.position)
|
|
204
|
+
continue;
|
|
205
|
+
const range = {
|
|
206
|
+
start: doc.positionAt(node.position.start.offset),
|
|
207
|
+
end: doc.positionAt(node.position.end.offset),
|
|
208
|
+
};
|
|
209
|
+
selectionRange = selectionRange
|
|
210
|
+
? { range, parent: selectionRange }
|
|
211
|
+
: { range };
|
|
212
|
+
}
|
|
213
|
+
return selectionRange ?? { range: { start: position, end: position } };
|
|
214
|
+
});
|
|
215
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type CompletionItem, type CompletionList, type Hover, type Position } from 'vscode-languageserver-protocol';
|
|
2
|
+
import type { Region } from './regions.js';
|
|
3
|
+
import type { SveltexConfigSnapshot } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Whether `region` carries a LaTeX/TeX environment, i.e. its opening tag is one
|
|
6
|
+
* of the configured `latexTags` (case-insensitive, as SvelTeX tags are).
|
|
7
|
+
*
|
|
8
|
+
* @param source - Full text of the `.sveltex` document.
|
|
9
|
+
* @param region - The region to test (only `verbatim` regions can match).
|
|
10
|
+
* @param latexTags - The configured LaTeX verbatim tags.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isLatexVerbatimRegion(source: string, region: Region, latexTags: readonly string[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A sink for human-readable, operational log lines about the lifecycle of the
|
|
15
|
+
* child language servers (TexLab and the math language server).
|
|
16
|
+
*
|
|
17
|
+
* Spawning a child is best-effort and was previously silent: if `texlab` was
|
|
18
|
+
* not on `PATH`, or a child crashed on start, forwarding simply returned
|
|
19
|
+
* nothing with no trace. The host wires this sink to the editor's output
|
|
20
|
+
* channel so those outcomes are visible and diagnosable.
|
|
21
|
+
*/
|
|
22
|
+
export type ForwarderLog = (message: string) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Relabels `Text`-kind completion items as `Function`.
|
|
25
|
+
*
|
|
26
|
+
* TexLab tags every command completion `CompletionItemKind.Text` — the generic
|
|
27
|
+
* "a word that occurs in the document" kind. VS Code routes `Text` items
|
|
28
|
+
* through the `editor.suggest.showWords` toggle and folds them in with
|
|
29
|
+
* buffer-word suggestions, so a user who has turned word suggestions off (a
|
|
30
|
+
* common preference) sees *no* `<tex>` completions at all. A LaTeX control
|
|
31
|
+
* sequence is a function-like macro, not a stray word, so it is presented as
|
|
32
|
+
* `Function`: both more accurate and immune to the `showWords` toggle.
|
|
33
|
+
*
|
|
34
|
+
* @param result - A completion result already remapped to `.sveltex` coords.
|
|
35
|
+
* @returns The same result with every `Text`-kind item relabelled `Function`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function withFunctionCompletionKind(result: CompletionItem[] | CompletionList | null): CompletionItem[] | CompletionList | null;
|
|
38
|
+
/**
|
|
39
|
+
* Manages the child language servers that back non-delegated regions of one
|
|
40
|
+
* SvelTeX workspace, and forwards hover/completion requests to them.
|
|
41
|
+
*
|
|
42
|
+
* One instance is created per `createServer` call. The children are spawned
|
|
43
|
+
* lazily on first use and reused for the lifetime of the server.
|
|
44
|
+
*/
|
|
45
|
+
export declare class RegionForwarder {
|
|
46
|
+
#private;
|
|
47
|
+
/**
|
|
48
|
+
* @param config - The resolved SvelTeX config snapshot. Replaceable via
|
|
49
|
+
* {@link updateConfig} when the workspace config is (re)loaded.
|
|
50
|
+
* @param log - Optional sink for child-server lifecycle log lines (TexLab /
|
|
51
|
+
* the math server found, started, or failed). Defaults to discarding them.
|
|
52
|
+
*/
|
|
53
|
+
constructor(config: SveltexConfigSnapshot, log?: ForwarderLog);
|
|
54
|
+
/** Replaces the config snapshot (e.g. after the config file is loaded). */
|
|
55
|
+
updateConfig(config: SveltexConfigSnapshot): void;
|
|
56
|
+
/**
|
|
57
|
+
* Overrides the location of the math language server child.
|
|
58
|
+
*
|
|
59
|
+
* Standalone use needs no override — the server is resolved from
|
|
60
|
+
* `node_modules`. A host that has bundled the server to a sibling file (the
|
|
61
|
+
* VS Code extension) calls this with that file's absolute path before any
|
|
62
|
+
* math region is forwarded, since `node_modules` will not exist at runtime.
|
|
63
|
+
*
|
|
64
|
+
* @param serverPath - Absolute path of the math server's `bin/server.js`,
|
|
65
|
+
* or `undefined` to keep resolving from `node_modules`.
|
|
66
|
+
*/
|
|
67
|
+
setMathServerPath(serverPath: string | undefined): void;
|
|
68
|
+
/**
|
|
69
|
+
* Forwards a hover request that lands in `region` to the appropriate child
|
|
70
|
+
* server.
|
|
71
|
+
*
|
|
72
|
+
* @param source - Full text of the `.sveltex` document.
|
|
73
|
+
* @param sourceUri - The `.sveltex` document URI.
|
|
74
|
+
* @param region - The region the request position falls in.
|
|
75
|
+
* @param position - The request position, in `.sveltex` coordinates.
|
|
76
|
+
* @returns The hover, mapped back to `.sveltex` coordinates, or `null` if
|
|
77
|
+
* the region is not forwardable or the child has nothing to offer.
|
|
78
|
+
*/
|
|
79
|
+
forwardHover(source: string, sourceUri: string, region: Region, position: Position): Promise<Hover | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Forwards a completion request that lands in `region` to the appropriate
|
|
82
|
+
* child server.
|
|
83
|
+
*
|
|
84
|
+
* @returns The completion result, mapped back to `.sveltex` coordinates,
|
|
85
|
+
* or `null` if the region is not forwardable.
|
|
86
|
+
*/
|
|
87
|
+
forwardCompletion(source: string, sourceUri: string, region: Region, position: Position): Promise<CompletionItem[] | CompletionList | null>;
|
|
88
|
+
/** Shuts every spawned child server down. */
|
|
89
|
+
stop(): Promise<void>;
|
|
90
|
+
}
|