@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.
- package/LICENSE +21 -0
- package/README.md +74 -2
- package/bin/markuplint-language-server.mjs +3 -0
- package/lib/const.d.ts +41 -0
- package/lib/const.js +44 -0
- package/lib/i18n.d.ts +19 -0
- package/lib/i18n.js +63 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.js +40 -0
- package/lib/lsp.d.ts +8 -0
- package/lib/lsp.js +8 -0
- package/lib/protocol.d.ts +10 -0
- package/lib/protocol.js +9 -0
- package/lib/server/client-channel.d.ts +38 -0
- package/lib/server/client-channel.js +89 -0
- package/lib/server/code-actions.d.ts +36 -0
- package/lib/server/code-actions.js +101 -0
- package/lib/server/convert-diagnostics.d.ts +28 -0
- package/lib/server/convert-diagnostics.js +82 -0
- package/lib/server/debug.d.ts +3 -0
- package/lib/server/debug.js +7 -0
- package/lib/server/document-events.d.ts +78 -0
- package/lib/server/document-events.js +299 -0
- package/lib/server/get-accessibility-by-location.d.ts +6 -0
- package/lib/server/get-accessibility-by-location.js +19 -0
- package/lib/server/get-module.d.ts +127 -0
- package/lib/server/get-module.js +257 -0
- package/lib/server/index.d.ts +1 -0
- package/lib/server/index.js +5 -0
- package/lib/server/initialize-params.d.ts +32 -0
- package/lib/server/initialize-params.js +28 -0
- package/lib/server/module-notices.d.ts +28 -0
- package/lib/server/module-notices.js +41 -0
- package/lib/server/parser-load-failure.d.ts +36 -0
- package/lib/server/parser-load-failure.js +85 -0
- package/lib/server/server.d.ts +20 -0
- package/lib/server/server.js +96 -0
- package/lib/server/suppression-support.d.ts +57 -0
- package/lib/server/suppression-support.js +233 -0
- package/lib/server/v2.d.ts +12 -0
- package/lib/server/v2.js +109 -0
- package/lib/server/v3.d.ts +19 -0
- package/lib/server/v3.js +208 -0
- package/lib/server/v4.d.ts +85 -0
- package/lib/server/v4.js +292 -0
- package/lib/server/v5.d.ts +89 -0
- package/lib/server/v5.js +339 -0
- package/lib/types.d.ts +88 -0
- package/lib/types.js +1 -0
- package/lib/utils/get-file-path.d.ts +15 -0
- package/lib/utils/get-file-path.js +45 -0
- package/lib/utils/resolve-working-directory.d.ts +44 -0
- package/lib/utils/resolve-working-directory.js +266 -0
- package/locales/en.json +7 -0
- package/locales/ja.json +21 -0
- package/package.json +59 -5
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CodeActionKind } from 'vscode-languageserver';
|
|
2
|
+
/**
|
|
3
|
+
* Code Action kind for "Fix all markuplint issues".
|
|
4
|
+
* Follows the `source.fixAll.<provider>` convention used by ESLint and others.
|
|
5
|
+
*/
|
|
6
|
+
export const SOURCE_FIX_ALL_MARKUPLINT = `${CodeActionKind.SourceFixAll}.markuplint`;
|
|
7
|
+
/**
|
|
8
|
+
* Converts a 0-based character offset to an LSP Position using the source code.
|
|
9
|
+
*
|
|
10
|
+
* @param sourceCode - The full raw source text
|
|
11
|
+
* @param offset - A zero-based character offset into the source
|
|
12
|
+
* @returns A 0-based LSP Position (line and character)
|
|
13
|
+
*/
|
|
14
|
+
export function offsetToPosition(sourceCode, offset) {
|
|
15
|
+
const lines = sourceCode.slice(0, offset).split('\n');
|
|
16
|
+
return { line: lines.length - 1, character: (lines.at(-1) ?? '').length };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Converts a markuplint TextEdit (offset-based) to an LSP Range.
|
|
20
|
+
*/
|
|
21
|
+
function toRange(sourceCode, edit) {
|
|
22
|
+
return {
|
|
23
|
+
start: offsetToPosition(sourceCode, edit.range[0]),
|
|
24
|
+
end: offsetToPosition(sourceCode, edit.range[1]),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates per-violation QuickFix Code Actions.
|
|
29
|
+
*
|
|
30
|
+
* @param uri - The document URI to associate edits with
|
|
31
|
+
* @param diagnostics - The diagnostics from the Code Action request context
|
|
32
|
+
* @param fixState - The latest lint result containing violations and fix data
|
|
33
|
+
* @returns An array of QuickFix Code Actions, one per fixable violation
|
|
34
|
+
*/
|
|
35
|
+
export function createQuickFixActions(uri,
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
37
|
+
diagnostics, fixState) {
|
|
38
|
+
const actions = [];
|
|
39
|
+
for (const diagnostic of diagnostics) {
|
|
40
|
+
const index = diagnostic.data?.violationIndex;
|
|
41
|
+
if (index == null)
|
|
42
|
+
continue;
|
|
43
|
+
const violation = fixState.violations[index];
|
|
44
|
+
if (!violation?.fix)
|
|
45
|
+
continue;
|
|
46
|
+
const edits = violation.fix.edits.map(edit => ({
|
|
47
|
+
range: toRange(fixState.sourceCode, edit),
|
|
48
|
+
newText: edit.text,
|
|
49
|
+
}));
|
|
50
|
+
actions.push({
|
|
51
|
+
title: `Fix: ${diagnostic.message}`,
|
|
52
|
+
kind: CodeActionKind.QuickFix,
|
|
53
|
+
diagnostics: [diagnostic],
|
|
54
|
+
isPreferred: true,
|
|
55
|
+
edit: { changes: { [uri]: edits } },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return actions;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates a SourceFixAll Code Action (`source.fixAll.markuplint`).
|
|
62
|
+
*
|
|
63
|
+
* Uses firstPassEdits from FixSummary for cursor-safe individual edits.
|
|
64
|
+
* Falls back to full-document replacement when firstPassEdits is unavailable.
|
|
65
|
+
*
|
|
66
|
+
* @param uri - The document URI to associate edits with
|
|
67
|
+
* @param diagnostics - The diagnostics to attach to the Code Action
|
|
68
|
+
* @param fixState - The latest lint result containing violations and fix data
|
|
69
|
+
* @returns A SourceFixAll Code Action, or `null` if nothing to fix
|
|
70
|
+
*/
|
|
71
|
+
export function createFixAllAction(uri,
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
73
|
+
diagnostics, fixState) {
|
|
74
|
+
if (fixState.fixedCode === fixState.sourceCode)
|
|
75
|
+
return null;
|
|
76
|
+
let textEdits;
|
|
77
|
+
if (fixState.fixSummary?.firstPassEdits && fixState.fixSummary.firstPassEdits.length > 0) {
|
|
78
|
+
textEdits = fixState.fixSummary.firstPassEdits.map(edit => ({
|
|
79
|
+
range: toRange(fixState.sourceCode, edit),
|
|
80
|
+
newText: edit.text,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const lines = fixState.sourceCode.split('\n');
|
|
85
|
+
textEdits = [
|
|
86
|
+
{
|
|
87
|
+
range: {
|
|
88
|
+
start: { line: 0, character: 0 },
|
|
89
|
+
end: { line: lines.length - 1, character: lines.at(-1).length },
|
|
90
|
+
},
|
|
91
|
+
newText: fixState.fixedCode,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
title: 'Fix all markuplint issues',
|
|
97
|
+
kind: SOURCE_FIX_ALL_MARKUPLINT,
|
|
98
|
+
diagnostics: [...diagnostics],
|
|
99
|
+
edit: { changes: { [uri]: textEdits } },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MLResultInfo } from 'markuplint';
|
|
2
|
+
import type { SeverityMap } from '../types.js';
|
|
3
|
+
import type { Diagnostic } from 'vscode-languageserver/node.js';
|
|
4
|
+
/**
|
|
5
|
+
* Re-reports diagnostics with the severities a client asked for.
|
|
6
|
+
*
|
|
7
|
+
* Runs on the LSP severity rather than on the markuplint one, so one place covers the v2–v5
|
|
8
|
+
* handlers. `convertDiagnostics` maps the two one to one, which makes the keys of the map
|
|
9
|
+
* mean the same on both sides.
|
|
10
|
+
*
|
|
11
|
+
* @param diagnostics - The diagnostics to re-report
|
|
12
|
+
* @param map - The client's `severityMap`; severities it does not list are kept
|
|
13
|
+
* @returns New diagnostics. The input is not modified.
|
|
14
|
+
*/
|
|
15
|
+
export declare function applySeverityMap<T extends Diagnostic>(diagnostics: readonly T[], map: SeverityMap | undefined): T[];
|
|
16
|
+
/**
|
|
17
|
+
* Converts markuplint violations into LSP diagnostics.
|
|
18
|
+
*
|
|
19
|
+
* Each diagnostic includes a `data.violationIndex` property that maps back
|
|
20
|
+
* to the original violation for use in Code Action resolution.
|
|
21
|
+
*
|
|
22
|
+
* @param result - The markuplint lint result, or null if no result is available
|
|
23
|
+
* @returns An array of LSP diagnostics augmented with line and col numbers
|
|
24
|
+
*/
|
|
25
|
+
export declare function convertDiagnostics(result: MLResultInfo | null): (Diagnostic & {
|
|
26
|
+
line: number;
|
|
27
|
+
col: number;
|
|
28
|
+
})[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from 'vscode-languageserver/node.js';
|
|
2
|
+
import { NAME, WEBSITE_URL_RULE_PAGE } from '../const.js';
|
|
3
|
+
const REPORTED = {
|
|
4
|
+
error: DiagnosticSeverity.Error,
|
|
5
|
+
warning: DiagnosticSeverity.Warning,
|
|
6
|
+
info: DiagnosticSeverity.Information,
|
|
7
|
+
hint: DiagnosticSeverity.Hint,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Re-reports diagnostics with the severities a client asked for.
|
|
11
|
+
*
|
|
12
|
+
* Runs on the LSP severity rather than on the markuplint one, so one place covers the v2–v5
|
|
13
|
+
* handlers. `convertDiagnostics` maps the two one to one, which makes the keys of the map
|
|
14
|
+
* mean the same on both sides.
|
|
15
|
+
*
|
|
16
|
+
* @param diagnostics - The diagnostics to re-report
|
|
17
|
+
* @param map - The client's `severityMap`; severities it does not list are kept
|
|
18
|
+
* @returns New diagnostics. The input is not modified.
|
|
19
|
+
*/
|
|
20
|
+
export function applySeverityMap(diagnostics, map) {
|
|
21
|
+
if (!map) {
|
|
22
|
+
return [...diagnostics];
|
|
23
|
+
}
|
|
24
|
+
return diagnostics.map(diagnostic => {
|
|
25
|
+
const from = diagnostic.severity === DiagnosticSeverity.Error
|
|
26
|
+
? 'error'
|
|
27
|
+
: diagnostic.severity === DiagnosticSeverity.Warning
|
|
28
|
+
? 'warning'
|
|
29
|
+
: diagnostic.severity === DiagnosticSeverity.Information
|
|
30
|
+
? 'info'
|
|
31
|
+
: undefined;
|
|
32
|
+
const to = from && map[from];
|
|
33
|
+
return to ? { ...diagnostic, severity: REPORTED[to] } : diagnostic;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Converts markuplint violations into LSP diagnostics.
|
|
38
|
+
*
|
|
39
|
+
* Each diagnostic includes a `data.violationIndex` property that maps back
|
|
40
|
+
* to the original violation for use in Code Action resolution.
|
|
41
|
+
*
|
|
42
|
+
* @param result - The markuplint lint result, or null if no result is available
|
|
43
|
+
* @returns An array of LSP diagnostics augmented with line and col numbers
|
|
44
|
+
*/
|
|
45
|
+
export function convertDiagnostics(result) {
|
|
46
|
+
const diagnostics = [];
|
|
47
|
+
if (!result) {
|
|
48
|
+
return diagnostics;
|
|
49
|
+
}
|
|
50
|
+
for (const [i, violation] of result.violations.entries()) {
|
|
51
|
+
diagnostics.push({
|
|
52
|
+
severity: violation.severity === 'error'
|
|
53
|
+
? DiagnosticSeverity.Error
|
|
54
|
+
: violation.severity === 'warning'
|
|
55
|
+
? DiagnosticSeverity.Warning
|
|
56
|
+
: DiagnosticSeverity.Information,
|
|
57
|
+
line: violation.line,
|
|
58
|
+
col: violation.col,
|
|
59
|
+
range: {
|
|
60
|
+
start: {
|
|
61
|
+
line: Math.max(violation.line - 1, 0),
|
|
62
|
+
character: Math.max(violation.col - 1, 0),
|
|
63
|
+
},
|
|
64
|
+
end: {
|
|
65
|
+
line: Math.max(violation.line - 1, 0),
|
|
66
|
+
character: Math.max(violation.col + violation.raw.length - 1, 0),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
message: violation.message +
|
|
70
|
+
(violation.specConformance ? ` [${violation.specConformance}]` : '') +
|
|
71
|
+
(violation.reason ? ' / ' + violation.reason : '') +
|
|
72
|
+
(violation.name ? ` (${violation.ruleId})` : ''),
|
|
73
|
+
source: NAME,
|
|
74
|
+
code: violation.name ?? violation.ruleId,
|
|
75
|
+
codeDescription: {
|
|
76
|
+
href: `${WEBSITE_URL_RULE_PAGE}${violation.ruleId}`,
|
|
77
|
+
},
|
|
78
|
+
data: { violationIndex: i },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return diagnostics;
|
|
82
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Module, ModuleResolver } from './get-module.js';
|
|
2
|
+
import type { Config, LangConfigs, Log } from '../types.js';
|
|
3
|
+
import type { WorkingDirectoryEntry } from '../utils/resolve-working-directory.js';
|
|
4
|
+
import type { CodeAction, CodeActionParams, PublishDiagnosticsParams, HoverParams } from 'vscode-languageserver/node.js';
|
|
5
|
+
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
6
|
+
/**
|
|
7
|
+
* The configuration of a language for a client that sends no `langConfigs`. It enables
|
|
8
|
+
* linting and leaves `defaultConfig` unset, so config discovery and the
|
|
9
|
+
* `markuplint:recommended` fallback behave as they do in the CLI.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_LANG_CONFIG: Config;
|
|
12
|
+
/**
|
|
13
|
+
* Callback for publishing diagnostics from the language server to the client.
|
|
14
|
+
*/
|
|
15
|
+
export type SendDiagnostics = (params: PublishDiagnosticsParams) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Options for creating the document event handlers.
|
|
18
|
+
*/
|
|
19
|
+
export type EventHandlerOptions = {
|
|
20
|
+
/** Loads the markuplint module for a document's working directory */
|
|
21
|
+
resolveModule: ModuleResolver;
|
|
22
|
+
/** The user's locale (e.g. `"en"`, `"ja"`) */
|
|
23
|
+
locale: string;
|
|
24
|
+
/**
|
|
25
|
+
* Per-language configuration from the client's settings. Without it every document is
|
|
26
|
+
* linted with {@link DEFAULT_LANG_CONFIG}.
|
|
27
|
+
*/
|
|
28
|
+
langConfigs?: LangConfigs;
|
|
29
|
+
/** User-configured working directories for monorepo support */
|
|
30
|
+
workingDirectories?: readonly WorkingDirectoryEntry[];
|
|
31
|
+
/** Absolute paths of the workspace folders */
|
|
32
|
+
workspaceFolders: readonly string[];
|
|
33
|
+
log: Log;
|
|
34
|
+
diagnosticsLog: Log;
|
|
35
|
+
errorLog: Log;
|
|
36
|
+
sendDiagnostics: SendDiagnostics;
|
|
37
|
+
/**
|
|
38
|
+
* Called on every document open with the module that document is linted with.
|
|
39
|
+
* The status bar therefore reflects the most recently opened document, not the active editor.
|
|
40
|
+
*/
|
|
41
|
+
reportStatus: (mod: Module) => void;
|
|
42
|
+
/** Path to the git binary used for bulk suppression. */
|
|
43
|
+
gitPath?: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Determines the working directory a document belongs to.
|
|
47
|
+
*
|
|
48
|
+
* Both config discovery and markuplint module resolution are anchored here, so the two never
|
|
49
|
+
* disagree about which installation governs the document.
|
|
50
|
+
*
|
|
51
|
+
* @param document - The text document
|
|
52
|
+
* @param workspaceFolders - Absolute paths of the workspace folders
|
|
53
|
+
* @param workingDirectories - User-configured working directories
|
|
54
|
+
* @param log - Logger for general messages
|
|
55
|
+
* @returns The matched working directory, or the document's own directory when nothing matches
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveDocumentWorkspace(document: TextDocument, workspaceFolders: readonly string[], workingDirectories: readonly WorkingDirectoryEntry[] | undefined, log: Log): string;
|
|
58
|
+
/**
|
|
59
|
+
* Creates version-aware event handlers for document open, change, code action, and hover events.
|
|
60
|
+
*
|
|
61
|
+
* Each document is linted with the markuplint module resolved from its own working directory,
|
|
62
|
+
* and dispatched to the handler (v2, v3, v4, or v5) matching that module's version.
|
|
63
|
+
*
|
|
64
|
+
* @param options - Configuration including the module resolver, locale, and settings
|
|
65
|
+
* @returns An object containing `onDidOpen`, `onDidChangeContent`, `onDidClose`, `onCodeAction`, and `onHover` handlers
|
|
66
|
+
*/
|
|
67
|
+
export declare function createEventHandlers(options: EventHandlerOptions): {
|
|
68
|
+
onDidOpen(document: TextDocument): void;
|
|
69
|
+
onDidChangeContent(document: TextDocument): void;
|
|
70
|
+
onDidClose(document: TextDocument): void;
|
|
71
|
+
onCodeAction(params: CodeActionParams): CodeAction[];
|
|
72
|
+
onHover(params: HoverParams): Promise<{
|
|
73
|
+
contents: {
|
|
74
|
+
kind: "markdown";
|
|
75
|
+
value: string;
|
|
76
|
+
};
|
|
77
|
+
} | undefined>;
|
|
78
|
+
};
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { isFatalError } from 'markuplint/suppressions';
|
|
2
|
+
import { satisfies, lt, gte } from 'semver';
|
|
3
|
+
import { MarkupKind } from 'vscode-languageserver/node.js';
|
|
4
|
+
import { t } from '../i18n.js';
|
|
5
|
+
import { getFilePath } from '../utils/get-file-path.js';
|
|
6
|
+
import { resolveWorkingDirectory } from '../utils/resolve-working-directory.js';
|
|
7
|
+
import { createParserErrorHandler } from './parser-load-failure.js';
|
|
8
|
+
import * as v2 from './v2.js';
|
|
9
|
+
import * as v3 from './v3.js';
|
|
10
|
+
import * as v4 from './v4.js';
|
|
11
|
+
import * as v5 from './v5.js';
|
|
12
|
+
/**
|
|
13
|
+
* The configuration of a language for a client that sends no `langConfigs`. It enables
|
|
14
|
+
* linting and leaves `defaultConfig` unset, so config discovery and the
|
|
15
|
+
* `markuplint:recommended` fallback behave as they do in the CLI.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_LANG_CONFIG = {
|
|
18
|
+
enable: true,
|
|
19
|
+
debug: false,
|
|
20
|
+
hover: { accessibility: { enable: true } },
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Determines the working directory a document belongs to.
|
|
24
|
+
*
|
|
25
|
+
* Both config discovery and markuplint module resolution are anchored here, so the two never
|
|
26
|
+
* disagree about which installation governs the document.
|
|
27
|
+
*
|
|
28
|
+
* @param document - The text document
|
|
29
|
+
* @param workspaceFolders - Absolute paths of the workspace folders
|
|
30
|
+
* @param workingDirectories - User-configured working directories
|
|
31
|
+
* @param log - Logger for general messages
|
|
32
|
+
* @returns The matched working directory, or the document's own directory when nothing matches
|
|
33
|
+
*/
|
|
34
|
+
export function resolveDocumentWorkspace(
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
36
|
+
document, workspaceFolders, workingDirectories, log) {
|
|
37
|
+
const filePath = getFilePath(document.uri, document.languageId);
|
|
38
|
+
const absoluteFilePath = `${filePath.dirname}/${filePath.basename}`;
|
|
39
|
+
const resolved = resolveWorkingDirectory(absoluteFilePath, workspaceFolders, workingDirectories);
|
|
40
|
+
const workspace = resolved?.directory ?? filePath.dirname;
|
|
41
|
+
if (resolved) {
|
|
42
|
+
log(`Resolved working directory: ${workspace} (for ${filePath.basename})`, 'debug');
|
|
43
|
+
}
|
|
44
|
+
return workspace;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates version-aware event handlers for document open, change, code action, and hover events.
|
|
48
|
+
*
|
|
49
|
+
* Each document is linted with the markuplint module resolved from its own working directory,
|
|
50
|
+
* and dispatched to the handler (v2, v3, v4, or v5) matching that module's version.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Configuration including the module resolver, locale, and settings
|
|
53
|
+
* @returns An object containing `onDidOpen`, `onDidChangeContent`, `onDidClose`, `onCodeAction`, and `onHover` handlers
|
|
54
|
+
*/
|
|
55
|
+
export function createEventHandlers(
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
57
|
+
options) {
|
|
58
|
+
const documentModules = new Map();
|
|
59
|
+
const queues = new Map();
|
|
60
|
+
/**
|
|
61
|
+
* Documents whose close has been received but not yet carried out. The module of a
|
|
62
|
+
* document is only forgotten when the queued close runs, which is after any open still in
|
|
63
|
+
* progress; until then change, code action and hover requests must not reach the engine.
|
|
64
|
+
*/
|
|
65
|
+
const closedDocuments = new Map();
|
|
66
|
+
function activeModule(uri) {
|
|
67
|
+
return closedDocuments.has(uri) ? undefined : documentModules.get(uri);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs `task` after every earlier open/close of the same document has finished.
|
|
71
|
+
*
|
|
72
|
+
* Opening is asynchronous (module resolution, then engine setup), so a close that
|
|
73
|
+
* arrives meanwhile would find nothing to release and the engine created afterwards
|
|
74
|
+
* would leak; a reopen that overlapped the release would start a second engine on
|
|
75
|
+
* the file while the first one's watcher is still shutting down.
|
|
76
|
+
*
|
|
77
|
+
* `task` must handle its own failures; only an error that must not be swallowed (a
|
|
78
|
+
* fatal one) may escape, and it is rethrown outside the queue so that it surfaces
|
|
79
|
+
* as an uncaught exception instead of stalling the document's later events.
|
|
80
|
+
*/
|
|
81
|
+
function enqueue(uri, task) {
|
|
82
|
+
const tail = (queues.get(uri) ?? Promise.resolve())
|
|
83
|
+
.then(task)
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
queueMicrotask(() => {
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
88
|
+
})
|
|
89
|
+
.then(() => {
|
|
90
|
+
if (queues.get(uri) === tail) {
|
|
91
|
+
queues.delete(uri);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
queues.set(uri, tail);
|
|
95
|
+
}
|
|
96
|
+
function parserErrorHandler(mod, languageId) {
|
|
97
|
+
return createParserErrorHandler({
|
|
98
|
+
languageId,
|
|
99
|
+
isLocalModule: mod.isLocalModule,
|
|
100
|
+
version: mod.version,
|
|
101
|
+
workspace: mod.workspace,
|
|
102
|
+
fallbackReason: mod.fallbackReason,
|
|
103
|
+
}, options.errorLog);
|
|
104
|
+
}
|
|
105
|
+
function dispatchOpen(mod,
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
107
|
+
document,
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
109
|
+
langConfig, workspace) {
|
|
110
|
+
const onError = parserErrorHandler(mod, document.languageId);
|
|
111
|
+
if (satisfies(mod.version, '2.x')) {
|
|
112
|
+
return v2.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.sendDiagnostics, onError, workspace, options.log);
|
|
113
|
+
}
|
|
114
|
+
if (satisfies(mod.version, '3.x')) {
|
|
115
|
+
return v3.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.log, options.diagnosticsLog, options.sendDiagnostics, onError, workspace);
|
|
116
|
+
}
|
|
117
|
+
if (satisfies(mod.version, '4.x')) {
|
|
118
|
+
return v4.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.log, options.diagnosticsLog, options.sendDiagnostics, onError, workspace);
|
|
119
|
+
}
|
|
120
|
+
// v5+
|
|
121
|
+
return v5.onDidOpen(document, mod.markuplint.MLEngine, langConfig, options.locale, options.log, options.diagnosticsLog, options.sendDiagnostics, onError, workspace);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
onDidOpen(
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
126
|
+
document) {
|
|
127
|
+
const languageId = document.languageId;
|
|
128
|
+
const langConfig = options.langConfigs ? (options.langConfigs[languageId] ?? null) : DEFAULT_LANG_CONFIG;
|
|
129
|
+
if (!langConfig?.enable) {
|
|
130
|
+
options.log(`Disabled for languageId:${languageId} according to the settings of the client.`, 'warn');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
options.log(`Evaluate ${document.uri} from languageId:${languageId}`, 'info');
|
|
134
|
+
const workspace = resolveDocumentWorkspace(document, options.workspaceFolders, options.workingDirectories, options.log);
|
|
135
|
+
closedDocuments.delete(document.uri);
|
|
136
|
+
enqueue(document.uri, async () => {
|
|
137
|
+
let mod;
|
|
138
|
+
try {
|
|
139
|
+
mod = documentModules.get(document.uri) ?? (await options.resolveModule(workspace));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (isFatalError(error)) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
options.errorLog(`Failed to load markuplint for ${workspace}: ${error}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Recorded before the engine exists so change events arriving during the
|
|
149
|
+
// asynchronous engine setup are routed to the same version handler.
|
|
150
|
+
documentModules.set(document.uri, mod);
|
|
151
|
+
options.reportStatus(mod);
|
|
152
|
+
try {
|
|
153
|
+
await dispatchOpen(mod, document, langConfig, workspace);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (isFatalError(error)) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
options.errorLog(`Failed to open ${document.uri} with markuplint ${mod.version}: ${error}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
onDidChangeContent(
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
165
|
+
document) {
|
|
166
|
+
const mod = activeModule(document.uri);
|
|
167
|
+
if (!mod) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const onError = parserErrorHandler(mod, document.languageId);
|
|
171
|
+
if (satisfies(mod.version, '2.x')) {
|
|
172
|
+
v2.onDidChangeContent(document, onError);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (satisfies(mod.version, '3.x')) {
|
|
176
|
+
v3.onDidChangeContent(document, options.log, onError);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (satisfies(mod.version, '4.x')) {
|
|
180
|
+
v4.onDidChangeContent(document, options.log, onError);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// v5+
|
|
184
|
+
v5.onDidChangeContent(document, options.log, onError);
|
|
185
|
+
},
|
|
186
|
+
onDidClose(
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
188
|
+
document) {
|
|
189
|
+
const uri = document.uri;
|
|
190
|
+
const closing = Symbol(uri);
|
|
191
|
+
closedDocuments.set(uri, closing);
|
|
192
|
+
enqueue(uri, async () => {
|
|
193
|
+
const mod = documentModules.get(uri);
|
|
194
|
+
if (!mod) {
|
|
195
|
+
if (closedDocuments.get(uri) === closing) {
|
|
196
|
+
closedDocuments.delete(uri);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
documentModules.delete(uri);
|
|
201
|
+
const release = satisfies(mod.version, '2.x')
|
|
202
|
+
? v2.onDidClose
|
|
203
|
+
: satisfies(mod.version, '3.x')
|
|
204
|
+
? v3.onDidClose
|
|
205
|
+
: satisfies(mod.version, '4.x')
|
|
206
|
+
? v4.onDidClose
|
|
207
|
+
: v5.onDidClose;
|
|
208
|
+
try {
|
|
209
|
+
await release(uri);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (isFatalError(error)) {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
options.errorLog(`Failed to release markuplint for ${uri}: ${error}`);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
// The client keeps showing the last diagnostics of a document until told
|
|
219
|
+
// otherwise. Sent after the release: the engine's listeners are gone by then,
|
|
220
|
+
// so no lint can publish over it.
|
|
221
|
+
options.sendDiagnostics({ uri, diagnostics: [] });
|
|
222
|
+
if (closedDocuments.get(uri) === closing) {
|
|
223
|
+
closedDocuments.delete(uri);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
229
|
+
onCodeAction(params) {
|
|
230
|
+
const uri = params.textDocument.uri;
|
|
231
|
+
const mod = activeModule(uri);
|
|
232
|
+
if (!mod) {
|
|
233
|
+
options.log(`Code Actions skipped: no module for ${uri}`, 'debug');
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
// Code Actions require v5.0.0+ (Violation.fix data + lint event fixSummary)
|
|
237
|
+
// Use '5.0.0-0' to include alpha/beta prereleases
|
|
238
|
+
if (!gte(mod.version, '5.0.0-0')) {
|
|
239
|
+
options.log(`Code Actions skipped: markuplint ${mod.version} < 5.0.0`, 'debug');
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const actions = v5.onCodeAction(params);
|
|
243
|
+
options.log(`Code Actions: ${actions.length} for ${uri}`, 'debug');
|
|
244
|
+
return actions;
|
|
245
|
+
},
|
|
246
|
+
async onHover(
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
248
|
+
params) {
|
|
249
|
+
const hoverSettings = (options.langConfigs ? options.langConfigs['html'] : DEFAULT_LANG_CONFIG)?.hover
|
|
250
|
+
.accessibility;
|
|
251
|
+
if (!hoverSettings?.enable) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const mod = activeModule(params.textDocument.uri);
|
|
255
|
+
if (!mod) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const ariaVersion = hoverSettings.ariaVersion ?? mod.ariaRecommendedVersion;
|
|
259
|
+
if (lt(mod.version, '4.0.0')) {
|
|
260
|
+
const node = v3.getNodeWithAccessibilityProps(params.textDocument, params.position, ariaVersion);
|
|
261
|
+
if (!node) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const heading = `\`<${node.nodeName}>\` **${t('Computed Accessibility Properties')}**:\n`;
|
|
265
|
+
const props = node.exposed
|
|
266
|
+
? `${Object.entries(node.aria)
|
|
267
|
+
.map(([key, value]) => `- ${key}: ${value}`)
|
|
268
|
+
.join('\n')}`
|
|
269
|
+
: `\n**${t('No exposed to accessibility tree')}** (${t('hidden element')})`;
|
|
270
|
+
return {
|
|
271
|
+
contents: {
|
|
272
|
+
kind: MarkupKind.Markdown,
|
|
273
|
+
value: heading + props,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const aria = satisfies(mod.version, '4.x')
|
|
278
|
+
? await v4.getNodeWithAccessibilityProps(params.textDocument, params.position, ariaVersion)
|
|
279
|
+
: await v5.getNodeWithAccessibilityProps(params.textDocument, params.position, ariaVersion);
|
|
280
|
+
if (!aria) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const heading = `\`<${aria.nodeName}>\` **${t('Computed Accessibility Properties')}**:\n`;
|
|
284
|
+
const body = 'unknown' in aria
|
|
285
|
+
? `\n**${t('Unknown')}**`
|
|
286
|
+
: aria.exposed
|
|
287
|
+
? `${Object.entries(aria.labels)
|
|
288
|
+
.map(([key, value]) => `- ${key}: ${value}`)
|
|
289
|
+
.join('\n')}`
|
|
290
|
+
: `\n**${t('No exposed to accessibility tree')}** (${t('hidden element')})`;
|
|
291
|
+
return {
|
|
292
|
+
contents: {
|
|
293
|
+
kind: MarkupKind.Markdown,
|
|
294
|
+
value: heading + body,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
2
|
+
import type { MLEngine } from 'markuplint';
|
|
3
|
+
export declare function getAccessibilityByLocation(engine: MLEngine, line: number, col: number, ariaVersion: ARIAVersion): Promise<{
|
|
4
|
+
node: string;
|
|
5
|
+
aria: import("@markuplint/ml-core").AccessibilityProperties | null;
|
|
6
|
+
} | null>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function getAccessibilityByLocation(
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
3
|
+
engine, line, col, ariaVersion) {
|
|
4
|
+
if (!engine?.document) {
|
|
5
|
+
await engine.exec();
|
|
6
|
+
}
|
|
7
|
+
if (!engine?.document) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const node = engine.document.searchNodeByLocation(line, col);
|
|
11
|
+
if (!node || !node.is(node.ELEMENT_NODE)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const aria = engine.document.getAccessibilityProp(node, ariaVersion);
|
|
15
|
+
return {
|
|
16
|
+
node: node.localName,
|
|
17
|
+
aria,
|
|
18
|
+
};
|
|
19
|
+
}
|