ast-mcp-server 0.6.0 → 0.7.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/CHANGELOG.md +27 -0
- package/README.md +47 -15
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +52 -24
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +8 -1
- package/dist/services/impact.js +334 -0
- package/dist/services/operations.js +343 -254
- package/dist/services/outline.js +94 -21
- package/dist/services/pagination.js +10 -4
- package/dist/services/project-operation-scheduler.js +475 -0
- package/dist/services/project-status.js +134 -52
- package/dist/services/project-watcher.js +230 -0
- package/dist/services/project.js +646 -66
- package/dist/services/public-errors.js +255 -0
- package/dist/services/references.js +7 -1
- package/dist/services/relationships.js +1198 -0
- package/dist/services/request-context.js +42 -0
- package/dist/services/runtime-activity.js +65 -0
- package/dist/services/runtime-logger.js +50 -0
- package/dist/services/runtime-policy.js +47 -0
- package/dist/services/setup-wizard.js +4 -7
- package/dist/services/shutdown.js +93 -0
- package/dist/services/symbol-index-limits.js +5 -0
- package/dist/services/symbol-index-order.js +13 -0
- package/dist/services/symbol-index-policy.js +91 -0
- package/dist/services/symbol-index-sqlite.js +850 -0
- package/dist/services/symbol-index.js +447 -0
- package/dist/services/symbols.js +403 -79
- package/dist/services/test-candidates.js +209 -0
- package/dist/services/workspace.js +18 -7
- package/dist/tools/apply_operation.js +8 -4
- package/dist/tools/explore.js +8 -6
- package/dist/tools/find_references.js +12 -13
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +23 -11
- package/dist/tools/get_impact.js +134 -0
- package/dist/tools/get_operation_preview.js +7 -4
- package/dist/tools/get_outline.js +8 -6
- package/dist/tools/get_project_status.js +77 -8
- package/dist/tools/get_symbol_source.js +8 -6
- package/dist/tools/list_files.js +11 -6
- package/dist/tools/rename_symbol.js +6 -4
- package/dist/tools/replace_symbol_body.js +6 -4
- package/dist/tools/result.js +27 -17
- package/dist/tools/scaffold_class.js +6 -4
- package/dist/tools/search_symbols.js +15 -14
- package/docs/support.md +80 -0
- package/package.json +16 -5
- package/skills/structural-code-editing/SKILL.md +37 -10
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { createSourceLocation } from "./read-contracts.js";
|
|
2
|
+
import { MAX_SYMBOL_INDEX_FILE_ENTRIES, MAX_SYMBOL_INDEX_QUERY_CANDIDATES, MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS, } from "./symbol-index-limits.js";
|
|
3
|
+
import { compareSymbolIndexText } from "./symbol-index-order.js";
|
|
4
|
+
import { symbolMatchRank } from "./symbols.js";
|
|
5
|
+
export const SYMBOL_INDEX_SCHEMA_VERSION = 3;
|
|
6
|
+
export { MAX_SYMBOL_INDEX_FILE_ENTRIES, MAX_SYMBOL_INDEX_QUERY_CANDIDATES, MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS, MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES, } from "./symbol-index-limits.js";
|
|
7
|
+
export class SymbolIndexScanLimitError extends Error {
|
|
8
|
+
code = "scan_limit_exceeded";
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Symbol index scan limit exceeded.");
|
|
11
|
+
this.name = "SymbolIndexScanLimitError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const SHA256_DIGEST = /^[a-f0-9]{64}$/;
|
|
15
|
+
const CANONICAL_UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
16
|
+
function assertNonEmptyString(value, name) {
|
|
17
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
18
|
+
throw new Error(`${name} must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function assertDigest(value, name) {
|
|
22
|
+
if (typeof value !== "string" || !SHA256_DIGEST.test(value)) {
|
|
23
|
+
throw new Error(`${name} must be a lowercase SHA-256 digest.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function assertCanonicalTimestamp(value) {
|
|
27
|
+
if (typeof value !== "string" ||
|
|
28
|
+
!CANONICAL_UTC_TIMESTAMP.test(value) ||
|
|
29
|
+
!Number.isFinite(Date.parse(value)) ||
|
|
30
|
+
new Date(value).toISOString() !== value) {
|
|
31
|
+
throw new Error("last_indexed_at must be a canonical UTC timestamp.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function assertProjectIdentity(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null) {
|
|
36
|
+
throw new Error("Symbol index project identity is invalid.");
|
|
37
|
+
}
|
|
38
|
+
const candidate = value;
|
|
39
|
+
assertNonEmptyString(candidate.project_id, "project.project_id");
|
|
40
|
+
if (candidate.config_id !== null) {
|
|
41
|
+
assertNonEmptyString(candidate.config_id, "project.config_id");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertPositiveInteger(value, name) {
|
|
45
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
46
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function projectKey(project) {
|
|
50
|
+
return `${project.project_id}\u0000${project.config_id ?? ""}`;
|
|
51
|
+
}
|
|
52
|
+
function entryKey(project, filePath) {
|
|
53
|
+
return `${projectKey(project)}\u0000${filePath}`;
|
|
54
|
+
}
|
|
55
|
+
function sameProject(left, right) {
|
|
56
|
+
return left.project_id === right.project_id && left.config_id === right.config_id;
|
|
57
|
+
}
|
|
58
|
+
function normalizedFilePath(filePath) {
|
|
59
|
+
return createSourceLocation(filePath, { start_line: 1 }).file;
|
|
60
|
+
}
|
|
61
|
+
export function createSymbolIndexRefreshPlan(input, existing) {
|
|
62
|
+
if (input.current_files.length > MAX_SYMBOL_INDEX_FILE_ENTRIES ||
|
|
63
|
+
input.files.length > MAX_SYMBOL_INDEX_FILE_ENTRIES ||
|
|
64
|
+
existing.length > MAX_SYMBOL_INDEX_FILE_ENTRIES) {
|
|
65
|
+
throw new Error("Symbol index refresh file-entry limit was exceeded.");
|
|
66
|
+
}
|
|
67
|
+
const currentFiles = input.current_files.map((file) => {
|
|
68
|
+
const filePath = normalizedFilePath(file.file_path);
|
|
69
|
+
assertDigest(file.content_hash, "current_file.content_hash");
|
|
70
|
+
return { file_path: filePath, content_hash: file.content_hash };
|
|
71
|
+
});
|
|
72
|
+
const currentPaths = new Set(currentFiles.map((file) => file.file_path));
|
|
73
|
+
if (currentPaths.size !== currentFiles.length) {
|
|
74
|
+
throw new Error("Symbol index refresh contains duplicate current file paths.");
|
|
75
|
+
}
|
|
76
|
+
const candidates = input.files.map((file) => createSymbolIndexFileEntry({
|
|
77
|
+
project: input.project,
|
|
78
|
+
file_path: file.file_path,
|
|
79
|
+
content_hash: file.content_hash,
|
|
80
|
+
config_digest: input.config_digest,
|
|
81
|
+
symbols: file.symbols,
|
|
82
|
+
last_indexed_at: input.last_indexed_at,
|
|
83
|
+
}));
|
|
84
|
+
const candidateByPath = new Map(candidates.map((entry) => [entry.file_path, entry]));
|
|
85
|
+
if (candidateByPath.size !== candidates.length) {
|
|
86
|
+
throw new Error("Symbol index refresh contains duplicate rebuild file paths.");
|
|
87
|
+
}
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
if (!currentPaths.has(candidate.file_path)) {
|
|
90
|
+
throw new Error(`Symbol index rebuild file is not in the current file set: ${candidate.file_path}`);
|
|
91
|
+
}
|
|
92
|
+
const current = currentFiles.find((file) => file.file_path === candidate.file_path);
|
|
93
|
+
if (current.content_hash !== candidate.content_hash) {
|
|
94
|
+
throw new Error(`Symbol index rebuild hash mismatch: ${candidate.file_path}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const existingByPath = new Map(existing.map((entry) => [entry.file_path, entry]));
|
|
98
|
+
const entriesToRebuild = [];
|
|
99
|
+
const reusedFiles = [];
|
|
100
|
+
for (const current of currentFiles) {
|
|
101
|
+
const previous = existingByPath.get(current.file_path);
|
|
102
|
+
const candidate = candidateByPath.get(current.file_path);
|
|
103
|
+
const requiresRebuild = !previous ||
|
|
104
|
+
previous.content_hash !== current.content_hash ||
|
|
105
|
+
previous.config_digest !== input.config_digest;
|
|
106
|
+
if (requiresRebuild && !candidate) {
|
|
107
|
+
throw new Error(`Missing symbol projection for rebuild file: ${current.file_path}`);
|
|
108
|
+
}
|
|
109
|
+
if (candidate)
|
|
110
|
+
entriesToRebuild.push(candidate);
|
|
111
|
+
else
|
|
112
|
+
reusedFiles.push(current.file_path);
|
|
113
|
+
}
|
|
114
|
+
const removedFiles = existing
|
|
115
|
+
.filter((entry) => !currentPaths.has(entry.file_path))
|
|
116
|
+
.map((entry) => entry.file_path)
|
|
117
|
+
.sort(compareSymbolIndexText);
|
|
118
|
+
entriesToRebuild.sort((left, right) => compareSymbolIndexText(left.file_path, right.file_path));
|
|
119
|
+
return {
|
|
120
|
+
entries_to_rebuild: entriesToRebuild,
|
|
121
|
+
rebuilt_files: entriesToRebuild.map((entry) => entry.file_path),
|
|
122
|
+
reused_files: reusedFiles.sort(compareSymbolIndexText),
|
|
123
|
+
removed_files: removedFiles,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function assertQueryLimit(value) {
|
|
127
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_SYMBOL_INDEX_QUERY_CANDIDATES) {
|
|
128
|
+
throw new Error(`Symbol index query limit must be between 1 and ${MAX_SYMBOL_INDEX_QUERY_CANDIDATES}.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function symbolIndexQueryOffset(query) {
|
|
132
|
+
const offset = query.offset ?? 0;
|
|
133
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
134
|
+
throw new Error("Symbol index query offset must be a non-negative safe integer.");
|
|
135
|
+
}
|
|
136
|
+
return offset;
|
|
137
|
+
}
|
|
138
|
+
const SYMBOL_INDEX_MATCH_RANKS = 5;
|
|
139
|
+
export function symbolIndexMatchRank(query, match) {
|
|
140
|
+
return symbolMatchRank(query, {
|
|
141
|
+
symbolPath: match.symbol_path,
|
|
142
|
+
name: match.name,
|
|
143
|
+
line: match.line,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export function symbolIndexRankWindow(rankCounts, offset, limit) {
|
|
147
|
+
const skips = Array(SYMBOL_INDEX_MATCH_RANKS).fill(0);
|
|
148
|
+
const takes = Array(SYMBOL_INDEX_MATCH_RANKS).fill(0);
|
|
149
|
+
let rankOffset = 0;
|
|
150
|
+
for (let rank = 0; rank < SYMBOL_INDEX_MATCH_RANKS; rank += 1) {
|
|
151
|
+
const rankEnd = rankOffset + (rankCounts[rank] ?? 0);
|
|
152
|
+
const pageStart = Math.max(offset, rankOffset);
|
|
153
|
+
const pageEnd = Math.min(offset + limit, rankEnd);
|
|
154
|
+
if (pageStart < pageEnd) {
|
|
155
|
+
skips[rank] = pageStart - rankOffset;
|
|
156
|
+
takes[rank] = pageEnd - pageStart;
|
|
157
|
+
}
|
|
158
|
+
rankOffset = rankEnd;
|
|
159
|
+
}
|
|
160
|
+
return { skips, takes };
|
|
161
|
+
}
|
|
162
|
+
function copyRange(range) {
|
|
163
|
+
const validated = createSourceLocation("index.ts", range);
|
|
164
|
+
return {
|
|
165
|
+
start_line: validated.start_line,
|
|
166
|
+
...(validated.start_column === undefined ? {} : { start_column: validated.start_column }),
|
|
167
|
+
...(validated.end_line === undefined ? {} : { end_line: validated.end_line }),
|
|
168
|
+
...(validated.end_column === undefined ? {} : { end_column: validated.end_column }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function createSymbolIndexSymbol(input) {
|
|
172
|
+
if (typeof input !== "object" || input === null) {
|
|
173
|
+
throw new Error("Symbol index symbol is invalid.");
|
|
174
|
+
}
|
|
175
|
+
assertNonEmptyString(input.name, "symbol.name");
|
|
176
|
+
assertNonEmptyString(input.symbol_path, "symbol.symbol_path");
|
|
177
|
+
assertNonEmptyString(input.selector, "symbol.selector");
|
|
178
|
+
assertNonEmptyString(input.kind, "symbol.kind");
|
|
179
|
+
assertNonEmptyString(input.signature, "symbol.signature");
|
|
180
|
+
assertPositiveInteger(input.line, "symbol.line");
|
|
181
|
+
const range = copyRange(input.range);
|
|
182
|
+
if (range.start_line !== input.line) {
|
|
183
|
+
throw new Error("symbol.line must match symbol.range.start_line.");
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
name: input.name,
|
|
187
|
+
symbol_path: input.symbol_path,
|
|
188
|
+
selector: input.selector,
|
|
189
|
+
kind: input.kind,
|
|
190
|
+
signature: input.signature,
|
|
191
|
+
line: input.line,
|
|
192
|
+
range,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function jsonStringByteLength(value) {
|
|
196
|
+
let bytes = 2;
|
|
197
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
198
|
+
const code = value.charCodeAt(index);
|
|
199
|
+
if (code === 0x22 || code === 0x5c) {
|
|
200
|
+
bytes += 2;
|
|
201
|
+
}
|
|
202
|
+
else if (code <= 0x1f) {
|
|
203
|
+
bytes +=
|
|
204
|
+
code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
|
|
205
|
+
}
|
|
206
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
207
|
+
const trailing = value.charCodeAt(index + 1);
|
|
208
|
+
if (trailing >= 0xdc00 && trailing <= 0xdfff) {
|
|
209
|
+
bytes += 4;
|
|
210
|
+
index += 1;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
bytes += 6;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
217
|
+
bytes += 6;
|
|
218
|
+
}
|
|
219
|
+
else if (code <= 0x7f) {
|
|
220
|
+
bytes += 1;
|
|
221
|
+
}
|
|
222
|
+
else if (code <= 0x7ff) {
|
|
223
|
+
bytes += 2;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
bytes += 3;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return bytes;
|
|
230
|
+
}
|
|
231
|
+
function jsonIntegerByteLength(value) {
|
|
232
|
+
return String(value).length;
|
|
233
|
+
}
|
|
234
|
+
function rangeJsonByteLength(range) {
|
|
235
|
+
let bytes = 14 + jsonIntegerByteLength(range.start_line);
|
|
236
|
+
if (range.start_column !== undefined)
|
|
237
|
+
bytes += 16 + jsonIntegerByteLength(range.start_column);
|
|
238
|
+
if (range.end_line !== undefined)
|
|
239
|
+
bytes += 12 + jsonIntegerByteLength(range.end_line);
|
|
240
|
+
if (range.end_column !== undefined)
|
|
241
|
+
bytes += 14 + jsonIntegerByteLength(range.end_column);
|
|
242
|
+
return bytes + 1;
|
|
243
|
+
}
|
|
244
|
+
export function symbolJsonByteLength(symbol) {
|
|
245
|
+
return (8 +
|
|
246
|
+
jsonStringByteLength(symbol.name) +
|
|
247
|
+
15 +
|
|
248
|
+
jsonStringByteLength(symbol.symbol_path) +
|
|
249
|
+
12 +
|
|
250
|
+
jsonStringByteLength(symbol.selector) +
|
|
251
|
+
8 +
|
|
252
|
+
jsonStringByteLength(symbol.kind) +
|
|
253
|
+
13 +
|
|
254
|
+
jsonStringByteLength(symbol.signature) +
|
|
255
|
+
8 +
|
|
256
|
+
jsonIntegerByteLength(symbol.line) +
|
|
257
|
+
9 +
|
|
258
|
+
rangeJsonByteLength(symbol.range) +
|
|
259
|
+
1);
|
|
260
|
+
}
|
|
261
|
+
export function symbolProjectionJsonByteLength(symbols) {
|
|
262
|
+
let bytes = 2;
|
|
263
|
+
for (let index = 0; index < symbols.length; index += 1) {
|
|
264
|
+
bytes += (index === 0 ? 0 : 1) + symbolJsonByteLength(symbols[index]);
|
|
265
|
+
if (bytes > MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES)
|
|
266
|
+
return bytes;
|
|
267
|
+
}
|
|
268
|
+
return bytes;
|
|
269
|
+
}
|
|
270
|
+
function createSymbolIndexMatch(entry, symbol, symbolPosition) {
|
|
271
|
+
return {
|
|
272
|
+
...createSymbolIndexSymbol(symbol),
|
|
273
|
+
project: {
|
|
274
|
+
project_id: entry.project.project_id,
|
|
275
|
+
config_id: entry.project.config_id,
|
|
276
|
+
},
|
|
277
|
+
file_path: entry.file_path,
|
|
278
|
+
content_hash: entry.content_hash,
|
|
279
|
+
config_digest: entry.config_digest,
|
|
280
|
+
index_schema_version: entry.index_schema_version,
|
|
281
|
+
symbol_position: symbolPosition,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export function createSymbolIndexFileEntry(input) {
|
|
285
|
+
if (typeof input !== "object" || input === null) {
|
|
286
|
+
throw new Error("Symbol index file entry is invalid.");
|
|
287
|
+
}
|
|
288
|
+
assertProjectIdentity(input.project);
|
|
289
|
+
const filePath = createSourceLocation(input.file_path, { start_line: 1 }).file;
|
|
290
|
+
assertDigest(input.content_hash, "content_hash");
|
|
291
|
+
assertDigest(input.config_digest, "config_digest");
|
|
292
|
+
assertCanonicalTimestamp(input.last_indexed_at);
|
|
293
|
+
if (!Array.isArray(input.symbols)) {
|
|
294
|
+
throw new Error("symbols must be an array.");
|
|
295
|
+
}
|
|
296
|
+
if (input.symbols.length > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
|
|
297
|
+
throw new Error("Symbol index row symbol limit was exceeded.");
|
|
298
|
+
}
|
|
299
|
+
const symbols = [];
|
|
300
|
+
let payloadBytes = 2;
|
|
301
|
+
for (const inputSymbol of input.symbols) {
|
|
302
|
+
const symbol = createSymbolIndexSymbol(inputSymbol);
|
|
303
|
+
payloadBytes += (symbols.length === 0 ? 0 : 1) + symbolJsonByteLength(symbol);
|
|
304
|
+
if (payloadBytes > MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES) {
|
|
305
|
+
throw new Error("Symbol index row payload limit was exceeded.");
|
|
306
|
+
}
|
|
307
|
+
symbols.push(symbol);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
index_schema_version: SYMBOL_INDEX_SCHEMA_VERSION,
|
|
311
|
+
project: {
|
|
312
|
+
project_id: input.project.project_id,
|
|
313
|
+
config_id: input.project.config_id,
|
|
314
|
+
},
|
|
315
|
+
file_path: filePath,
|
|
316
|
+
content_hash: input.content_hash,
|
|
317
|
+
config_digest: input.config_digest,
|
|
318
|
+
symbols,
|
|
319
|
+
last_indexed_at: input.last_indexed_at,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export class InMemorySymbolIndex {
|
|
323
|
+
entries = new Map();
|
|
324
|
+
projectEntries(project, schemaVersion) {
|
|
325
|
+
const selected = [];
|
|
326
|
+
for (const entry of this.entries.values()) {
|
|
327
|
+
if (!sameProject(entry.project, project) || entry.index_schema_version !== schemaVersion) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (selected.length >= MAX_SYMBOL_INDEX_FILE_ENTRIES) {
|
|
331
|
+
throw new SymbolIndexScanLimitError();
|
|
332
|
+
}
|
|
333
|
+
selected.push(entry);
|
|
334
|
+
}
|
|
335
|
+
return selected;
|
|
336
|
+
}
|
|
337
|
+
async load(project, schemaVersion) {
|
|
338
|
+
return this.projectEntries(project, schemaVersion)
|
|
339
|
+
.sort((left, right) => compareSymbolIndexText(left.file_path, right.file_path))
|
|
340
|
+
.map((entry) => createSymbolIndexFileEntry(entry));
|
|
341
|
+
}
|
|
342
|
+
async upsert(entry) {
|
|
343
|
+
if (entry.index_schema_version !== SYMBOL_INDEX_SCHEMA_VERSION) {
|
|
344
|
+
throw new Error("Unsupported symbol index schema version.");
|
|
345
|
+
}
|
|
346
|
+
const normalized = createSymbolIndexFileEntry({
|
|
347
|
+
project: entry.project,
|
|
348
|
+
file_path: entry.file_path,
|
|
349
|
+
content_hash: entry.content_hash,
|
|
350
|
+
config_digest: entry.config_digest,
|
|
351
|
+
symbols: entry.symbols,
|
|
352
|
+
last_indexed_at: entry.last_indexed_at,
|
|
353
|
+
});
|
|
354
|
+
const key = entryKey(normalized.project, normalized.file_path);
|
|
355
|
+
if (!this.entries.has(key) &&
|
|
356
|
+
this.projectEntries(normalized.project, SYMBOL_INDEX_SCHEMA_VERSION).length >=
|
|
357
|
+
MAX_SYMBOL_INDEX_FILE_ENTRIES) {
|
|
358
|
+
throw new SymbolIndexScanLimitError();
|
|
359
|
+
}
|
|
360
|
+
this.entries.set(key, normalized);
|
|
361
|
+
}
|
|
362
|
+
async remove(project, filePath) {
|
|
363
|
+
this.entries.delete(entryKey(project, normalizedFilePath(filePath)));
|
|
364
|
+
}
|
|
365
|
+
*matchingSymbols(query) {
|
|
366
|
+
const normalizedQuery = query.query.toLowerCase();
|
|
367
|
+
const normalizedFileFilter = query.filters?.file_path?.toLowerCase();
|
|
368
|
+
const kindSet = query.filters?.kinds ? new Set(query.filters.kinds) : undefined;
|
|
369
|
+
let scannedEntries = 0;
|
|
370
|
+
let scannedSymbols = 0;
|
|
371
|
+
const entries = this.projectEntries(query.project, SYMBOL_INDEX_SCHEMA_VERSION).sort((left, right) => compareSymbolIndexText(left.file_path, right.file_path));
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
if (normalizedFileFilter && !entry.file_path.toLowerCase().includes(normalizedFileFilter)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
scannedEntries += 1;
|
|
377
|
+
if (scannedEntries > MAX_SYMBOL_INDEX_FILE_ENTRIES) {
|
|
378
|
+
throw new SymbolIndexScanLimitError();
|
|
379
|
+
}
|
|
380
|
+
for (const [symbolPosition, symbol] of entry.symbols.entries()) {
|
|
381
|
+
scannedSymbols += 1;
|
|
382
|
+
if (scannedSymbols > MAX_SYMBOL_INDEX_SCANNED_SYMBOLS) {
|
|
383
|
+
throw new SymbolIndexScanLimitError();
|
|
384
|
+
}
|
|
385
|
+
if (kindSet && !kindSet.has(symbol.kind))
|
|
386
|
+
continue;
|
|
387
|
+
if (!symbol.name.toLowerCase().includes(normalizedQuery) &&
|
|
388
|
+
!symbol.symbol_path.toLowerCase().includes(normalizedQuery) &&
|
|
389
|
+
!symbol.selector.toLowerCase().includes(normalizedQuery)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
yield { entry, symbol, symbolPosition };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async countSymbols(query) {
|
|
397
|
+
let count = 0;
|
|
398
|
+
const matches = this.matchingSymbols(query);
|
|
399
|
+
while (!matches.next().done)
|
|
400
|
+
count += 1;
|
|
401
|
+
return count;
|
|
402
|
+
}
|
|
403
|
+
async querySymbols(query) {
|
|
404
|
+
assertQueryLimit(query.limit);
|
|
405
|
+
const offset = symbolIndexQueryOffset(query);
|
|
406
|
+
const rankCounts = [0, 0, 0, 0, 0];
|
|
407
|
+
for (const { entry, symbol, symbolPosition } of this.matchingSymbols(query)) {
|
|
408
|
+
rankCounts[symbolIndexMatchRank(query.query, createSymbolIndexMatch(entry, symbol, symbolPosition))] += 1;
|
|
409
|
+
}
|
|
410
|
+
const window = symbolIndexRankWindow(rankCounts, offset, query.limit);
|
|
411
|
+
const seenByRank = [0, 0, 0, 0, 0];
|
|
412
|
+
const selectedByRank = [[], [], [], [], []];
|
|
413
|
+
for (const { entry, symbol, symbolPosition } of this.matchingSymbols(query)) {
|
|
414
|
+
const match = createSymbolIndexMatch(entry, symbol, symbolPosition);
|
|
415
|
+
const rank = symbolIndexMatchRank(query.query, match);
|
|
416
|
+
const position = seenByRank[rank];
|
|
417
|
+
seenByRank[rank] += 1;
|
|
418
|
+
if (position < window.skips[rank] || selectedByRank[rank].length >= window.takes[rank]) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
selectedByRank[rank].push(match);
|
|
422
|
+
}
|
|
423
|
+
return selectedByRank.flat();
|
|
424
|
+
}
|
|
425
|
+
async queryAllSymbols(query) {
|
|
426
|
+
return this.querySymbols({ ...query, limit: MAX_SYMBOL_INDEX_QUERY_CANDIDATES });
|
|
427
|
+
}
|
|
428
|
+
async clear(project) {
|
|
429
|
+
for (const entry of await this.load(project, SYMBOL_INDEX_SCHEMA_VERSION)) {
|
|
430
|
+
this.entries.delete(entryKey(project, entry.file_path));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async flush() { }
|
|
434
|
+
async refresh(input) {
|
|
435
|
+
const existing = await this.load(input.project, SYMBOL_INDEX_SCHEMA_VERSION);
|
|
436
|
+
const plan = createSymbolIndexRefreshPlan(input, existing);
|
|
437
|
+
for (const entry of plan.entries_to_rebuild)
|
|
438
|
+
await this.upsert(entry);
|
|
439
|
+
for (const filePath of plan.removed_files)
|
|
440
|
+
await this.remove(input.project, filePath);
|
|
441
|
+
return {
|
|
442
|
+
rebuilt_files: plan.rebuilt_files,
|
|
443
|
+
reused_files: plan.reused_files,
|
|
444
|
+
removed_files: plan.removed_files,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|