@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,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>;
|
package/lib/server/v2.js
ADDED
|
@@ -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;
|