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