@geoql/doctor-language-server 0.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/LICENSE +21 -0
- package/bin/doctor-language-server.mjs +15 -0
- package/dist/index.d.mts +207 -0
- package/dist/index.mjs +407 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinayak Kulkarni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import module from 'node:module';
|
|
4
|
+
|
|
5
|
+
if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
|
|
6
|
+
try {
|
|
7
|
+
module.enableCompileCache();
|
|
8
|
+
} catch {
|
|
9
|
+
// Ignore compile-cache errors.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { startLanguageServer } = await import('../dist/index.mjs');
|
|
14
|
+
|
|
15
|
+
startLanguageServer();
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { AuditReport, Diagnostic, ProjectInfo, Severity } from "@geoql/doctor-core";
|
|
2
|
+
import { Connection } from "vscode-languageserver/node";
|
|
3
|
+
import { Diagnostic as Diagnostic$1, DiagnosticSeverity, Position, Range } from "vscode-languageserver";
|
|
4
|
+
|
|
5
|
+
//#region src/server.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Builds and wires the Doctor language server onto a connection. Exposed
|
|
8
|
+
* separately from {@link startLanguageServer} so the I/O shell stays thin and
|
|
9
|
+
* the pure transforms (mapper, positions, severity, group, scheduler, cache,
|
|
10
|
+
* selection) carry the behavior — and the coverage.
|
|
11
|
+
*/
|
|
12
|
+
declare const createServer: (connection: Connection) => void;
|
|
13
|
+
/** Entry point: starts the server over stdio. */
|
|
14
|
+
declare const startLanguageServer: () => void;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/cache.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* Per-document version cache. The LSP `TextDocument.version` increments on
|
|
19
|
+
* every edit, so a request to audit a `(uri, version)` pair already audited
|
|
20
|
+
* (or in flight) is redundant. `shouldAudit` is the pure decision; the
|
|
21
|
+
* server records the version it acted on via `markAudited`.
|
|
22
|
+
*/
|
|
23
|
+
interface VersionCache {
|
|
24
|
+
/** Whether `uri` at `version` warrants a fresh audit (not already seen). */
|
|
25
|
+
readonly shouldAudit: (uri: string, version: number) => boolean;
|
|
26
|
+
/** Records that `uri` was audited at `version`. */
|
|
27
|
+
readonly markAudited: (uri: string, version: number) => void;
|
|
28
|
+
/** Forgets a document (on close) so a later reopen always re-audits. */
|
|
29
|
+
readonly forget: (uri: string) => void;
|
|
30
|
+
}
|
|
31
|
+
/** Pure decision: audit only when this version is newer than the last seen. */
|
|
32
|
+
declare const shouldAuditVersion: (lastAuditedVersion: number | undefined, version: number) => boolean;
|
|
33
|
+
declare const createVersionCache: () => VersionCache;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/scheduler.d.ts
|
|
36
|
+
/**
|
|
37
|
+
* Per-key debounced scheduler. Each editor scope (a document URI, or a
|
|
38
|
+
* workspace root) gets one in-flight audit at a time: rapid edits to the same
|
|
39
|
+
* document collapse into a single trailing run, and a monotonic generation
|
|
40
|
+
* per key supersedes stale work so a slow audit can never clobber a fresher
|
|
41
|
+
* result. Save scans pass `debounceMs: 0` to run immediately.
|
|
42
|
+
*/
|
|
43
|
+
interface SchedulerOptions {
|
|
44
|
+
/** Trailing debounce window for an enqueued key, in ms. Default 400. */
|
|
45
|
+
readonly debounceMs?: number;
|
|
46
|
+
/** Runs the audit for a key; rejections are surfaced via `onError`. */
|
|
47
|
+
readonly performScan: (key: string, token: CancellationToken) => Promise<void>;
|
|
48
|
+
/** Called when a `performScan` promise rejects. */
|
|
49
|
+
readonly onError?: (error: unknown, key: string) => void;
|
|
50
|
+
}
|
|
51
|
+
/** Cooperative cancellation: `true` once a newer enqueue superseded the run. */
|
|
52
|
+
interface CancellationToken {
|
|
53
|
+
readonly isCancelled: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface Scheduler {
|
|
56
|
+
/** Queue (or re-queue) an audit for `key`, debounced. `delayMs` overrides the default. */
|
|
57
|
+
readonly enqueue: (key: string, delayMs?: number) => void;
|
|
58
|
+
/** Cancel any pending/in-flight work for `key`. */
|
|
59
|
+
readonly cancel: (key: string) => void;
|
|
60
|
+
/** Pending timers + in-flight runs; for tests and idle checks. */
|
|
61
|
+
readonly pendingCount: () => number;
|
|
62
|
+
/** Stop all timers and abandon queued work. */
|
|
63
|
+
readonly dispose: () => void;
|
|
64
|
+
}
|
|
65
|
+
declare const createScheduler: (options: SchedulerOptions) => Scheduler;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/group.d.ts
|
|
68
|
+
interface GroupDiagnosticsInput {
|
|
69
|
+
readonly report: AuditReport;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the live text for a diagnostic's file (open buffer first, then
|
|
72
|
+
* disk) so range fallback can extend to end-of-line; `null` when unknown.
|
|
73
|
+
*/
|
|
74
|
+
readonly textForFile: (file: string) => string | null;
|
|
75
|
+
/**
|
|
76
|
+
* URIs that previously held diagnostics and must be republished even when
|
|
77
|
+
* the new audit found nothing in them — otherwise stale squiggles linger.
|
|
78
|
+
*/
|
|
79
|
+
readonly previousUris?: Iterable<string>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Transforms an `AuditReport` into a per-URI map of LSP diagnostics. Every
|
|
83
|
+
* URI in `previousUris` that has no findings this pass is included with an
|
|
84
|
+
* empty array so the server can clear it. Grouping is keyed by the resolved
|
|
85
|
+
* `file://` URI of each diagnostic's `file`.
|
|
86
|
+
*/
|
|
87
|
+
declare const groupDiagnosticsByUri: (input: GroupDiagnosticsInput) => Map<string, Diagnostic$1[]>;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/mapper.d.ts
|
|
90
|
+
interface MapDiagnosticInput {
|
|
91
|
+
readonly diagnostic: Diagnostic;
|
|
92
|
+
/** Text of the diagnostic's file for end-of-line range fallback; `null` → single column. */
|
|
93
|
+
readonly text: string | null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Converts a core doctor `Diagnostic` into an LSP `Diagnostic` with a precise
|
|
97
|
+
* 0-based range, `source: 'doctor'`, `code: ruleId`, a `codeDescription`
|
|
98
|
+
* pointing at the rule's docs page, and the recommendation appended to the
|
|
99
|
+
* message when present.
|
|
100
|
+
*/
|
|
101
|
+
declare const toLspDiagnostic: (input: MapDiagnosticInput) => Diagnostic$1;
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/positions.d.ts
|
|
104
|
+
/**
|
|
105
|
+
* doctor-core reports diagnostics with 1-indexed `line` / `column` (and an
|
|
106
|
+
* optional 1-indexed `endLine` / `endColumn`), but LSP positions are
|
|
107
|
+
* 0-indexed. These helpers convert between the two against the document text
|
|
108
|
+
* so squiggles land exactly on the offending token.
|
|
109
|
+
*/
|
|
110
|
+
/** Converts a 1-indexed line/column pair to a 0-indexed LSP `Position`. */
|
|
111
|
+
declare const toZeroBasedPosition: (line: number, column: number) => Position;
|
|
112
|
+
/**
|
|
113
|
+
* Builds an LSP `Range` from doctor-core's 1-indexed coordinates. When an
|
|
114
|
+
* explicit `endLine` / `endColumn` is present it is used directly; otherwise
|
|
115
|
+
* the range extends to the end of the start line (when document text is
|
|
116
|
+
* known) so the squiggle is visible, falling back to a single-character span
|
|
117
|
+
* when the text is unavailable.
|
|
118
|
+
*/
|
|
119
|
+
declare const rangeFromLineColumn: (text: string | null, line: number, column: number, endLine?: number, endColumn?: number) => Range;
|
|
120
|
+
/** Whether two ranges overlap (touching endpoints count as overlap). */
|
|
121
|
+
declare const rangesOverlap: (first: Range, second: Range) => boolean;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/severity.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Maps doctor-core's internal severity vocabulary (`error | warn | info`) to
|
|
126
|
+
* the LSP `DiagnosticSeverity` enum: `error → Error`, `warn → Warning`,
|
|
127
|
+
* `info → Information`. Doctor never emits LSP `Hint`.
|
|
128
|
+
*/
|
|
129
|
+
declare const toLspSeverity: (severity: Severity) => DiagnosticSeverity;
|
|
130
|
+
/**
|
|
131
|
+
* Human-readable label for an LSP diagnostic's severity. Covers all four LSP
|
|
132
|
+
* severities so a value outside doctor's vocabulary isn't mislabeled.
|
|
133
|
+
*/
|
|
134
|
+
declare const severityLabel: (severity: DiagnosticSeverity | undefined) => string;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/selection.d.ts
|
|
137
|
+
/** Editor-facing project flavor the audit runs as. */
|
|
138
|
+
type DoctorProjectKind = 'vue' | 'nuxt' | 'unknown';
|
|
139
|
+
/**
|
|
140
|
+
* Picks which doctor plugin set applies to a project from its detected
|
|
141
|
+
* framework. Nuxt projects get the Nuxt + Vue rule passes; plain Vue
|
|
142
|
+
* projects get the Vue passes; an unrecognized project is `unknown` and the
|
|
143
|
+
* server skips auditing it (no framework → no applicable rules).
|
|
144
|
+
*/
|
|
145
|
+
declare const selectProjectKind: (project: Pick<ProjectInfo, "framework">) => DoctorProjectKind;
|
|
146
|
+
/** Whether a detected project is one doctor can audit (Vue or Nuxt). */
|
|
147
|
+
declare const isAuditableProject: (project: Pick<ProjectInfo, "framework">) => boolean;
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/uri.d.ts
|
|
150
|
+
/**
|
|
151
|
+
* Resolves a doctor-core diagnostic's `file` (relative to the audit root, or
|
|
152
|
+
* already absolute) to a `file://` URI the editor can address. Paths are
|
|
153
|
+
* normalized to forward slashes by `vscode-uri`, so casing/encoding stay
|
|
154
|
+
* consistent across platforms.
|
|
155
|
+
*/
|
|
156
|
+
declare const diagnosticFileToUri: (rootDir: string, file: string) => string;
|
|
157
|
+
/** Converts a `file://` URI back to an absolute fs path. */
|
|
158
|
+
declare const uriToFsPath: (uri: string) => string;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/constants.d.ts
|
|
161
|
+
/** Display name used in client-facing messages and progress titles. */
|
|
162
|
+
declare const SERVER_DISPLAY_NAME = "Doctor";
|
|
163
|
+
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
164
|
+
declare const DIAGNOSTIC_SOURCE = "doctor";
|
|
165
|
+
/**
|
|
166
|
+
* Debounce window between an open document's last edit and the rescan it
|
|
167
|
+
* triggers. Long enough that fast typing collapses into a single audit,
|
|
168
|
+
* short enough to still feel live. Save scans run with no debounce.
|
|
169
|
+
*/
|
|
170
|
+
declare const DOCUMENT_CHANGE_DEBOUNCE_MS = 400;
|
|
171
|
+
/** Delay after `initialized` before the first background workspace scan. */
|
|
172
|
+
declare const INITIAL_WORKSPACE_SCAN_DELAY_MS = 300;
|
|
173
|
+
/**
|
|
174
|
+
* Source file extensions the server scans on open / change / save. Mirrors
|
|
175
|
+
* doctor-core's default include set so editor scanning covers the same
|
|
176
|
+
* files the CLI audits.
|
|
177
|
+
*/
|
|
178
|
+
declare const SCANNABLE_EXTENSIONS: readonly [".vue", ".ts", ".tsx", ".js", ".jsx"];
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/index.d.ts
|
|
181
|
+
/** Public API surface of `@geoql/doctor-language-server`. */
|
|
182
|
+
declare const api: {
|
|
183
|
+
readonly createServer: (connection: import("vscode-languageserver").Connection) => void;
|
|
184
|
+
readonly startLanguageServer: () => void;
|
|
185
|
+
readonly toLspDiagnostic: (input: MapDiagnosticInput) => import("vscode-languageserver").Diagnostic;
|
|
186
|
+
readonly rangeFromLineColumn: (text: string | null, line: number, column: number, endLine?: number, endColumn?: number) => import("vscode-languageserver").Range;
|
|
187
|
+
readonly rangesOverlap: (first: import("vscode-languageserver").Range, second: import("vscode-languageserver").Range) => boolean;
|
|
188
|
+
readonly toZeroBasedPosition: (line: number, column: number) => import("vscode-languageserver").Position;
|
|
189
|
+
readonly toLspSeverity: (severity: import("@geoql/doctor-core").Severity) => import("vscode-languageserver").DiagnosticSeverity;
|
|
190
|
+
readonly severityLabel: (severity: import("vscode-languageserver").DiagnosticSeverity | undefined) => string;
|
|
191
|
+
readonly groupDiagnosticsByUri: (input: GroupDiagnosticsInput) => Map<string, import("vscode-languageserver").Diagnostic[]>;
|
|
192
|
+
readonly selectProjectKind: (project: Pick<import("@geoql/doctor-core").ProjectInfo, "framework">) => DoctorProjectKind;
|
|
193
|
+
readonly isAuditableProject: (project: Pick<import("@geoql/doctor-core").ProjectInfo, "framework">) => boolean;
|
|
194
|
+
readonly diagnosticFileToUri: (rootDir: string, file: string) => string;
|
|
195
|
+
readonly uriToFsPath: (uri: string) => string;
|
|
196
|
+
readonly createScheduler: (options: SchedulerOptions) => Scheduler;
|
|
197
|
+
readonly createVersionCache: () => VersionCache;
|
|
198
|
+
readonly shouldAuditVersion: (lastAuditedVersion: number | undefined, version: number) => boolean;
|
|
199
|
+
readonly DIAGNOSTIC_SOURCE: "doctor";
|
|
200
|
+
readonly SERVER_DISPLAY_NAME: "Doctor";
|
|
201
|
+
readonly DOCUMENT_CHANGE_DEBOUNCE_MS: 400;
|
|
202
|
+
readonly INITIAL_WORKSPACE_SCAN_DELAY_MS: 300;
|
|
203
|
+
readonly SCANNABLE_EXTENSIONS: readonly [".vue", ".ts", ".tsx", ".js", ".jsx"];
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
export { type CancellationToken, DIAGNOSTIC_SOURCE, DOCUMENT_CHANGE_DEBOUNCE_MS, type DoctorProjectKind, type GroupDiagnosticsInput, INITIAL_WORKSPACE_SCAN_DELAY_MS, type MapDiagnosticInput, SCANNABLE_EXTENSIONS, SERVER_DISPLAY_NAME, type Scheduler, type SchedulerOptions, type VersionCache, api, createScheduler, createServer, createVersionCache, diagnosticFileToUri, groupDiagnosticsByUri, isAuditableProject, rangeFromLineColumn, rangesOverlap, selectProjectKind, severityLabel, shouldAuditVersion, startLanguageServer, toLspDiagnostic, toLspSeverity, toZeroBasedPosition, uriToFsPath };
|
|
207
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { audit, detectProject, docsUrl } from "@geoql/doctor-core";
|
|
3
|
+
import { TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node";
|
|
4
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5
|
+
import { DiagnosticSeverity } from "vscode-languageserver";
|
|
6
|
+
import { isAbsolute, resolve } from "node:path";
|
|
7
|
+
import { URI } from "vscode-uri";
|
|
8
|
+
//#region src/constants.ts
|
|
9
|
+
/** Display name used in client-facing messages and progress titles. */
|
|
10
|
+
const SERVER_DISPLAY_NAME = "Doctor";
|
|
11
|
+
/** `Diagnostic.source` shown next to every published diagnostic. */
|
|
12
|
+
const DIAGNOSTIC_SOURCE = "doctor";
|
|
13
|
+
/**
|
|
14
|
+
* Debounce window between an open document's last edit and the rescan it
|
|
15
|
+
* triggers. Long enough that fast typing collapses into a single audit,
|
|
16
|
+
* short enough to still feel live. Save scans run with no debounce.
|
|
17
|
+
*/
|
|
18
|
+
const DOCUMENT_CHANGE_DEBOUNCE_MS = 400;
|
|
19
|
+
/** Delay after `initialized` before the first background workspace scan. */
|
|
20
|
+
const INITIAL_WORKSPACE_SCAN_DELAY_MS = 300;
|
|
21
|
+
/**
|
|
22
|
+
* Source file extensions the server scans on open / change / save. Mirrors
|
|
23
|
+
* doctor-core's default include set so editor scanning covers the same
|
|
24
|
+
* files the CLI audits.
|
|
25
|
+
*/
|
|
26
|
+
const SCANNABLE_EXTENSIONS = [
|
|
27
|
+
".vue",
|
|
28
|
+
".ts",
|
|
29
|
+
".tsx",
|
|
30
|
+
".js",
|
|
31
|
+
".jsx"
|
|
32
|
+
];
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/cache.ts
|
|
35
|
+
/** Pure decision: audit only when this version is newer than the last seen. */
|
|
36
|
+
const shouldAuditVersion = (lastAuditedVersion, version) => lastAuditedVersion === void 0 || version > lastAuditedVersion;
|
|
37
|
+
const createVersionCache = () => {
|
|
38
|
+
const lastAudited = /* @__PURE__ */ new Map();
|
|
39
|
+
return {
|
|
40
|
+
shouldAudit: (uri, version) => shouldAuditVersion(lastAudited.get(uri), version),
|
|
41
|
+
markAudited: (uri, version) => {
|
|
42
|
+
lastAudited.set(uri, version);
|
|
43
|
+
},
|
|
44
|
+
forget: (uri) => {
|
|
45
|
+
lastAudited.delete(uri);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/positions.ts
|
|
51
|
+
/**
|
|
52
|
+
* doctor-core reports diagnostics with 1-indexed `line` / `column` (and an
|
|
53
|
+
* optional 1-indexed `endLine` / `endColumn`), but LSP positions are
|
|
54
|
+
* 0-indexed. These helpers convert between the two against the document text
|
|
55
|
+
* so squiggles land exactly on the offending token.
|
|
56
|
+
*/
|
|
57
|
+
/** Converts a 1-indexed line/column pair to a 0-indexed LSP `Position`. */
|
|
58
|
+
const toZeroBasedPosition = (line, column) => ({
|
|
59
|
+
line: Math.max(0, (line || 1) - 1),
|
|
60
|
+
character: Math.max(0, (column || 1) - 1)
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Builds an LSP `Range` from doctor-core's 1-indexed coordinates. When an
|
|
64
|
+
* explicit `endLine` / `endColumn` is present it is used directly; otherwise
|
|
65
|
+
* the range extends to the end of the start line (when document text is
|
|
66
|
+
* known) so the squiggle is visible, falling back to a single-character span
|
|
67
|
+
* when the text is unavailable.
|
|
68
|
+
*/
|
|
69
|
+
const rangeFromLineColumn = (text, line, column, endLine, endColumn) => {
|
|
70
|
+
const start = toZeroBasedPosition(line, column);
|
|
71
|
+
if (endLine !== void 0 && endColumn !== void 0) return {
|
|
72
|
+
start,
|
|
73
|
+
end: toZeroBasedPosition(endLine, endColumn)
|
|
74
|
+
};
|
|
75
|
+
if (text !== null) {
|
|
76
|
+
const lineText = text.split("\n")[start.line] ?? "";
|
|
77
|
+
const endCharacter = Math.max(start.character + 1, lineText.replace(/\r$/, "").length);
|
|
78
|
+
return {
|
|
79
|
+
start,
|
|
80
|
+
end: {
|
|
81
|
+
line: start.line,
|
|
82
|
+
character: endCharacter
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
start,
|
|
88
|
+
end: {
|
|
89
|
+
line: start.line,
|
|
90
|
+
character: start.character + 1
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const isBefore = (first, second) => first.line < second.line || first.line === second.line && first.character < second.character;
|
|
95
|
+
/** Whether two ranges overlap (touching endpoints count as overlap). */
|
|
96
|
+
const rangesOverlap = (first, second) => !isBefore(first.end, second.start) && !isBefore(second.end, first.start);
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/severity.ts
|
|
99
|
+
/**
|
|
100
|
+
* Maps doctor-core's internal severity vocabulary (`error | warn | info`) to
|
|
101
|
+
* the LSP `DiagnosticSeverity` enum: `error → Error`, `warn → Warning`,
|
|
102
|
+
* `info → Information`. Doctor never emits LSP `Hint`.
|
|
103
|
+
*/
|
|
104
|
+
const toLspSeverity = (severity) => {
|
|
105
|
+
switch (severity) {
|
|
106
|
+
case "error": return DiagnosticSeverity.Error;
|
|
107
|
+
case "warn": return DiagnosticSeverity.Warning;
|
|
108
|
+
case "info": return DiagnosticSeverity.Information;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Human-readable label for an LSP diagnostic's severity. Covers all four LSP
|
|
113
|
+
* severities so a value outside doctor's vocabulary isn't mislabeled.
|
|
114
|
+
*/
|
|
115
|
+
const severityLabel = (severity) => {
|
|
116
|
+
switch (severity) {
|
|
117
|
+
case DiagnosticSeverity.Error: return "error";
|
|
118
|
+
case DiagnosticSeverity.Information: return "info";
|
|
119
|
+
case DiagnosticSeverity.Hint: return "hint";
|
|
120
|
+
default: return "warning";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/mapper.ts
|
|
125
|
+
/**
|
|
126
|
+
* Converts a core doctor `Diagnostic` into an LSP `Diagnostic` with a precise
|
|
127
|
+
* 0-based range, `source: 'doctor'`, `code: ruleId`, a `codeDescription`
|
|
128
|
+
* pointing at the rule's docs page, and the recommendation appended to the
|
|
129
|
+
* message when present.
|
|
130
|
+
*/
|
|
131
|
+
const toLspDiagnostic = (input) => {
|
|
132
|
+
const { diagnostic, text } = input;
|
|
133
|
+
return {
|
|
134
|
+
range: rangeFromLineColumn(text, diagnostic.line, diagnostic.column, diagnostic.endLine, diagnostic.endColumn),
|
|
135
|
+
severity: toLspSeverity(diagnostic.severity),
|
|
136
|
+
code: diagnostic.ruleId,
|
|
137
|
+
codeDescription: { href: docsUrl(diagnostic.ruleId) },
|
|
138
|
+
source: DIAGNOSTIC_SOURCE,
|
|
139
|
+
message: diagnostic.message
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/uri.ts
|
|
144
|
+
/**
|
|
145
|
+
* Resolves a doctor-core diagnostic's `file` (relative to the audit root, or
|
|
146
|
+
* already absolute) to a `file://` URI the editor can address. Paths are
|
|
147
|
+
* normalized to forward slashes by `vscode-uri`, so casing/encoding stay
|
|
148
|
+
* consistent across platforms.
|
|
149
|
+
*/
|
|
150
|
+
const diagnosticFileToUri = (rootDir, file) => {
|
|
151
|
+
const absolute = isAbsolute(file) ? file : resolve(rootDir, file);
|
|
152
|
+
return URI.file(absolute).toString();
|
|
153
|
+
};
|
|
154
|
+
/** Converts a `file://` URI back to an absolute fs path. */
|
|
155
|
+
const uriToFsPath = (uri) => URI.parse(uri).fsPath;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/group.ts
|
|
158
|
+
/**
|
|
159
|
+
* Transforms an `AuditReport` into a per-URI map of LSP diagnostics. Every
|
|
160
|
+
* URI in `previousUris` that has no findings this pass is included with an
|
|
161
|
+
* empty array so the server can clear it. Grouping is keyed by the resolved
|
|
162
|
+
* `file://` URI of each diagnostic's `file`.
|
|
163
|
+
*/
|
|
164
|
+
const groupDiagnosticsByUri = (input) => {
|
|
165
|
+
const { report, textForFile, previousUris } = input;
|
|
166
|
+
const byUri = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const uri of previousUris ?? []) byUri.set(uri, []);
|
|
168
|
+
const textCache = /* @__PURE__ */ new Map();
|
|
169
|
+
const readText = (file) => {
|
|
170
|
+
const cached = textCache.get(file);
|
|
171
|
+
if (cached !== void 0) return cached;
|
|
172
|
+
const text = textForFile(file);
|
|
173
|
+
textCache.set(file, text);
|
|
174
|
+
return text;
|
|
175
|
+
};
|
|
176
|
+
for (const diagnostic of report.diagnostics) appendDiagnostic(byUri, report.rootDir, diagnostic, readText);
|
|
177
|
+
return byUri;
|
|
178
|
+
};
|
|
179
|
+
const appendDiagnostic = (byUri, rootDir, diagnostic, readText) => {
|
|
180
|
+
const uri = diagnosticFileToUri(rootDir, diagnostic.file);
|
|
181
|
+
const lsp = toLspDiagnostic({
|
|
182
|
+
diagnostic,
|
|
183
|
+
text: readText(diagnostic.file)
|
|
184
|
+
});
|
|
185
|
+
const existing = byUri.get(uri);
|
|
186
|
+
if (existing) existing.push(lsp);
|
|
187
|
+
else byUri.set(uri, [lsp]);
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/selection.ts
|
|
191
|
+
/**
|
|
192
|
+
* Picks which doctor plugin set applies to a project from its detected
|
|
193
|
+
* framework. Nuxt projects get the Nuxt + Vue rule passes; plain Vue
|
|
194
|
+
* projects get the Vue passes; an unrecognized project is `unknown` and the
|
|
195
|
+
* server skips auditing it (no framework → no applicable rules).
|
|
196
|
+
*/
|
|
197
|
+
const selectProjectKind = (project) => {
|
|
198
|
+
const framework = project.framework;
|
|
199
|
+
if (framework === "nuxt") return "nuxt";
|
|
200
|
+
if (framework === "vue") return "vue";
|
|
201
|
+
return "unknown";
|
|
202
|
+
};
|
|
203
|
+
/** Whether a detected project is one doctor can audit (Vue or Nuxt). */
|
|
204
|
+
const isAuditableProject = (project) => selectProjectKind(project) !== "unknown";
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/scheduler.ts
|
|
207
|
+
const createScheduler = (options) => {
|
|
208
|
+
const defaultDebounceMs = options.debounceMs ?? 400;
|
|
209
|
+
let disposed = false;
|
|
210
|
+
let generation = 0;
|
|
211
|
+
let running = 0;
|
|
212
|
+
const timers = /* @__PURE__ */ new Map();
|
|
213
|
+
const latestGeneration = /* @__PURE__ */ new Map();
|
|
214
|
+
const run = (key, id) => {
|
|
215
|
+
running += 1;
|
|
216
|
+
Promise.resolve(options.performScan(key, { get isCancelled() {
|
|
217
|
+
return disposed || latestGeneration.get(key) !== id;
|
|
218
|
+
} })).catch((error) => {
|
|
219
|
+
options.onError?.(error, key);
|
|
220
|
+
}).finally(() => {
|
|
221
|
+
running -= 1;
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const enqueue = (key, delayMs) => {
|
|
225
|
+
if (disposed) return;
|
|
226
|
+
const id = generation += 1;
|
|
227
|
+
latestGeneration.set(key, id);
|
|
228
|
+
const existing = timers.get(key);
|
|
229
|
+
if (existing) clearTimeout(existing);
|
|
230
|
+
const timer = setTimeout(() => {
|
|
231
|
+
timers.delete(key);
|
|
232
|
+
run(key, id);
|
|
233
|
+
}, delayMs ?? defaultDebounceMs);
|
|
234
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
235
|
+
timers.set(key, timer);
|
|
236
|
+
};
|
|
237
|
+
const cancel = (key) => {
|
|
238
|
+
const timer = timers.get(key);
|
|
239
|
+
if (timer) {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
timers.delete(key);
|
|
242
|
+
}
|
|
243
|
+
latestGeneration.set(key, generation += 1);
|
|
244
|
+
};
|
|
245
|
+
const dispose = () => {
|
|
246
|
+
disposed = true;
|
|
247
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
248
|
+
timers.clear();
|
|
249
|
+
};
|
|
250
|
+
const pendingCount = () => timers.size + running;
|
|
251
|
+
return {
|
|
252
|
+
enqueue,
|
|
253
|
+
cancel,
|
|
254
|
+
pendingCount,
|
|
255
|
+
dispose
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/server.ts
|
|
260
|
+
const SERVER_VERSION = "0.1.0";
|
|
261
|
+
/**
|
|
262
|
+
* Builds and wires the Doctor language server onto a connection. Exposed
|
|
263
|
+
* separately from {@link startLanguageServer} so the I/O shell stays thin and
|
|
264
|
+
* the pure transforms (mapper, positions, severity, group, scheduler, cache,
|
|
265
|
+
* selection) carry the behavior — and the coverage.
|
|
266
|
+
*/
|
|
267
|
+
const createServer = (connection) => {
|
|
268
|
+
const documents = new TextDocuments(TextDocument);
|
|
269
|
+
const versionCache = createVersionCache();
|
|
270
|
+
let workspaceRoot = null;
|
|
271
|
+
const publishedUris = /* @__PURE__ */ new Set();
|
|
272
|
+
const readText = (fsPath) => {
|
|
273
|
+
const fileUri = `file://${fsPath}`;
|
|
274
|
+
const open = documents.get(fileUri);
|
|
275
|
+
if (open) return open.getText();
|
|
276
|
+
try {
|
|
277
|
+
return readFileSync(fsPath, "utf8");
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const runAudit = async (rootDir, scopeFsPath) => {
|
|
283
|
+
if (!isAuditableProject(await detectProject(rootDir))) return;
|
|
284
|
+
const byUri = groupDiagnosticsByUri({
|
|
285
|
+
report: await audit({
|
|
286
|
+
rootDir,
|
|
287
|
+
...scopeFsPath !== null ? { scopeFiles: [scopeFsPath] } : {}
|
|
288
|
+
}),
|
|
289
|
+
textForFile: (file) => readText(file.startsWith("/") ? file : `${rootDir}/${file}`),
|
|
290
|
+
previousUris: publishedUris
|
|
291
|
+
});
|
|
292
|
+
for (const [uri, diagnostics] of byUri) {
|
|
293
|
+
connection.sendDiagnostics({
|
|
294
|
+
uri,
|
|
295
|
+
diagnostics
|
|
296
|
+
});
|
|
297
|
+
if (diagnostics.length > 0) publishedUris.add(uri);
|
|
298
|
+
else publishedUris.delete(uri);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const scheduler = createScheduler({
|
|
302
|
+
debounceMs: 400,
|
|
303
|
+
performScan: async (key) => {
|
|
304
|
+
if (workspaceRoot === null) return;
|
|
305
|
+
const scope = key === workspaceRoot ? null : uriToFsPath(key);
|
|
306
|
+
await runAudit(workspaceRoot, scope);
|
|
307
|
+
},
|
|
308
|
+
onError: (error, key) => connection.console.error(`Audit of ${key} failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
309
|
+
});
|
|
310
|
+
connection.onInitialize((params) => {
|
|
311
|
+
workspaceRoot = resolveWorkspaceRoot(params);
|
|
312
|
+
return {
|
|
313
|
+
capabilities: { textDocumentSync: {
|
|
314
|
+
openClose: true,
|
|
315
|
+
change: TextDocumentSyncKind.Incremental,
|
|
316
|
+
save: { includeText: false }
|
|
317
|
+
} },
|
|
318
|
+
serverInfo: {
|
|
319
|
+
name: SERVER_DISPLAY_NAME,
|
|
320
|
+
version: SERVER_VERSION
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
connection.onInitialized(() => {
|
|
325
|
+
if (workspaceRoot !== null) {
|
|
326
|
+
const root = workspaceRoot;
|
|
327
|
+
setTimeout(() => scheduler.enqueue(root, 300), 0);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
documents.onDidOpen((event) => {
|
|
331
|
+
if (versionCache.shouldAudit(event.document.uri, event.document.version)) {
|
|
332
|
+
versionCache.markAudited(event.document.uri, event.document.version);
|
|
333
|
+
scheduler.enqueue(event.document.uri);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
documents.onDidChangeContent((event) => {
|
|
337
|
+
if (versionCache.shouldAudit(event.document.uri, event.document.version)) {
|
|
338
|
+
versionCache.markAudited(event.document.uri, event.document.version);
|
|
339
|
+
scheduler.enqueue(event.document.uri);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
documents.onDidSave((event) => {
|
|
343
|
+
scheduler.enqueue(event.document.uri, 0);
|
|
344
|
+
});
|
|
345
|
+
documents.onDidClose((event) => {
|
|
346
|
+
versionCache.forget(event.document.uri);
|
|
347
|
+
scheduler.cancel(event.document.uri);
|
|
348
|
+
});
|
|
349
|
+
connection.onShutdown(() => {
|
|
350
|
+
scheduler.dispose();
|
|
351
|
+
});
|
|
352
|
+
documents.listen(connection);
|
|
353
|
+
connection.listen();
|
|
354
|
+
};
|
|
355
|
+
const resolveWorkspaceRoot = (params) => {
|
|
356
|
+
if (params.workspaceFolders && params.workspaceFolders.length > 0) return uriToFsPath(params.workspaceFolders[0].uri);
|
|
357
|
+
if (params.rootUri) return uriToFsPath(params.rootUri);
|
|
358
|
+
if (params.rootPath) return params.rootPath;
|
|
359
|
+
return null;
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* stdout is the LSP message channel — any stray write corrupts the protocol
|
|
363
|
+
* stream. Route accidental `console.log` / `info` / `debug` to stderr.
|
|
364
|
+
*/
|
|
365
|
+
const protectStdoutChannel = () => {
|
|
366
|
+
const toStderr = (...args) => {
|
|
367
|
+
process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`);
|
|
368
|
+
};
|
|
369
|
+
console.log = toStderr;
|
|
370
|
+
console.info = toStderr;
|
|
371
|
+
console.debug = toStderr;
|
|
372
|
+
};
|
|
373
|
+
/** Entry point: starts the server over stdio. */
|
|
374
|
+
const startLanguageServer = () => {
|
|
375
|
+
protectStdoutChannel();
|
|
376
|
+
createServer(createConnection(process.stdin, process.stdout));
|
|
377
|
+
};
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/index.ts
|
|
380
|
+
/** Public API surface of `@geoql/doctor-language-server`. */
|
|
381
|
+
const api = {
|
|
382
|
+
createServer,
|
|
383
|
+
startLanguageServer,
|
|
384
|
+
toLspDiagnostic,
|
|
385
|
+
rangeFromLineColumn,
|
|
386
|
+
rangesOverlap,
|
|
387
|
+
toZeroBasedPosition,
|
|
388
|
+
toLspSeverity,
|
|
389
|
+
severityLabel,
|
|
390
|
+
groupDiagnosticsByUri,
|
|
391
|
+
selectProjectKind,
|
|
392
|
+
isAuditableProject,
|
|
393
|
+
diagnosticFileToUri,
|
|
394
|
+
uriToFsPath,
|
|
395
|
+
createScheduler,
|
|
396
|
+
createVersionCache,
|
|
397
|
+
shouldAuditVersion,
|
|
398
|
+
DIAGNOSTIC_SOURCE,
|
|
399
|
+
SERVER_DISPLAY_NAME,
|
|
400
|
+
DOCUMENT_CHANGE_DEBOUNCE_MS: 400,
|
|
401
|
+
INITIAL_WORKSPACE_SCAN_DELAY_MS: 300,
|
|
402
|
+
SCANNABLE_EXTENSIONS
|
|
403
|
+
};
|
|
404
|
+
//#endregion
|
|
405
|
+
export { DIAGNOSTIC_SOURCE, DOCUMENT_CHANGE_DEBOUNCE_MS, INITIAL_WORKSPACE_SCAN_DELAY_MS, SCANNABLE_EXTENSIONS, SERVER_DISPLAY_NAME, api, createScheduler, createServer, createVersionCache, diagnosticFileToUri, groupDiagnosticsByUri, isAuditableProject, rangeFromLineColumn, rangesOverlap, selectProjectKind, severityLabel, shouldAuditVersion, startLanguageServer, toLspDiagnostic, toLspSeverity, toZeroBasedPosition, uriToFsPath };
|
|
406
|
+
|
|
407
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/cache.ts","../src/positions.ts","../src/severity.ts","../src/mapper.ts","../src/uri.ts","../src/group.ts","../src/selection.ts","../src/scheduler.ts","../src/server.ts","../src/index.ts"],"sourcesContent":["/** Display name used in client-facing messages and progress titles. */\nexport const SERVER_DISPLAY_NAME = 'Doctor';\n\n/** `Diagnostic.source` shown next to every published diagnostic. */\nexport const DIAGNOSTIC_SOURCE = 'doctor';\n\n/**\n * Debounce window between an open document's last edit and the rescan it\n * triggers. Long enough that fast typing collapses into a single audit,\n * short enough to still feel live. Save scans run with no debounce.\n */\nexport const DOCUMENT_CHANGE_DEBOUNCE_MS = 400;\n\n/** Delay after `initialized` before the first background workspace scan. */\nexport const INITIAL_WORKSPACE_SCAN_DELAY_MS = 300;\n\n/**\n * Source file extensions the server scans on open / change / save. Mirrors\n * doctor-core's default include set so editor scanning covers the same\n * files the CLI audits.\n */\nexport const SCANNABLE_EXTENSIONS = [\n '.vue',\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n] as const;\n","/**\n * Per-document version cache. The LSP `TextDocument.version` increments on\n * every edit, so a request to audit a `(uri, version)` pair already audited\n * (or in flight) is redundant. `shouldAudit` is the pure decision; the\n * server records the version it acted on via `markAudited`.\n */\nexport interface VersionCache {\n /** Whether `uri` at `version` warrants a fresh audit (not already seen). */\n readonly shouldAudit: (uri: string, version: number) => boolean;\n /** Records that `uri` was audited at `version`. */\n readonly markAudited: (uri: string, version: number) => void;\n /** Forgets a document (on close) so a later reopen always re-audits. */\n readonly forget: (uri: string) => void;\n}\n\n/** Pure decision: audit only when this version is newer than the last seen. */\nexport const shouldAuditVersion = (\n lastAuditedVersion: number | undefined,\n version: number,\n): boolean => lastAuditedVersion === undefined || version > lastAuditedVersion;\n\nexport const createVersionCache = (): VersionCache => {\n const lastAudited = new Map<string, number>();\n return {\n shouldAudit: (uri, version) =>\n shouldAuditVersion(lastAudited.get(uri), version),\n markAudited: (uri, version) => {\n lastAudited.set(uri, version);\n },\n forget: (uri) => {\n lastAudited.delete(uri);\n },\n };\n};\n","import type { Position, Range } from 'vscode-languageserver';\n\n/**\n * doctor-core reports diagnostics with 1-indexed `line` / `column` (and an\n * optional 1-indexed `endLine` / `endColumn`), but LSP positions are\n * 0-indexed. These helpers convert between the two against the document text\n * so squiggles land exactly on the offending token.\n */\n\n/** Converts a 1-indexed line/column pair to a 0-indexed LSP `Position`. */\nexport const toZeroBasedPosition = (\n line: number,\n column: number,\n): Position => ({\n line: Math.max(0, (line || 1) - 1),\n character: Math.max(0, (column || 1) - 1),\n});\n\n/**\n * Builds an LSP `Range` from doctor-core's 1-indexed coordinates. When an\n * explicit `endLine` / `endColumn` is present it is used directly; otherwise\n * the range extends to the end of the start line (when document text is\n * known) so the squiggle is visible, falling back to a single-character span\n * when the text is unavailable.\n */\nexport const rangeFromLineColumn = (\n text: string | null,\n line: number,\n column: number,\n endLine?: number,\n endColumn?: number,\n): Range => {\n const start = toZeroBasedPosition(line, column);\n\n if (endLine !== undefined && endColumn !== undefined) {\n return { start, end: toZeroBasedPosition(endLine, endColumn) };\n }\n\n if (text !== null) {\n const lines = text.split('\\n');\n const lineText = lines[start.line] ?? '';\n const endCharacter = Math.max(\n start.character + 1,\n lineText.replace(/\\r$/, '').length,\n );\n return { start, end: { line: start.line, character: endCharacter } };\n }\n\n return { start, end: { line: start.line, character: start.character + 1 } };\n};\n\nconst isBefore = (first: Position, second: Position): boolean =>\n first.line < second.line ||\n (first.line === second.line && first.character < second.character);\n\n/** Whether two ranges overlap (touching endpoints count as overlap). */\nexport const rangesOverlap = (first: Range, second: Range): boolean =>\n !isBefore(first.end, second.start) && !isBefore(second.end, first.start);\n","import { DiagnosticSeverity } from 'vscode-languageserver';\nimport type { Severity } from '@geoql/doctor-core';\n\n/**\n * Maps doctor-core's internal severity vocabulary (`error | warn | info`) to\n * the LSP `DiagnosticSeverity` enum: `error → Error`, `warn → Warning`,\n * `info → Information`. Doctor never emits LSP `Hint`.\n */\nexport const toLspSeverity = (severity: Severity): DiagnosticSeverity => {\n switch (severity) {\n case 'error':\n return DiagnosticSeverity.Error;\n case 'warn':\n return DiagnosticSeverity.Warning;\n case 'info':\n return DiagnosticSeverity.Information;\n }\n};\n\n/**\n * Human-readable label for an LSP diagnostic's severity. Covers all four LSP\n * severities so a value outside doctor's vocabulary isn't mislabeled.\n */\nexport const severityLabel = (\n severity: DiagnosticSeverity | undefined,\n): string => {\n switch (severity) {\n case DiagnosticSeverity.Error:\n return 'error';\n case DiagnosticSeverity.Information:\n return 'info';\n case DiagnosticSeverity.Hint:\n return 'hint';\n default:\n return 'warning';\n }\n};\n","import { docsUrl } from '@geoql/doctor-core';\nimport type { Diagnostic as CoreDiagnostic } from '@geoql/doctor-core';\nimport type { Diagnostic as LspDiagnostic } from 'vscode-languageserver';\nimport { DIAGNOSTIC_SOURCE } from './constants.js';\nimport { rangeFromLineColumn } from './positions.js';\nimport { toLspSeverity } from './severity.js';\n\nexport interface MapDiagnosticInput {\n readonly diagnostic: CoreDiagnostic;\n /** Text of the diagnostic's file for end-of-line range fallback; `null` → single column. */\n readonly text: string | null;\n}\n\n/**\n * Converts a core doctor `Diagnostic` into an LSP `Diagnostic` with a precise\n * 0-based range, `source: 'doctor'`, `code: ruleId`, a `codeDescription`\n * pointing at the rule's docs page, and the recommendation appended to the\n * message when present.\n */\nexport const toLspDiagnostic = (input: MapDiagnosticInput): LspDiagnostic => {\n const { diagnostic, text } = input;\n const range = rangeFromLineColumn(\n text,\n diagnostic.line,\n diagnostic.column,\n diagnostic.endLine,\n diagnostic.endColumn,\n );\n\n return {\n range,\n severity: toLspSeverity(diagnostic.severity),\n code: diagnostic.ruleId,\n codeDescription: { href: docsUrl(diagnostic.ruleId) },\n source: DIAGNOSTIC_SOURCE,\n message: diagnostic.message,\n };\n};\n","import { isAbsolute, resolve } from 'node:path';\nimport { URI } from 'vscode-uri';\n\n/**\n * Resolves a doctor-core diagnostic's `file` (relative to the audit root, or\n * already absolute) to a `file://` URI the editor can address. Paths are\n * normalized to forward slashes by `vscode-uri`, so casing/encoding stay\n * consistent across platforms.\n */\nexport const diagnosticFileToUri = (rootDir: string, file: string): string => {\n const absolute = isAbsolute(file) ? file : resolve(rootDir, file);\n return URI.file(absolute).toString();\n};\n\n/** Converts a `file://` URI back to an absolute fs path. */\nexport const uriToFsPath = (uri: string): string => URI.parse(uri).fsPath;\n","import type {\n AuditReport,\n Diagnostic as CoreDiagnostic,\n} from '@geoql/doctor-core';\nimport type { Diagnostic as LspDiagnostic } from 'vscode-languageserver';\nimport { toLspDiagnostic } from './mapper.js';\nimport { diagnosticFileToUri } from './uri.js';\n\nexport interface GroupDiagnosticsInput {\n readonly report: AuditReport;\n /**\n * Returns the live text for a diagnostic's file (open buffer first, then\n * disk) so range fallback can extend to end-of-line; `null` when unknown.\n */\n readonly textForFile: (file: string) => string | null;\n /**\n * URIs that previously held diagnostics and must be republished even when\n * the new audit found nothing in them — otherwise stale squiggles linger.\n */\n readonly previousUris?: Iterable<string>;\n}\n\n/**\n * Transforms an `AuditReport` into a per-URI map of LSP diagnostics. Every\n * URI in `previousUris` that has no findings this pass is included with an\n * empty array so the server can clear it. Grouping is keyed by the resolved\n * `file://` URI of each diagnostic's `file`.\n */\nexport const groupDiagnosticsByUri = (\n input: GroupDiagnosticsInput,\n): Map<string, LspDiagnostic[]> => {\n const { report, textForFile, previousUris } = input;\n const byUri = new Map<string, LspDiagnostic[]>();\n\n for (const uri of previousUris ?? []) {\n byUri.set(uri, []);\n }\n\n const textCache = new Map<string, string | null>();\n const readText = (file: string): string | null => {\n const cached = textCache.get(file);\n if (cached !== undefined) return cached;\n const text = textForFile(file);\n textCache.set(file, text);\n return text;\n };\n\n for (const diagnostic of report.diagnostics) {\n appendDiagnostic(byUri, report.rootDir, diagnostic, readText);\n }\n\n return byUri;\n};\n\nconst appendDiagnostic = (\n byUri: Map<string, LspDiagnostic[]>,\n rootDir: string,\n diagnostic: CoreDiagnostic,\n readText: (file: string) => string | null,\n): void => {\n const uri = diagnosticFileToUri(rootDir, diagnostic.file);\n const lsp = toLspDiagnostic({ diagnostic, text: readText(diagnostic.file) });\n const existing = byUri.get(uri);\n if (existing) existing.push(lsp);\n else byUri.set(uri, [lsp]);\n};\n","import type { Framework, ProjectInfo } from '@geoql/doctor-core';\n\n/** Editor-facing project flavor the audit runs as. */\nexport type DoctorProjectKind = 'vue' | 'nuxt' | 'unknown';\n\n/**\n * Picks which doctor plugin set applies to a project from its detected\n * framework. Nuxt projects get the Nuxt + Vue rule passes; plain Vue\n * projects get the Vue passes; an unrecognized project is `unknown` and the\n * server skips auditing it (no framework → no applicable rules).\n */\nexport const selectProjectKind = (\n project: Pick<ProjectInfo, 'framework'>,\n): DoctorProjectKind => {\n const framework: Framework = project.framework;\n if (framework === 'nuxt') return 'nuxt';\n if (framework === 'vue') return 'vue';\n return 'unknown';\n};\n\n/** Whether a detected project is one doctor can audit (Vue or Nuxt). */\nexport const isAuditableProject = (\n project: Pick<ProjectInfo, 'framework'>,\n): boolean => selectProjectKind(project) !== 'unknown';\n","/**\n * Per-key debounced scheduler. Each editor scope (a document URI, or a\n * workspace root) gets one in-flight audit at a time: rapid edits to the same\n * document collapse into a single trailing run, and a monotonic generation\n * per key supersedes stale work so a slow audit can never clobber a fresher\n * result. Save scans pass `debounceMs: 0` to run immediately.\n */\nexport interface SchedulerOptions {\n /** Trailing debounce window for an enqueued key, in ms. Default 400. */\n readonly debounceMs?: number;\n /** Runs the audit for a key; rejections are surfaced via `onError`. */\n readonly performScan: (\n key: string,\n token: CancellationToken,\n ) => Promise<void>;\n /** Called when a `performScan` promise rejects. */\n readonly onError?: (error: unknown, key: string) => void;\n}\n\n/** Cooperative cancellation: `true` once a newer enqueue superseded the run. */\nexport interface CancellationToken {\n readonly isCancelled: boolean;\n}\n\nexport interface Scheduler {\n /** Queue (or re-queue) an audit for `key`, debounced. `delayMs` overrides the default. */\n readonly enqueue: (key: string, delayMs?: number) => void;\n /** Cancel any pending/in-flight work for `key`. */\n readonly cancel: (key: string) => void;\n /** Pending timers + in-flight runs; for tests and idle checks. */\n readonly pendingCount: () => number;\n /** Stop all timers and abandon queued work. */\n readonly dispose: () => void;\n}\n\nexport const createScheduler = (options: SchedulerOptions): Scheduler => {\n const defaultDebounceMs = options.debounceMs ?? 400;\n let disposed = false;\n let generation = 0;\n let running = 0;\n const timers = new Map<string, ReturnType<typeof setTimeout>>();\n const latestGeneration = new Map<string, number>();\n\n const run = (key: string, id: number): void => {\n running += 1;\n const token: CancellationToken = {\n get isCancelled() {\n return disposed || latestGeneration.get(key) !== id;\n },\n };\n Promise.resolve(options.performScan(key, token))\n .catch((error: unknown) => {\n options.onError?.(error, key);\n })\n .finally(() => {\n running -= 1;\n });\n };\n\n const enqueue = (key: string, delayMs?: number): void => {\n if (disposed) return;\n const id = (generation += 1);\n latestGeneration.set(key, id);\n\n const existing = timers.get(key);\n if (existing) clearTimeout(existing);\n\n const delay = delayMs ?? defaultDebounceMs;\n const timer = setTimeout(() => {\n timers.delete(key);\n run(key, id);\n }, delay);\n if (typeof timer.unref === 'function') timer.unref();\n timers.set(key, timer);\n };\n\n const cancel = (key: string): void => {\n const timer = timers.get(key);\n if (timer) {\n clearTimeout(timer);\n timers.delete(key);\n }\n // Bump to a generation no live request carries → supersedes in-flight work.\n latestGeneration.set(key, (generation += 1));\n };\n\n const dispose = (): void => {\n disposed = true;\n for (const timer of timers.values()) clearTimeout(timer);\n timers.clear();\n };\n\n const pendingCount = (): number => timers.size + running;\n\n return { enqueue, cancel, pendingCount, dispose };\n};\n","import { readFileSync } from 'node:fs';\nimport { audit, detectProject } from '@geoql/doctor-core';\nimport {\n TextDocuments,\n TextDocumentSyncKind,\n createConnection,\n type Connection,\n type InitializeParams,\n type InitializeResult,\n type ServerCapabilities,\n} from 'vscode-languageserver/node';\nimport { TextDocument } from 'vscode-languageserver-textdocument';\nimport {\n DOCUMENT_CHANGE_DEBOUNCE_MS,\n INITIAL_WORKSPACE_SCAN_DELAY_MS,\n SERVER_DISPLAY_NAME,\n} from './constants.js';\nimport { createVersionCache } from './cache.js';\nimport { groupDiagnosticsByUri } from './group.js';\nimport { isAuditableProject } from './selection.js';\nimport { createScheduler } from './scheduler.js';\nimport { uriToFsPath } from './uri.js';\n\nconst SERVER_VERSION = process.env.VERSION ?? '0.0.0-dev';\n\n/**\n * Builds and wires the Doctor language server onto a connection. Exposed\n * separately from {@link startLanguageServer} so the I/O shell stays thin and\n * the pure transforms (mapper, positions, severity, group, scheduler, cache,\n * selection) carry the behavior — and the coverage.\n */\nexport const createServer = (connection: Connection): void => {\n const documents = new TextDocuments(TextDocument);\n const versionCache = createVersionCache();\n let workspaceRoot: string | null = null;\n\n // URIs that currently hold published diagnostics, so a clean re-audit can\n // clear stale squiggles instead of leaving them behind.\n const publishedUris = new Set<string>();\n\n const readText = (fsPath: string): string | null => {\n const fileUri = `file://${fsPath}`;\n const open = documents.get(fileUri);\n if (open) return open.getText();\n try {\n return readFileSync(fsPath, 'utf8');\n } catch {\n return null;\n }\n };\n\n const runAudit = async (\n rootDir: string,\n scopeFsPath: string | null,\n ): Promise<void> => {\n const project = await detectProject(rootDir);\n if (!isAuditableProject(project)) return;\n\n const report = await audit({\n rootDir,\n ...(scopeFsPath !== null ? { scopeFiles: [scopeFsPath] } : {}),\n });\n\n const byUri = groupDiagnosticsByUri({\n report,\n textForFile: (file) =>\n readText(file.startsWith('/') ? file : `${rootDir}/${file}`),\n previousUris: publishedUris,\n });\n\n for (const [uri, diagnostics] of byUri) {\n connection.sendDiagnostics({ uri, diagnostics });\n if (diagnostics.length > 0) publishedUris.add(uri);\n else publishedUris.delete(uri);\n }\n };\n\n const scheduler = createScheduler({\n debounceMs: DOCUMENT_CHANGE_DEBOUNCE_MS,\n performScan: async (key) => {\n if (workspaceRoot === null) return;\n const scope = key === workspaceRoot ? null : uriToFsPath(key);\n await runAudit(workspaceRoot, scope);\n },\n onError: (error, key) =>\n connection.console.error(\n `Audit of ${key} failed: ${error instanceof Error ? error.message : String(error)}`,\n ),\n });\n\n connection.onInitialize((params: InitializeParams): InitializeResult => {\n workspaceRoot = resolveWorkspaceRoot(params);\n const capabilities: ServerCapabilities = {\n textDocumentSync: {\n openClose: true,\n change: TextDocumentSyncKind.Incremental,\n save: { includeText: false },\n },\n };\n return {\n capabilities,\n serverInfo: { name: SERVER_DISPLAY_NAME, version: SERVER_VERSION },\n };\n });\n\n connection.onInitialized(() => {\n if (workspaceRoot !== null) {\n const root = workspaceRoot;\n setTimeout(\n () => scheduler.enqueue(root, INITIAL_WORKSPACE_SCAN_DELAY_MS),\n 0,\n );\n }\n });\n\n // Open + save audit the touched file (save with no debounce); per-keystroke\n // edits ride the version cache + debounce so a burst collapses to one run.\n documents.onDidOpen((event) => {\n if (versionCache.shouldAudit(event.document.uri, event.document.version)) {\n versionCache.markAudited(event.document.uri, event.document.version);\n scheduler.enqueue(event.document.uri);\n }\n });\n\n documents.onDidChangeContent((event) => {\n if (versionCache.shouldAudit(event.document.uri, event.document.version)) {\n versionCache.markAudited(event.document.uri, event.document.version);\n scheduler.enqueue(event.document.uri);\n }\n });\n\n documents.onDidSave((event) => {\n scheduler.enqueue(event.document.uri, 0);\n });\n\n documents.onDidClose((event) => {\n versionCache.forget(event.document.uri);\n scheduler.cancel(event.document.uri);\n });\n\n connection.onShutdown(() => {\n scheduler.dispose();\n });\n\n documents.listen(connection);\n connection.listen();\n};\n\nconst resolveWorkspaceRoot = (params: InitializeParams): string | null => {\n if (params.workspaceFolders && params.workspaceFolders.length > 0) {\n return uriToFsPath(params.workspaceFolders[0].uri);\n }\n if (params.rootUri) return uriToFsPath(params.rootUri);\n if (params.rootPath) return params.rootPath;\n return null;\n};\n\n/**\n * stdout is the LSP message channel — any stray write corrupts the protocol\n * stream. Route accidental `console.log` / `info` / `debug` to stderr.\n */\nconst protectStdoutChannel = (): void => {\n const toStderr = (...args: unknown[]): void => {\n process.stderr.write(`${args.map((arg) => String(arg)).join(' ')}\\n`);\n };\n console.log = toStderr;\n console.info = toStderr;\n console.debug = toStderr;\n};\n\n/** Entry point: starts the server over stdio. */\nexport const startLanguageServer = (): void => {\n protectStdoutChannel();\n const connection = createConnection(process.stdin, process.stdout);\n createServer(connection);\n};\n","import { createServer, startLanguageServer } from './server.js';\nimport {\n createVersionCache,\n shouldAuditVersion,\n type VersionCache,\n} from './cache.js';\nimport {\n createScheduler,\n type CancellationToken,\n type Scheduler,\n type SchedulerOptions,\n} from './scheduler.js';\nimport { groupDiagnosticsByUri, type GroupDiagnosticsInput } from './group.js';\nimport { toLspDiagnostic, type MapDiagnosticInput } from './mapper.js';\nimport {\n rangeFromLineColumn,\n rangesOverlap,\n toZeroBasedPosition,\n} from './positions.js';\nimport { severityLabel, toLspSeverity } from './severity.js';\nimport {\n isAuditableProject,\n selectProjectKind,\n type DoctorProjectKind,\n} from './selection.js';\nimport { diagnosticFileToUri, uriToFsPath } from './uri.js';\nimport {\n DIAGNOSTIC_SOURCE,\n DOCUMENT_CHANGE_DEBOUNCE_MS,\n INITIAL_WORKSPACE_SCAN_DELAY_MS,\n SCANNABLE_EXTENSIONS,\n SERVER_DISPLAY_NAME,\n} from './constants.js';\n\n/** Public API surface of `@geoql/doctor-language-server`. */\nexport const api = {\n createServer,\n startLanguageServer,\n toLspDiagnostic,\n rangeFromLineColumn,\n rangesOverlap,\n toZeroBasedPosition,\n toLspSeverity,\n severityLabel,\n groupDiagnosticsByUri,\n selectProjectKind,\n isAuditableProject,\n diagnosticFileToUri,\n uriToFsPath,\n createScheduler,\n createVersionCache,\n shouldAuditVersion,\n DIAGNOSTIC_SOURCE,\n SERVER_DISPLAY_NAME,\n DOCUMENT_CHANGE_DEBOUNCE_MS,\n INITIAL_WORKSPACE_SCAN_DELAY_MS,\n SCANNABLE_EXTENSIONS,\n} as const;\n\nexport {\n createServer,\n startLanguageServer,\n createVersionCache,\n shouldAuditVersion,\n type VersionCache,\n createScheduler,\n type CancellationToken,\n type Scheduler,\n type SchedulerOptions,\n groupDiagnosticsByUri,\n type GroupDiagnosticsInput,\n toLspDiagnostic,\n type MapDiagnosticInput,\n rangeFromLineColumn,\n rangesOverlap,\n toZeroBasedPosition,\n severityLabel,\n toLspSeverity,\n isAuditableProject,\n selectProjectKind,\n type DoctorProjectKind,\n diagnosticFileToUri,\n uriToFsPath,\n DIAGNOSTIC_SOURCE,\n DOCUMENT_CHANGE_DEBOUNCE_MS,\n INITIAL_WORKSPACE_SCAN_DELAY_MS,\n SCANNABLE_EXTENSIONS,\n SERVER_DISPLAY_NAME,\n};\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geoql/doctor-language-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "LSP server that runs the @geoql/doctor audit engine over Vue 3 + Nuxt 4 files and publishes diagnostics to any LSP-capable editor. TypeScript, ESM, stdio.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"audit",
|
|
8
|
+
"doctor",
|
|
9
|
+
"geoql",
|
|
10
|
+
"language-server",
|
|
11
|
+
"lint",
|
|
12
|
+
"lsp",
|
|
13
|
+
"nuxt",
|
|
14
|
+
"vue",
|
|
15
|
+
"vue3"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/geoql/doctor#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/geoql/doctor/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "Vinayak Kulkarni",
|
|
24
|
+
"email": "inbox.vinayak@gmail.com",
|
|
25
|
+
"url": "https://vinayakkulkarni.dev"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/geoql/doctor.git",
|
|
30
|
+
"directory": "packages/doctor-language-server"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"doctor-language-server": "./bin/doctor-language-server.mjs"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"bin"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"types": "./dist/index.d.mts",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"import": "./dist/index.mjs",
|
|
44
|
+
"types": "./dist/index.d.mts"
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"vscode-languageserver": "^10.0.0",
|
|
53
|
+
"vscode-languageserver-textdocument": "^1.0.12",
|
|
54
|
+
"vscode-uri": "^3.1.0",
|
|
55
|
+
"@geoql/doctor-core": "^1.1.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^25.9.3",
|
|
59
|
+
"typescript": "^6.0.3",
|
|
60
|
+
"vitest": "^4.1.9"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=24"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"lint": "vp lint src",
|
|
67
|
+
"lint:fix": "vp lint src --fix",
|
|
68
|
+
"format": "vp fmt",
|
|
69
|
+
"format:check": "vp fmt --check",
|
|
70
|
+
"build": "vp pack",
|
|
71
|
+
"test": "vitest run"
|
|
72
|
+
}
|
|
73
|
+
}
|