@myagentroam/node 0.9.94 → 0.9.96
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/computer-use-runtime-version.js +1 -0
- package/dist/computer-use-runtime.js +330 -0
- package/dist/config.js +35 -1
- package/dist/connector.js +49 -7
- package/dist/ctags-binary.js +18 -0
- package/dist/runner/mar-agent/computer-use.js +750 -0
- package/dist/runner/mar-agent/managed-run-controller.js +55 -7
- package/dist/service/computer-use-enablement-service.js +172 -0
- package/dist/service/continuous-run-coordinator.js +1 -0
- package/dist/service/node-connection-lifecycle-service.js +1 -0
- package/dist/service/session-lifecycle-service.js +1 -0
- package/dist/service/session-message-service.js +1 -0
- package/dist/service/workspace-code-navigation-service.js +1219 -0
- package/dist/service/workspace-content-search-service.js +55 -25
- package/dist/service/workspace-file-service.js +1 -1
- package/dist/service/workspace-workbench-service.js +28 -1
- package/dist/workspace.js +70 -6
- package/package.json +7 -4
- package/vendor/ctags/NOTICE +10 -0
- package/vendor/ctags/licenses/COPYING +340 -0
- package/vendor/ctags/licenses/COPYING.MinGW-w64-runtime.txt +240 -0
- package/vendor/ctags/licenses/LICENSE.janssen +19 -0
- package/vendor/ctags/licenses/LICENSE.libyaml +19 -0
- package/vendor/ctags/linux-x64/ctags +0 -0
- package/vendor/ctags/manifest.json +25 -0
- package/vendor/ctags/source/universal-ctags-6.2.1.tar.gz +0 -0
- package/vendor/ctags/win32-x64/ctags.exe +0 -0
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants, createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
+
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { posix, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { bundledCtagsPath, BUNDLED_CTAGS_VERSION } from '../ctags-binary.js';
|
|
9
|
+
import { nodeLog } from '../operational.js';
|
|
10
|
+
import { normalizeWorkspacePath } from '../workspace.js';
|
|
11
|
+
const INDEX_SCHEMA_VERSION = 1;
|
|
12
|
+
const INDEX_OPTIONS_VERSION = 2;
|
|
13
|
+
const BUILD_TIMEOUT_MS = 120_000;
|
|
14
|
+
const BUILD_QUEUE_LIMIT = 4;
|
|
15
|
+
const BUILD_QUEUE_TIMEOUT_MS = 120_000;
|
|
16
|
+
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
const MAX_INDEX_BYTES = 8 * 1024 * 1024 * 1024;
|
|
18
|
+
const MAX_TAGS_BYTES = 512 * 1024 * 1024;
|
|
19
|
+
const MAX_CANDIDATES = 100;
|
|
20
|
+
const MAX_RESULTS = 20;
|
|
21
|
+
const MAX_REFERENCES = 100;
|
|
22
|
+
const MAX_TAG_LINE_BYTES = 16 * 1024;
|
|
23
|
+
const READ_BLOCK_BYTES = 4096;
|
|
24
|
+
const MAX_TARGET_LINE_SCAN_BYTES = MAX_SOURCE_BYTES;
|
|
25
|
+
const MAX_TARGET_PREVIEW_CHARACTERS = 512;
|
|
26
|
+
const NAVIGABLE_EXTENSIONS = new Set([
|
|
27
|
+
'.c',
|
|
28
|
+
'.cc',
|
|
29
|
+
'.cpp',
|
|
30
|
+
'.cs',
|
|
31
|
+
'.cxx',
|
|
32
|
+
'.dart',
|
|
33
|
+
'.ex',
|
|
34
|
+
'.exs',
|
|
35
|
+
'.fs',
|
|
36
|
+
'.fsx',
|
|
37
|
+
'.go',
|
|
38
|
+
'.h',
|
|
39
|
+
'.hh',
|
|
40
|
+
'.hpp',
|
|
41
|
+
'.hrl',
|
|
42
|
+
'.hs',
|
|
43
|
+
'.hxx',
|
|
44
|
+
'.java',
|
|
45
|
+
'.js',
|
|
46
|
+
'.jsx',
|
|
47
|
+
'.kt',
|
|
48
|
+
'.kts',
|
|
49
|
+
'.lua',
|
|
50
|
+
'.m',
|
|
51
|
+
'.mm',
|
|
52
|
+
'.php',
|
|
53
|
+
'.pl',
|
|
54
|
+
'.pm',
|
|
55
|
+
'.py',
|
|
56
|
+
'.pyw',
|
|
57
|
+
'.r',
|
|
58
|
+
'.rb',
|
|
59
|
+
'.rs',
|
|
60
|
+
'.scala',
|
|
61
|
+
'.scm',
|
|
62
|
+
'.sh',
|
|
63
|
+
'.sql',
|
|
64
|
+
'.swift',
|
|
65
|
+
'.ts',
|
|
66
|
+
'.tsx',
|
|
67
|
+
'.mts',
|
|
68
|
+
'.cts',
|
|
69
|
+
'.vb'
|
|
70
|
+
]);
|
|
71
|
+
const RELATIVE_SOURCE_SPECIFIER_HANDLERS = [
|
|
72
|
+
{ importExtension: '.js', sourceExtensions: ['.ts', '.tsx'] },
|
|
73
|
+
{ importExtension: '.jsx', sourceExtensions: ['.tsx'] },
|
|
74
|
+
{ importExtension: '.mjs', sourceExtensions: ['.mts'] },
|
|
75
|
+
{ importExtension: '.cjs', sourceExtensions: ['.cts'] }
|
|
76
|
+
];
|
|
77
|
+
const EXTENSIONLESS_RELATIVE_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.py'];
|
|
78
|
+
const DOTTED_PACKAGE_SPECIFIER_HANDLERS = [
|
|
79
|
+
{ packageSuffixes: ['.java', '.kt', '.kts'] },
|
|
80
|
+
{ packageSuffixes: ['.py', '.pyw', '/__init__.py'] }
|
|
81
|
+
];
|
|
82
|
+
let activeBuilds = 0;
|
|
83
|
+
const buildWaiters = [];
|
|
84
|
+
/**
|
|
85
|
+
* Builds a demand-driven, standard u-ctags declaration index. It never keeps
|
|
86
|
+
* a complete symbol table, watcher, or idle child process in memory.
|
|
87
|
+
*/
|
|
88
|
+
export class WorkspaceCodeNavigationService {
|
|
89
|
+
options;
|
|
90
|
+
builds = new Map();
|
|
91
|
+
preparations = new Map();
|
|
92
|
+
candidateRefreshes = new Map();
|
|
93
|
+
locations = new Map();
|
|
94
|
+
failures = new Map();
|
|
95
|
+
forgotten = new Set();
|
|
96
|
+
forceFreshCandidates = new Map();
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.options = options;
|
|
99
|
+
}
|
|
100
|
+
async prepare(workspace, input) {
|
|
101
|
+
if (typeof input.path !== 'string' || input.path.length === 0 || input.path.length > 4096)
|
|
102
|
+
throw new Error('SYMBOL_DEFINITION_INVALID');
|
|
103
|
+
if (!isNavigablePath(input.path))
|
|
104
|
+
return { state: 'READY' };
|
|
105
|
+
if (!(await this.available()))
|
|
106
|
+
return { state: 'UNAVAILABLE' };
|
|
107
|
+
const location = await this.indexLocation(workspace);
|
|
108
|
+
this.locations.set(workspace.id, location);
|
|
109
|
+
this.forgotten.delete(location.directory);
|
|
110
|
+
const manifest = await readManifest(location.manifest);
|
|
111
|
+
const tags = manifest === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: await tagsPath(location, manifest).catch(() => undefined);
|
|
114
|
+
const reusable = manifest !== undefined &&
|
|
115
|
+
tags !== undefined &&
|
|
116
|
+
(await tagsAvailable(tags)) &&
|
|
117
|
+
manifest.sourceFiles !== undefined;
|
|
118
|
+
const current = reusable &&
|
|
119
|
+
manifest.schemaVersion === INDEX_SCHEMA_VERSION &&
|
|
120
|
+
manifest.optionsVersion === INDEX_OPTIONS_VERSION &&
|
|
121
|
+
manifest.ctagsVersion === BUNDLED_CTAGS_VERSION;
|
|
122
|
+
const cachedCandidates = this.options.files.index(workspace).peekContentCandidates();
|
|
123
|
+
const forceRefreshGeneration = this.forceFreshCandidates.get(workspace.id);
|
|
124
|
+
if (reusable) {
|
|
125
|
+
if (!current || cachedCandidates === undefined || forceRefreshGeneration !== undefined)
|
|
126
|
+
this.refreshCandidatesInBackground(workspace, location, manifest, {
|
|
127
|
+
requireFresh: forceRefreshGeneration !== undefined,
|
|
128
|
+
mustBuild: !current || forceRefreshGeneration !== undefined,
|
|
129
|
+
forceRefreshGeneration
|
|
130
|
+
});
|
|
131
|
+
return { state: current ? 'READY' : 'STALE' };
|
|
132
|
+
}
|
|
133
|
+
const failure = this.failures.get(location.directory);
|
|
134
|
+
if (failure !== undefined &&
|
|
135
|
+
!this.builds.has(location.directory) &&
|
|
136
|
+
!this.preparations.has(location.directory)) {
|
|
137
|
+
this.failures.delete(location.directory);
|
|
138
|
+
return { state: 'FAILED', failureCode: failure };
|
|
139
|
+
}
|
|
140
|
+
this.startInitialBuild(workspace, location);
|
|
141
|
+
return { state: 'BUILDING' };
|
|
142
|
+
}
|
|
143
|
+
async definition(workspace, input) {
|
|
144
|
+
const path = input.path;
|
|
145
|
+
const symbol = input.symbol;
|
|
146
|
+
const specifier = input.specifier;
|
|
147
|
+
if (typeof path !== 'string' ||
|
|
148
|
+
path.length === 0 ||
|
|
149
|
+
path.length > 4096 ||
|
|
150
|
+
(typeof symbol === 'string' && typeof specifier === 'string') ||
|
|
151
|
+
(typeof symbol !== 'string' && typeof specifier !== 'string') ||
|
|
152
|
+
(typeof symbol === 'string' && !isSymbol(symbol)) ||
|
|
153
|
+
(typeof specifier === 'string' && !isNavigableSpecifier(specifier)))
|
|
154
|
+
throw new Error('SYMBOL_DEFINITION_INVALID');
|
|
155
|
+
if (typeof specifier === 'string')
|
|
156
|
+
return this.resolvePath(workspace, path, specifier);
|
|
157
|
+
const requestedSymbol = symbol;
|
|
158
|
+
if (!(await this.available()))
|
|
159
|
+
return emptyDefinitionResult({ state: 'UNAVAILABLE' });
|
|
160
|
+
const location = await this.indexLocation(workspace);
|
|
161
|
+
this.locations.set(workspace.id, location);
|
|
162
|
+
this.forgotten.delete(location.directory);
|
|
163
|
+
const manifest = await readManifest(location.manifest);
|
|
164
|
+
const tags = manifest === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: await tagsPath(location, manifest).catch(() => undefined);
|
|
167
|
+
const tagsReady = tags !== undefined && (await tagsAvailable(tags));
|
|
168
|
+
const reusable = manifest !== undefined &&
|
|
169
|
+
tags !== undefined &&
|
|
170
|
+
tagsReady &&
|
|
171
|
+
manifest.sourceFiles !== undefined;
|
|
172
|
+
const current = reusable &&
|
|
173
|
+
manifest.schemaVersion === INDEX_SCHEMA_VERSION &&
|
|
174
|
+
manifest.optionsVersion === INDEX_OPTIONS_VERSION &&
|
|
175
|
+
manifest.ctagsVersion === BUNDLED_CTAGS_VERSION;
|
|
176
|
+
const fileIndex = this.options.files.index(workspace);
|
|
177
|
+
const cachedCandidates = fileIndex.peekContentCandidates();
|
|
178
|
+
const forceRefreshGeneration = this.forceFreshCandidates.get(workspace.id);
|
|
179
|
+
if (reusable &&
|
|
180
|
+
(!current || cachedCandidates === undefined || forceRefreshGeneration !== undefined)) {
|
|
181
|
+
const prepared = await this.manifestInput(workspace, manifest);
|
|
182
|
+
const targets = await this.findTargets(tags, requestedSymbol, path, prepared);
|
|
183
|
+
this.refreshCandidatesInBackground(workspace, location, manifest, {
|
|
184
|
+
requireFresh: forceRefreshGeneration !== undefined,
|
|
185
|
+
mustBuild: !current || forceRefreshGeneration !== undefined,
|
|
186
|
+
forceRefreshGeneration
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
state: 'STALE',
|
|
190
|
+
targets: targets.slice(0, MAX_RESULTS),
|
|
191
|
+
references: [],
|
|
192
|
+
referenceTruncated: false,
|
|
193
|
+
paths: [],
|
|
194
|
+
truncated: targets.length > MAX_RESULTS || manifest.truncated
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const requireFresh = !current;
|
|
198
|
+
const prepared = await this.indexInput(workspace, requireFresh);
|
|
199
|
+
if (!current) {
|
|
200
|
+
const failure = this.failures.get(location.directory);
|
|
201
|
+
if (failure !== undefined) {
|
|
202
|
+
this.failures.delete(location.directory);
|
|
203
|
+
return emptyDefinitionResult({ state: 'FAILED', failureCode: failure });
|
|
204
|
+
}
|
|
205
|
+
this.startBuild(prepared, location);
|
|
206
|
+
return emptyDefinitionResult({ state: 'BUILDING' });
|
|
207
|
+
}
|
|
208
|
+
const fresh = manifest.sourceFingerprint === prepared.sourceFingerprint;
|
|
209
|
+
const targets = await this.findTargets(tags, requestedSymbol, path, prepared);
|
|
210
|
+
if (!fresh) {
|
|
211
|
+
const failure = this.failures.get(location.directory);
|
|
212
|
+
if (failure !== undefined && targets.length === 0) {
|
|
213
|
+
this.failures.delete(location.directory);
|
|
214
|
+
return emptyDefinitionResult({ state: 'FAILED', failureCode: failure });
|
|
215
|
+
}
|
|
216
|
+
this.startBuild(prepared, location, manifest);
|
|
217
|
+
if (targets.length === 0) {
|
|
218
|
+
return emptyDefinitionResult({
|
|
219
|
+
state: 'BUILDING',
|
|
220
|
+
truncated: prepared.truncated || manifest.truncated
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const references = await this.findReferences(workspace, requestedSymbol, targets);
|
|
225
|
+
return {
|
|
226
|
+
state: fresh ? 'READY' : 'STALE',
|
|
227
|
+
targets: targets.slice(0, MAX_RESULTS),
|
|
228
|
+
references: references.matches,
|
|
229
|
+
referenceTruncated: references.truncated,
|
|
230
|
+
paths: [],
|
|
231
|
+
truncated: targets.length > MAX_RESULTS || prepared.truncated || manifest.truncated
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
forget(workspaceId) {
|
|
235
|
+
const location = this.locations.get(workspaceId);
|
|
236
|
+
this.locations.delete(workspaceId);
|
|
237
|
+
this.forceFreshCandidates.delete(workspaceId);
|
|
238
|
+
if (location !== undefined) {
|
|
239
|
+
this.failures.delete(location.directory);
|
|
240
|
+
this.forgotten.add(location.directory);
|
|
241
|
+
if (!this.builds.has(location.directory))
|
|
242
|
+
void rm(location.directory, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** A Node-originated write makes the next background refresh authoritative. */
|
|
246
|
+
invalidate(workspaceId) {
|
|
247
|
+
this.forceFreshCandidates.set(workspaceId, (this.forceFreshCandidates.get(workspaceId) ?? 0) + 1);
|
|
248
|
+
}
|
|
249
|
+
async available() {
|
|
250
|
+
try {
|
|
251
|
+
await access(this.executable(), constants.X_OK);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
executable() {
|
|
259
|
+
return (this.options.ctagsExecutable?.() ??
|
|
260
|
+
bundledCtagsPath(this.options.platform ?? process.platform, this.options.architecture ?? process.arch));
|
|
261
|
+
}
|
|
262
|
+
async indexInput(workspace, requireFresh) {
|
|
263
|
+
const startedAt = Date.now();
|
|
264
|
+
const candidates = await this.options.files
|
|
265
|
+
.index(workspace)
|
|
266
|
+
.contentCandidates({ requireFresh });
|
|
267
|
+
return this.indexInputFromCandidates(await realpath(workspace.path), candidates.entries, candidates.truncated, Date.now() - startedAt);
|
|
268
|
+
}
|
|
269
|
+
async manifestInput(workspace, manifest) {
|
|
270
|
+
const entries = Object.entries(manifest.sourceFiles ?? {})
|
|
271
|
+
.map(([path, fingerprint]) => manifestEntry(path, fingerprint))
|
|
272
|
+
.filter((entry) => entry !== undefined);
|
|
273
|
+
return this.indexInputFromCandidates(await realpath(workspace.path), entries, manifest.truncated, 0, manifest.sourceFingerprint, manifest.sourceFiles ?? {}, true);
|
|
274
|
+
}
|
|
275
|
+
indexInputFromCandidates(root, candidates, truncated, candidateScanMs, knownFingerprint, knownSourceFiles, revalidateSourceFingerprint = false) {
|
|
276
|
+
const entries = candidates
|
|
277
|
+
.filter((entry) => entry.size !== null &&
|
|
278
|
+
entry.size <= MAX_SOURCE_BYTES &&
|
|
279
|
+
isNavigablePath(entry.path) &&
|
|
280
|
+
!entry.path.includes('\n') &&
|
|
281
|
+
!entry.path.includes('\r'))
|
|
282
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
283
|
+
let bytes = 0;
|
|
284
|
+
const bounded = [];
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
if (entry.size === null || bytes + entry.size > MAX_INDEX_BYTES)
|
|
287
|
+
break;
|
|
288
|
+
bytes += entry.size;
|
|
289
|
+
bounded.push(entry);
|
|
290
|
+
}
|
|
291
|
+
const sourceFiles = knownSourceFiles ??
|
|
292
|
+
Object.fromEntries(bounded.map((entry) => [entry.path, sourceFileFingerprint(entry.size, entry.modifiedAt)]));
|
|
293
|
+
return {
|
|
294
|
+
root,
|
|
295
|
+
entries: bounded,
|
|
296
|
+
sourceFingerprint: knownFingerprint ?? sourceFingerprint(bounded),
|
|
297
|
+
sourceFiles,
|
|
298
|
+
truncated: truncated || bounded.length !== entries.length,
|
|
299
|
+
candidateScanMs,
|
|
300
|
+
revalidateSourceFingerprint
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async indexLocation(workspace) {
|
|
304
|
+
const path = normalizeWorkspacePath(await realpath(workspace.path));
|
|
305
|
+
const hash = createHash('sha256').update(path).digest('hex');
|
|
306
|
+
const nodeId = this.options.nodeId();
|
|
307
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(nodeId))
|
|
308
|
+
throw new Error('SYMBOL_INDEX_UNAVAILABLE');
|
|
309
|
+
const base = this.options.indexHome?.() ?? resolve(homedir(), '.mar', 'workspace-indexes');
|
|
310
|
+
const nodeDirectory = resolve(base, nodeId);
|
|
311
|
+
const directory = resolve(nodeDirectory, hash);
|
|
312
|
+
if (relative(nodeDirectory, directory)
|
|
313
|
+
.split(sep)
|
|
314
|
+
.some((part) => part === '..'))
|
|
315
|
+
throw new Error('SYMBOL_INDEX_UNAVAILABLE');
|
|
316
|
+
return {
|
|
317
|
+
directory,
|
|
318
|
+
manifest: resolve(directory, 'manifest.json'),
|
|
319
|
+
generations: resolve(directory, 'generations')
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
startBuild(input, location, previous) {
|
|
323
|
+
void this.ensureBuild(input, location, previous).catch((error) => this.recordBuildFailure(location, error, input));
|
|
324
|
+
}
|
|
325
|
+
startInitialBuild(workspace, location) {
|
|
326
|
+
if (this.builds.has(location.directory) || this.preparations.has(location.directory))
|
|
327
|
+
return;
|
|
328
|
+
const preparation = this.indexInput(workspace, true)
|
|
329
|
+
.then((input) => {
|
|
330
|
+
if (this.forgotten.has(location.directory))
|
|
331
|
+
return;
|
|
332
|
+
return this.ensureBuild(input, location);
|
|
333
|
+
})
|
|
334
|
+
.catch((error) => this.recordBuildFailure(location, error))
|
|
335
|
+
.finally(() => {
|
|
336
|
+
if (this.preparations.get(location.directory) === preparation)
|
|
337
|
+
this.preparations.delete(location.directory);
|
|
338
|
+
});
|
|
339
|
+
this.preparations.set(location.directory, preparation);
|
|
340
|
+
}
|
|
341
|
+
recordBuildFailure(location, error, input) {
|
|
342
|
+
const failure = buildFailure(error);
|
|
343
|
+
this.failures.set(location.directory, failure);
|
|
344
|
+
nodeLog('workspace.code_navigation.index.failed', {
|
|
345
|
+
nodeId: this.options.nodeId(),
|
|
346
|
+
failureCode: failure,
|
|
347
|
+
sourceFiles: input?.entries.length ?? 0,
|
|
348
|
+
candidateScanMs: input?.candidateScanMs ?? 0
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
refreshCandidatesInBackground(workspace, location, manifest, options = {
|
|
352
|
+
requireFresh: false,
|
|
353
|
+
mustBuild: false,
|
|
354
|
+
forceRefreshGeneration: undefined
|
|
355
|
+
}) {
|
|
356
|
+
if (this.candidateRefreshes.has(location.directory))
|
|
357
|
+
return;
|
|
358
|
+
const startedAt = Date.now();
|
|
359
|
+
const refresh = this.options.files
|
|
360
|
+
.index(workspace)
|
|
361
|
+
.refreshContentCandidates(options.requireFresh)
|
|
362
|
+
.then(async () => {
|
|
363
|
+
const prepared = await this.indexInput(workspace, false);
|
|
364
|
+
const changed = prepared.sourceFingerprint !== manifest.sourceFingerprint;
|
|
365
|
+
nodeLog('workspace.code_navigation.candidates.refreshed', {
|
|
366
|
+
nodeId: this.options.nodeId(),
|
|
367
|
+
sourceFiles: prepared.entries.length,
|
|
368
|
+
candidateScanMs: prepared.candidateScanMs,
|
|
369
|
+
elapsedMs: Date.now() - startedAt,
|
|
370
|
+
changed
|
|
371
|
+
});
|
|
372
|
+
if (changed || options.mustBuild)
|
|
373
|
+
this.startBuild(prepared, location, manifest);
|
|
374
|
+
if (options.forceRefreshGeneration !== undefined &&
|
|
375
|
+
this.forceFreshCandidates.get(workspace.id) === options.forceRefreshGeneration)
|
|
376
|
+
this.forceFreshCandidates.delete(workspace.id);
|
|
377
|
+
})
|
|
378
|
+
.catch(() => undefined)
|
|
379
|
+
.finally(() => {
|
|
380
|
+
if (this.candidateRefreshes.get(location.directory) === refresh)
|
|
381
|
+
this.candidateRefreshes.delete(location.directory);
|
|
382
|
+
});
|
|
383
|
+
this.candidateRefreshes.set(location.directory, refresh);
|
|
384
|
+
}
|
|
385
|
+
ensureBuild(input, location, previous) {
|
|
386
|
+
const active = this.builds.get(location.directory);
|
|
387
|
+
if (active !== undefined)
|
|
388
|
+
return active;
|
|
389
|
+
const build = this.build(input, location, previous).finally(async () => {
|
|
390
|
+
if (this.builds.get(location.directory) === build)
|
|
391
|
+
this.builds.delete(location.directory);
|
|
392
|
+
if (this.forgotten.delete(location.directory))
|
|
393
|
+
await rm(location.directory, { recursive: true, force: true });
|
|
394
|
+
});
|
|
395
|
+
this.builds.set(location.directory, build);
|
|
396
|
+
return build;
|
|
397
|
+
}
|
|
398
|
+
async build(input, location, previous) {
|
|
399
|
+
const startedAt = Date.now();
|
|
400
|
+
const executable = this.executable();
|
|
401
|
+
await access(executable, constants.X_OK);
|
|
402
|
+
await ensurePrivateDirectory(location.directory);
|
|
403
|
+
await ensurePrivateDirectory(location.generations);
|
|
404
|
+
const generation = randomUUID();
|
|
405
|
+
const staging = resolve(location.generations, `${generation}.staging`);
|
|
406
|
+
const committed = resolve(location.generations, generation);
|
|
407
|
+
try {
|
|
408
|
+
await ensurePrivateDirectory(staging);
|
|
409
|
+
const fileList = resolve(staging, 'files.list');
|
|
410
|
+
const tags = resolve(staging, 'tags');
|
|
411
|
+
const oldTags = previous?.sourceFiles === undefined
|
|
412
|
+
? undefined
|
|
413
|
+
: await tagsPath(location, previous).catch(() => undefined);
|
|
414
|
+
const delta = oldTags === undefined || previous?.sourceFiles === undefined
|
|
415
|
+
? undefined
|
|
416
|
+
: sourceDelta(previous.sourceFiles, input.sourceFiles);
|
|
417
|
+
let ctagsMs;
|
|
418
|
+
let mode;
|
|
419
|
+
if (delta === undefined || delta.changed.length + delta.removed.length > 1024) {
|
|
420
|
+
mode = 'full';
|
|
421
|
+
await writeFile(fileList, input.entries.map((entry) => entry.path).join('\n'), {
|
|
422
|
+
encoding: 'utf8',
|
|
423
|
+
mode: 0o600
|
|
424
|
+
});
|
|
425
|
+
const release = await acquireBuild();
|
|
426
|
+
try {
|
|
427
|
+
ctagsMs = await runCtags(executable, input.root, fileList, tags);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
release();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
mode = 'incremental';
|
|
435
|
+
ctagsMs = await buildIncrementalTags(executable, input.root, oldTags, delta, staging, tags);
|
|
436
|
+
}
|
|
437
|
+
const tagsMetadata = await stat(tags);
|
|
438
|
+
if (!tagsMetadata.isFile() || tagsMetadata.size > MAX_TAGS_BYTES)
|
|
439
|
+
throw new Error('SYMBOL_INDEX_LIMIT_EXCEEDED');
|
|
440
|
+
if (this.forgotten.has(location.directory))
|
|
441
|
+
return;
|
|
442
|
+
await rename(staging, committed);
|
|
443
|
+
const manifest = {
|
|
444
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
445
|
+
optionsVersion: INDEX_OPTIONS_VERSION,
|
|
446
|
+
ctagsVersion: BUNDLED_CTAGS_VERSION,
|
|
447
|
+
sourceFingerprint: input.sourceFingerprint,
|
|
448
|
+
sourceFiles: input.sourceFiles,
|
|
449
|
+
truncated: input.truncated,
|
|
450
|
+
generation
|
|
451
|
+
};
|
|
452
|
+
const manifestNext = resolve(location.directory, `manifest.${generation}.next.json`);
|
|
453
|
+
await writeFile(manifestNext, JSON.stringify(manifest), { encoding: 'utf8', mode: 0o600 });
|
|
454
|
+
await rename(manifestNext, location.manifest);
|
|
455
|
+
this.failures.delete(location.directory);
|
|
456
|
+
await removeInactiveGenerations(location.generations, generation);
|
|
457
|
+
nodeLog('workspace.code_navigation.index.completed', {
|
|
458
|
+
nodeId: this.options.nodeId(),
|
|
459
|
+
mode,
|
|
460
|
+
sourceFiles: input.entries.length,
|
|
461
|
+
sourceBytes: input.entries.reduce((total, entry) => total + (entry.size ?? 0), 0),
|
|
462
|
+
candidateScanMs: input.candidateScanMs,
|
|
463
|
+
ctagsMs,
|
|
464
|
+
tagsBytes: tagsMetadata.size,
|
|
465
|
+
elapsedMs: Date.now() - startedAt
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
await rm(staging, { recursive: true, force: true });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async findTargets(tags, symbol, currentPath, input) {
|
|
473
|
+
const handle = await open(tags, 'r');
|
|
474
|
+
try {
|
|
475
|
+
const size = (await handle.stat()).size;
|
|
476
|
+
const first = await firstTagOffset(handle, size, symbol);
|
|
477
|
+
const candidates = new Set(input.entries.map((entry) => entry.path));
|
|
478
|
+
const records = [];
|
|
479
|
+
let offset = first;
|
|
480
|
+
while (offset < size && records.length < MAX_CANDIDATES) {
|
|
481
|
+
const line = await readTagLine(handle, offset, size);
|
|
482
|
+
if (line === undefined)
|
|
483
|
+
break;
|
|
484
|
+
offset = line.nextOffset;
|
|
485
|
+
const tag = parseTag(line.text);
|
|
486
|
+
if (tag === undefined || tag.name !== symbol) {
|
|
487
|
+
if (tag !== undefined && compareTagNames(tag.name, symbol) > 0)
|
|
488
|
+
break;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (!candidates.has(tag.path))
|
|
492
|
+
continue;
|
|
493
|
+
records.push(tag);
|
|
494
|
+
}
|
|
495
|
+
const valid = await Promise.all(records.map(async (record) => ({
|
|
496
|
+
record,
|
|
497
|
+
preview: await validateTarget(input.root, record, symbol, input.revalidateSourceFingerprint ? input.sourceFiles[record.path] : undefined)
|
|
498
|
+
})));
|
|
499
|
+
return valid
|
|
500
|
+
.flatMap(({ record, preview }) => (preview === undefined ? [] : [{ ...record, preview }]))
|
|
501
|
+
.sort((left, right) => compareTargets(left, right, currentPath))
|
|
502
|
+
.map(({ path, line, kind, scope, preview }) => ({ path, line, kind, scope, preview }));
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
await handle.close();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async findReferences(workspace, symbol, definitions) {
|
|
509
|
+
if (this.options.searchContent === undefined)
|
|
510
|
+
return { matches: [], truncated: false };
|
|
511
|
+
try {
|
|
512
|
+
const search = await this.options.searchContent(workspace, symbol);
|
|
513
|
+
const definitionLocations = new Set(definitions.map((target) => `${target.path}:${target.line}`));
|
|
514
|
+
const matches = search.matches
|
|
515
|
+
.filter((match) => !definitionLocations.has(`${match.path}:${match.line}`) &&
|
|
516
|
+
containsSymbol(match.preview, symbol))
|
|
517
|
+
.slice(0, MAX_REFERENCES)
|
|
518
|
+
.map(({ path, line, column, preview }) => ({ path, line, column, preview }));
|
|
519
|
+
return {
|
|
520
|
+
matches,
|
|
521
|
+
truncated: search.truncated || search.matches.length > MAX_REFERENCES
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return { matches: [], truncated: false };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async resolvePath(workspace, currentPath, specifier) {
|
|
529
|
+
const location = await this.indexLocation(workspace);
|
|
530
|
+
this.locations.set(workspace.id, location);
|
|
531
|
+
this.forgotten.delete(location.directory);
|
|
532
|
+
const manifest = await readManifest(location.manifest);
|
|
533
|
+
const tags = manifest === undefined
|
|
534
|
+
? undefined
|
|
535
|
+
: await tagsPath(location, manifest).catch(() => undefined);
|
|
536
|
+
const tagsReady = tags !== undefined && (await tagsAvailable(tags));
|
|
537
|
+
const fileIndex = this.options.files.index(workspace);
|
|
538
|
+
const cachedCandidates = fileIndex.peekContentCandidates();
|
|
539
|
+
const forceRefreshGeneration = this.forceFreshCandidates.get(workspace.id);
|
|
540
|
+
const reusable = manifest !== undefined &&
|
|
541
|
+
tags !== undefined &&
|
|
542
|
+
tagsReady &&
|
|
543
|
+
manifest.sourceFiles !== undefined;
|
|
544
|
+
const current = reusable &&
|
|
545
|
+
manifest.schemaVersion === INDEX_SCHEMA_VERSION &&
|
|
546
|
+
manifest.optionsVersion === INDEX_OPTIONS_VERSION &&
|
|
547
|
+
manifest.ctagsVersion === BUNDLED_CTAGS_VERSION;
|
|
548
|
+
const stale = reusable &&
|
|
549
|
+
(!current || cachedCandidates === undefined || forceRefreshGeneration !== undefined);
|
|
550
|
+
const input = stale
|
|
551
|
+
? await this.manifestInput(workspace, manifest)
|
|
552
|
+
: await this.indexInput(workspace, false);
|
|
553
|
+
if (stale)
|
|
554
|
+
this.refreshCandidatesInBackground(workspace, location, manifest, {
|
|
555
|
+
requireFresh: forceRefreshGeneration !== undefined,
|
|
556
|
+
mustBuild: !current || forceRefreshGeneration !== undefined,
|
|
557
|
+
forceRefreshGeneration
|
|
558
|
+
});
|
|
559
|
+
const resolved = await this.resolvePathTargets(input, currentPath, specifier);
|
|
560
|
+
return {
|
|
561
|
+
state: stale ? 'STALE' : 'READY',
|
|
562
|
+
targets: [],
|
|
563
|
+
references: [],
|
|
564
|
+
referenceTruncated: false,
|
|
565
|
+
paths: resolved.targets,
|
|
566
|
+
truncated: input.truncated || resolved.truncated
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async resolvePathTargets(input, currentPath, specifier) {
|
|
570
|
+
const targets = input.entries
|
|
571
|
+
.map((entry) => entry.path)
|
|
572
|
+
.filter((path) => isNavigablePath(path) && matchesSpecifier(path, currentPath, specifier))
|
|
573
|
+
.sort((left, right) => pathTargetScore(left, currentPath, specifier) -
|
|
574
|
+
pathTargetScore(right, currentPath, specifier))
|
|
575
|
+
.slice(0, MAX_RESULTS);
|
|
576
|
+
const resolved = await Promise.all(targets.map(async (path) => {
|
|
577
|
+
const preview = await validatePathTarget(input.root, path, input.revalidateSourceFingerprint ? input.sourceFiles[path] : undefined);
|
|
578
|
+
return preview === undefined
|
|
579
|
+
? undefined
|
|
580
|
+
: {
|
|
581
|
+
path,
|
|
582
|
+
line: 1,
|
|
583
|
+
kind: 'file',
|
|
584
|
+
...(preview.length === 0 ? {} : { preview: targetPreview(preview) }),
|
|
585
|
+
scope: undefined
|
|
586
|
+
};
|
|
587
|
+
}));
|
|
588
|
+
return {
|
|
589
|
+
targets: resolved.flatMap((target) => (target === undefined ? [] : [target])),
|
|
590
|
+
truncated: targets.length >= MAX_RESULTS
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function emptyDefinitionResult(input) {
|
|
595
|
+
return {
|
|
596
|
+
state: input.state,
|
|
597
|
+
targets: [],
|
|
598
|
+
references: [],
|
|
599
|
+
referenceTruncated: false,
|
|
600
|
+
paths: [],
|
|
601
|
+
truncated: input.truncated ?? false,
|
|
602
|
+
...(input.failureCode === undefined ? {} : { failureCode: input.failureCode })
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function sourceDelta(previous, current) {
|
|
606
|
+
const changed = Object.entries(current)
|
|
607
|
+
.filter(([path, fingerprint]) => previous[path] !== fingerprint)
|
|
608
|
+
.map(([path]) => path);
|
|
609
|
+
const removed = Object.keys(previous).filter((path) => current[path] === undefined);
|
|
610
|
+
return { changed, removed };
|
|
611
|
+
}
|
|
612
|
+
function manifestEntry(path, fingerprint) {
|
|
613
|
+
const match = /^([0-9]+):(-1|[0-9]+(?:\.[0-9]+)?)$/u.exec(fingerprint);
|
|
614
|
+
if (match === null)
|
|
615
|
+
return undefined;
|
|
616
|
+
const size = Number(match[1]);
|
|
617
|
+
const modifiedAt = match[2] === '-1' ? null : Number(match[2]);
|
|
618
|
+
if (!Number.isSafeInteger(size) ||
|
|
619
|
+
size < 0 ||
|
|
620
|
+
(modifiedAt !== null && !Number.isFinite(modifiedAt)))
|
|
621
|
+
return undefined;
|
|
622
|
+
return {
|
|
623
|
+
path,
|
|
624
|
+
name: path.slice(path.lastIndexOf('/') + 1),
|
|
625
|
+
kind: 'FILE',
|
|
626
|
+
size,
|
|
627
|
+
modifiedAt
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function sourceFileFingerprint(size, modifiedAt) {
|
|
631
|
+
return `${size}:${modifiedAt ?? -1}`;
|
|
632
|
+
}
|
|
633
|
+
function sourceFingerprint(entries) {
|
|
634
|
+
const hash = createHash('sha256');
|
|
635
|
+
for (const entry of entries)
|
|
636
|
+
hash.update(`${entry.path}\0${sourceFileFingerprint(entry.size ?? -1, entry.modifiedAt)}\0`, 'utf8');
|
|
637
|
+
return hash.digest('hex');
|
|
638
|
+
}
|
|
639
|
+
async function buildIncrementalTags(executable, root, previousTags, delta, staging, destination) {
|
|
640
|
+
const changedTags = resolve(staging, 'changed.tags');
|
|
641
|
+
if (delta.changed.length > 0) {
|
|
642
|
+
const list = resolve(staging, 'changed.files.list');
|
|
643
|
+
await writeFile(list, delta.changed.join('\n'), { encoding: 'utf8', mode: 0o600 });
|
|
644
|
+
const release = await acquireBuild();
|
|
645
|
+
try {
|
|
646
|
+
const ctagsMs = await runCtags(executable, root, list, changedTags);
|
|
647
|
+
await mergeTags(previousTags, changedTags, new Set([...delta.changed, ...delta.removed]), destination);
|
|
648
|
+
return ctagsMs;
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
release();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else
|
|
655
|
+
await writeFile(changedTags, '', { encoding: 'utf8', mode: 0o600 });
|
|
656
|
+
await mergeTags(previousTags, changedTags, new Set([...delta.changed, ...delta.removed]), destination);
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
659
|
+
async function mergeTags(previous, changed, replacedPaths, destination) {
|
|
660
|
+
const output = createWriteStream(destination, { encoding: 'utf8', mode: 0o600 });
|
|
661
|
+
const oldLines = tagLines(previous)[Symbol.asyncIterator]();
|
|
662
|
+
const changedLines = tagLines(changed)[Symbol.asyncIterator]();
|
|
663
|
+
const write = async (line) => {
|
|
664
|
+
if (!output.write(`${line}\n`))
|
|
665
|
+
await new Promise((resolvePromise) => output.once('drain', resolvePromise));
|
|
666
|
+
};
|
|
667
|
+
let old = await oldLines.next();
|
|
668
|
+
while (!old.done && old.value.startsWith('!_TAG_')) {
|
|
669
|
+
await write(old.value);
|
|
670
|
+
old = await oldLines.next();
|
|
671
|
+
}
|
|
672
|
+
let nextChanged = await nextDataTag(changedLines);
|
|
673
|
+
while (!old.done || nextChanged !== undefined) {
|
|
674
|
+
while (!old.done && tagPath(old.value) !== undefined && replacedPaths.has(tagPath(old.value)))
|
|
675
|
+
old = await oldLines.next();
|
|
676
|
+
if (old.done) {
|
|
677
|
+
await write(nextChanged);
|
|
678
|
+
nextChanged = await nextDataTag(changedLines);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (nextChanged === undefined || compareTagLines(old.value, nextChanged) <= 0) {
|
|
682
|
+
await write(old.value);
|
|
683
|
+
old = await oldLines.next();
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
await write(nextChanged);
|
|
687
|
+
nextChanged = await nextDataTag(changedLines);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
await new Promise((resolvePromise, reject) => {
|
|
691
|
+
output.once('error', reject);
|
|
692
|
+
output.end(resolvePromise);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function* tagLines(path) {
|
|
696
|
+
const input = createInterface({
|
|
697
|
+
input: createReadStream(path, { encoding: 'utf8' }),
|
|
698
|
+
crlfDelay: Infinity
|
|
699
|
+
});
|
|
700
|
+
for await (const line of input)
|
|
701
|
+
yield line;
|
|
702
|
+
}
|
|
703
|
+
async function nextDataTag(iterator) {
|
|
704
|
+
for (;;) {
|
|
705
|
+
const next = await iterator.next();
|
|
706
|
+
if (next.done)
|
|
707
|
+
return undefined;
|
|
708
|
+
if (!next.value.startsWith('!_TAG_'))
|
|
709
|
+
return next.value;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function tagPath(line) {
|
|
713
|
+
const second = line.indexOf('\t');
|
|
714
|
+
if (second < 0)
|
|
715
|
+
return undefined;
|
|
716
|
+
const third = line.indexOf('\t', second + 1);
|
|
717
|
+
return third < 0 ? undefined : line.slice(second + 1, third);
|
|
718
|
+
}
|
|
719
|
+
function compareTagLines(left, right) {
|
|
720
|
+
const leftName = left.slice(0, left.indexOf('\t'));
|
|
721
|
+
const rightName = right.slice(0, right.indexOf('\t'));
|
|
722
|
+
return (compareTagNames(leftName, rightName) || Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
723
|
+
}
|
|
724
|
+
async function acquireBuild() {
|
|
725
|
+
if (activeBuilds >= 1) {
|
|
726
|
+
if (buildWaiters.length >= BUILD_QUEUE_LIMIT)
|
|
727
|
+
throw new Error('SYMBOL_INDEX_BUILD_FAILED');
|
|
728
|
+
await new Promise((resolvePromise, reject) => {
|
|
729
|
+
const resume = () => {
|
|
730
|
+
clearTimeout(timer);
|
|
731
|
+
resolvePromise();
|
|
732
|
+
};
|
|
733
|
+
const timer = setTimeout(() => {
|
|
734
|
+
const index = buildWaiters.indexOf(resume);
|
|
735
|
+
if (index >= 0)
|
|
736
|
+
buildWaiters.splice(index, 1);
|
|
737
|
+
reject(new Error('SYMBOL_INDEX_BUILD_FAILED'));
|
|
738
|
+
}, BUILD_QUEUE_TIMEOUT_MS);
|
|
739
|
+
timer.unref();
|
|
740
|
+
buildWaiters.push(resume);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
activeBuilds += 1;
|
|
744
|
+
return () => {
|
|
745
|
+
activeBuilds -= 1;
|
|
746
|
+
buildWaiters.shift()?.();
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function runCtags(executable, cwd, fileList, destination) {
|
|
750
|
+
const startedAt = Date.now();
|
|
751
|
+
await new Promise((resolvePromise, reject) => {
|
|
752
|
+
const child = spawn(executable, [
|
|
753
|
+
'--options=NONE',
|
|
754
|
+
'--output-format=u-ctags',
|
|
755
|
+
'--excmd=number',
|
|
756
|
+
'--sort=yes',
|
|
757
|
+
'--tag-relative=no',
|
|
758
|
+
'--pseudo-tags=-TAG_PROC_CWD',
|
|
759
|
+
'--fields=+nKS',
|
|
760
|
+
'--extras=+q',
|
|
761
|
+
'-L',
|
|
762
|
+
fileList,
|
|
763
|
+
'-f',
|
|
764
|
+
destination
|
|
765
|
+
], {
|
|
766
|
+
cwd,
|
|
767
|
+
detached: process.platform !== 'win32',
|
|
768
|
+
windowsHide: true,
|
|
769
|
+
stdio: ['ignore', 'ignore', 'pipe']
|
|
770
|
+
});
|
|
771
|
+
let settled = false;
|
|
772
|
+
const settle = (callback) => {
|
|
773
|
+
if (settled)
|
|
774
|
+
return;
|
|
775
|
+
settled = true;
|
|
776
|
+
clearTimeout(timer);
|
|
777
|
+
callback();
|
|
778
|
+
};
|
|
779
|
+
const terminate = () => {
|
|
780
|
+
if (child.pid !== undefined && process.platform !== 'win32') {
|
|
781
|
+
try {
|
|
782
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// Fall through to the direct process kill.
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
child.kill('SIGKILL');
|
|
790
|
+
};
|
|
791
|
+
const timer = setTimeout(() => {
|
|
792
|
+
terminate();
|
|
793
|
+
settle(() => reject(new Error('SYMBOL_INDEX_BUILD_FAILED')));
|
|
794
|
+
}, BUILD_TIMEOUT_MS);
|
|
795
|
+
timer.unref();
|
|
796
|
+
child.stderr.on('data', () => undefined);
|
|
797
|
+
child.once('error', () => settle(() => reject(new Error('SYMBOL_INDEX_BUILD_FAILED'))));
|
|
798
|
+
child.once('exit', (code) => settle(() => (code === 0 ? resolvePromise() : reject(new Error('SYMBOL_INDEX_BUILD_FAILED')))));
|
|
799
|
+
});
|
|
800
|
+
return Date.now() - startedAt;
|
|
801
|
+
}
|
|
802
|
+
async function ensurePrivateDirectory(path) {
|
|
803
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
804
|
+
const metadata = await lstat(path);
|
|
805
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
806
|
+
throw new Error('SYMBOL_INDEX_UNAVAILABLE');
|
|
807
|
+
}
|
|
808
|
+
async function removeInactiveGenerations(directory, activeGeneration) {
|
|
809
|
+
const generations = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
810
|
+
await Promise.all(generations
|
|
811
|
+
.filter((entry) => entry.isDirectory() &&
|
|
812
|
+
entry.name !== activeGeneration &&
|
|
813
|
+
/^[0-9a-f-]{36}(?:\.staging)?$/u.test(entry.name))
|
|
814
|
+
.map((entry) => rm(resolve(directory, entry.name), { recursive: true, force: true })));
|
|
815
|
+
}
|
|
816
|
+
async function readManifest(path) {
|
|
817
|
+
try {
|
|
818
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
819
|
+
const sourceFiles = validManifestSourceFiles(value.sourceFiles) ? value.sourceFiles : undefined;
|
|
820
|
+
const schemaVersion = value.schemaVersion;
|
|
821
|
+
const optionsVersion = value.optionsVersion;
|
|
822
|
+
return typeof value === 'object' &&
|
|
823
|
+
value !== null &&
|
|
824
|
+
typeof schemaVersion === 'number' &&
|
|
825
|
+
Number.isInteger(schemaVersion) &&
|
|
826
|
+
schemaVersion >= 0 &&
|
|
827
|
+
typeof optionsVersion === 'number' &&
|
|
828
|
+
Number.isInteger(optionsVersion) &&
|
|
829
|
+
optionsVersion >= 0 &&
|
|
830
|
+
typeof value.ctagsVersion === 'string' &&
|
|
831
|
+
typeof value.sourceFingerprint === 'string' &&
|
|
832
|
+
/^[a-f0-9]{64}$/u.test(value.sourceFingerprint) &&
|
|
833
|
+
typeof value.truncated === 'boolean' &&
|
|
834
|
+
typeof value.generation === 'string' &&
|
|
835
|
+
/^[0-9a-f-]{36}$/u.test(value.generation)
|
|
836
|
+
? {
|
|
837
|
+
schemaVersion,
|
|
838
|
+
optionsVersion,
|
|
839
|
+
ctagsVersion: value.ctagsVersion,
|
|
840
|
+
sourceFingerprint: value.sourceFingerprint,
|
|
841
|
+
...(sourceFiles === undefined ? {} : { sourceFiles }),
|
|
842
|
+
truncated: value.truncated,
|
|
843
|
+
generation: value.generation
|
|
844
|
+
}
|
|
845
|
+
: undefined;
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function validManifestSourceFiles(value) {
|
|
852
|
+
if (value === undefined)
|
|
853
|
+
return false;
|
|
854
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
855
|
+
return false;
|
|
856
|
+
const entries = Object.entries(value);
|
|
857
|
+
return (entries.length <= 100_000 &&
|
|
858
|
+
entries.every(([path, fingerprint]) => path.length > 0 &&
|
|
859
|
+
path.length <= 4096 &&
|
|
860
|
+
!path.includes('\0') &&
|
|
861
|
+
!path.includes('\\') &&
|
|
862
|
+
!path.startsWith('/') &&
|
|
863
|
+
!path.split('/').some((part) => part.length === 0 || part === '.' || part === '..') &&
|
|
864
|
+
typeof fingerprint === 'string' &&
|
|
865
|
+
/^[0-9]+:(?:-1|[0-9]+(?:\.[0-9]+)?)$/u.test(fingerprint)));
|
|
866
|
+
}
|
|
867
|
+
async function tagsPath(location, manifest) {
|
|
868
|
+
const path = resolve(location.generations, manifest.generation, 'tags');
|
|
869
|
+
if (relative(location.generations, path)
|
|
870
|
+
.split(sep)
|
|
871
|
+
.some((part) => part === '..'))
|
|
872
|
+
throw new Error('SYMBOL_INDEX_UNAVAILABLE');
|
|
873
|
+
return path;
|
|
874
|
+
}
|
|
875
|
+
async function tagsAvailable(path) {
|
|
876
|
+
try {
|
|
877
|
+
const metadata = await stat(path);
|
|
878
|
+
return metadata.isFile() && metadata.size <= MAX_TAGS_BYTES;
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function isNavigablePath(path) {
|
|
885
|
+
const dot = path.lastIndexOf('.');
|
|
886
|
+
return dot >= 0 && NAVIGABLE_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
887
|
+
}
|
|
888
|
+
function isSymbol(value) {
|
|
889
|
+
return value.length > 0 && value.length <= 512 && /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(value);
|
|
890
|
+
}
|
|
891
|
+
function isNavigableSpecifier(value) {
|
|
892
|
+
return (value.length > 0 &&
|
|
893
|
+
value.length <= 1024 &&
|
|
894
|
+
(/^(?:\.\.?\/)[A-Za-z0-9_./$-]+$/u.test(value) ||
|
|
895
|
+
/^[\p{L}_$][\p{L}\p{N}_$]*(?:\.[\p{L}_$][\p{L}\p{N}_$]*)+$/u.test(value) ||
|
|
896
|
+
/^(?:@?[A-Za-z0-9_$-]+)(?:\/[A-Za-z0-9_.$-]+)+$/u.test(value)));
|
|
897
|
+
}
|
|
898
|
+
function matchesSpecifier(path, currentPath, specifier) {
|
|
899
|
+
if (specifier.startsWith('.')) {
|
|
900
|
+
const resolved = resolveRelativeSpecifier(currentPath, specifier);
|
|
901
|
+
return (resolved !== undefined &&
|
|
902
|
+
relativeSpecifierCandidates(resolved).some((candidate) => path === candidate ||
|
|
903
|
+
path === `${candidate}/index.ts` ||
|
|
904
|
+
path === `${candidate}/index.tsx` ||
|
|
905
|
+
path === `${candidate}/index.js` ||
|
|
906
|
+
path === `${candidate}/index.jsx` ||
|
|
907
|
+
path === `${candidate}/__init__.py`));
|
|
908
|
+
}
|
|
909
|
+
if (specifier.includes('/'))
|
|
910
|
+
return sourceFileStem(path) === sourceFileStem(specifier);
|
|
911
|
+
if (specifier.includes('.')) {
|
|
912
|
+
const packagePath = specifier.replaceAll('.', '/');
|
|
913
|
+
return DOTTED_PACKAGE_SPECIFIER_HANDLERS.some((handler) => handler.packageSuffixes.some((suffix) => matchesPackagePath(path, `${packagePath}${suffix}`)));
|
|
914
|
+
}
|
|
915
|
+
return sourceFileStem(path) === sourceFileStem(specifier);
|
|
916
|
+
}
|
|
917
|
+
function matchesPackagePath(path, packagePath) {
|
|
918
|
+
return path === packagePath || path.endsWith(`/${packagePath}`);
|
|
919
|
+
}
|
|
920
|
+
function pathTargetScore(path, currentPath, specifier) {
|
|
921
|
+
if (!specifier.startsWith('.'))
|
|
922
|
+
return path.length;
|
|
923
|
+
const resolved = resolveRelativeSpecifier(currentPath, specifier);
|
|
924
|
+
if (resolved === undefined)
|
|
925
|
+
return Number.MAX_SAFE_INTEGER;
|
|
926
|
+
const candidates = relativeSpecifierCandidates(resolved);
|
|
927
|
+
const exact = candidates.indexOf(path);
|
|
928
|
+
if (exact >= 0)
|
|
929
|
+
return exact;
|
|
930
|
+
const index = candidates.findIndex((candidate) => path === `${candidate}/index.ts` ||
|
|
931
|
+
path === `${candidate}/index.tsx` ||
|
|
932
|
+
path === `${candidate}/index.js` ||
|
|
933
|
+
path === `${candidate}/index.jsx` ||
|
|
934
|
+
path === `${candidate}/__init__.py`);
|
|
935
|
+
return index >= 0 ? candidates.length + index : Number.MAX_SAFE_INTEGER;
|
|
936
|
+
}
|
|
937
|
+
function resolveRelativeSpecifier(currentPath, specifier) {
|
|
938
|
+
const resolved = posix.normalize(posix.join(posix.dirname(currentPath), specifier));
|
|
939
|
+
if (resolved.startsWith('../') || resolved === '..')
|
|
940
|
+
return undefined;
|
|
941
|
+
return resolved.replace(/^\.\//u, '');
|
|
942
|
+
}
|
|
943
|
+
function relativeSpecifierCandidates(path) {
|
|
944
|
+
const handler = RELATIVE_SOURCE_SPECIFIER_HANDLERS.find(({ importExtension }) => path.toLowerCase().endsWith(importExtension));
|
|
945
|
+
if (handler !== undefined) {
|
|
946
|
+
const sourceBase = path.slice(0, -handler.importExtension.length);
|
|
947
|
+
return [path, ...handler.sourceExtensions.map((extension) => `${sourceBase}${extension}`)];
|
|
948
|
+
}
|
|
949
|
+
return path.slice(path.lastIndexOf('/') + 1).includes('.')
|
|
950
|
+
? [path]
|
|
951
|
+
: [path, ...EXTENSIONLESS_RELATIVE_SOURCE_EXTENSIONS.map((extension) => `${path}${extension}`)];
|
|
952
|
+
}
|
|
953
|
+
function sourceFileStem(path) {
|
|
954
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
955
|
+
return name.replace(/(?:\.d)?\.[^.]+$/u, '');
|
|
956
|
+
}
|
|
957
|
+
async function firstTagOffset(handle, size, symbol) {
|
|
958
|
+
let low = 0;
|
|
959
|
+
let high = size;
|
|
960
|
+
while (low < high) {
|
|
961
|
+
const middle = Math.floor((low + high) / 2);
|
|
962
|
+
const start = await findTagLineStart(handle, middle);
|
|
963
|
+
if (start === undefined) {
|
|
964
|
+
high = middle;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const line = await readTagLine(handle, start, size);
|
|
968
|
+
if (line === undefined) {
|
|
969
|
+
high = middle;
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const tag = parseTag(line.text);
|
|
973
|
+
if (tag === undefined || compareTagNames(tag.name, symbol) < 0)
|
|
974
|
+
low = line.nextOffset;
|
|
975
|
+
else
|
|
976
|
+
high = line.start;
|
|
977
|
+
}
|
|
978
|
+
return low;
|
|
979
|
+
}
|
|
980
|
+
async function findTagLineStart(handle, offset) {
|
|
981
|
+
let cursor = offset;
|
|
982
|
+
let scanned = 0;
|
|
983
|
+
while (cursor > 0 && scanned < MAX_TAG_LINE_BYTES) {
|
|
984
|
+
const start = Math.max(0, cursor - Math.min(READ_BLOCK_BYTES, MAX_TAG_LINE_BYTES - scanned));
|
|
985
|
+
const chunk = Buffer.alloc(cursor - start);
|
|
986
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, start);
|
|
987
|
+
if (bytesRead === 0)
|
|
988
|
+
return undefined;
|
|
989
|
+
const newline = chunk.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
990
|
+
if (newline >= 0)
|
|
991
|
+
return start + newline + 1;
|
|
992
|
+
cursor = start;
|
|
993
|
+
scanned += bytesRead;
|
|
994
|
+
}
|
|
995
|
+
return cursor === 0 ? 0 : undefined;
|
|
996
|
+
}
|
|
997
|
+
async function readTagLine(handle, offset, size) {
|
|
998
|
+
if (offset >= size)
|
|
999
|
+
return undefined;
|
|
1000
|
+
let start = offset;
|
|
1001
|
+
if (start > 0) {
|
|
1002
|
+
const before = Buffer.alloc(1);
|
|
1003
|
+
await handle.read(before, 0, 1, start - 1);
|
|
1004
|
+
if (before[0] !== 0x0a) {
|
|
1005
|
+
const aligned = await findNewline(handle, start, size, MAX_TAG_LINE_BYTES);
|
|
1006
|
+
if (aligned === undefined)
|
|
1007
|
+
return undefined;
|
|
1008
|
+
start = aligned + 1;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (start >= size)
|
|
1012
|
+
return undefined;
|
|
1013
|
+
const bytes = [];
|
|
1014
|
+
let total = 0;
|
|
1015
|
+
let cursor = start;
|
|
1016
|
+
while (cursor < size && total <= MAX_TAG_LINE_BYTES) {
|
|
1017
|
+
const chunk = Buffer.alloc(Math.min(READ_BLOCK_BYTES, size - cursor));
|
|
1018
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, cursor);
|
|
1019
|
+
if (bytesRead === 0)
|
|
1020
|
+
break;
|
|
1021
|
+
const content = chunk.subarray(0, bytesRead);
|
|
1022
|
+
const newline = content.indexOf(0x0a);
|
|
1023
|
+
const part = newline >= 0 ? content.subarray(0, newline) : content;
|
|
1024
|
+
total += part.length;
|
|
1025
|
+
if (total > MAX_TAG_LINE_BYTES)
|
|
1026
|
+
return undefined;
|
|
1027
|
+
bytes.push(part);
|
|
1028
|
+
if (newline >= 0)
|
|
1029
|
+
return {
|
|
1030
|
+
start,
|
|
1031
|
+
nextOffset: cursor + newline + 1,
|
|
1032
|
+
text: Buffer.concat(bytes, total).toString('utf8')
|
|
1033
|
+
};
|
|
1034
|
+
cursor += bytesRead;
|
|
1035
|
+
}
|
|
1036
|
+
return undefined;
|
|
1037
|
+
}
|
|
1038
|
+
async function findNewline(handle, offset, size, limit) {
|
|
1039
|
+
let cursor = offset;
|
|
1040
|
+
let remaining = limit;
|
|
1041
|
+
while (cursor < size && remaining > 0) {
|
|
1042
|
+
const chunk = Buffer.alloc(Math.min(READ_BLOCK_BYTES, size - cursor, remaining));
|
|
1043
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, cursor);
|
|
1044
|
+
if (bytesRead === 0)
|
|
1045
|
+
return undefined;
|
|
1046
|
+
const newline = chunk.subarray(0, bytesRead).indexOf(0x0a);
|
|
1047
|
+
if (newline >= 0)
|
|
1048
|
+
return cursor + newline;
|
|
1049
|
+
cursor += bytesRead;
|
|
1050
|
+
remaining -= bytesRead;
|
|
1051
|
+
}
|
|
1052
|
+
return undefined;
|
|
1053
|
+
}
|
|
1054
|
+
function parseTag(text) {
|
|
1055
|
+
if (text.length === 0 ||
|
|
1056
|
+
text.startsWith('!_TAG_') ||
|
|
1057
|
+
Buffer.byteLength(text) > MAX_TAG_LINE_BYTES)
|
|
1058
|
+
return undefined;
|
|
1059
|
+
const fields = text.split('\t');
|
|
1060
|
+
if (fields.length < 3)
|
|
1061
|
+
return undefined;
|
|
1062
|
+
const [name, path, excmd, ...extensions] = fields;
|
|
1063
|
+
if (name === undefined ||
|
|
1064
|
+
path === undefined ||
|
|
1065
|
+
excmd === undefined ||
|
|
1066
|
+
name.length === 0 ||
|
|
1067
|
+
path.length === 0 ||
|
|
1068
|
+
path.startsWith('/') ||
|
|
1069
|
+
path.includes('\\') ||
|
|
1070
|
+
path.split('/').some((part) => part === '' || part === '.' || part === '..'))
|
|
1071
|
+
return undefined;
|
|
1072
|
+
const match = /^([1-9][0-9]*);?"?$/u.exec(excmd);
|
|
1073
|
+
if (match === null)
|
|
1074
|
+
return undefined;
|
|
1075
|
+
const line = Number(match[1]);
|
|
1076
|
+
if (!Number.isSafeInteger(line) || line < 1)
|
|
1077
|
+
return undefined;
|
|
1078
|
+
let kind;
|
|
1079
|
+
let scope;
|
|
1080
|
+
for (const extension of extensions) {
|
|
1081
|
+
if (extension.length === 1)
|
|
1082
|
+
kind ??= extension;
|
|
1083
|
+
else if (extension.startsWith('kind:'))
|
|
1084
|
+
kind ??= extension.slice('kind:'.length);
|
|
1085
|
+
else if (extension.startsWith('scope:') ||
|
|
1086
|
+
extension.startsWith('class:') ||
|
|
1087
|
+
extension.startsWith('namespace:'))
|
|
1088
|
+
scope ??= extension.slice(extension.indexOf(':') + 1);
|
|
1089
|
+
}
|
|
1090
|
+
return { name, path, line, kind, scope };
|
|
1091
|
+
}
|
|
1092
|
+
async function validateTarget(root, target, symbol, expectedFingerprint) {
|
|
1093
|
+
const path = resolve(root, ...target.path.split('/'));
|
|
1094
|
+
const pathMetadata = await lstat(path).catch(() => undefined);
|
|
1095
|
+
if (pathMetadata === undefined ||
|
|
1096
|
+
!pathMetadata.isFile() ||
|
|
1097
|
+
pathMetadata.isSymbolicLink() ||
|
|
1098
|
+
pathMetadata.size > MAX_SOURCE_BYTES)
|
|
1099
|
+
return undefined;
|
|
1100
|
+
const actual = await realpath(path).catch(() => undefined);
|
|
1101
|
+
if (actual === undefined ||
|
|
1102
|
+
relative(root, actual)
|
|
1103
|
+
.split(sep)
|
|
1104
|
+
.some((part) => part === '..'))
|
|
1105
|
+
return undefined;
|
|
1106
|
+
const metadata = await lstat(actual).catch(() => undefined);
|
|
1107
|
+
if (metadata === undefined ||
|
|
1108
|
+
!metadata.isFile() ||
|
|
1109
|
+
metadata.isSymbolicLink() ||
|
|
1110
|
+
metadata.size > MAX_SOURCE_BYTES)
|
|
1111
|
+
return undefined;
|
|
1112
|
+
if (expectedFingerprint !== undefined &&
|
|
1113
|
+
sourceFileFingerprint(metadata.size, metadata.mtimeMs) !== expectedFingerprint)
|
|
1114
|
+
return undefined;
|
|
1115
|
+
const line = await lineAt(actual, target.line);
|
|
1116
|
+
return line !== undefined && containsSymbol(line, symbol) ? targetPreview(line) : undefined;
|
|
1117
|
+
}
|
|
1118
|
+
async function validatePathTarget(root, path, expectedFingerprint) {
|
|
1119
|
+
const requested = resolve(root, ...path.split('/'));
|
|
1120
|
+
const requestedMetadata = await lstat(requested).catch(() => undefined);
|
|
1121
|
+
if (requestedMetadata === undefined ||
|
|
1122
|
+
!requestedMetadata.isFile() ||
|
|
1123
|
+
requestedMetadata.isSymbolicLink() ||
|
|
1124
|
+
requestedMetadata.size > MAX_SOURCE_BYTES)
|
|
1125
|
+
return undefined;
|
|
1126
|
+
const actual = await realpath(requested).catch(() => undefined);
|
|
1127
|
+
if (actual === undefined ||
|
|
1128
|
+
relative(root, actual)
|
|
1129
|
+
.split(sep)
|
|
1130
|
+
.some((part) => part === '..'))
|
|
1131
|
+
return undefined;
|
|
1132
|
+
const metadata = await lstat(actual).catch(() => undefined);
|
|
1133
|
+
if (metadata === undefined ||
|
|
1134
|
+
!metadata.isFile() ||
|
|
1135
|
+
metadata.isSymbolicLink() ||
|
|
1136
|
+
metadata.size > MAX_SOURCE_BYTES ||
|
|
1137
|
+
(expectedFingerprint !== undefined &&
|
|
1138
|
+
sourceFileFingerprint(metadata.size, metadata.mtimeMs) !== expectedFingerprint))
|
|
1139
|
+
return undefined;
|
|
1140
|
+
return lineAt(actual, 1);
|
|
1141
|
+
}
|
|
1142
|
+
function targetPreview(line) {
|
|
1143
|
+
if (line.length <= MAX_TARGET_PREVIEW_CHARACTERS)
|
|
1144
|
+
return line;
|
|
1145
|
+
const end = line.charCodeAt(MAX_TARGET_PREVIEW_CHARACTERS - 1);
|
|
1146
|
+
const safeEnd = end >= 0xd800 && end <= 0xdbff
|
|
1147
|
+
? MAX_TARGET_PREVIEW_CHARACTERS - 1
|
|
1148
|
+
: MAX_TARGET_PREVIEW_CHARACTERS;
|
|
1149
|
+
return `${line.slice(0, safeEnd)}…`;
|
|
1150
|
+
}
|
|
1151
|
+
async function lineAt(path, targetLine) {
|
|
1152
|
+
const handle = await open(path, 'r');
|
|
1153
|
+
try {
|
|
1154
|
+
let offset = 0;
|
|
1155
|
+
let line = 1;
|
|
1156
|
+
let pending = Buffer.alloc(0);
|
|
1157
|
+
while (offset < MAX_TARGET_LINE_SCAN_BYTES) {
|
|
1158
|
+
const chunk = Buffer.alloc(READ_BLOCK_BYTES);
|
|
1159
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, offset);
|
|
1160
|
+
if (bytesRead === 0)
|
|
1161
|
+
return line === targetLine ? pending.toString('utf8') : undefined;
|
|
1162
|
+
offset += bytesRead;
|
|
1163
|
+
let content = pending.length === 0
|
|
1164
|
+
? chunk.subarray(0, bytesRead)
|
|
1165
|
+
: Buffer.concat([pending, chunk.subarray(0, bytesRead)]);
|
|
1166
|
+
let newline;
|
|
1167
|
+
while ((newline = content.indexOf(0x0a)) >= 0) {
|
|
1168
|
+
if (line === targetLine)
|
|
1169
|
+
return content.subarray(0, newline).toString('utf8');
|
|
1170
|
+
line += 1;
|
|
1171
|
+
content = content.subarray(newline + 1);
|
|
1172
|
+
}
|
|
1173
|
+
pending = content;
|
|
1174
|
+
}
|
|
1175
|
+
return line === targetLine ? pending.toString('utf8') : undefined;
|
|
1176
|
+
}
|
|
1177
|
+
finally {
|
|
1178
|
+
await handle.close();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function containsSymbol(line, symbol) {
|
|
1182
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
1183
|
+
return new RegExp(`(?:^|[^\\p{L}\\p{N}_$])${escaped}(?![\\p{L}\\p{N}_$])`, 'u').test(line);
|
|
1184
|
+
}
|
|
1185
|
+
function compareTagNames(left, right) {
|
|
1186
|
+
return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
|
|
1187
|
+
}
|
|
1188
|
+
function compareTargets(left, right, currentPath) {
|
|
1189
|
+
const leftScore = targetScore(left, currentPath);
|
|
1190
|
+
const rightScore = targetScore(right, currentPath);
|
|
1191
|
+
return (rightScore - leftScore ||
|
|
1192
|
+
left.path.localeCompare(right.path) ||
|
|
1193
|
+
left.line - right.line ||
|
|
1194
|
+
(left.kind ?? '').localeCompare(right.kind ?? ''));
|
|
1195
|
+
}
|
|
1196
|
+
function targetScore(target, currentPath) {
|
|
1197
|
+
if (target.path === currentPath)
|
|
1198
|
+
return 100;
|
|
1199
|
+
const currentDirectory = currentPath.includes('/')
|
|
1200
|
+
? currentPath.slice(0, currentPath.lastIndexOf('/'))
|
|
1201
|
+
: '';
|
|
1202
|
+
const targetDirectory = target.path.includes('/')
|
|
1203
|
+
? target.path.slice(0, target.path.lastIndexOf('/'))
|
|
1204
|
+
: '';
|
|
1205
|
+
return commonPathSegments(currentDirectory, targetDirectory);
|
|
1206
|
+
}
|
|
1207
|
+
function commonPathSegments(left, right) {
|
|
1208
|
+
const leftParts = left === '' ? [] : left.split('/');
|
|
1209
|
+
const rightParts = right === '' ? [] : right.split('/');
|
|
1210
|
+
let count = 0;
|
|
1211
|
+
while (count < leftParts.length && leftParts[count] === rightParts[count])
|
|
1212
|
+
count += 1;
|
|
1213
|
+
return count;
|
|
1214
|
+
}
|
|
1215
|
+
function buildFailure(error) {
|
|
1216
|
+
return error instanceof Error && error.message === 'SYMBOL_INDEX_LIMIT_EXCEEDED'
|
|
1217
|
+
? 'SYMBOL_INDEX_LIMIT_EXCEEDED'
|
|
1218
|
+
: 'SYMBOL_INDEX_BUILD_FAILED';
|
|
1219
|
+
}
|