@markuplint/language-server 0.0.0-stage → 5.1.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/bin/markuplint-language-server.mjs +3 -0
  4. package/lib/const.d.ts +41 -0
  5. package/lib/const.js +44 -0
  6. package/lib/i18n.d.ts +19 -0
  7. package/lib/i18n.js +63 -0
  8. package/lib/index.d.ts +41 -0
  9. package/lib/index.js +40 -0
  10. package/lib/lsp.d.ts +8 -0
  11. package/lib/lsp.js +8 -0
  12. package/lib/protocol.d.ts +10 -0
  13. package/lib/protocol.js +9 -0
  14. package/lib/server/client-channel.d.ts +38 -0
  15. package/lib/server/client-channel.js +89 -0
  16. package/lib/server/code-actions.d.ts +36 -0
  17. package/lib/server/code-actions.js +101 -0
  18. package/lib/server/convert-diagnostics.d.ts +28 -0
  19. package/lib/server/convert-diagnostics.js +82 -0
  20. package/lib/server/debug.d.ts +3 -0
  21. package/lib/server/debug.js +7 -0
  22. package/lib/server/document-events.d.ts +78 -0
  23. package/lib/server/document-events.js +299 -0
  24. package/lib/server/get-accessibility-by-location.d.ts +6 -0
  25. package/lib/server/get-accessibility-by-location.js +19 -0
  26. package/lib/server/get-module.d.ts +127 -0
  27. package/lib/server/get-module.js +257 -0
  28. package/lib/server/index.d.ts +1 -0
  29. package/lib/server/index.js +5 -0
  30. package/lib/server/initialize-params.d.ts +32 -0
  31. package/lib/server/initialize-params.js +28 -0
  32. package/lib/server/module-notices.d.ts +28 -0
  33. package/lib/server/module-notices.js +41 -0
  34. package/lib/server/parser-load-failure.d.ts +36 -0
  35. package/lib/server/parser-load-failure.js +85 -0
  36. package/lib/server/server.d.ts +20 -0
  37. package/lib/server/server.js +96 -0
  38. package/lib/server/suppression-support.d.ts +57 -0
  39. package/lib/server/suppression-support.js +233 -0
  40. package/lib/server/v2.d.ts +12 -0
  41. package/lib/server/v2.js +109 -0
  42. package/lib/server/v3.d.ts +19 -0
  43. package/lib/server/v3.js +208 -0
  44. package/lib/server/v4.d.ts +85 -0
  45. package/lib/server/v4.js +292 -0
  46. package/lib/server/v5.d.ts +89 -0
  47. package/lib/server/v5.js +339 -0
  48. package/lib/types.d.ts +88 -0
  49. package/lib/types.js +1 -0
  50. package/lib/utils/get-file-path.d.ts +15 -0
  51. package/lib/utils/get-file-path.js +45 -0
  52. package/lib/utils/resolve-working-directory.d.ts +44 -0
  53. package/lib/utils/resolve-working-directory.js +266 -0
  54. package/locales/en.json +7 -0
  55. package/locales/ja.json +21 -0
  56. package/package.json +59 -5
@@ -0,0 +1,96 @@
1
+ import { CodeActionKind, TextDocuments, TextDocumentSyncKind } from 'vscode-languageserver/node.js';
2
+ import { TextDocument } from 'vscode-languageserver-textdocument';
3
+ import { setLocale } from '../i18n.js';
4
+ import { createClientChannel } from './client-channel.js';
5
+ import { SOURCE_FIX_ALL_MARKUPLINT } from './code-actions.js';
6
+ import { applySeverityMap } from './convert-diagnostics.js';
7
+ import { verbosely } from './debug.js';
8
+ import { createEventHandlers } from './document-events.js';
9
+ import { createModuleResolver } from './get-module.js';
10
+ import { resolveInitialization } from './initialize-params.js';
11
+ import { createModuleNotices } from './module-notices.js';
12
+ import { configureGitPath } from './suppression-support.js';
13
+ const DEBUG = false;
14
+ /**
15
+ * Bootstrap the LSP language server on a connection and start listening.
16
+ *
17
+ * Any LSP client can use the server, so nothing the client sends beyond the LSP itself is
18
+ * required: `initializationOptions` is optional (see {@link InitializationOptions}), and the
19
+ * `markuplint/*` custom messages are sent only to a client that opts in with `extendedProtocol`
20
+ * (see {@link createClientChannel}). Only the VS Code extension relies on both.
21
+ *
22
+ * The markuplint module is resolved lazily per working directory as documents open; the
23
+ * bundled-fallback notice is logged once per directory and the import assertion popup is shown
24
+ * once per session (see `createModuleNotices`).
25
+ *
26
+ * Diagnostics must not write to stdout: over `--stdio` it carries the protocol, and a client
27
+ * such as Claude Code disconnects the server on any other output. Logs go through the channel,
28
+ * and `debug` writes to stderr.
29
+ *
30
+ * @param connection - The connection to the client, from `createConnection`
31
+ */
32
+ export function bootServer(connection) {
33
+ const channel = createClientChannel(connection);
34
+ const documents = new TextDocuments(TextDocument);
35
+ documents.listen(connection);
36
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
37
+ let codeActionHandler = () => [];
38
+ connection.onCodeAction(params => codeActionHandler(params));
39
+ connection.onInitialize((params) => {
40
+ const { locale, options, workspaceFolders } = resolveInitialization(params);
41
+ channel.configure(options.extendedProtocol === true);
42
+ setLocale(locale);
43
+ const log = channel.log;
44
+ log('onInitialize');
45
+ const { langConfigs, workingDirectories, gitPath, severityMap } = options;
46
+ configureGitPath(gitPath);
47
+ const sendDiagnostics = ({ diagnostics, ...rest }) => {
48
+ void connection.sendDiagnostics({ ...rest, diagnostics: applySeverityMap(diagnostics, severityMap) });
49
+ };
50
+ connection.onInitialized(() => {
51
+ log('onInitialized');
52
+ if (DEBUG) {
53
+ verbosely();
54
+ }
55
+ log(`Locale: ${locale}`, 'info');
56
+ if (workingDirectories) {
57
+ log(`Working directories: ${JSON.stringify(workingDirectories)}`, 'info');
58
+ }
59
+ const notices = createModuleNotices({
60
+ log,
61
+ popup: channel.warningPopup,
62
+ sendStatus: channel.reportStatus,
63
+ });
64
+ const resolveModule = createModuleResolver({ log, onFirstResolve: notices.onFirstResolve });
65
+ const { onDidOpen, onDidChangeContent, onDidClose, onHover, onCodeAction } = createEventHandlers({
66
+ resolveModule,
67
+ locale,
68
+ langConfigs,
69
+ workingDirectories,
70
+ workspaceFolders,
71
+ log,
72
+ diagnosticsLog: channel.diagnosticsLog,
73
+ errorLog: channel.errorLog,
74
+ sendDiagnostics,
75
+ reportStatus: notices.reportStatus,
76
+ });
77
+ documents.onDidOpen(e => onDidOpen(e.document));
78
+ // eslint-disable-next-line unicorn/no-array-for-each
79
+ documents.all().forEach(onDidOpen);
80
+ documents.onDidChangeContent(e => onDidChangeContent(e.document));
81
+ documents.onDidClose(e => onDidClose(e.document));
82
+ connection.onHover(onHover);
83
+ codeActionHandler = onCodeAction;
84
+ });
85
+ return {
86
+ capabilities: {
87
+ textDocumentSync: TextDocumentSyncKind.Incremental,
88
+ hoverProvider: true,
89
+ codeActionProvider: {
90
+ codeActionKinds: [CodeActionKind.QuickFix, SOURCE_FIX_ALL_MARKUPLINT],
91
+ },
92
+ },
93
+ };
94
+ });
95
+ connection.listen();
96
+ }
@@ -0,0 +1,57 @@
1
+ import type { Violation } from '@markuplint/ml-config';
2
+ import type { SuppressionsData, DowngradedViolation } from 'markuplint/suppressions';
3
+ import type { Log } from '../types.js';
4
+ export type { DowngradedViolation } from 'markuplint/suppressions';
5
+ /**
6
+ * @experimental
7
+ * Cached state for a workspace's suppression file.
8
+ */
9
+ export type SuppressionCache = {
10
+ readonly data: SuppressionsData;
11
+ readonly filePath: string;
12
+ readonly lastCommitDate: Date | null;
13
+ };
14
+ /**
15
+ * @experimental
16
+ * Configures the git binary path for suppression support.
17
+ * Should be called once during server initialization.
18
+ *
19
+ * @param gitPath - Path to the git binary from the client's `gitPath` option
20
+ */
21
+ export declare function configureGitPath(gitPath: string | undefined): void;
22
+ /**
23
+ * @experimental
24
+ * Loads suppression data for a workspace, with TTL-based caching.
25
+ * Returns null if no suppressions file exists.
26
+ */
27
+ export declare function loadSuppressions(workspace: string, log: Log): Promise<SuppressionCache | null>;
28
+ /**
29
+ * @experimental
30
+ * Invalidates the suppression cache for a workspace.
31
+ * Should be called when the suppressions file changes.
32
+ */
33
+ export declare function invalidateSuppressionCache(workspace: string): void;
34
+ /**
35
+ * @experimental
36
+ * Applies suppression severity downgrade to violations.
37
+ *
38
+ * - If count <= suppressed count: all violations for that rule → info
39
+ * - If count > suppressed count: use git blame to distinguish old (→ info) vs new (→ original)
40
+ * - Falls back to original severity when git is unavailable
41
+ *
42
+ * The git blame date comparison uses the suppressions file's last commit date
43
+ * as the threshold. Lines authored before that date are considered "old"
44
+ * (existed when suppressions were generated) and downgraded to info.
45
+ * Lines authored after are considered "new" and keep their original severity.
46
+ */
47
+ export declare function applySuppressionsToViolations(absoluteFilePath: string, violations: readonly Violation[], cache: SuppressionCache, log: Log): Promise<readonly DowngradedViolation[]>;
48
+ /**
49
+ * @experimental
50
+ * Parses git blame porcelain output into a Map of line number → commit date.
51
+ *
52
+ * In porcelain format, the first occurrence of a commit SHA includes full
53
+ * header (author-time, etc.), but subsequent occurrences of the same SHA
54
+ * only include the abbreviated header (no author-time).
55
+ * We cache timestamps by SHA to handle repeated commits.
56
+ */
57
+ export declare function parseBlamePorcelain(stdout: string): Map<number, Date>;
@@ -0,0 +1,233 @@
1
+ import { execFile } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { isFatalError, readSuppressionsFile, analyzeForDowngrade, applyDowngrade } from 'markuplint/suppressions';
4
+ const suppressionCaches = new Map();
5
+ /** Cache TTL in milliseconds. Re-reads the suppressions file after this period. */
6
+ const CACHE_TTL_MS = 30_000;
7
+ const DEFAULT_FILE_NAME = 'markuplint-suppressions.json';
8
+ /**
9
+ * Path to the git binary. Resolved from the client's `gitPath` option,
10
+ * falling back to `'git'` (system PATH lookup).
11
+ */
12
+ let gitBinaryPath = 'git';
13
+ /**
14
+ * Tracks whether git is available in this environment.
15
+ * Once determined to be unavailable (ENOENT / EACCES), all git calls are skipped
16
+ * for the lifetime of the language server to avoid repeated child process failures.
17
+ */
18
+ let gitAvailable;
19
+ /**
20
+ * @experimental
21
+ * Configures the git binary path for suppression support.
22
+ * Should be called once during server initialization.
23
+ *
24
+ * @param gitPath - Path to the git binary from the client's `gitPath` option
25
+ */
26
+ export function configureGitPath(gitPath) {
27
+ if (gitPath) {
28
+ gitBinaryPath = gitPath;
29
+ }
30
+ }
31
+ /**
32
+ * @experimental
33
+ * Resolves the suppressions file path for a workspace.
34
+ */
35
+ function findSuppressionsFile(startDir) {
36
+ return path.resolve(startDir, DEFAULT_FILE_NAME);
37
+ }
38
+ /**
39
+ * @experimental
40
+ * Loads suppression data for a workspace, with TTL-based caching.
41
+ * Returns null if no suppressions file exists.
42
+ */
43
+ export async function loadSuppressions(workspace, log) {
44
+ const filePath = findSuppressionsFile(workspace);
45
+ const cached = suppressionCaches.get(filePath);
46
+ if (cached && Date.now() < cached.expiresAt) {
47
+ return cached.cache;
48
+ }
49
+ const data = await readSuppressionsFile(filePath);
50
+ if (Object.keys(data).length === 0) {
51
+ suppressionCaches.delete(filePath);
52
+ return null;
53
+ }
54
+ const lastCommitDate = await getSuppressionsFileCommitDate(filePath, log);
55
+ const cache = { data, filePath, lastCommitDate };
56
+ suppressionCaches.set(filePath, { cache, expiresAt: Date.now() + CACHE_TTL_MS });
57
+ log(`Loaded suppressions from ${filePath} (committed: ${lastCommitDate?.toISOString() ?? 'uncommitted'})`, 'debug');
58
+ return cache;
59
+ }
60
+ /**
61
+ * @experimental
62
+ * Invalidates the suppression cache for a workspace.
63
+ * Should be called when the suppressions file changes.
64
+ */
65
+ export function invalidateSuppressionCache(workspace) {
66
+ const filePath = findSuppressionsFile(workspace);
67
+ suppressionCaches.delete(filePath);
68
+ }
69
+ /**
70
+ * @experimental
71
+ * Gets the last commit date of the suppressions file via `git log`.
72
+ * Returns null if the file is not tracked or git is unavailable.
73
+ */
74
+ async function getSuppressionsFileCommitDate(filePath, log) {
75
+ if (gitAvailable === false) {
76
+ return null;
77
+ }
78
+ try {
79
+ const stdout = await execGit(['log', '-1', '--format=%aI', '--', path.basename(filePath)], path.dirname(filePath));
80
+ gitAvailable = true;
81
+ const dateStr = stdout.trim();
82
+ if (!dateStr) {
83
+ return null;
84
+ }
85
+ return new Date(dateStr);
86
+ }
87
+ catch (error) {
88
+ if (isFatalError(error)) {
89
+ throw error;
90
+ }
91
+ if (isGitUnavailableError(error)) {
92
+ gitAvailable = false;
93
+ log('git is not available; git blame features will be disabled', 'warn');
94
+ }
95
+ else {
96
+ log('git log for suppressions file failed; git blame will be disabled', 'debug');
97
+ }
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * @experimental
103
+ * Gets blame dates for specific lines in a file.
104
+ * Returns a Map of line number → commit date.
105
+ */
106
+ async function getBlameForLines(filePath, lines, log) {
107
+ const result = new Map();
108
+ if (lines.length === 0 || gitAvailable === false) {
109
+ return result;
110
+ }
111
+ try {
112
+ const args = ['blame', '--porcelain'];
113
+ for (const line of lines) {
114
+ args.push('-L', `${line},${line}`);
115
+ }
116
+ args.push('--', filePath);
117
+ const stdout = await execGit(args, path.dirname(filePath));
118
+ return parseBlamePorcelain(stdout);
119
+ }
120
+ catch (error) {
121
+ if (isFatalError(error)) {
122
+ throw error;
123
+ }
124
+ if (isGitUnavailableError(error)) {
125
+ gitAvailable = false;
126
+ log('git is not available; git blame features will be disabled', 'warn');
127
+ }
128
+ else {
129
+ log(`git blame failed for ${filePath}; treating all violations as old`, 'debug');
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+ /**
135
+ * @experimental
136
+ * Applies suppression severity downgrade to violations.
137
+ *
138
+ * - If count <= suppressed count: all violations for that rule → info
139
+ * - If count > suppressed count: use git blame to distinguish old (→ info) vs new (→ original)
140
+ * - Falls back to original severity when git is unavailable
141
+ *
142
+ * The git blame date comparison uses the suppressions file's last commit date
143
+ * as the threshold. Lines authored before that date are considered "old"
144
+ * (existed when suppressions were generated) and downgraded to info.
145
+ * Lines authored after are considered "new" and keep their original severity.
146
+ */
147
+ export async function applySuppressionsToViolations(absoluteFilePath, violations,
148
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
149
+ cache, log) {
150
+ const { withinThreshold, exceedingThreshold } = analyzeForDowngrade(absoluteFilePath, violations, cache.data, cache.filePath);
151
+ // Collect all indices to downgrade
152
+ const downgradeIndices = new Set(withinThreshold);
153
+ // For exceeding threshold rules, use git blame if available
154
+ if (exceedingThreshold.size > 0 && cache.lastCommitDate) {
155
+ const uniqueLines = new Set();
156
+ for (const indices of exceedingThreshold.values()) {
157
+ for (const idx of indices) {
158
+ uniqueLines.add(violations[idx].line);
159
+ }
160
+ }
161
+ const blameDates = await getBlameForLines(absoluteFilePath, [...uniqueLines], log);
162
+ for (const indices of exceedingThreshold.values()) {
163
+ for (const idx of indices) {
164
+ const violation = violations[idx];
165
+ const blameDate = blameDates.get(violation.line);
166
+ if (blameDate && blameDate <= cache.lastCommitDate) {
167
+ downgradeIndices.add(idx);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ return applyDowngrade(violations, downgradeIndices);
173
+ }
174
+ /**
175
+ * @experimental
176
+ * Parses git blame porcelain output into a Map of line number → commit date.
177
+ *
178
+ * In porcelain format, the first occurrence of a commit SHA includes full
179
+ * header (author-time, etc.), but subsequent occurrences of the same SHA
180
+ * only include the abbreviated header (no author-time).
181
+ * We cache timestamps by SHA to handle repeated commits.
182
+ */
183
+ export function parseBlamePorcelain(stdout) {
184
+ const result = new Map();
185
+ const commitTimestamps = new Map();
186
+ const shaLinePattern = /^([0-9a-f]{40}) \d+ (\d+)/gm;
187
+ const authorTimePattern = /^author-time (\d+)$/gm;
188
+ let shaMatch;
189
+ while ((shaMatch = shaLinePattern.exec(stdout)) !== null) {
190
+ const sha = shaMatch[1];
191
+ const lineNo = Number.parseInt(shaMatch[2], 10);
192
+ if (!commitTimestamps.has(sha)) {
193
+ authorTimePattern.lastIndex = shaMatch.index;
194
+ const timeMatch = authorTimePattern.exec(stdout);
195
+ if (timeMatch) {
196
+ const timestamp = Number.parseInt(timeMatch[1], 10);
197
+ commitTimestamps.set(sha, new Date(timestamp * 1000));
198
+ }
199
+ }
200
+ const date = commitTimestamps.get(sha);
201
+ if (date) {
202
+ result.set(lineNo, date);
203
+ }
204
+ }
205
+ return result;
206
+ }
207
+ /** Timeout for git commands (log, blame) in milliseconds. */
208
+ const GIT_TIMEOUT_MS = 5000;
209
+ /**
210
+ * Executes a git command and returns stdout.
211
+ */
212
+ function execGit(args, cwd) {
213
+ return new Promise((resolve, reject) => {
214
+ execFile(gitBinaryPath, args, { cwd, timeout: GIT_TIMEOUT_MS }, (error, stdout) => {
215
+ if (error) {
216
+ reject(error);
217
+ return;
218
+ }
219
+ resolve(stdout);
220
+ });
221
+ });
222
+ }
223
+ /**
224
+ * Checks if an error indicates git is not available (not installed or permission denied).
225
+ * Used to permanently disable git features after the first failure.
226
+ */
227
+ function isGitUnavailableError(error) {
228
+ if (!(error instanceof Error)) {
229
+ return false;
230
+ }
231
+ const code = error.code;
232
+ return code === 'ENOENT' || code === 'EACCES';
233
+ }
@@ -0,0 +1,12 @@
1
+ import type { SendDiagnostics } from './document-events.js';
2
+ import type { Config, Log } from '../types.js';
3
+ import type { MLEngine as _MLEngine } from 'markuplint';
4
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
5
+ export declare function onDidOpen(document: TextDocument, MLEngine: typeof _MLEngine, config: Config, locale: string, sendDiagnostics: SendDiagnostics, notFoundParserError: (e: unknown) => void, workspace: string, log?: Log): Promise<void>;
6
+ export declare function onDidChangeContent(document: TextDocument, notFoundParserError: (e: unknown) => void): void;
7
+ /**
8
+ * Handles the `textDocument/didClose` event by releasing the engine of the document.
9
+ *
10
+ * @param uri - The URI of the closed document
11
+ */
12
+ export declare function onDidClose(uri: string): Promise<void>;
@@ -0,0 +1,109 @@
1
+ import path from 'node:path';
2
+ import { getFilePath } from '../utils/get-file-path.js';
3
+ import { convertDiagnostics } from './convert-diagnostics.js';
4
+ const engines = new Map();
5
+ export async function onDidOpen(
6
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
7
+ document,
8
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
9
+ MLEngine,
10
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
11
+ config, locale, sendDiagnostics, notFoundParserError, workspace, log) {
12
+ const key = document.uri;
13
+ console.log(`Opened: ${key}`);
14
+ const currentEngine = engines.get(key);
15
+ if (currentEngine) {
16
+ return;
17
+ }
18
+ const filePath = getFilePath(document.uri, document.languageId);
19
+ if (config.debug) {
20
+ console.log(filePath);
21
+ }
22
+ const absoluteFilePath = `${filePath.dirname}/${filePath.basename}`;
23
+ const sourceCode = document.getText();
24
+ // `name` must be the workspace-relative path (not just the basename), or
25
+ // MLFile.path collapses subdirectory files to workspace-root + basename —
26
+ // which breaks pretender import resolution, nested config discovery, and
27
+ // excludeFiles/overrides glob matching for anything outside the workspace root.
28
+ const relativeFilePath = path.relative(workspace, absoluteFilePath);
29
+ const file = await MLEngine.toMLFile({ sourceCode, name: relativeFilePath, workspace });
30
+ if (!file) {
31
+ console.warn(`File not found: ${filePath.basename}`);
32
+ return;
33
+ }
34
+ const engine = new MLEngine(file, {
35
+ locale,
36
+ debug: config.debug,
37
+ defaultConfig: config.defaultConfig,
38
+ watch: true,
39
+ });
40
+ engines.set(key, engine);
41
+ engine.on('config', (filePath, configSet) => {
42
+ if (config.debug) {
43
+ console.log(`get config: ${filePath}`, configSet);
44
+ }
45
+ });
46
+ engine.on('log', (phase, message) => {
47
+ if (config.debug) {
48
+ console.log(phase, message);
49
+ }
50
+ });
51
+ engine.on('lint-error', (_filePath, _sourceCode, error) => {
52
+ if (config.debug) {
53
+ console.log('❌', { error });
54
+ }
55
+ });
56
+ engine.on('lint', (filePath, sourceCode, violations, fixedCode, debug) => {
57
+ if (config.debug && debug) {
58
+ console.log(debug.join('\n'));
59
+ }
60
+ const date = new Date().toLocaleDateString();
61
+ const time = new Date().toLocaleTimeString();
62
+ console.log(`Linted(${date} ${time}): ${document.uri}`);
63
+ const diagnostics = convertDiagnostics({ filePath, sourceCode, violations, fixedCode, status: 'processed' });
64
+ sendDiagnostics({
65
+ uri: document.uri,
66
+ diagnostics,
67
+ });
68
+ console.log(`diagnostics: ${diagnostics.length}`);
69
+ });
70
+ console.log('exec (onDidOpen)');
71
+ engine.exec().catch((error) => notFoundParserError(error));
72
+ }
73
+ let debounceTimer;
74
+ export function onDidChangeContent(
75
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
76
+ document, notFoundParserError) {
77
+ clearTimeout(debounceTimer);
78
+ const key = document.uri;
79
+ const engine = engines.get(key);
80
+ debounceTimer = setTimeout(async () => {
81
+ // The document may have been closed within the debounce delay.
82
+ if (!engine || engines.get(key) !== engine) {
83
+ return;
84
+ }
85
+ const code = document.getText();
86
+ try {
87
+ await engine.setCode(code);
88
+ console.log('exec (onDidChangeContent)');
89
+ engine.exec().catch((error) => notFoundParserError(error));
90
+ }
91
+ catch (error) {
92
+ console.log(error);
93
+ // continue;
94
+ }
95
+ }, 300);
96
+ }
97
+ /**
98
+ * Handles the `textDocument/didClose` event by releasing the engine of the document.
99
+ *
100
+ * @param uri - The URI of the closed document
101
+ */
102
+ export async function onDidClose(uri) {
103
+ const engine = engines.get(uri);
104
+ if (!engine) {
105
+ return;
106
+ }
107
+ engines.delete(uri);
108
+ await engine.close();
109
+ }
@@ -0,0 +1,19 @@
1
+ import type { SendDiagnostics } from './document-events.js';
2
+ import type { Config, Log } from '../types.js';
3
+ import type { ARIAVersion } from '@markuplint/ml-spec';
4
+ import type { MLEngine as _MLEngine } from 'markuplint';
5
+ import type { Position, TextDocumentIdentifier } from 'vscode-languageserver';
6
+ import type { TextDocument } from 'vscode-languageserver-textdocument';
7
+ 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>;
8
+ export declare function onDidChangeContent(document: TextDocument, log: Log, notFoundParserError: (e: unknown) => void): void;
9
+ /**
10
+ * Handles the `textDocument/didClose` event by releasing the engine of the document.
11
+ *
12
+ * @param uri - The URI of the closed document
13
+ */
14
+ export declare function onDidClose(uri: string): Promise<void>;
15
+ export declare function getNodeWithAccessibilityProps(textDocument: TextDocumentIdentifier, position: Position, ariaVersion: ARIAVersion): {
16
+ nodeName: string;
17
+ exposed: boolean;
18
+ aria: Record<string, string>;
19
+ } | null;