@geoql/doctor-language-server 0.1.0 → 0.1.2

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/dist/index.mjs CHANGED
@@ -257,7 +257,7 @@ const createScheduler = (options) => {
257
257
  };
258
258
  //#endregion
259
259
  //#region src/server.ts
260
- const SERVER_VERSION = "0.1.0";
260
+ const SERVER_VERSION = "0.1.2";
261
261
  /**
262
262
  * Builds and wires the Doctor language server onto a connection. Exposed
263
263
  * separately from {@link startLanguageServer} so the I/O shell stays thin and
@@ -1 +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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"}
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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geoql/doctor-language-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
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
6
  "keywords": [
@@ -52,10 +52,10 @@
52
52
  "vscode-languageserver": "^10.0.0",
53
53
  "vscode-languageserver-textdocument": "^1.0.12",
54
54
  "vscode-uri": "^3.1.0",
55
- "@geoql/doctor-core": "^1.1.1"
55
+ "@geoql/doctor-core": "^1.2.1"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/node": "^25.9.3",
58
+ "@types/node": "^25.9.4",
59
59
  "typescript": "^6.0.3",
60
60
  "vitest": "^4.1.9"
61
61
  },