@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.
- 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 +58 -5
package/lib/server/v5.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isFatalError } from 'markuplint/suppressions';
|
|
3
|
+
import { t } from '../i18n.js';
|
|
4
|
+
import { getFilePath } from '../utils/get-file-path.js';
|
|
5
|
+
import { createQuickFixActions, createFixAllAction } from './code-actions.js';
|
|
6
|
+
import { convertDiagnostics } from './convert-diagnostics.js';
|
|
7
|
+
import { getAccessibilityByLocation } from './get-accessibility-by-location.js';
|
|
8
|
+
import { applySuppressionsToViolations, loadSuppressions } from './suppression-support.js';
|
|
9
|
+
const engines = new Map();
|
|
10
|
+
const fixStates = new Map();
|
|
11
|
+
let moduleLog;
|
|
12
|
+
/**
|
|
13
|
+
* Returns the latest fix state for a document.
|
|
14
|
+
*
|
|
15
|
+
* @param uri - The document URI
|
|
16
|
+
* @returns The fix state, or `undefined` if the document has not been linted yet
|
|
17
|
+
*/
|
|
18
|
+
export function getFixState(uri) {
|
|
19
|
+
return fixStates.get(uri);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Handles the `textDocument/didOpen` event by creating an MLEngine for the document.
|
|
23
|
+
*
|
|
24
|
+
* Sets up linting, diagnostics, fix-state tracking, and bulk suppression
|
|
25
|
+
* severity downgrade for the opened document.
|
|
26
|
+
*
|
|
27
|
+
* @param document - The opened text document
|
|
28
|
+
* @param MLEngine - The MLEngine constructor
|
|
29
|
+
* @param config - The language configuration
|
|
30
|
+
* @param locale - The locale for diagnostic messages
|
|
31
|
+
* @param log - Logger for general messages
|
|
32
|
+
* @param diagnosticsLog - Logger for diagnostic-specific messages
|
|
33
|
+
* @param sendDiagnostics - Callback to publish diagnostics to the client
|
|
34
|
+
* @param notFoundParserError - Callback invoked when a parser is not found
|
|
35
|
+
* @param workspace - The working directory the document belongs to; the base for config discovery
|
|
36
|
+
*/
|
|
37
|
+
export async function onDidOpen(
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
39
|
+
document,
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
41
|
+
MLEngine,
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
43
|
+
config, locale, log, diagnosticsLog, sendDiagnostics, notFoundParserError, workspace) {
|
|
44
|
+
moduleLog = log;
|
|
45
|
+
const key = document.uri;
|
|
46
|
+
log(`Opened: ${key}`, 'debug');
|
|
47
|
+
const currentEngine = engines.get(key);
|
|
48
|
+
if (currentEngine) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const filePath = getFilePath(document.uri, document.languageId);
|
|
52
|
+
log(`${filePath.dirname}/${filePath.basename}`, 'debug');
|
|
53
|
+
const absoluteFilePath = `${filePath.dirname}/${filePath.basename}`;
|
|
54
|
+
const sourceCode = document.getText();
|
|
55
|
+
// `name` must be the workspace-relative path (not just the basename), or
|
|
56
|
+
// MLFile.path collapses subdirectory files to workspace-root + basename —
|
|
57
|
+
// which breaks pretender import resolution, nested config discovery, and
|
|
58
|
+
// excludeFiles/overrides glob matching for anything outside the workspace root.
|
|
59
|
+
const relativeFilePath = path.relative(workspace, absoluteFilePath);
|
|
60
|
+
const file = await MLEngine.toMLFile({ sourceCode, name: relativeFilePath, workspace });
|
|
61
|
+
if (!file) {
|
|
62
|
+
log(`File not found: ${filePath.basename}`, 'warn');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const engine = new MLEngine(file, {
|
|
66
|
+
locale,
|
|
67
|
+
debug: config.debug,
|
|
68
|
+
defaultConfig: config.defaultConfig,
|
|
69
|
+
watch: true,
|
|
70
|
+
fix: true,
|
|
71
|
+
});
|
|
72
|
+
log(`Engine created: ${key}`);
|
|
73
|
+
let configSet = null;
|
|
74
|
+
engines.set(key, engine);
|
|
75
|
+
engine.on('config', (_, _configSet) => {
|
|
76
|
+
configSet = _configSet;
|
|
77
|
+
});
|
|
78
|
+
engine.on('log', (phase, message) => {
|
|
79
|
+
log(`[${phase}]: ${message}`, 'trace');
|
|
80
|
+
});
|
|
81
|
+
engine.on('lint-error', (_filePath, _sourceCode, error) => {
|
|
82
|
+
diagnosticsLog(error.message, 'error');
|
|
83
|
+
});
|
|
84
|
+
engine.on('config-errors', (_filePath, errors) => {
|
|
85
|
+
for (const error of errors) {
|
|
86
|
+
diagnosticsLog('ConfigError: ' + error.message, 'error');
|
|
87
|
+
log('ConfigError: ' + error.message, 'warn');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
engine.on('lint', (lintFilePath, lintSourceCode, violations, fixedCode, debug, fixSummary) => {
|
|
91
|
+
clearTimeout(debounceTimers.get(key));
|
|
92
|
+
diagnosticsLog('', 'clear');
|
|
93
|
+
// Execute after 300ms from the last change.
|
|
94
|
+
debounceTimers.set(key, setTimeout(() => {
|
|
95
|
+
lint().catch((error) => {
|
|
96
|
+
log(String(error), 'error');
|
|
97
|
+
});
|
|
98
|
+
}, 300));
|
|
99
|
+
async function lint() {
|
|
100
|
+
diagnosticsLog(`Lint: ${document.uri}`);
|
|
101
|
+
// Apply bulk suppression severity downgrade and prefix suppressed messages
|
|
102
|
+
const suppressedPrefix = t('[suppressed] this warning is suppressed but should be fixed:');
|
|
103
|
+
const downgraded = await applyBulkSuppressions(absoluteFilePath, violations, workspace, log);
|
|
104
|
+
// The document may have been closed while the suppressions were loading; publishing
|
|
105
|
+
// now would resurrect the diagnostics `onDidClose` cleared and re-add the fix state.
|
|
106
|
+
if (engines.get(key) !== engine) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const effectiveViolations = downgraded.map(v => 'originalSeverity' in v && v.originalSeverity
|
|
110
|
+
? { ...v, message: `${suppressedPrefix} ${v.message}` }
|
|
111
|
+
: v);
|
|
112
|
+
const errors = effectiveViolations.filter(v => v.severity === 'error');
|
|
113
|
+
const warns = effectiveViolations.filter(v => v.severity === 'warning');
|
|
114
|
+
const suppressed = effectiveViolations.filter(v => v.severity === 'info');
|
|
115
|
+
log(`Errors: ${errors.length}`, 'debug');
|
|
116
|
+
log(`Warnings: ${warns.length}`, 'debug');
|
|
117
|
+
if (suppressed.length > 0) {
|
|
118
|
+
log(`Suppressed (info): ${suppressed.length}`, 'debug');
|
|
119
|
+
}
|
|
120
|
+
if (debug) {
|
|
121
|
+
diagnosticsLog(' Tracing AST Mapping:\n' + debug.map(line => ` ${line}`).join('\n'), 'trace');
|
|
122
|
+
}
|
|
123
|
+
if (configSet) {
|
|
124
|
+
if (configSet.files.size > 0) {
|
|
125
|
+
diagnosticsLog(' Used configs:');
|
|
126
|
+
for (const files of configSet.files.values()) {
|
|
127
|
+
diagnosticsLog(` ${files}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
diagnosticsLog(' No use configs');
|
|
132
|
+
}
|
|
133
|
+
if (configSet.plugins.length > 0) {
|
|
134
|
+
diagnosticsLog(' Used plugins:');
|
|
135
|
+
for (const plugin of configSet.plugins.values()) {
|
|
136
|
+
diagnosticsLog(` ${plugin.name}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
diagnosticsLog(' No use plugins');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const diagnostics = convertDiagnostics({
|
|
144
|
+
filePath: lintFilePath,
|
|
145
|
+
sourceCode: lintSourceCode,
|
|
146
|
+
violations: effectiveViolations,
|
|
147
|
+
fixedCode,
|
|
148
|
+
status: 'processed',
|
|
149
|
+
});
|
|
150
|
+
fixStates.set(key, {
|
|
151
|
+
sourceCode: lintSourceCode,
|
|
152
|
+
violations: effectiveViolations,
|
|
153
|
+
fixedCode,
|
|
154
|
+
fixSummary,
|
|
155
|
+
});
|
|
156
|
+
if (diagnostics.length > 0) {
|
|
157
|
+
diagnosticsLog(` Violations(${diagnostics.length}):`);
|
|
158
|
+
for (const d of diagnostics) {
|
|
159
|
+
diagnosticsLog(` [${d.line}:${d.col}] ${d.code}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
diagnosticsLog(' ✔ No violations');
|
|
164
|
+
}
|
|
165
|
+
sendDiagnostics({
|
|
166
|
+
uri: document.uri,
|
|
167
|
+
diagnostics,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
log('Run `engine.exec()` in `onDidOpen`', 'debug');
|
|
172
|
+
engine.exec().catch((error) => {
|
|
173
|
+
log(String(error), 'error');
|
|
174
|
+
notFoundParserError(error);
|
|
175
|
+
throw error;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const debounceTimers = new Map();
|
|
179
|
+
/**
|
|
180
|
+
* Handles the `textDocument/didChangeContent` event by re-running the lint engine.
|
|
181
|
+
*
|
|
182
|
+
* Debounces execution so that rapid successive changes do not trigger
|
|
183
|
+
* multiple lint passes.
|
|
184
|
+
*
|
|
185
|
+
* @param document - The changed text document
|
|
186
|
+
* @param log - Logger for messages
|
|
187
|
+
* @param notFoundParserError - Callback invoked when a parser is not found
|
|
188
|
+
*/
|
|
189
|
+
export function onDidChangeContent(
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
191
|
+
document, log, notFoundParserError) {
|
|
192
|
+
const key = document.uri;
|
|
193
|
+
clearTimeout(debounceTimers.get(key));
|
|
194
|
+
const engine = engines.get(key);
|
|
195
|
+
debounceTimers.set(key, setTimeout(async () => {
|
|
196
|
+
if (!engine) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const code = document.getText();
|
|
200
|
+
try {
|
|
201
|
+
await engine.setCode(code);
|
|
202
|
+
log('Run `engine.exec()` in `onDidChangeContent`', 'debug');
|
|
203
|
+
engine.exec().catch((error) => notFoundParserError(error));
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof Error) {
|
|
207
|
+
log(error.message, 'error');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
log(`UnknownError: ${error}`, 'error');
|
|
211
|
+
}
|
|
212
|
+
}, 300));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Handles the `textDocument/didClose` event by releasing everything kept for the document.
|
|
216
|
+
*
|
|
217
|
+
* Without it every document opened during a session would keep its engine, its DOM and
|
|
218
|
+
* its config watcher (`watch: true`) until the server exits.
|
|
219
|
+
*
|
|
220
|
+
* @param uri - The URI of the closed document
|
|
221
|
+
*/
|
|
222
|
+
export async function onDidClose(uri) {
|
|
223
|
+
clearTimeout(debounceTimers.get(uri));
|
|
224
|
+
debounceTimers.delete(uri);
|
|
225
|
+
fixStates.delete(uri);
|
|
226
|
+
const engine = engines.get(uri);
|
|
227
|
+
if (!engine) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
engines.delete(uri);
|
|
231
|
+
await engine.close();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Retrieves the ARIA accessibility properties of the element at the given position.
|
|
235
|
+
*
|
|
236
|
+
* Used by the hover provider to display accessibility information such as
|
|
237
|
+
* role, exposed state, and ARIA property labels.
|
|
238
|
+
*
|
|
239
|
+
* @param textDocument - The text document identifier
|
|
240
|
+
* @param position - The cursor position within the document
|
|
241
|
+
* @param ariaVersion - The ARIA specification version to use
|
|
242
|
+
* @returns The node's accessibility properties, or null if unavailable
|
|
243
|
+
*/
|
|
244
|
+
export async function getNodeWithAccessibilityProps(
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
246
|
+
textDocument,
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
248
|
+
position, ariaVersion) {
|
|
249
|
+
const key = textDocument.uri;
|
|
250
|
+
const engine = engines.get(key);
|
|
251
|
+
if (!engine) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const a11y = await getAccessibilityByLocation(engine, position.line + 1, position.character, ariaVersion);
|
|
255
|
+
if (!a11y) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const { node, aria } = a11y;
|
|
259
|
+
if (!aria) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (aria.unknown) {
|
|
263
|
+
return {
|
|
264
|
+
nodeName: node,
|
|
265
|
+
unknown: true,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (!aria.exposedToTree) {
|
|
269
|
+
return {
|
|
270
|
+
nodeName: node,
|
|
271
|
+
exposed: false,
|
|
272
|
+
labels: {},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const labels = {};
|
|
276
|
+
const requiredLabel = `\u26A0\uFE0F**${t('Required')}**`;
|
|
277
|
+
labels.role = aria.role ? `\`${aria.role}\`` : t('No corresponding role');
|
|
278
|
+
labels.name = aria.nameProhibited
|
|
279
|
+
? `**${t('Prohibited')}**`
|
|
280
|
+
: // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
281
|
+
aria.name
|
|
282
|
+
? typeof aria.name === 'string'
|
|
283
|
+
? `\`"${aria.name}"\``
|
|
284
|
+
: `**${t('Unknown')}**`
|
|
285
|
+
: `${t('None')}${aria.nameRequired ? ` ${requiredLabel}` : ''}`;
|
|
286
|
+
labels.focusable = `\`${aria.focusable}\``;
|
|
287
|
+
for (const [propName, { value, required }] of Object.entries(aria.props ?? {})) {
|
|
288
|
+
labels[propName] =
|
|
289
|
+
value === undefined ? t('Undefined') + (required ? ` ${requiredLabel}` : '') : `\`${value}\``;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
nodeName: node,
|
|
293
|
+
exposed: true,
|
|
294
|
+
labels,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Handles `textDocument/codeAction` requests for markuplint v5.
|
|
299
|
+
*
|
|
300
|
+
* Returns per-violation QuickFix actions and a SourceFixAll action
|
|
301
|
+
* (`source.fixAll.markuplint`) when fixable violations exist.
|
|
302
|
+
*
|
|
303
|
+
* @param params - The LSP Code Action request parameters
|
|
304
|
+
* @returns An array of Code Actions (QuickFix and/or SourceFixAll)
|
|
305
|
+
*/
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
307
|
+
export function onCodeAction(params) {
|
|
308
|
+
const uri = params.textDocument.uri;
|
|
309
|
+
const fixState = getFixState(uri);
|
|
310
|
+
if (!fixState) {
|
|
311
|
+
moduleLog?.(`onCodeAction: no fixState for ${uri}`, 'debug');
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
const fixableCount = fixState.violations.filter(v => v.fix).length;
|
|
315
|
+
moduleLog?.(`onCodeAction: ${fixState.violations.length} violations (${fixableCount} fixable), ${params.context.diagnostics.length} diagnostics`, 'debug');
|
|
316
|
+
const quickFixes = createQuickFixActions(uri, params.context.diagnostics, fixState);
|
|
317
|
+
const fixAll = createFixAllAction(uri, params.context.diagnostics, fixState);
|
|
318
|
+
return fixAll ? [...quickFixes, fixAll] : quickFixes;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Applies bulk suppression to violations, downgrading severity to 'info'
|
|
322
|
+
* for suppressed violations. Uses git blame for count-exceeded cases.
|
|
323
|
+
*/
|
|
324
|
+
async function applyBulkSuppressions(absoluteFilePath, violations, workspace, log) {
|
|
325
|
+
try {
|
|
326
|
+
const cache = await loadSuppressions(workspace, log);
|
|
327
|
+
if (!cache) {
|
|
328
|
+
return violations;
|
|
329
|
+
}
|
|
330
|
+
return await applySuppressionsToViolations(absoluteFilePath, violations, cache, log);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (isFatalError(error)) {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
log(`Bulk suppression failed: ${error instanceof Error ? error.message : String(error)}`, 'warn');
|
|
337
|
+
return violations;
|
|
338
|
+
}
|
|
339
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Config as MLConfig } from '@markuplint/ml-config';
|
|
2
|
+
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
3
|
+
import type { WorkingDirectoryEntry } from './utils/resolve-working-directory.js';
|
|
4
|
+
/**
|
|
5
|
+
* Per-language configuration sent by clients that manage settings themselves (the VS Code extension).
|
|
6
|
+
*/
|
|
7
|
+
export type Config = {
|
|
8
|
+
enable: boolean;
|
|
9
|
+
debug: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Applied only when no config file is found. Left out, markuplint falls back to
|
|
12
|
+
* `markuplint:recommended`, exactly as the CLI does.
|
|
13
|
+
*/
|
|
14
|
+
defaultConfig?: MLConfig;
|
|
15
|
+
hover: {
|
|
16
|
+
accessibility: {
|
|
17
|
+
enable: boolean;
|
|
18
|
+
ariaVersion?: ARIAVersion;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Status information sent from the language server to the client for the status bar.
|
|
24
|
+
*/
|
|
25
|
+
export type Status = {
|
|
26
|
+
readonly version: string;
|
|
27
|
+
readonly isLocalModule: boolean;
|
|
28
|
+
readonly message: string | null;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A map of language IDs to their markuplint configuration.
|
|
32
|
+
*/
|
|
33
|
+
export type LangConfigs = Record<string, Config>;
|
|
34
|
+
/**
|
|
35
|
+
* The severities a diagnostic can be reported with.
|
|
36
|
+
*/
|
|
37
|
+
export type ReportedSeverity = 'error' | 'warning' | 'info' | 'hint';
|
|
38
|
+
/**
|
|
39
|
+
* Maps the severity markuplint assigns to a violation onto the severity reported to the client.
|
|
40
|
+
* Severities missing from the map are reported unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export type SeverityMap = Partial<Record<'error' | 'warning' | 'info', ReportedSeverity>>;
|
|
43
|
+
/**
|
|
44
|
+
* Options a client may pass in `initializationOptions`. Every field is optional so that
|
|
45
|
+
* clients which know nothing about markuplint (Claude Code, OpenCode, Neovim) can start the
|
|
46
|
+
* server without configuring it.
|
|
47
|
+
*/
|
|
48
|
+
export type InitializationOptions = {
|
|
49
|
+
/**
|
|
50
|
+
* Per-language configuration. When present, only languages listed here with `enable: true`
|
|
51
|
+
* are linted. When absent, every document the client opens is linted.
|
|
52
|
+
*/
|
|
53
|
+
readonly langConfigs?: LangConfigs;
|
|
54
|
+
/** User-configured working directories for monorepo support */
|
|
55
|
+
readonly workingDirectories?: readonly WorkingDirectoryEntry[];
|
|
56
|
+
/**
|
|
57
|
+
* Absolute paths of the workspace folders. Falls back to the LSP-standard
|
|
58
|
+
* `workspaceFolders` and `rootUri` of the `initialize` request.
|
|
59
|
+
*/
|
|
60
|
+
readonly workspaceFolders?: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Path to the git binary. Falls back to `'git'` (PATH lookup) when unset.
|
|
63
|
+
*/
|
|
64
|
+
readonly gitPath?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Opts in to the `markuplint/*` custom requests and notifications (status bar, output
|
|
67
|
+
* channels, popups). Clients that do not set it receive `window/logMessage` and
|
|
68
|
+
* `window/showMessage` instead, which every LSP client understands.
|
|
69
|
+
*/
|
|
70
|
+
readonly extendedProtocol?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Re-reports diagnostics with another severity, for clients that surface only some
|
|
73
|
+
* severities to their user (OpenCode hands only errors to the model).
|
|
74
|
+
*/
|
|
75
|
+
readonly severityMap?: SeverityMap;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* A logging function that writes to an output channel.
|
|
79
|
+
*/
|
|
80
|
+
export type Log = (...args: LogArg) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Log severity levels for the output channels.
|
|
83
|
+
*/
|
|
84
|
+
export type LogType = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'clear';
|
|
85
|
+
/**
|
|
86
|
+
* Arguments for a log call: a message string and an optional log type.
|
|
87
|
+
*/
|
|
88
|
+
export type LogArg = readonly [message: string, type?: LogType];
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a directory name and base file name from a document URI.
|
|
3
|
+
*
|
|
4
|
+
* Handles `file:` and `untitled:` URI schemes. For untitled documents,
|
|
5
|
+
* appends the language ID as a file extension so markuplint can detect
|
|
6
|
+
* the correct parser.
|
|
7
|
+
*
|
|
8
|
+
* @param uri - The document URI (e.g. `"file:///path/to/file.html"`)
|
|
9
|
+
* @param langId - The language identifier (e.g. `"html"`, `"vue"`)
|
|
10
|
+
* @returns An object with `dirname` (absolute directory path) and `basename` (file name)
|
|
11
|
+
*/
|
|
12
|
+
export declare function getFilePath(uri: string, langId: string): {
|
|
13
|
+
dirname: string;
|
|
14
|
+
basename: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/**
|
|
4
|
+
* Extracts a directory name and base file name from a document URI.
|
|
5
|
+
*
|
|
6
|
+
* Handles `file:` and `untitled:` URI schemes. For untitled documents,
|
|
7
|
+
* appends the language ID as a file extension so markuplint can detect
|
|
8
|
+
* the correct parser.
|
|
9
|
+
*
|
|
10
|
+
* @param uri - The document URI (e.g. `"file:///path/to/file.html"`)
|
|
11
|
+
* @param langId - The language identifier (e.g. `"html"`, `"vue"`)
|
|
12
|
+
* @returns An object with `dirname` (absolute directory path) and `basename` (file name)
|
|
13
|
+
*/
|
|
14
|
+
export function getFilePath(uri, langId) {
|
|
15
|
+
if (/^untitled:/i.test(uri)) {
|
|
16
|
+
const name = uri.replace(/^untitled:/i, '');
|
|
17
|
+
const basename = `${name}.${langId}`;
|
|
18
|
+
return {
|
|
19
|
+
dirname: path.resolve(),
|
|
20
|
+
basename,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const decodePath = fileURLToPath(decodeURIComponent(uri));
|
|
24
|
+
let filePath;
|
|
25
|
+
let untitled = false;
|
|
26
|
+
if (decodePath.startsWith('file:')) {
|
|
27
|
+
filePath = decodePath.replace(/^file:\/+/i, '/');
|
|
28
|
+
}
|
|
29
|
+
else if (decodePath.startsWith('untitled:')) {
|
|
30
|
+
filePath = decodePath.replace(/^untitled:/i, '');
|
|
31
|
+
untitled = true;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
filePath = decodePath;
|
|
35
|
+
}
|
|
36
|
+
const dirname = path.resolve(path.dirname(filePath));
|
|
37
|
+
let basename = path.basename(filePath);
|
|
38
|
+
if (untitled) {
|
|
39
|
+
basename += `.${langId}`;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
dirname,
|
|
43
|
+
basename,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A working directory entry configuration, modeled after the ESLint VS Code extension.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Plain string: a directory path relative to the workspace folder
|
|
6
|
+
* - `{ directory, "!cwd"? }`: explicit directory with optional process.cwd control
|
|
7
|
+
* - `{ pattern, "!cwd"? }`: glob pattern for dynamic directory detection
|
|
8
|
+
* - `{ mode: "auto" | "location" }`: automatic working directory detection
|
|
9
|
+
*/
|
|
10
|
+
export type WorkingDirectoryEntry = string | {
|
|
11
|
+
readonly directory: string;
|
|
12
|
+
readonly '!cwd'?: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
readonly pattern: string;
|
|
15
|
+
readonly '!cwd'?: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
readonly mode: 'auto' | 'location';
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The result of resolving a working directory for a file.
|
|
21
|
+
*/
|
|
22
|
+
export type ResolvedWorkingDirectory = {
|
|
23
|
+
/** The resolved working directory path */
|
|
24
|
+
readonly directory: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Converts a simple glob pattern (supporting `*` and `**`) into a RegExp.
|
|
28
|
+
*
|
|
29
|
+
* This is a simplified implementation for matching directory paths against
|
|
30
|
+
* patterns like `./packages/* /` or `./apps/** /`.
|
|
31
|
+
*
|
|
32
|
+
* @param pattern - A glob pattern string (e.g. `"./packages/* /"`)
|
|
33
|
+
* @returns A RegExp anchored at the start that matches directory paths
|
|
34
|
+
*/
|
|
35
|
+
export declare function convertGlobToRegex(pattern: string): RegExp;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the working directory for a given file based on workingDirectories configuration.
|
|
38
|
+
*
|
|
39
|
+
* @param filePath - Absolute file path of the document being linted
|
|
40
|
+
* @param workspaceFolders - Workspace folder paths (absolute)
|
|
41
|
+
* @param workingDirectories - The user-configured working directories
|
|
42
|
+
* @returns The resolved working directory, or `undefined` to use the default behavior
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveWorkingDirectory(filePath: string, workspaceFolders: readonly string[], workingDirectories?: readonly WorkingDirectoryEntry[]): ResolvedWorkingDirectory | undefined;
|