@markuplint/language-server 0.0.0-stage → 5.1.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 (55) hide show
  1. package/README.md +74 -2
  2. package/bin/markuplint-language-server.mjs +3 -0
  3. package/lib/const.d.ts +41 -0
  4. package/lib/const.js +44 -0
  5. package/lib/i18n.d.ts +19 -0
  6. package/lib/i18n.js +63 -0
  7. package/lib/index.d.ts +41 -0
  8. package/lib/index.js +40 -0
  9. package/lib/lsp.d.ts +8 -0
  10. package/lib/lsp.js +8 -0
  11. package/lib/protocol.d.ts +10 -0
  12. package/lib/protocol.js +9 -0
  13. package/lib/server/client-channel.d.ts +38 -0
  14. package/lib/server/client-channel.js +89 -0
  15. package/lib/server/code-actions.d.ts +36 -0
  16. package/lib/server/code-actions.js +101 -0
  17. package/lib/server/convert-diagnostics.d.ts +28 -0
  18. package/lib/server/convert-diagnostics.js +82 -0
  19. package/lib/server/debug.d.ts +3 -0
  20. package/lib/server/debug.js +7 -0
  21. package/lib/server/document-events.d.ts +78 -0
  22. package/lib/server/document-events.js +299 -0
  23. package/lib/server/get-accessibility-by-location.d.ts +6 -0
  24. package/lib/server/get-accessibility-by-location.js +19 -0
  25. package/lib/server/get-module.d.ts +127 -0
  26. package/lib/server/get-module.js +257 -0
  27. package/lib/server/index.d.ts +1 -0
  28. package/lib/server/index.js +5 -0
  29. package/lib/server/initialize-params.d.ts +32 -0
  30. package/lib/server/initialize-params.js +28 -0
  31. package/lib/server/module-notices.d.ts +28 -0
  32. package/lib/server/module-notices.js +41 -0
  33. package/lib/server/parser-load-failure.d.ts +36 -0
  34. package/lib/server/parser-load-failure.js +85 -0
  35. package/lib/server/server.d.ts +20 -0
  36. package/lib/server/server.js +96 -0
  37. package/lib/server/suppression-support.d.ts +57 -0
  38. package/lib/server/suppression-support.js +233 -0
  39. package/lib/server/v2.d.ts +12 -0
  40. package/lib/server/v2.js +109 -0
  41. package/lib/server/v3.d.ts +19 -0
  42. package/lib/server/v3.js +208 -0
  43. package/lib/server/v4.d.ts +85 -0
  44. package/lib/server/v4.js +292 -0
  45. package/lib/server/v5.d.ts +89 -0
  46. package/lib/server/v5.js +339 -0
  47. package/lib/types.d.ts +88 -0
  48. package/lib/types.js +1 -0
  49. package/lib/utils/get-file-path.d.ts +15 -0
  50. package/lib/utils/get-file-path.js +45 -0
  51. package/lib/utils/resolve-working-directory.d.ts +44 -0
  52. package/lib/utils/resolve-working-directory.js +266 -0
  53. package/locales/en.json +7 -0
  54. package/locales/ja.json +21 -0
  55. package/package.json +58 -5
@@ -0,0 +1,85 @@
1
+ import type { SendDiagnostics } from './document-events.js';
2
+ import type { Config, Log } from '../types.js';
3
+ import type { Violation } from '@markuplint/ml-config';
4
+ import type { ARIAVersion } from '@markuplint/ml-spec';
5
+ import type { FixSummary, MLEngine as _MLEngine } from 'markuplint';
6
+ import type { CodeActionParams, Position, TextDocumentIdentifier, CodeAction } from 'vscode-languageserver';
7
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
8
+ /**
9
+ * Snapshot of the latest lint result for a document, used to produce Code Actions.
10
+ */
11
+ export type FixState = {
12
+ readonly sourceCode: string;
13
+ readonly violations: readonly Violation[];
14
+ readonly fixedCode: string;
15
+ readonly fixSummary: FixSummary | null;
16
+ };
17
+ /**
18
+ * Returns the latest fix state for a document.
19
+ *
20
+ * @param uri - The document URI
21
+ * @returns The fix state, or `undefined` if the document has not been linted yet
22
+ */
23
+ export declare function getFixState(uri: string): FixState | undefined;
24
+ /**
25
+ * Handles the `textDocument/didOpen` event by creating an MLEngine for the document.
26
+ *
27
+ * Sets up linting, diagnostics, and fix-state tracking for the opened document.
28
+ *
29
+ * @param document - The opened text document
30
+ * @param MLEngine - The MLEngine constructor
31
+ * @param config - The language configuration
32
+ * @param locale - The locale for diagnostic messages
33
+ * @param log - Logger for general messages
34
+ * @param diagnosticsLog - Logger for diagnostic-specific messages
35
+ * @param sendDiagnostics - Callback to publish diagnostics to the client
36
+ * @param notFoundParserError - Callback invoked when a parser is not found
37
+ * @param workspace - The working directory the document belongs to; the base for config discovery
38
+ */
39
+ export declare function onDidOpen(document: TextDocument, MLEngine: typeof _MLEngine, config: Config, locale: string, log: Log, diagnosticsLog: Log, sendDiagnostics: SendDiagnostics, notFoundParserError: (e: unknown) => void, workspace: string): Promise<void>;
40
+ /**
41
+ * Handles the `textDocument/didChangeContent` event by re-running the lint engine.
42
+ *
43
+ * Debounces execution so that rapid successive changes do not trigger
44
+ * multiple lint passes.
45
+ *
46
+ * @param document - The changed text document
47
+ * @param log - Logger for messages
48
+ * @param notFoundParserError - Callback invoked when a parser is not found
49
+ */
50
+ export declare function onDidChangeContent(document: TextDocument, log: Log, notFoundParserError: (e: unknown) => void): void;
51
+ /**
52
+ * Handles the `textDocument/didClose` event by releasing the engine and the fix state of the document.
53
+ *
54
+ * @param uri - The URI of the closed document
55
+ */
56
+ export declare function onDidClose(uri: string): Promise<void>;
57
+ /**
58
+ * Retrieves the ARIA accessibility properties of the element at the given position.
59
+ *
60
+ * Used by the hover provider to display accessibility information such as
61
+ * role, exposed state, and ARIA property labels.
62
+ *
63
+ * @param textDocument - The text document identifier
64
+ * @param position - The cursor position within the document
65
+ * @param ariaVersion - The ARIA specification version to use
66
+ * @returns The node's accessibility properties, or null if unavailable
67
+ */
68
+ export declare function getNodeWithAccessibilityProps(textDocument: TextDocumentIdentifier, position: Position, ariaVersion: ARIAVersion): Promise<{
69
+ nodeName: string;
70
+ exposed: boolean;
71
+ labels: Record<string, string>;
72
+ } | {
73
+ nodeName: string;
74
+ unknown: true;
75
+ } | null>;
76
+ /**
77
+ * Handles `textDocument/codeAction` requests for markuplint v4+.
78
+ *
79
+ * Returns per-violation QuickFix actions and a SourceFixAll action
80
+ * (`source.fixAll.markuplint`) when fixable violations exist.
81
+ *
82
+ * @param params - The LSP Code Action request parameters
83
+ * @returns An array of Code Actions (QuickFix and/or SourceFixAll)
84
+ */
85
+ export declare function onCodeAction(params: CodeActionParams): CodeAction[];
@@ -0,0 +1,292 @@
1
+ import path from 'node:path';
2
+ import { t } from '../i18n.js';
3
+ import { getFilePath } from '../utils/get-file-path.js';
4
+ import { createQuickFixActions, createFixAllAction } from './code-actions.js';
5
+ import { convertDiagnostics } from './convert-diagnostics.js';
6
+ import { getAccessibilityByLocation } from './get-accessibility-by-location.js';
7
+ const engines = new Map();
8
+ const fixStates = new Map();
9
+ let moduleLog;
10
+ /**
11
+ * Returns the latest fix state for a document.
12
+ *
13
+ * @param uri - The document URI
14
+ * @returns The fix state, or `undefined` if the document has not been linted yet
15
+ */
16
+ export function getFixState(uri) {
17
+ return fixStates.get(uri);
18
+ }
19
+ /**
20
+ * Handles the `textDocument/didOpen` event by creating an MLEngine for the document.
21
+ *
22
+ * Sets up linting, diagnostics, and fix-state tracking for the opened document.
23
+ *
24
+ * @param document - The opened text document
25
+ * @param MLEngine - The MLEngine constructor
26
+ * @param config - The language configuration
27
+ * @param locale - The locale for diagnostic messages
28
+ * @param log - Logger for general messages
29
+ * @param diagnosticsLog - Logger for diagnostic-specific messages
30
+ * @param sendDiagnostics - Callback to publish diagnostics to the client
31
+ * @param notFoundParserError - Callback invoked when a parser is not found
32
+ * @param workspace - The working directory the document belongs to; the base for config discovery
33
+ */
34
+ export async function onDidOpen(
35
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
36
+ document,
37
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
38
+ MLEngine,
39
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
40
+ config, locale, log, diagnosticsLog, sendDiagnostics, notFoundParserError, workspace) {
41
+ moduleLog = log;
42
+ const key = document.uri;
43
+ log(`Opened: ${key}`, 'debug');
44
+ const currentEngine = engines.get(key);
45
+ if (currentEngine) {
46
+ return;
47
+ }
48
+ const filePath = getFilePath(document.uri, document.languageId);
49
+ log(`${filePath.dirname}/${filePath.basename}`, 'debug');
50
+ const absoluteFilePath = `${filePath.dirname}/${filePath.basename}`;
51
+ const sourceCode = document.getText();
52
+ // `name` must be the workspace-relative path (not just the basename), or
53
+ // MLFile.path collapses subdirectory files to workspace-root + basename —
54
+ // which breaks pretender import resolution, nested config discovery, and
55
+ // excludeFiles/overrides glob matching for anything outside the workspace root.
56
+ const relativeFilePath = path.relative(workspace, absoluteFilePath);
57
+ const file = await MLEngine.toMLFile({ sourceCode, name: relativeFilePath, workspace });
58
+ if (!file) {
59
+ log(`File not found: ${filePath.basename}`, 'warn');
60
+ return;
61
+ }
62
+ const engine = new MLEngine(file, {
63
+ locale,
64
+ debug: config.debug,
65
+ defaultConfig: config.defaultConfig,
66
+ watch: true,
67
+ fix: true,
68
+ });
69
+ log(`Engine created: ${key}`);
70
+ let configSet = null;
71
+ engines.set(key, engine);
72
+ engine.on('config', (_, _configSet) => {
73
+ configSet = _configSet;
74
+ });
75
+ engine.on('log', (phase, message) => {
76
+ log(`[${phase}]: ${message}`, 'trace');
77
+ });
78
+ engine.on('lint-error', (_filePath, _sourceCode, error) => {
79
+ diagnosticsLog(error.message, 'error');
80
+ });
81
+ engine.on('config-errors', (_filePath, errors) => {
82
+ for (const error of errors) {
83
+ diagnosticsLog('ConfigError: ' + error.message, 'error');
84
+ log('ConfigError: ' + error.message, 'warn');
85
+ }
86
+ });
87
+ engine.on('lint', (filePath, sourceCode, violations, fixedCode, debug, fixSummary) => {
88
+ clearTimeout(debounceTimer);
89
+ diagnosticsLog('', 'clear');
90
+ // Execute after 300ms from the last change.
91
+ debounceTimer = setTimeout(lint, 300);
92
+ function lint() {
93
+ // `onDidClose` cannot clear this timer: it is shared by every document.
94
+ if (engines.get(key) !== engine) {
95
+ return;
96
+ }
97
+ diagnosticsLog(`Lint: ${document.uri}`);
98
+ const errors = violations.filter(v => v.severity === 'error');
99
+ const warns = violations.filter(v => v.severity === 'warning');
100
+ log(`Errors: ${errors.length}`, 'debug');
101
+ log(`Warnings: ${warns.length}`, 'debug');
102
+ if (debug) {
103
+ diagnosticsLog(' Tracing AST Mapping:\n' + debug.map(line => ` ${line}`).join('\n'), 'trace');
104
+ }
105
+ if (configSet) {
106
+ if (configSet.files.size > 0) {
107
+ diagnosticsLog(' Used configs:');
108
+ for (const files of configSet.files.values()) {
109
+ diagnosticsLog(` ${files}`);
110
+ }
111
+ }
112
+ else {
113
+ diagnosticsLog(' No use configs');
114
+ }
115
+ if (configSet.plugins.length > 0) {
116
+ diagnosticsLog(' Used plugins:');
117
+ for (const plugin of configSet.plugins.values()) {
118
+ diagnosticsLog(` ${plugin.name}`);
119
+ }
120
+ }
121
+ else {
122
+ diagnosticsLog(' No use plugins');
123
+ }
124
+ }
125
+ const diagnostics = convertDiagnostics({
126
+ filePath,
127
+ sourceCode,
128
+ violations,
129
+ fixedCode,
130
+ status: 'processed',
131
+ });
132
+ fixStates.set(key, { sourceCode, violations, fixedCode, fixSummary });
133
+ if (diagnostics.length > 0) {
134
+ diagnosticsLog(` Violations(${diagnostics.length}):`);
135
+ for (const d of diagnostics) {
136
+ diagnosticsLog(` [${d.line}:${d.col}] ${d.code}`);
137
+ }
138
+ }
139
+ else {
140
+ diagnosticsLog(' ✔ No violations');
141
+ }
142
+ sendDiagnostics({
143
+ uri: document.uri,
144
+ diagnostics,
145
+ });
146
+ }
147
+ });
148
+ log('Run `engine.exec()` in `onDidOpen`', 'debug');
149
+ engine.exec().catch((error) => {
150
+ log(String(error), 'error');
151
+ notFoundParserError(error);
152
+ throw error;
153
+ });
154
+ }
155
+ let debounceTimer;
156
+ /**
157
+ * Handles the `textDocument/didChangeContent` event by re-running the lint engine.
158
+ *
159
+ * Debounces execution so that rapid successive changes do not trigger
160
+ * multiple lint passes.
161
+ *
162
+ * @param document - The changed text document
163
+ * @param log - Logger for messages
164
+ * @param notFoundParserError - Callback invoked when a parser is not found
165
+ */
166
+ export function onDidChangeContent(
167
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
168
+ document, log, notFoundParserError) {
169
+ clearTimeout(debounceTimer);
170
+ const key = document.uri;
171
+ const engine = engines.get(key);
172
+ debounceTimer = setTimeout(async () => {
173
+ // The document may have been closed within the debounce delay.
174
+ if (!engine || engines.get(key) !== engine) {
175
+ return;
176
+ }
177
+ const code = document.getText();
178
+ try {
179
+ await engine.setCode(code);
180
+ log('Run `engine.exec()` in `onDidChangeContent`', 'debug');
181
+ engine.exec().catch((error) => notFoundParserError(error));
182
+ }
183
+ catch (error) {
184
+ if (error instanceof Error) {
185
+ log(error.message, 'error');
186
+ return;
187
+ }
188
+ log(`UnknownError: ${error}`, 'error');
189
+ }
190
+ }, 300);
191
+ }
192
+ /**
193
+ * Handles the `textDocument/didClose` event by releasing the engine and the fix state of the document.
194
+ *
195
+ * @param uri - The URI of the closed document
196
+ */
197
+ export async function onDidClose(uri) {
198
+ fixStates.delete(uri);
199
+ const engine = engines.get(uri);
200
+ if (!engine) {
201
+ return;
202
+ }
203
+ engines.delete(uri);
204
+ await engine.close();
205
+ }
206
+ /**
207
+ * Retrieves the ARIA accessibility properties of the element at the given position.
208
+ *
209
+ * Used by the hover provider to display accessibility information such as
210
+ * role, exposed state, and ARIA property labels.
211
+ *
212
+ * @param textDocument - The text document identifier
213
+ * @param position - The cursor position within the document
214
+ * @param ariaVersion - The ARIA specification version to use
215
+ * @returns The node's accessibility properties, or null if unavailable
216
+ */
217
+ export async function getNodeWithAccessibilityProps(
218
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
219
+ textDocument,
220
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
221
+ position, ariaVersion) {
222
+ const key = textDocument.uri;
223
+ const engine = engines.get(key);
224
+ if (!engine) {
225
+ return null;
226
+ }
227
+ const a11y = await getAccessibilityByLocation(engine, position.line + 1, position.character, ariaVersion);
228
+ if (!a11y) {
229
+ return null;
230
+ }
231
+ const { node, aria } = a11y;
232
+ if (!aria) {
233
+ return null;
234
+ }
235
+ if (aria.unknown) {
236
+ return {
237
+ nodeName: node,
238
+ unknown: true,
239
+ };
240
+ }
241
+ if (!aria.exposedToTree) {
242
+ return {
243
+ nodeName: node,
244
+ exposed: false,
245
+ labels: {},
246
+ };
247
+ }
248
+ const labels = {};
249
+ const requiredLabel = `\u26A0\uFE0F**${t('Required')}**`;
250
+ labels.role = aria.role ? `\`${aria.role}\`` : t('No corresponding role');
251
+ labels.name = aria.nameProhibited
252
+ ? `**${t('Prohibited')}**`
253
+ : // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
254
+ aria.name
255
+ ? typeof aria.name === 'string'
256
+ ? `\`"${aria.name}"\``
257
+ : `**${t('Unknown')}**`
258
+ : `${t('None')}${aria.nameRequired ? ` ${requiredLabel}` : ''}`;
259
+ labels.focusable = `\`${aria.focusable}\``;
260
+ for (const [propName, { value, required }] of Object.entries(aria.props ?? {})) {
261
+ labels[propName] =
262
+ value === undefined ? t('Undefined') + (required ? ` ${requiredLabel}` : '') : `\`${value}\``;
263
+ }
264
+ return {
265
+ nodeName: node,
266
+ exposed: true,
267
+ labels,
268
+ };
269
+ }
270
+ /**
271
+ * Handles `textDocument/codeAction` requests for markuplint v4+.
272
+ *
273
+ * Returns per-violation QuickFix actions and a SourceFixAll action
274
+ * (`source.fixAll.markuplint`) when fixable violations exist.
275
+ *
276
+ * @param params - The LSP Code Action request parameters
277
+ * @returns An array of Code Actions (QuickFix and/or SourceFixAll)
278
+ */
279
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
280
+ export function onCodeAction(params) {
281
+ const uri = params.textDocument.uri;
282
+ const fixState = getFixState(uri);
283
+ if (!fixState) {
284
+ moduleLog?.(`onCodeAction: no fixState for ${uri}`, 'debug');
285
+ return [];
286
+ }
287
+ const fixableCount = fixState.violations.filter(v => v.fix).length;
288
+ moduleLog?.(`onCodeAction: ${fixState.violations.length} violations (${fixableCount} fixable), ${params.context.diagnostics.length} diagnostics`, 'debug');
289
+ const quickFixes = createQuickFixActions(uri, params.context.diagnostics, fixState);
290
+ const fixAll = createFixAllAction(uri, params.context.diagnostics, fixState);
291
+ return fixAll ? [...quickFixes, fixAll] : quickFixes;
292
+ }
@@ -0,0 +1,89 @@
1
+ import type { SendDiagnostics } from './document-events.js';
2
+ import type { Config, Log } from '../types.js';
3
+ import type { Violation } from '@markuplint/ml-config';
4
+ import type { ARIAVersion } from '@markuplint/ml-spec';
5
+ import type { FixSummary, MLEngine as _MLEngine } from 'markuplint';
6
+ import type { CodeActionParams, Position, TextDocumentIdentifier, CodeAction } from 'vscode-languageserver';
7
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
8
+ /**
9
+ * Snapshot of the latest lint result for a document, used to produce Code Actions.
10
+ */
11
+ export type FixState = {
12
+ readonly sourceCode: string;
13
+ readonly violations: readonly Violation[];
14
+ readonly fixedCode: string;
15
+ readonly fixSummary: FixSummary | null;
16
+ };
17
+ /**
18
+ * Returns the latest fix state for a document.
19
+ *
20
+ * @param uri - The document URI
21
+ * @returns The fix state, or `undefined` if the document has not been linted yet
22
+ */
23
+ export declare function getFixState(uri: string): FixState | undefined;
24
+ /**
25
+ * Handles the `textDocument/didOpen` event by creating an MLEngine for the document.
26
+ *
27
+ * Sets up linting, diagnostics, fix-state tracking, and bulk suppression
28
+ * severity downgrade for the opened document.
29
+ *
30
+ * @param document - The opened text document
31
+ * @param MLEngine - The MLEngine constructor
32
+ * @param config - The language configuration
33
+ * @param locale - The locale for diagnostic messages
34
+ * @param log - Logger for general messages
35
+ * @param diagnosticsLog - Logger for diagnostic-specific messages
36
+ * @param sendDiagnostics - Callback to publish diagnostics to the client
37
+ * @param notFoundParserError - Callback invoked when a parser is not found
38
+ * @param workspace - The working directory the document belongs to; the base for config discovery
39
+ */
40
+ export declare function onDidOpen(document: TextDocument, MLEngine: typeof _MLEngine, config: Config, locale: string, log: Log, diagnosticsLog: Log, sendDiagnostics: SendDiagnostics, notFoundParserError: (e: unknown) => void, workspace: string): Promise<void>;
41
+ /**
42
+ * Handles the `textDocument/didChangeContent` event by re-running the lint engine.
43
+ *
44
+ * Debounces execution so that rapid successive changes do not trigger
45
+ * multiple lint passes.
46
+ *
47
+ * @param document - The changed text document
48
+ * @param log - Logger for messages
49
+ * @param notFoundParserError - Callback invoked when a parser is not found
50
+ */
51
+ export declare function onDidChangeContent(document: TextDocument, log: Log, notFoundParserError: (e: unknown) => void): void;
52
+ /**
53
+ * Handles the `textDocument/didClose` event by releasing everything kept for the document.
54
+ *
55
+ * Without it every document opened during a session would keep its engine, its DOM and
56
+ * its config watcher (`watch: true`) until the server exits.
57
+ *
58
+ * @param uri - The URI of the closed document
59
+ */
60
+ export declare function onDidClose(uri: string): Promise<void>;
61
+ /**
62
+ * Retrieves the ARIA accessibility properties of the element at the given position.
63
+ *
64
+ * Used by the hover provider to display accessibility information such as
65
+ * role, exposed state, and ARIA property labels.
66
+ *
67
+ * @param textDocument - The text document identifier
68
+ * @param position - The cursor position within the document
69
+ * @param ariaVersion - The ARIA specification version to use
70
+ * @returns The node's accessibility properties, or null if unavailable
71
+ */
72
+ export declare function getNodeWithAccessibilityProps(textDocument: TextDocumentIdentifier, position: Position, ariaVersion: ARIAVersion): Promise<{
73
+ nodeName: string;
74
+ exposed: boolean;
75
+ labels: Record<string, string>;
76
+ } | {
77
+ nodeName: string;
78
+ unknown: true;
79
+ } | null>;
80
+ /**
81
+ * Handles `textDocument/codeAction` requests for markuplint v5.
82
+ *
83
+ * Returns per-violation QuickFix actions and a SourceFixAll action
84
+ * (`source.fixAll.markuplint`) when fixable violations exist.
85
+ *
86
+ * @param params - The LSP Code Action request parameters
87
+ * @returns An array of Code Actions (QuickFix and/or SourceFixAll)
88
+ */
89
+ export declare function onCodeAction(params: CodeActionParams): CodeAction[];