@nvl/sveltex-math-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.
@@ -0,0 +1,144 @@
1
+ // File description: Human-readable descriptions and hover text for TeX
2
+ // commands, used in both completion-item documentation and hover.
3
+ //
4
+ // A command's one-line description is the first available of three sources:
5
+ // the documentation metadata merged into `data/commands.generated.ts` from
6
+ // each engine's reference docs (the richest, but not exhaustive); a small
7
+ // curated map of hand-written glosses for high-frequency commands; and finally
8
+ // a generic, category-driven sentence, so EVERY supported command gets *some*
9
+ // description. Hover additionally shows the command's signature, the Unicode
10
+ // glyph it stands for (when any), and a backend/package/category footer.
11
+ // Nothing here claims a command exists: the command set itself is sourced from
12
+ // the backend in `data/commands.generated.ts`.
13
+ /**
14
+ * A small, curated map of bare command name → one-line gloss, for the commands
15
+ * a description sentence alone would not usefully explain.
16
+ */
17
+ const GLOSSES = {
18
+ frac: 'Typesets a fraction with the given numerator and denominator.',
19
+ dfrac: 'Display-style fraction (always full size).',
20
+ tfrac: 'Text-style fraction (always small).',
21
+ sqrt: 'Square root; an optional argument gives the index (e.g. cube root).',
22
+ sum: 'Summation operator (∑), commonly used with sub/superscript limits.',
23
+ prod: 'Product operator (∏).',
24
+ int: 'Integral operator (∫).',
25
+ iint: 'Double integral operator (∬).',
26
+ iiint: 'Triple integral operator (∭).',
27
+ oint: 'Contour integral operator (∮).',
28
+ lim: 'Limit operator, typically with an underscript (e.g. x → 0).',
29
+ text: 'Typesets its argument as upright text inside math mode.',
30
+ mathrm: 'Upright (roman) math font.',
31
+ mathbf: 'Bold math font.',
32
+ mathit: 'Italic math font.',
33
+ mathsf: 'Sans-serif math font.',
34
+ mathtt: 'Monospace (typewriter) math font.',
35
+ mathcal: 'Calligraphic math font (uppercase letters).',
36
+ mathbb: 'Blackboard-bold math font (e.g. ℝ, ℕ, ℤ).',
37
+ mathfrak: 'Fraktur math font.',
38
+ boldsymbol: 'Bold version of its argument, including symbols.',
39
+ hat: 'Places a hat accent over its argument.',
40
+ bar: 'Places a bar accent over its argument.',
41
+ vec: 'Places a vector arrow over its argument.',
42
+ dot: 'Places a single dot accent over its argument.',
43
+ ddot: 'Places a double dot accent over its argument.',
44
+ tilde: 'Places a tilde accent over its argument.',
45
+ overline: 'Draws a line over its argument.',
46
+ underline: 'Draws a line under its argument.',
47
+ overbrace: 'Draws a horizontal brace over its argument.',
48
+ underbrace: 'Draws a horizontal brace under its argument.',
49
+ left: 'Opens an auto-sized delimiter; pair with \\right.',
50
+ right: 'Closes an auto-sized delimiter; pair with \\left.',
51
+ begin: 'Opens an environment, e.g. \\begin{matrix}.',
52
+ end: 'Closes an environment opened with \\begin.',
53
+ binom: 'Typesets a binomial coefficient.',
54
+ cdot: 'Centred multiplication dot (⋅).',
55
+ times: 'Multiplication cross (×).',
56
+ div: 'Division sign (÷).',
57
+ pm: 'Plus-or-minus sign (±).',
58
+ mp: 'Minus-or-plus sign (∓).',
59
+ leq: 'Less-than-or-equal sign (≤).',
60
+ geq: 'Greater-than-or-equal sign (≥).',
61
+ neq: 'Not-equal sign (≠).',
62
+ approx: 'Approximately-equal sign (≈).',
63
+ equiv: 'Equivalence / identity sign (≡).',
64
+ infty: 'Infinity symbol (∞).',
65
+ partial: 'Partial-derivative symbol (∂).',
66
+ nabla: 'Nabla / del operator (∇).',
67
+ forall: 'Universal quantifier (∀).',
68
+ exists: 'Existential quantifier (∃).',
69
+ in: 'Set-membership sign (∈).',
70
+ subset: 'Subset sign (⊂).',
71
+ cup: 'Set union (∪).',
72
+ cap: 'Set intersection (∩).',
73
+ rightarrow: 'Rightward arrow (→).',
74
+ leftarrow: 'Leftward arrow (←).',
75
+ Rightarrow: 'Rightward double arrow (⇒).',
76
+ Leftarrow: 'Leftward double arrow (⇐).',
77
+ operatorname: 'Typesets its argument as a named operator (upright).',
78
+ color: 'Sets the colour of the following material.',
79
+ textcolor: 'Typesets its argument in the given colour.',
80
+ ce: 'mhchem: typesets a chemical equation or formula.',
81
+ pu: 'mhchem: typesets a physical unit.',
82
+ };
83
+ /** Category-driven fallback sentences. */
84
+ const CATEGORY_SENTENCE = {
85
+ function: 'A TeX command that takes one or more arguments.',
86
+ symbol: 'A TeX symbol command (produces a single glyph).',
87
+ macro: 'A TeX macro (defined in terms of other commands).',
88
+ environment: 'A TeX environment, used with \\begin{...} and \\end{...}.',
89
+ };
90
+ /** Human-readable backend names for hover text. */
91
+ const BACKEND_LABEL = {
92
+ katex: 'KaTeX',
93
+ mathjax: 'MathJax',
94
+ };
95
+ /**
96
+ * Returns the one-line description of a command: the documentation-sourced
97
+ * description when the generator merged one in, else a curated gloss, else the
98
+ * generic category sentence (which is always available).
99
+ *
100
+ * @param command - The command to describe.
101
+ */
102
+ export function describeCommand(command) {
103
+ return (command.description ??
104
+ GLOSSES[command.name] ??
105
+ CATEGORY_SENTENCE[command.category]);
106
+ }
107
+ /**
108
+ * Builds the Markdown body shown on hover for a command.
109
+ *
110
+ * The body is, in order: a fenced `latex` block with the command's signature
111
+ * (its arguments spelled out when it takes any); for a command that stands for
112
+ * a Unicode glyph, that glyph and its Unicode standard name; the one-line
113
+ * description; and a dimmed footer naming the backend, the providing package
114
+ * (when known) and the category.
115
+ *
116
+ * @param command - The command being hovered.
117
+ * @param backend - The active backend, named in the footer so the user knows
118
+ * which engine's support they are seeing.
119
+ * @returns A Markdown string.
120
+ */
121
+ export function hoverMarkdown(command, backend) {
122
+ const lines = [];
123
+ // The signature comes first: how the command is actually used. A merged-in
124
+ // `signature` spells out the arguments; otherwise the bare `\name` (or a
125
+ // `\begin…\end` pair for an environment) stands in.
126
+ const usage = command.signature ??
127
+ (command.category === 'environment'
128
+ ? `\\begin{${command.name}} … \\end{${command.name}}`
129
+ : `\\${command.name}`);
130
+ lines.push('```latex', usage, '```', '');
131
+ // For a command that denotes a Unicode glyph, show the glyph and its
132
+ // Unicode standard name — e.g. `∮ (contour integral)`.
133
+ if (command.unicode) {
134
+ const named = command.unicodeName ? ` (${command.unicodeName})` : '';
135
+ lines.push(`**${command.unicode}**${named}`, '');
136
+ }
137
+ lines.push(describeCommand(command), '');
138
+ // A dimmed footer: backend · package (when known) · category.
139
+ const footer = [BACKEND_LABEL[backend], command.package, command.category]
140
+ .filter((part) => Boolean(part))
141
+ .join(' · ');
142
+ lines.push(`_${footer}_`);
143
+ return lines.join('\n');
144
+ }
@@ -0,0 +1,24 @@
1
+ import { type CompletionList, type Hover, type Position } from 'vscode-languageserver-protocol';
2
+ import type { CommandTable, MathLspBackend } from './commands.js';
3
+ /**
4
+ * Computes the completion result for a caret in a TeX math document.
5
+ *
6
+ * @param text - Full text of the (virtual) TeX math document.
7
+ * @param position - The caret position.
8
+ * @param table - The active backend's command table.
9
+ * @returns A {@link CompletionList}. The list is empty (but never `null`) when
10
+ * the caret is not in a command-typing context, so the editor caches the
11
+ * "nothing here" answer rather than re-asking on every keystroke.
12
+ */
13
+ export declare function computeCompletion(text: string, position: Position, table: CommandTable): CompletionList;
14
+ /**
15
+ * Computes the hover result for a caret in a TeX math document.
16
+ *
17
+ * @param text - Full text of the (virtual) TeX math document.
18
+ * @param position - The caret position.
19
+ * @param table - The active backend's command table.
20
+ * @param backend - The active backend (named in the hover text).
21
+ * @returns A {@link Hover}, or `null` when the caret is not on a command the
22
+ * backend supports.
23
+ */
24
+ export declare function computeHover(text: string, position: Position, table: CommandTable, backend: MathLspBackend): Hover | null;
@@ -0,0 +1,140 @@
1
+ // File description: The pure language-feature layer — turning a TeX document +
2
+ // caret position into LSP completion and hover results.
3
+ //
4
+ // Everything here is deterministic and side-effect-free: it takes the document
5
+ // text, a caret offset, and the active backend's `CommandTable`, and returns
6
+ // plain LSP payloads. The transport/connection wiring lives in `server.ts`;
7
+ // keeping the features pure makes them trivial to unit-test.
8
+ import { CompletionItemKind, InsertTextFormat, MarkupKind, } from 'vscode-languageserver-protocol';
9
+ import { TextDocument } from 'vscode-languageserver-textdocument';
10
+ import { commandAtCaret, completionContextAt } from './context.js';
11
+ import { describeCommand, hoverMarkdown } from './describe.js';
12
+ /** Maps a {@link CommandCategory} to the LSP completion-item icon. */
13
+ function completionKind(category) {
14
+ switch (category) {
15
+ case 'function':
16
+ return CompletionItemKind.Function;
17
+ case 'macro':
18
+ return CompletionItemKind.Method;
19
+ case 'environment':
20
+ return CompletionItemKind.Module;
21
+ case 'symbol':
22
+ return CompletionItemKind.Constant;
23
+ default:
24
+ return CompletionItemKind.Text;
25
+ }
26
+ }
27
+ /** A short, fixed sort prefix so functions/symbols rank above rare macros. */
28
+ function sortPrefix(category) {
29
+ switch (category) {
30
+ case 'function':
31
+ return '0';
32
+ case 'symbol':
33
+ return '1';
34
+ case 'environment':
35
+ return '2';
36
+ case 'macro':
37
+ return '3';
38
+ default:
39
+ return '4';
40
+ }
41
+ }
42
+ /**
43
+ * Builds the {@link CompletionItem} for one command.
44
+ *
45
+ * @param command - The command to offer.
46
+ * @param replaceRange - The source range the inserted text replaces (from the
47
+ * opening backslash, or the start of the environment name, to the caret).
48
+ * @param isEnvironmentName - When `true` the caret is inside `\begin{...}`, so
49
+ * the inserted text is the bare environment name; otherwise it is
50
+ * `\command`.
51
+ */
52
+ function buildCompletionItem(command, replaceRange, isEnvironmentName) {
53
+ // Inside `\begin{...}` the slot holds a bare environment name; everywhere
54
+ // else a command is `\name`. The label mirrors what is inserted.
55
+ const insertText = isEnvironmentName ? command.name : `\\${command.name}`;
56
+ const label = insertText;
57
+ return {
58
+ label,
59
+ kind: completionKind(command.category),
60
+ detail: command.category,
61
+ documentation: {
62
+ kind: MarkupKind.Markdown,
63
+ value: describeCommand(command),
64
+ },
65
+ // A fixed sort group keeps categories clustered; the name disambiguates
66
+ // within a group, so completion order is stable and predictable.
67
+ sortText: `${sortPrefix(command.category)}${command.name}`,
68
+ // No `filterText` — it defaults to `label`. `textEdit.range` starts at
69
+ // the `\`, so the editor's filter query is the typed text *including*
70
+ // that `\` (`\alp`); it must be matched against `\alpha` (the label),
71
+ // not the bare `alpha`, or the item is filtered out and never shown.
72
+ insertTextFormat: InsertTextFormat.PlainText,
73
+ textEdit: { range: replaceRange, newText: insertText },
74
+ };
75
+ }
76
+ /** Upper bound on how many completion items are returned in one response. */
77
+ const MAX_COMPLETION_ITEMS = 500;
78
+ /**
79
+ * Computes the completion result for a caret in a TeX math document.
80
+ *
81
+ * @param text - Full text of the (virtual) TeX math document.
82
+ * @param position - The caret position.
83
+ * @param table - The active backend's command table.
84
+ * @returns A {@link CompletionList}. The list is empty (but never `null`) when
85
+ * the caret is not in a command-typing context, so the editor caches the
86
+ * "nothing here" answer rather than re-asking on every keystroke.
87
+ */
88
+ export function computeCompletion(text, position, table) {
89
+ const doc = TextDocument.create('mem://tex', 'latex', 0, text);
90
+ const offset = doc.offsetAt(position);
91
+ const context = completionContextAt(text, offset);
92
+ if (!context) {
93
+ return { isIncomplete: false, items: [] };
94
+ }
95
+ const matches = table.withPrefix(context.prefix, context.isEnvironmentName);
96
+ const replaceRange = {
97
+ start: doc.positionAt(context.backslashOffset),
98
+ end: position,
99
+ };
100
+ const limited = matches.slice(0, MAX_COMPLETION_ITEMS);
101
+ return {
102
+ // `isIncomplete` is set when the list was truncated, so the editor
103
+ // re-queries as the user narrows the prefix.
104
+ isIncomplete: matches.length > limited.length,
105
+ items: limited.map((command) => buildCompletionItem(command, replaceRange, context.isEnvironmentName)),
106
+ };
107
+ }
108
+ /**
109
+ * Computes the hover result for a caret in a TeX math document.
110
+ *
111
+ * @param text - Full text of the (virtual) TeX math document.
112
+ * @param position - The caret position.
113
+ * @param table - The active backend's command table.
114
+ * @param backend - The active backend (named in the hover text).
115
+ * @returns A {@link Hover}, or `null` when the caret is not on a command the
116
+ * backend supports.
117
+ */
118
+ export function computeHover(text, position, table, backend) {
119
+ const doc = TextDocument.create('mem://tex', 'latex', 0, text);
120
+ const offset = doc.offsetAt(position);
121
+ const found = commandAtCaret(text, offset);
122
+ if (!found)
123
+ return null;
124
+ // A hovered control word might be an environment name without the
125
+ // surrounding `\begin{}`; try the command form first, then accept an
126
+ // environment of the same name.
127
+ const command = table.get(found.name);
128
+ if (!command)
129
+ return null;
130
+ return {
131
+ contents: {
132
+ kind: MarkupKind.Markdown,
133
+ value: hoverMarkdown(command, backend),
134
+ },
135
+ range: {
136
+ start: doc.positionAt(found.start),
137
+ end: doc.positionAt(found.end),
138
+ },
139
+ };
140
+ }
@@ -0,0 +1,24 @@
1
+ import { type Connection, type InitializeParams } from 'vscode-languageserver';
2
+ import { type MathLspBackend } from './commands.js';
3
+ /**
4
+ * Parses the math backend out of the `initialize` request's
5
+ * `initializationOptions`.
6
+ *
7
+ * @param params - The LSP `initialize` params.
8
+ * @returns `'katex'` or `'mathjax'`. Defaults to `'mathjax'` when the option is
9
+ * absent or unrecognised — MathJax is SvelTeX's default math backend, so an
10
+ * unconfigured client gets the most representative behaviour.
11
+ */
12
+ export declare function resolveBackend(params: InitializeParams): MathLspBackend;
13
+ /**
14
+ * Wires a SvelTeX math language server onto the given connection.
15
+ *
16
+ * @param connection - An LSP {@link Connection}, already created for whatever
17
+ * transport the host uses. This function never calls `listen()`; the caller
18
+ * owns the connection lifecycle.
19
+ *
20
+ * @remarks
21
+ * Transport-agnostic by construction (no `vscode` import, no stdio access), so
22
+ * the same core backs `bin/server.js` and any in-process host.
23
+ */
24
+ export declare function createServer(connection: Connection): void;
@@ -0,0 +1,93 @@
1
+ // File description: `createServer` — the transport-agnostic core of the
2
+ // SvelTeX math language server.
3
+ //
4
+ // This server is small and standalone: it provides command completion and
5
+ // hover for TeX math, for one of two backends — KaTeX or MathJax — chosen via
6
+ // the LSP `initialize` request's `initializationOptions.backend`. It is spawned
7
+ // (one child per backend) by `@nvl/sveltex-language-server`, which feeds it the
8
+ // math regions of a `.sveltex` file as tiny virtual TeX documents, but it is a
9
+ // perfectly ordinary LSP and could equally be launched directly by any editor.
10
+ //
11
+ // `createServer(connection)` takes an already-built LSP `Connection` and never
12
+ // touches the transport — exactly the core/wrapper split used by
13
+ // `@nvl/sveltex-language-server`, so the same core backs the stdio `bin/server.js`
14
+ // and any in-process host.
15
+ import { TextDocuments, TextDocumentSyncKind, } from 'vscode-languageserver';
16
+ import { TextDocument } from 'vscode-languageserver-textdocument';
17
+ import { createCommandTable, } from './commands.js';
18
+ import { computeCompletion, computeHover } from './features.js';
19
+ /**
20
+ * Parses the math backend out of the `initialize` request's
21
+ * `initializationOptions`.
22
+ *
23
+ * @param params - The LSP `initialize` params.
24
+ * @returns `'katex'` or `'mathjax'`. Defaults to `'mathjax'` when the option is
25
+ * absent or unrecognised — MathJax is SvelTeX's default math backend, so an
26
+ * unconfigured client gets the most representative behaviour.
27
+ */
28
+ export function resolveBackend(params) {
29
+ const options = params.initializationOptions;
30
+ if (options && typeof options === 'object' && 'backend' in options) {
31
+ const backend = options.backend;
32
+ if (backend === 'katex' || backend === 'mathjax')
33
+ return backend;
34
+ }
35
+ return 'mathjax';
36
+ }
37
+ /**
38
+ * Wires a SvelTeX math language server onto the given connection.
39
+ *
40
+ * @param connection - An LSP {@link Connection}, already created for whatever
41
+ * transport the host uses. This function never calls `listen()`; the caller
42
+ * owns the connection lifecycle.
43
+ *
44
+ * @remarks
45
+ * Transport-agnostic by construction (no `vscode` import, no stdio access), so
46
+ * the same core backs `bin/server.js` and any in-process host.
47
+ */
48
+ export function createServer(connection) {
49
+ // Open documents are tracked with the standard `TextDocuments` manager,
50
+ // which applies incremental sync for us — math regions are small, but a
51
+ // shared manager keeps the server simple and correct.
52
+ const documents = new TextDocuments(TextDocument);
53
+ /** The backend selected at `initialize`; until then, the default. */
54
+ let backend = 'mathjax';
55
+ /** The command table for {@link backend}, rebuilt when the backend is set. */
56
+ let table = createCommandTable(backend);
57
+ connection.onInitialize((params) => {
58
+ backend = resolveBackend(params);
59
+ table = createCommandTable(backend);
60
+ return {
61
+ capabilities: {
62
+ textDocumentSync: TextDocumentSyncKind.Incremental,
63
+ // `\` opens a command; `{` opens the environment-name slot of
64
+ // a `\begin{...}` — both should re-trigger completion.
65
+ completionProvider: {
66
+ triggerCharacters: ['\\', '{'],
67
+ resolveProvider: false,
68
+ },
69
+ hoverProvider: true,
70
+ },
71
+ serverInfo: {
72
+ name: 'sveltex-math-language-server',
73
+ },
74
+ };
75
+ });
76
+ connection.onCompletion((params) => {
77
+ const doc = documents.get(params.textDocument.uri);
78
+ if (!doc)
79
+ return { isIncomplete: false, items: [] };
80
+ return computeCompletion(doc.getText(), params.position, table);
81
+ });
82
+ connection.onHover((params) => {
83
+ const doc = documents.get(params.textDocument.uri);
84
+ if (!doc)
85
+ return null;
86
+ return computeHover(doc.getText(), params.position, table, backend);
87
+ });
88
+ // `TextDocuments` needs the connection to receive `didOpen`/`didChange`/
89
+ // `didClose`; `listen` only registers handlers, it does not start the
90
+ // transport (that is the caller's job). Completion and hover read the
91
+ // current document text on demand, so no change listener is needed.
92
+ documents.listen(connection);
93
+ }
@@ -0,0 +1,13 @@
1
+ import type { CommandCategory, MathCommand } from '../core/commands.js';
2
+ /**
3
+ * Every TeX command KaTeX supports (1059 entries), extracted from
4
+ * `katex`'s `functions`, `symbols` and `macros` tables.
5
+ */
6
+ export declare const KATEX_COMMANDS: readonly MathCommand[];
7
+ /**
8
+ * Every TeX command the default MathJax `input/tex` configuration supports
9
+ * (946 entries), extracted from `@mathjax/src`'s registered token maps
10
+ * for the always-loaded and `autoload`-reachable packages.
11
+ */
12
+ export declare const MATHJAX_COMMANDS: readonly MathCommand[];
13
+ export type { CommandCategory, MathCommand };