@danypops/lector 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/adapters/lsp/discover-seed-file.ts +17 -9
- package/src/adapters/lsp/lsp-symbol-index.ts +41 -14
- package/src/adapters/polyglot-code-intelligence-index.ts +135 -0
- package/src/adapters/sqlite-symbol-graph.ts +31 -10
- package/src/cli.ts +10 -0
- package/src/domain/intelligence-provenance.ts +8 -0
- package/src/domain/language-server-descriptor.ts +4 -0
- package/src/domain/symbol-graph-generation.ts +2 -0
- package/src/domain/workspace-symbol.ts +6 -1
- package/src/index.ts +9 -2
- package/src/service.ts +64 -19
package/README.md
CHANGED
|
@@ -64,5 +64,7 @@ Three independent `SymbolIndexPort` implementations use one result DTO with expl
|
|
|
64
64
|
- **TypeScript compiler** extracts bounded structural declarations from cold, malformed, or partially configured projects. It does not claim cross-file identity.
|
|
65
65
|
- **tree-sitter** extracts bounded structural declarations and caches them by content hash. It does not claim type or language-server identity.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
File-position operations dispatch by extension and seed a cold server with the requested file. Workspace symbol search and graph population detect every enabled language, merge results deterministically, and retain per-symbol and per-backend provenance; one failed backend produces an explicit partial result instead of hiding successful languages. Bash remains explicit-file-only until its vulnerable upstream dependency chain is removed; generic YAML is explicit-file-only because lockfiles and data files do not establish a workspace language.
|
|
68
|
+
|
|
69
|
+
Symbol results report `fidelity`, `backend`, `authority`, `freshness`, `limitations`, `truncated`, and composite `sources`/`completeness` when applicable. Language-server messages, pending requests, opened files, file bytes, settling, parser files, parser nodes, parser bytes, language indexes, and returned symbols are bounded.
|
|
68
70
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import { refineTypescriptSeedFile } from "./typescript-project-files.ts";
|
|
|
6
6
|
const SKIP_DIRECTORY_NAMES = new Set(["node_modules", "dist", "build", "out", "coverage"]);
|
|
7
7
|
const MAX_SCAN_DEPTH = 4;
|
|
8
8
|
const MAX_ENTRIES_SCANNED = 2_000;
|
|
9
|
+
const MAX_WORKSPACE_DESCRIPTORS = 32;
|
|
9
10
|
|
|
10
11
|
/** Raised when no matching source file could be found to warm a language server with. */
|
|
11
12
|
export class NoSeedFileFound extends Error {
|
|
@@ -110,21 +111,28 @@ export function resolveSeedFile(rootPath: string, descriptor: LanguageServerDesc
|
|
|
110
111
|
return descriptor.languageId === "typescript" ? refineTypescriptSeedFile(rootPath, candidate) : candidate;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
* Tries each descriptor's own discoverSeedFile in declared order; first real match wins.
|
|
116
|
-
*/
|
|
117
|
-
export function discoverWorkspaceDescriptor(
|
|
114
|
+
/** Detects every auto-enabled language in descriptor order; each language keeps the same bounded seed scan. */
|
|
115
|
+
export function discoverWorkspaceDescriptors(
|
|
118
116
|
rootPath: string,
|
|
119
117
|
descriptors: readonly LanguageServerDescriptor[],
|
|
120
|
-
): { descriptor: LanguageServerDescriptor; seedFile: string }
|
|
118
|
+
): readonly { descriptor: LanguageServerDescriptor; seedFile: string }[] {
|
|
119
|
+
if (descriptors.length > MAX_WORKSPACE_DESCRIPTORS) throw new TypeError(`workspace descriptor count exceeds ${MAX_WORKSPACE_DESCRIPTORS}`);
|
|
120
|
+
const discovered: { descriptor: LanguageServerDescriptor; seedFile: string }[] = [];
|
|
121
121
|
for (const descriptor of descriptors) {
|
|
122
|
+
if (descriptor.workspaceDiscovery === "explicit-only") continue;
|
|
122
123
|
try {
|
|
123
|
-
|
|
124
|
-
return { descriptor, seedFile };
|
|
124
|
+
discovered.push({ descriptor, seedFile: resolveSeedFile(rootPath, descriptor) });
|
|
125
125
|
} catch (error) {
|
|
126
126
|
if (!(error instanceof NoSeedFileFound)) throw error;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
return
|
|
129
|
+
return discovered;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Compatibility helper for callers that still need one deterministic primary language. */
|
|
133
|
+
export function discoverWorkspaceDescriptor(
|
|
134
|
+
rootPath: string,
|
|
135
|
+
descriptors: readonly LanguageServerDescriptor[],
|
|
136
|
+
): { descriptor: LanguageServerDescriptor; seedFile: string } | undefined {
|
|
137
|
+
return discoverWorkspaceDescriptors(rootPath, descriptors)[0];
|
|
130
138
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
-
import { extname,
|
|
2
|
+
import { extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import type { CallHierarchyEntry, IncomingCall, OutgoingCall } from "../../domain/call-hierarchy.ts";
|
|
5
5
|
import type { CodeRange } from "../../domain/code-range.ts";
|
|
@@ -27,6 +27,16 @@ export interface LspSymbolIndexOptions {
|
|
|
27
27
|
readonly maxFallbackSeedFiles?: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export class LanguageFileOutsideWorkspace extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
readonly path: string,
|
|
33
|
+
readonly root: string,
|
|
34
|
+
) {
|
|
35
|
+
super(`language file "${path}" resolves outside workspace root "${root}"`);
|
|
36
|
+
this.name = "LanguageFileOutsideWorkspace";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
export class LanguageFileLimitExceeded extends Error {
|
|
31
41
|
constructor(
|
|
32
42
|
readonly limit: "open-files" | "file-bytes" | "refresh-bytes",
|
|
@@ -270,6 +280,7 @@ function normalizeDocumentSymbol(path: string, item: LspDocumentSymbol | LspSymb
|
|
|
270
280
|
export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
271
281
|
readonly provenance: IntelligenceProvenance;
|
|
272
282
|
private readonly cwd: string;
|
|
283
|
+
private readonly canonicalCwd: string;
|
|
273
284
|
private readonly descriptor: LanguageServerDescriptor;
|
|
274
285
|
private readonly explicitSeedFile: string | undefined;
|
|
275
286
|
private fallbackSeedFile: string | undefined;
|
|
@@ -284,7 +295,8 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
284
295
|
private initializing: Promise<LanguageServerProcess> | undefined;
|
|
285
296
|
|
|
286
297
|
constructor(cwd: string, descriptor: LanguageServerDescriptor, seedFile?: string, options: LspSymbolIndexOptions = {}) {
|
|
287
|
-
this.cwd = cwd;
|
|
298
|
+
this.cwd = resolve(cwd);
|
|
299
|
+
this.canonicalCwd = realpathSync(this.cwd);
|
|
288
300
|
this.descriptor = descriptor;
|
|
289
301
|
this.explicitSeedFile = seedFile;
|
|
290
302
|
this.maxOpenFiles = positiveLimit(options.maxOpenFiles, DEFAULT_MAX_OPEN_FILES, "maxOpenFiles");
|
|
@@ -309,7 +321,18 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
309
321
|
return this.process?.pid;
|
|
310
322
|
}
|
|
311
323
|
|
|
312
|
-
private
|
|
324
|
+
private resolveTargetPath(path: string): string {
|
|
325
|
+
const absolute = resolve(this.cwd, path);
|
|
326
|
+
const canonical = realpathSync(absolute);
|
|
327
|
+
const relativeToRoot = relative(this.canonicalCwd, canonical);
|
|
328
|
+
if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${sep}`) || isAbsolute(relativeToRoot)) {
|
|
329
|
+
throw new LanguageFileOutsideWorkspace(path, this.cwd);
|
|
330
|
+
}
|
|
331
|
+
return absolute;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async ensureInitialized(initialPath?: string): Promise<LanguageServerProcess> {
|
|
335
|
+
const initialTargetPath = initialPath ? this.resolveTargetPath(initialPath) : undefined;
|
|
313
336
|
if (this.process) return this.process;
|
|
314
337
|
if (!this.initializing) {
|
|
315
338
|
let spawned: LanguageServerProcess | undefined;
|
|
@@ -350,8 +373,11 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
350
373
|
});
|
|
351
374
|
proc.notify("initialized", {});
|
|
352
375
|
|
|
353
|
-
const
|
|
354
|
-
|
|
376
|
+
const configuredSeedFile = this.fallbackSeedFile ?? this.explicitSeedFile;
|
|
377
|
+
const seedPath = configuredSeedFile
|
|
378
|
+
? resolve(this.cwd, configuredSeedFile)
|
|
379
|
+
: (initialTargetPath ?? resolve(this.cwd, resolveSeedFile(this.cwd, this.descriptor)));
|
|
380
|
+
await this.ensureFileOpen(proc, seedPath);
|
|
355
381
|
|
|
356
382
|
this.process = proc;
|
|
357
383
|
return proc;
|
|
@@ -374,6 +400,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
374
400
|
|
|
375
401
|
/** Opens a file or sends a monotonic full-document change when its disk content changed since the prior query. */
|
|
376
402
|
private async ensureFileOpen(proc: LanguageServerProcess, path: string): Promise<void> {
|
|
403
|
+
path = this.resolveTargetPath(path);
|
|
377
404
|
const content = this.readBoundedFile(path);
|
|
378
405
|
const opened = this.openedFiles.get(path);
|
|
379
406
|
if (opened?.content === content) return;
|
|
@@ -459,7 +486,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
459
486
|
}
|
|
460
487
|
|
|
461
488
|
async goToDefinition(at: WorkspaceLocation): Promise<WorkspaceLocation[]> {
|
|
462
|
-
const proc = await this.ensureInitialized();
|
|
489
|
+
const proc = await this.ensureInitialized(at.path);
|
|
463
490
|
await this.ensureFileOpen(proc, at.path);
|
|
464
491
|
const result = await proc.request<LspLocation | LspLocation[] | LspLocationLink[] | null>("textDocument/definition", {
|
|
465
492
|
textDocument: { uri: pathToFileURL(at.path).href },
|
|
@@ -469,7 +496,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
469
496
|
}
|
|
470
497
|
|
|
471
498
|
async goToImplementation(at: WorkspaceLocation): Promise<WorkspaceLocation[]> {
|
|
472
|
-
const proc = await this.ensureInitialized();
|
|
499
|
+
const proc = await this.ensureInitialized(at.path);
|
|
473
500
|
await this.ensureFileOpen(proc, at.path);
|
|
474
501
|
const result = await proc.request<LspLocation | LspLocation[] | LspLocationLink[] | null>("textDocument/implementation", {
|
|
475
502
|
textDocument: { uri: pathToFileURL(at.path).href },
|
|
@@ -479,7 +506,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
479
506
|
}
|
|
480
507
|
|
|
481
508
|
async findReferences(at: WorkspaceLocation, includeDeclaration: boolean): Promise<WorkspaceLocation[]> {
|
|
482
|
-
const proc = await this.ensureInitialized();
|
|
509
|
+
const proc = await this.ensureInitialized(at.path);
|
|
483
510
|
await this.ensureFileOpen(proc, at.path);
|
|
484
511
|
const results =
|
|
485
512
|
(await proc.request<LspLocation[] | null>("textDocument/references", {
|
|
@@ -491,7 +518,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
491
518
|
}
|
|
492
519
|
|
|
493
520
|
async hover(at: WorkspaceLocation): Promise<Hover | undefined> {
|
|
494
|
-
const proc = await this.ensureInitialized();
|
|
521
|
+
const proc = await this.ensureInitialized(at.path);
|
|
495
522
|
await this.ensureFileOpen(proc, at.path);
|
|
496
523
|
const result = await proc.request<LspHover | null>("textDocument/hover", {
|
|
497
524
|
textDocument: { uri: pathToFileURL(at.path).href },
|
|
@@ -502,7 +529,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
502
529
|
}
|
|
503
530
|
|
|
504
531
|
async documentSymbols(path: string): Promise<DocumentSymbolEntry[]> {
|
|
505
|
-
const proc = await this.ensureInitialized();
|
|
532
|
+
const proc = await this.ensureInitialized(path);
|
|
506
533
|
await this.ensureFileOpen(proc, path);
|
|
507
534
|
const results =
|
|
508
535
|
(await proc.request<(LspDocumentSymbol | LspSymbolInformation)[] | null>("textDocument/documentSymbol", {
|
|
@@ -512,7 +539,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
512
539
|
}
|
|
513
540
|
|
|
514
541
|
async diagnostics(path: string): Promise<Diagnostic[]> {
|
|
515
|
-
const proc = await this.ensureInitialized();
|
|
542
|
+
const proc = await this.ensureInitialized(path);
|
|
516
543
|
await this.ensureFileOpen(proc, path);
|
|
517
544
|
if (!this.latestDiagnostics.has(path)) await this.waitForDiagnosticsNotification(path, 5000);
|
|
518
545
|
return this.latestDiagnostics.get(path) ?? [];
|
|
@@ -529,13 +556,13 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
529
556
|
}
|
|
530
557
|
|
|
531
558
|
async prepareCallHierarchy(at: WorkspaceLocation): Promise<CallHierarchyEntry[]> {
|
|
532
|
-
const proc = await this.ensureInitialized();
|
|
559
|
+
const proc = await this.ensureInitialized(at.path);
|
|
533
560
|
const items = await this.prepareCallHierarchyRaw(proc, at);
|
|
534
561
|
return items.map((item) => normalizeCallHierarchyItem(item));
|
|
535
562
|
}
|
|
536
563
|
|
|
537
564
|
async incomingCalls(at: WorkspaceLocation): Promise<IncomingCall[]> {
|
|
538
|
-
const proc = await this.ensureInitialized();
|
|
565
|
+
const proc = await this.ensureInitialized(at.path);
|
|
539
566
|
const root = (await this.prepareCallHierarchyRaw(proc, at))[0];
|
|
540
567
|
if (!root) return [];
|
|
541
568
|
const results = (await proc.request<LspCallHierarchyIncomingCall[] | null>("callHierarchy/incomingCalls", { item: root })) ?? [];
|
|
@@ -546,7 +573,7 @@ export class LspSymbolIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
|
546
573
|
}
|
|
547
574
|
|
|
548
575
|
async outgoingCalls(at: WorkspaceLocation): Promise<OutgoingCall[]> {
|
|
549
|
-
const proc = await this.ensureInitialized();
|
|
576
|
+
const proc = await this.ensureInitialized(at.path);
|
|
550
577
|
const root = (await this.prepareCallHierarchyRaw(proc, at))[0];
|
|
551
578
|
if (!root) return [];
|
|
552
579
|
const results = (await proc.request<LspCallHierarchyOutgoingCall[] | null>("callHierarchy/outgoingCalls", { item: root })) ?? [];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
import type { CallHierarchyEntry, IncomingCall, OutgoingCall } from "../domain/call-hierarchy.ts";
|
|
3
|
+
import type { Diagnostic } from "../domain/diagnostic.ts";
|
|
4
|
+
import type { DocumentSymbolEntry } from "../domain/document-symbol.ts";
|
|
5
|
+
import type { Hover } from "../domain/hover.ts";
|
|
6
|
+
import type { IntelligenceProvenance, IntelligenceSourceOutcome, SymbolSearchBounds } from "../domain/intelligence-provenance.ts";
|
|
7
|
+
import type { LanguageServerDescriptor } from "../domain/language-server-descriptor.ts";
|
|
8
|
+
import type { SymbolSearchResult, WorkspaceLocation, WorkspaceSymbol } from "../domain/workspace-symbol.ts";
|
|
9
|
+
import type { CodeIntelligencePort } from "../ports/code-intelligence-port.ts";
|
|
10
|
+
import type { SymbolIndexPort } from "../ports/symbol-index-port.ts";
|
|
11
|
+
|
|
12
|
+
const MAX_SOURCE_ERROR_LENGTH = 500;
|
|
13
|
+
const MAX_LANGUAGE_INDEXES = 16;
|
|
14
|
+
const DEFAULT_MAX_RESULTS = 1_000;
|
|
15
|
+
|
|
16
|
+
export interface PolyglotIndexEntry {
|
|
17
|
+
readonly descriptor: LanguageServerDescriptor;
|
|
18
|
+
readonly index: SymbolIndexPort;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function boundedError(error: unknown): { code: string; message: string } {
|
|
22
|
+
const code = error instanceof Error && error.name ? error.name : "CodeIntelligenceError";
|
|
23
|
+
const message = (error instanceof Error ? error.message : String(error)).slice(0, MAX_SOURCE_ERROR_LENGTH);
|
|
24
|
+
return { code, message };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function supportsCodeIntelligence(index: SymbolIndexPort): index is SymbolIndexPort & CodeIntelligencePort {
|
|
28
|
+
return "goToDefinition" in index && typeof index.goToDefinition === "function";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareSymbols(left: WorkspaceSymbol, right: WorkspaceSymbol): number {
|
|
32
|
+
return (
|
|
33
|
+
left.location.path.localeCompare(right.location.path) ||
|
|
34
|
+
left.location.line - right.location.line ||
|
|
35
|
+
left.location.character - right.location.character ||
|
|
36
|
+
left.name.localeCompare(right.name) ||
|
|
37
|
+
left.kind.localeCompare(right.kind)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class PolyglotCodeIntelligenceIndex implements SymbolIndexPort, CodeIntelligencePort {
|
|
42
|
+
readonly provenance: IntelligenceProvenance = {
|
|
43
|
+
fidelity: "semantic",
|
|
44
|
+
backend: "polyglot-language-servers",
|
|
45
|
+
languageId: "polyglot",
|
|
46
|
+
authority: "language-server",
|
|
47
|
+
freshness: "live-process",
|
|
48
|
+
limitations: [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
constructor(private readonly entries: readonly PolyglotIndexEntry[]) {
|
|
52
|
+
if (entries.length === 0 || entries.length > MAX_LANGUAGE_INDEXES) {
|
|
53
|
+
throw new TypeError(`polyglot index requires between 1 and ${MAX_LANGUAGE_INDEXES} language indexes`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private indexForPath(path: string): SymbolIndexPort & CodeIntelligencePort {
|
|
58
|
+
const extension = extname(path);
|
|
59
|
+
const entry = this.entries.find((candidate) => candidate.descriptor.extensions.includes(extension));
|
|
60
|
+
if (!entry) throw new Error(`no language index available for extension "${extension}"`);
|
|
61
|
+
if (!supportsCodeIntelligence(entry.index)) throw new Error(`code intelligence unavailable for ${entry.descriptor.languageId}`);
|
|
62
|
+
return entry.index;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async findSymbols(query: string, bounds: SymbolSearchBounds = { maxResults: DEFAULT_MAX_RESULTS }): Promise<SymbolSearchResult> {
|
|
66
|
+
if (!Number.isSafeInteger(bounds.maxResults) || bounds.maxResults < 1) throw new TypeError("maxResults must be a positive safe integer");
|
|
67
|
+
const settled = await Promise.allSettled(this.entries.map(({ index }) => index.findSymbols(query, bounds)));
|
|
68
|
+
const symbols: WorkspaceSymbol[] = [];
|
|
69
|
+
const sources: IntelligenceSourceOutcome[] = [];
|
|
70
|
+
let sourceTruncated = false;
|
|
71
|
+
|
|
72
|
+
for (const [position, outcome] of settled.entries()) {
|
|
73
|
+
const entry = this.entries[position];
|
|
74
|
+
if (!entry) continue;
|
|
75
|
+
if (outcome.status === "rejected") {
|
|
76
|
+
sources.push({ provenance: entry.index.provenance, status: "failed", symbolCount: 0, error: boundedError(outcome.reason) });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
sources.push({
|
|
80
|
+
provenance: outcome.value.provenance,
|
|
81
|
+
status: "ready",
|
|
82
|
+
symbolCount: outcome.value.symbols.length,
|
|
83
|
+
truncated: outcome.value.truncated,
|
|
84
|
+
});
|
|
85
|
+
sourceTruncated ||= outcome.value.truncated;
|
|
86
|
+
for (const symbol of outcome.value.symbols) symbols.push({ ...symbol, provenance: outcome.value.provenance });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
symbols.sort(compareSymbols);
|
|
90
|
+
const truncated = sourceTruncated || symbols.length > bounds.maxResults;
|
|
91
|
+
return {
|
|
92
|
+
symbols: symbols.slice(0, bounds.maxResults),
|
|
93
|
+
truncated,
|
|
94
|
+
provenance: this.provenance,
|
|
95
|
+
completeness: sources.every((source) => source.status === "ready") ? "complete" : "partial",
|
|
96
|
+
sources,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
goToDefinition(at: WorkspaceLocation) {
|
|
101
|
+
return this.indexForPath(at.path).goToDefinition(at);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
goToImplementation(at: WorkspaceLocation) {
|
|
105
|
+
return this.indexForPath(at.path).goToImplementation(at);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
findReferences(at: WorkspaceLocation, includeDeclaration: boolean) {
|
|
109
|
+
return this.indexForPath(at.path).findReferences(at, includeDeclaration);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
hover(at: WorkspaceLocation): Promise<Hover | undefined> {
|
|
113
|
+
return this.indexForPath(at.path).hover(at);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
documentSymbols(path: string): Promise<DocumentSymbolEntry[]> {
|
|
117
|
+
return this.indexForPath(path).documentSymbols(path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
diagnostics(path: string): Promise<Diagnostic[]> {
|
|
121
|
+
return this.indexForPath(path).diagnostics(path);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
prepareCallHierarchy(at: WorkspaceLocation): Promise<CallHierarchyEntry[]> {
|
|
125
|
+
return this.indexForPath(at.path).prepareCallHierarchy(at);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
incomingCalls(at: WorkspaceLocation): Promise<IncomingCall[]> {
|
|
129
|
+
return this.indexForPath(at.path).incomingCalls(at);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
outgoingCalls(at: WorkspaceLocation): Promise<OutgoingCall[]> {
|
|
133
|
+
return this.indexForPath(at.path).outgoingCalls(at);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -28,6 +28,10 @@ const MIGRATIONS: Migration[] = [
|
|
|
28
28
|
version: 3,
|
|
29
29
|
up: (db) => db.exec("ALTER TABLE symbol_graph_generation ADD COLUMN provenance_json TEXT"),
|
|
30
30
|
},
|
|
31
|
+
{
|
|
32
|
+
version: 4,
|
|
33
|
+
up: (db) => db.exec("ALTER TABLE symbol_graph_generation ADD COLUMN sources_json TEXT"),
|
|
34
|
+
},
|
|
31
35
|
];
|
|
32
36
|
|
|
33
37
|
interface NodeRow {
|
|
@@ -48,20 +52,14 @@ interface GenerationRow {
|
|
|
48
52
|
nodes_added: number;
|
|
49
53
|
edges_added: number;
|
|
50
54
|
provenance_json: string | null;
|
|
55
|
+
sources_json: string | null;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
59
|
return typeof value === "object" && value !== null;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
function
|
|
58
|
-
if (!json) return undefined;
|
|
59
|
-
let value: unknown;
|
|
60
|
-
try {
|
|
61
|
-
value = JSON.parse(json);
|
|
62
|
-
} catch {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
62
|
+
function asProvenance(value: unknown): IntelligenceProvenance | undefined {
|
|
65
63
|
if (!isRecord(value)) return undefined;
|
|
66
64
|
const candidate = value;
|
|
67
65
|
if (
|
|
@@ -85,6 +83,26 @@ function parseProvenance(json: string | null): IntelligenceProvenance | undefine
|
|
|
85
83
|
};
|
|
86
84
|
}
|
|
87
85
|
|
|
86
|
+
function parseJson(json: string | null): unknown {
|
|
87
|
+
if (!json) return undefined;
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(json);
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseProvenance(json: string | null): IntelligenceProvenance | undefined {
|
|
96
|
+
return asProvenance(parseJson(json));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseSources(json: string | null): readonly IntelligenceProvenance[] | undefined {
|
|
100
|
+
const value = parseJson(json);
|
|
101
|
+
if (!Array.isArray(value)) return undefined;
|
|
102
|
+
const sources = value.map(asProvenance);
|
|
103
|
+
return sources.every((source) => source !== undefined) ? sources : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
/**
|
|
89
107
|
* SQLite-backed SymbolGraphPort; survives a daemon restart pointed at the
|
|
90
108
|
* same database file. reachableFrom uses `WITH RECURSIVE` rather than an
|
|
@@ -154,17 +172,19 @@ export class SqliteSymbolGraph implements SymbolGraphPort {
|
|
|
154
172
|
async getGeneration(): Promise<SymbolGraphGeneration | undefined> {
|
|
155
173
|
const row = this.db
|
|
156
174
|
.query(
|
|
157
|
-
"SELECT source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json FROM symbol_graph_generation WHERE singleton = 1",
|
|
175
|
+
"SELECT source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json, sources_json FROM symbol_graph_generation WHERE singleton = 1",
|
|
158
176
|
)
|
|
159
177
|
.get() as GenerationRow | null;
|
|
160
178
|
if (!row) return undefined;
|
|
161
179
|
const provenance = parseProvenance(row.provenance_json);
|
|
180
|
+
const sources = parseSources(row.sources_json);
|
|
162
181
|
return {
|
|
163
182
|
sourceFingerprint: row.source_fingerprint,
|
|
164
183
|
maxFiles: row.max_files,
|
|
165
184
|
maxSymbolsPerFile: row.max_symbols_per_file,
|
|
166
185
|
completedAt: row.completed_at,
|
|
167
186
|
...(provenance ? { provenance } : {}),
|
|
187
|
+
...(sources ? { sources } : {}),
|
|
168
188
|
result: {
|
|
169
189
|
filesProcessed: row.files_processed,
|
|
170
190
|
symbolsProcessed: row.symbols_processed,
|
|
@@ -177,7 +197,7 @@ export class SqliteSymbolGraph implements SymbolGraphPort {
|
|
|
177
197
|
async setGeneration(generation: SymbolGraphGeneration): Promise<void> {
|
|
178
198
|
this.db
|
|
179
199
|
.query(
|
|
180
|
-
"INSERT INTO symbol_graph_generation (singleton, source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET source_fingerprint = excluded.source_fingerprint, max_files = excluded.max_files, max_symbols_per_file = excluded.max_symbols_per_file, completed_at = excluded.completed_at, files_processed = excluded.files_processed, symbols_processed = excluded.symbols_processed, nodes_added = excluded.nodes_added, edges_added = excluded.edges_added, provenance_json = excluded.provenance_json",
|
|
200
|
+
"INSERT INTO symbol_graph_generation (singleton, source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json, sources_json) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET source_fingerprint = excluded.source_fingerprint, max_files = excluded.max_files, max_symbols_per_file = excluded.max_symbols_per_file, completed_at = excluded.completed_at, files_processed = excluded.files_processed, symbols_processed = excluded.symbols_processed, nodes_added = excluded.nodes_added, edges_added = excluded.edges_added, provenance_json = excluded.provenance_json, sources_json = excluded.sources_json",
|
|
181
201
|
)
|
|
182
202
|
.run(
|
|
183
203
|
generation.sourceFingerprint,
|
|
@@ -189,6 +209,7 @@ export class SqliteSymbolGraph implements SymbolGraphPort {
|
|
|
189
209
|
generation.result.nodesAdded,
|
|
190
210
|
generation.result.edgesAdded,
|
|
191
211
|
generation.provenance ? JSON.stringify(generation.provenance) : null,
|
|
212
|
+
generation.sources ? JSON.stringify(generation.sources) : null,
|
|
192
213
|
);
|
|
193
214
|
}
|
|
194
215
|
|
package/src/cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { serveMain } from "./daemon.ts";
|
|
|
12
12
|
import type { JobSnapshot } from "./domain/bounded-job-executor.ts";
|
|
13
13
|
import type { ContentHash } from "./domain/content-hash.ts";
|
|
14
14
|
import { DEFAULT_PACKAGE_SOURCE_BOUNDS, type PackageSourceOperationResult } from "./domain/package-source.ts";
|
|
15
|
+
import type { SymbolSearchResult } from "./domain/workspace-symbol.ts";
|
|
15
16
|
import type { WorkspacePort } from "./ports/workspace-port.ts";
|
|
16
17
|
import type { WorkspaceId } from "./service.ts";
|
|
17
18
|
|
|
@@ -130,6 +131,14 @@ async function runWorkspaceRegister(dir: string | undefined, flags: string[]): P
|
|
|
130
131
|
console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : `${result.workspaceId} (${result.created ? "created" : "already registered"})`);
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function formatSymbolSources(result: SymbolSearchResult): readonly string[] {
|
|
135
|
+
return (result.sources ?? []).map((source) => {
|
|
136
|
+
const identity = `${source.provenance.languageId}: ${source.status} via ${source.provenance.backend}`;
|
|
137
|
+
if (source.status === "failed") return source.error ? `${identity} [${source.error.code}] ${source.error.message}` : identity;
|
|
138
|
+
return `${identity} (${source.symbolCount} symbol${source.symbolCount === 1 ? "" : "s"}${source.truncated ? ", truncated" : ""})`;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
async function runWorkspaceSymbols(workspaceId: string | undefined, query: string | undefined, flags: string[]): Promise<void> {
|
|
134
143
|
if (!workspaceId || !query) fail(USAGE);
|
|
135
144
|
const seedFile = flagValue(flags, "--seed-file"); // omit to auto-discover one
|
|
@@ -141,6 +150,7 @@ async function runWorkspaceSymbols(workspaceId: string | undefined, query: strin
|
|
|
141
150
|
}
|
|
142
151
|
const { symbols, provenance, truncated } = result;
|
|
143
152
|
console.log(`${provenance.fidelity} via ${provenance.backend}${truncated ? " (truncated)" : ""}`);
|
|
153
|
+
for (const source of formatSymbolSources(result)) console.log(source);
|
|
144
154
|
if (symbols.length === 0) {
|
|
145
155
|
console.log(`no symbols matched "${query}"`);
|
|
146
156
|
return;
|
|
@@ -9,6 +9,14 @@ export interface IntelligenceProvenance {
|
|
|
9
9
|
readonly limitations: readonly string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface IntelligenceSourceOutcome {
|
|
13
|
+
readonly provenance: IntelligenceProvenance;
|
|
14
|
+
readonly status: "ready" | "failed";
|
|
15
|
+
readonly symbolCount: number;
|
|
16
|
+
readonly truncated?: boolean;
|
|
17
|
+
readonly error?: { readonly code: string; readonly message: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export interface SymbolSearchBounds {
|
|
13
21
|
readonly maxResults: number;
|
|
14
22
|
}
|
|
@@ -23,6 +23,8 @@ export interface LanguageServerDescriptor {
|
|
|
23
23
|
readonly rootMarkers: readonly string[];
|
|
24
24
|
/** Tried before the bounded directory scan when picking a file to warm the server with (e.g. a language's usual entry-point names). */
|
|
25
25
|
readonly commonSeedCandidates: readonly string[];
|
|
26
|
+
/** Excludes unsafe or unusually expensive servers from automatic workspace-wide fan-out while preserving explicit file operations. */
|
|
27
|
+
readonly workspaceDiscovery?: "enabled" | "explicit-only";
|
|
26
28
|
/** Extra textDocument/workspace capabilities this specific server gates real features behind (e.g. typescript-language-server withholds diagnostics/callHierarchy unless declared). */
|
|
27
29
|
readonly extraCapabilities?: Record<string, unknown>;
|
|
28
30
|
/** Milliseconds to wait after opening a file before trusting the server's answers -- no server signals "project loaded". Default 1000ms; rust-analyzer needs more (see RUST_DESCRIPTOR). */
|
|
@@ -101,6 +103,7 @@ export const BASH_DESCRIPTOR: LanguageServerDescriptor = {
|
|
|
101
103
|
languageId: "shellscript",
|
|
102
104
|
backendId: "bash-language-server",
|
|
103
105
|
extensions: [".sh", ".bash"],
|
|
106
|
+
workspaceDiscovery: "explicit-only",
|
|
104
107
|
launch: { kind: "npm-module", entryModule: "bash-language-server/out/cli.js" },
|
|
105
108
|
args: ["start"],
|
|
106
109
|
rootMarkers: [],
|
|
@@ -111,6 +114,7 @@ export const YAML_DESCRIPTOR: LanguageServerDescriptor = {
|
|
|
111
114
|
languageId: "yaml",
|
|
112
115
|
backendId: "yaml-language-server",
|
|
113
116
|
extensions: [".yaml", ".yml"],
|
|
117
|
+
workspaceDiscovery: "explicit-only",
|
|
114
118
|
launch: { kind: "npm-module", entryModule: "yaml-language-server/bin/yaml-language-server" },
|
|
115
119
|
args: ["--stdio"],
|
|
116
120
|
rootMarkers: [],
|
|
@@ -8,6 +8,8 @@ export interface SymbolGraphGeneration {
|
|
|
8
8
|
readonly completedAt: number;
|
|
9
9
|
/** Absent only for generations persisted before provenance was recorded. */
|
|
10
10
|
readonly provenance?: IntelligenceProvenance;
|
|
11
|
+
/** Per-language authorities included when provenance is polyglot. */
|
|
12
|
+
readonly sources?: readonly IntelligenceProvenance[];
|
|
11
13
|
readonly result: PopulateSymbolGraphResult;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IntelligenceProvenance } from "./intelligence-provenance.ts";
|
|
1
|
+
import type { IntelligenceProvenance, IntelligenceSourceOutcome } from "./intelligence-provenance.ts";
|
|
2
2
|
|
|
3
3
|
/** A location within a workspace file: 1-indexed line and character, matching how humans and CLIs present positions. */
|
|
4
4
|
export interface WorkspaceLocation {
|
|
@@ -13,10 +13,15 @@ export interface WorkspaceSymbol {
|
|
|
13
13
|
readonly kind: string;
|
|
14
14
|
readonly location: WorkspaceLocation;
|
|
15
15
|
readonly containerName?: string;
|
|
16
|
+
/** Present when a workspace-wide composite query needs to preserve which backend produced this symbol. */
|
|
17
|
+
readonly provenance?: IntelligenceProvenance;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface SymbolSearchResult {
|
|
19
21
|
readonly symbols: readonly WorkspaceSymbol[];
|
|
20
22
|
readonly truncated: boolean;
|
|
21
23
|
readonly provenance: IntelligenceProvenance;
|
|
24
|
+
/** Present for composite workspace queries; omitted for one-language adapters. */
|
|
25
|
+
readonly completeness?: "complete" | "partial";
|
|
26
|
+
readonly sources?: readonly IntelligenceSourceOutcome[];
|
|
22
27
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export {
|
|
|
12
12
|
LanguageServerProcessExited,
|
|
13
13
|
LanguageServerRequestTimedOut,
|
|
14
14
|
} from "./adapters/lsp/language-server-process.ts";
|
|
15
|
-
export { LanguageFileLimitExceeded, LspSymbolIndex, type LspSymbolIndexOptions } from "./adapters/lsp/lsp-symbol-index.ts";
|
|
15
|
+
export { LanguageFileLimitExceeded, LanguageFileOutsideWorkspace, LspSymbolIndex, type LspSymbolIndexOptions } from "./adapters/lsp/lsp-symbol-index.ts";
|
|
16
16
|
export { InvalidInstalledPackageVersionRequest, NpmLockfileVersionResolver } from "./adapters/npm-lockfile-version-resolver.ts";
|
|
17
17
|
export { NpmPackageSourceResolver, type NpmPackageSourceResolverOptions } from "./adapters/npm-package-source-resolver.ts";
|
|
18
18
|
export {
|
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
NpmRegistryResponseLimitExceeded,
|
|
27
27
|
NpmVersionNotFound,
|
|
28
28
|
} from "./adapters/npm-registry-client.ts";
|
|
29
|
+
export { PolyglotCodeIntelligenceIndex, type PolyglotIndexEntry } from "./adapters/polyglot-code-intelligence-index.ts";
|
|
29
30
|
export { ReadOnlyWorkspace, WorkspaceIsReadOnly } from "./adapters/read-only-workspace.ts";
|
|
30
31
|
export { RipgrepTextSearch } from "./adapters/ripgrep-text-search.ts";
|
|
31
32
|
export { deriveSourceManifest, type SourceManifest, SourceManifestLimitExceeded } from "./adapters/source-manifest.ts";
|
|
@@ -92,7 +93,13 @@ export type {
|
|
|
92
93
|
ResolvedInstalledPackageVersion,
|
|
93
94
|
UnavailableInstalledPackageVersion,
|
|
94
95
|
} from "./domain/installed-package-version.ts";
|
|
95
|
-
export type {
|
|
96
|
+
export type {
|
|
97
|
+
IntelligenceFidelity,
|
|
98
|
+
IntelligenceProvenance,
|
|
99
|
+
IntelligenceSourceOutcome,
|
|
100
|
+
ProvenancedResult,
|
|
101
|
+
SymbolSearchBounds,
|
|
102
|
+
} from "./domain/intelligence-provenance.ts";
|
|
96
103
|
export {
|
|
97
104
|
descriptorForExtension,
|
|
98
105
|
LANGUAGE_SERVER_DESCRIPTORS,
|
package/src/service.ts
CHANGED
|
@@ -6,11 +6,12 @@ import { InMemorySearchCache } from "./adapters/in-memory-search-cache.ts";
|
|
|
6
6
|
import { InMemorySymbolGraph } from "./adapters/in-memory-symbol-graph.ts";
|
|
7
7
|
import { LocalFilesystemWorkspace } from "./adapters/local-filesystem-workspace.ts";
|
|
8
8
|
import { LocalGit } from "./adapters/local-git.ts";
|
|
9
|
-
import { discoverWorkspaceDescriptor } from "./adapters/lsp/discover-seed-file.ts";
|
|
9
|
+
import { discoverWorkspaceDescriptor, discoverWorkspaceDescriptors } from "./adapters/lsp/discover-seed-file.ts";
|
|
10
10
|
import { LspSymbolIndex } from "./adapters/lsp/lsp-symbol-index.ts";
|
|
11
11
|
import { NpmLockfileVersionResolver } from "./adapters/npm-lockfile-version-resolver.ts";
|
|
12
12
|
import { NpmPackageSourceResolver } from "./adapters/npm-package-source-resolver.ts";
|
|
13
13
|
import { NpmRegistryClient } from "./adapters/npm-registry-client.ts";
|
|
14
|
+
import { PolyglotCodeIntelligenceIndex } from "./adapters/polyglot-code-intelligence-index.ts";
|
|
14
15
|
import { ReadOnlyWorkspace } from "./adapters/read-only-workspace.ts";
|
|
15
16
|
import { RipgrepTextSearch } from "./adapters/ripgrep-text-search.ts";
|
|
16
17
|
import { deriveSourceManifest } from "./adapters/source-manifest.ts";
|
|
@@ -408,7 +409,7 @@ function resolveWorkspace(registry: MutableRegistry, workspaceId: WorkspaceId):
|
|
|
408
409
|
|
|
409
410
|
/** True when a warm SymbolIndexPort is also a real CodeIntelligencePort (currently: any LspSymbolIndex, never TreeSitterSymbolIndex). */
|
|
410
411
|
function supportsCodeIntelligence(index: SymbolIndexPort): index is SymbolIndexPort & CodeIntelligencePort {
|
|
411
|
-
return
|
|
412
|
+
return "goToDefinition" in index && typeof index.goToDefinition === "function";
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
type OperationHandlers = {
|
|
@@ -677,6 +678,18 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
|
|
|
677
678
|
return { descriptor: discovered.descriptor, seedFile: discovered.seedFile };
|
|
678
679
|
}
|
|
679
680
|
|
|
681
|
+
function ensureLanguageIndex(workspaceId: WorkspaceId, rootPath: string, descriptor: LanguageServerDescriptor, seedFile?: string): ClosableSymbolIndex {
|
|
682
|
+
const key = symbolIndexKey(workspaceId, descriptor.languageId);
|
|
683
|
+
let entryIndex = symbolIndexes.get(key);
|
|
684
|
+
if (!entryIndex) {
|
|
685
|
+
entryIndex = { index: createSymbolIndex(rootPath, descriptor, seedFile), workspaceId, lastUsedAt: Date.now() };
|
|
686
|
+
symbolIndexes.set(key, entryIndex);
|
|
687
|
+
} else {
|
|
688
|
+
entryIndex.lastUsedAt = Date.now();
|
|
689
|
+
}
|
|
690
|
+
return entryIndex.index;
|
|
691
|
+
}
|
|
692
|
+
|
|
680
693
|
async function ensureWarmIndex(input: {
|
|
681
694
|
workspaceId: WorkspaceId;
|
|
682
695
|
path?: string;
|
|
@@ -685,18 +698,45 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
|
|
|
685
698
|
const entry = registry.get(input.workspaceId);
|
|
686
699
|
if (!entry) throw new UnknownWorkspace(input.workspaceId);
|
|
687
700
|
if (!entry.rootPath) throw new SymbolQueryUnavailable(input.workspaceId);
|
|
688
|
-
const
|
|
701
|
+
const { descriptor, seedFile } = resolveDescriptor(entry.rootPath, input);
|
|
702
|
+
return { index: ensureLanguageIndex(input.workspaceId, entry.rootPath, descriptor, seedFile), descriptor };
|
|
703
|
+
}
|
|
689
704
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
705
|
+
function ensureWorkspaceIndex(
|
|
706
|
+
workspaceId: WorkspaceId,
|
|
707
|
+
preferredSeedFile?: string,
|
|
708
|
+
): {
|
|
709
|
+
index: SymbolIndexPort;
|
|
710
|
+
descriptors: readonly LanguageServerDescriptor[];
|
|
711
|
+
sources: readonly IntelligenceProvenance[];
|
|
712
|
+
} {
|
|
713
|
+
const entry = registry.get(workspaceId);
|
|
714
|
+
if (!entry) throw new UnknownWorkspace(workspaceId);
|
|
715
|
+
if (!entry.rootPath) throw new SymbolQueryUnavailable(workspaceId);
|
|
716
|
+
const rootPath = entry.rootPath;
|
|
717
|
+
const preferredDescriptor = preferredSeedFile ? descriptorForPath(preferredSeedFile) : undefined;
|
|
718
|
+
if (preferredSeedFile && !preferredDescriptor) throw new UnsupportedLanguage(preferredSeedFile);
|
|
719
|
+
const discovered = [...discoverWorkspaceDescriptors(rootPath, LANGUAGE_SERVER_DESCRIPTORS)];
|
|
720
|
+
if (preferredDescriptor && preferredSeedFile && !discovered.some(({ descriptor }) => descriptor.languageId === preferredDescriptor.languageId)) {
|
|
721
|
+
discovered.push({ descriptor: preferredDescriptor, seedFile: preferredSeedFile });
|
|
722
|
+
}
|
|
723
|
+
if (discovered.length === 0) throw new UnsupportedLanguage(rootPath);
|
|
724
|
+
const indexes = discovered.map(({ descriptor, seedFile }) => ({
|
|
725
|
+
descriptor,
|
|
726
|
+
index: ensureLanguageIndex(workspaceId, rootPath, descriptor, preferredDescriptor?.languageId === descriptor.languageId ? preferredSeedFile : seedFile),
|
|
727
|
+
}));
|
|
728
|
+
const first = indexes[0];
|
|
729
|
+
let index: SymbolIndexPort;
|
|
730
|
+
if (indexes.length === 1 && first) {
|
|
731
|
+
index = first.index;
|
|
696
732
|
} else {
|
|
697
|
-
|
|
733
|
+
index = new PolyglotCodeIntelligenceIndex(indexes);
|
|
698
734
|
}
|
|
699
|
-
return { index:
|
|
735
|
+
return { index, descriptors: discovered.map(({ descriptor }) => descriptor), sources: indexes.map(({ index: source }) => source.provenance) };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function workspaceSourceExtensions(descriptors: readonly LanguageServerDescriptor[]): readonly string[] {
|
|
739
|
+
return Array.from(new Set(descriptors.flatMap((descriptor) => descriptor.extensions)));
|
|
700
740
|
}
|
|
701
741
|
|
|
702
742
|
/** Never spawns -- a caller deciding whether to enrich a result with LSP-backed info must not pay a cold-start cost just to check. With a path, checks that file's own language; without one, whether anything is warm for the workspace at all. */
|
|
@@ -723,7 +763,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
|
|
|
723
763
|
if (!Number.isSafeInteger(maxResults) || maxResults < 1 || maxResults > MAX_SYMBOL_RESULTS) {
|
|
724
764
|
throw new TypeError(`maxResults must be a positive safe integer no greater than ${MAX_SYMBOL_RESULTS}`);
|
|
725
765
|
}
|
|
726
|
-
const { index } =
|
|
766
|
+
const { index } = ensureWorkspaceIndex(input.workspaceId, input.seedFile);
|
|
727
767
|
return findWorkspaceSymbols(index, input.query, { maxResults });
|
|
728
768
|
}
|
|
729
769
|
|
|
@@ -819,21 +859,24 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
|
|
|
819
859
|
_registry: MutableRegistry,
|
|
820
860
|
input: OperationInputs["workspace.populateSymbolGraph"],
|
|
821
861
|
): Promise<OperationOutputs["workspace.populateSymbolGraph"]> {
|
|
822
|
-
const
|
|
862
|
+
const workspaceIndex = ensureWorkspaceIndex(input.workspaceId);
|
|
863
|
+
if (!supportsCodeIntelligence(workspaceIndex.index)) throw new CodeIntelligenceUnavailable(input.workspaceId);
|
|
823
864
|
const entry = registry.get(input.workspaceId);
|
|
824
865
|
if (!entry?.rootPath) throw new SymbolQueryUnavailable(input.workspaceId);
|
|
825
866
|
const rootPath = entry.rootPath;
|
|
826
|
-
const
|
|
867
|
+
const extensions = workspaceSourceExtensions(workspaceIndex.descriptors);
|
|
868
|
+
const before = await deriveSourceManifest(rootPath, extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES);
|
|
827
869
|
const graph = ensureSymbolGraph(input.workspaceId);
|
|
828
|
-
const result = await populateSymbolGraphQuery(index, graph, before.absoluteFiles, input.maxSymbolsPerFile);
|
|
829
|
-
const after = await deriveSourceManifest(rootPath,
|
|
870
|
+
const result = await populateSymbolGraphQuery(workspaceIndex.index, graph, before.absoluteFiles, input.maxSymbolsPerFile);
|
|
871
|
+
const after = await deriveSourceManifest(rootPath, extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES);
|
|
830
872
|
if (after.fingerprint !== before.fingerprint) throw new WorkspaceChangedDuringPopulation(input.workspaceId);
|
|
831
873
|
await graph.setGeneration({
|
|
832
874
|
sourceFingerprint: after.fingerprint,
|
|
833
875
|
maxFiles: input.maxFiles,
|
|
834
876
|
maxSymbolsPerFile: input.maxSymbolsPerFile,
|
|
835
877
|
completedAt: Date.now(),
|
|
836
|
-
provenance: index.provenance,
|
|
878
|
+
provenance: workspaceIndex.index.provenance,
|
|
879
|
+
sources: workspaceIndex.sources,
|
|
837
880
|
result,
|
|
838
881
|
});
|
|
839
882
|
return result;
|
|
@@ -858,10 +901,12 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
|
|
|
858
901
|
if (generation.maxFiles !== input.maxFiles || generation.maxSymbolsPerFile !== input.maxSymbolsPerFile) {
|
|
859
902
|
return { status: "not-cached", reason: "bounds-changed" };
|
|
860
903
|
}
|
|
861
|
-
const
|
|
904
|
+
const discovered = discoverWorkspaceDescriptors(entry.rootPath, LANGUAGE_SERVER_DESCRIPTORS);
|
|
905
|
+
if (discovered.length === 0) return { status: "not-cached", reason: "source-changed" };
|
|
906
|
+
const extensions = workspaceSourceExtensions(discovered.map(({ descriptor }) => descriptor));
|
|
862
907
|
let currentFingerprint: string;
|
|
863
908
|
try {
|
|
864
|
-
currentFingerprint = (await deriveSourceManifest(entry.rootPath,
|
|
909
|
+
currentFingerprint = (await deriveSourceManifest(entry.rootPath, extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES)).fingerprint;
|
|
865
910
|
} catch {
|
|
866
911
|
return { status: "not-cached", reason: "source-changed" };
|
|
867
912
|
}
|