@myagentroam/node 0.9.96 → 0.9.98
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/codex-app-server.js +4 -15
- package/dist/connector.js +0 -18
- package/dist/ctags-binary.js +10 -0
- package/dist/runner/claude/managed-run-controller.js +1 -1
- package/dist/runner/claude-code-runner.js +14 -48
- package/dist/runner/codex/managed-run-controller.js +1 -1
- package/dist/runner/codex-runner.js +6 -13
- package/dist/runner/mar-agent-runner.js +3 -10
- package/dist/runner/opencode-runner.js +7 -19
- package/dist/service/code-navigation/c-family-path-specifier-resolver.js +40 -0
- package/dist/service/code-navigation/csharp-path-specifier-resolver.js +32 -0
- package/dist/service/code-navigation/ctags-index-storage.js +290 -0
- package/dist/service/code-navigation/ctags-query.js +140 -0
- package/dist/service/code-navigation/ecmascript-path-specifier-resolver.js +85 -0
- package/dist/service/code-navigation/file-reference-parser.js +28 -0
- package/dist/service/code-navigation/go-path-specifier-resolver.js +31 -0
- package/dist/service/code-navigation/jvm-path-specifier-resolver.js +28 -0
- package/dist/service/code-navigation/markdown-path-specifier-resolver.js +80 -0
- package/dist/service/code-navigation/navigation-resolution-definition.js +1 -0
- package/dist/service/code-navigation/navigation-resolution-registry.js +36 -0
- package/dist/service/code-navigation/navigation-target-validator.js +121 -0
- package/dist/service/code-navigation/path-specifier-resolver.js +6 -0
- package/dist/service/code-navigation/python-path-specifier-resolver.js +41 -0
- package/dist/service/code-navigation/rust-path-specifier-resolver.js +31 -0
- package/dist/service/code-navigation/stylesheet-path-specifier-resolver.js +55 -0
- package/dist/service/conversation-history-service.js +1 -1
- package/dist/service/external-session-resume-service.js +5 -22
- package/dist/service/node-control-message-service.js +1 -1
- package/dist/service/node-upgrade-coordinator.js +0 -1
- package/dist/service/session-catalog-service.js +2 -27
- package/dist/service/session-command-service.js +3 -3
- package/dist/service/session-message-service.js +3 -15
- package/dist/service/session-presentation-service.js +0 -1
- package/dist/service/workspace-code-navigation-service.js +80 -694
- package/package.json +4 -3
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { constants
|
|
4
|
-
import { access,
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, open, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
4
|
import { homedir } from 'node:os';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { supportsSymbolNavigationPath } from '@myagentroam/code-navigation/symbol-navigation';
|
|
7
|
+
import { workspaceFileReferenceSchema } from '@myagentroam/protocol';
|
|
8
|
+
import { bundledCtagsPath, BUNDLED_CTAGS_VERSION, repairBundledCtagsExecutable } from '../ctags-binary.js';
|
|
9
9
|
import { nodeLog } from '../operational.js';
|
|
10
10
|
import { normalizeWorkspacePath } from '../workspace.js';
|
|
11
|
+
import { pathSpecifierResolver } from './code-navigation/navigation-resolution-registry.js';
|
|
12
|
+
import { reparseFileReference } from './code-navigation/file-reference-parser.js';
|
|
13
|
+
import { acquireBuild, buildIncrementalTags, ensurePrivateDirectory, manifestEntry, readManifest, removeInactiveGenerations, runCtags, sourceDelta, sourceFingerprint, tagsAvailable, tagsPath } from './code-navigation/ctags-index-storage.js';
|
|
14
|
+
import { compareTagNames, firstTagOffset, parseTag, readTagLine } from './code-navigation/ctags-query.js';
|
|
15
|
+
import { compareTargets, containsSymbol, sourceFileFingerprint, targetPreview, validatePathTarget, validateSymbolTarget } from './code-navigation/navigation-target-validator.js';
|
|
11
16
|
const INDEX_SCHEMA_VERSION = 1;
|
|
12
17
|
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
18
|
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
17
19
|
const MAX_INDEX_BYTES = 8 * 1024 * 1024 * 1024;
|
|
18
20
|
const MAX_TAGS_BYTES = 512 * 1024 * 1024;
|
|
@@ -23,64 +25,6 @@ const MAX_TAG_LINE_BYTES = 16 * 1024;
|
|
|
23
25
|
const READ_BLOCK_BYTES = 4096;
|
|
24
26
|
const MAX_TARGET_LINE_SCAN_BYTES = MAX_SOURCE_BYTES;
|
|
25
27
|
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
28
|
/**
|
|
85
29
|
* Builds a demand-driven, standard u-ctags declaration index. It never keeps
|
|
86
30
|
* a complete symbol table, watcher, or idle child process in memory.
|
|
@@ -100,7 +44,7 @@ export class WorkspaceCodeNavigationService {
|
|
|
100
44
|
async prepare(workspace, input) {
|
|
101
45
|
if (typeof input.path !== 'string' || input.path.length === 0 || input.path.length > 4096)
|
|
102
46
|
throw new Error('SYMBOL_DEFINITION_INVALID');
|
|
103
|
-
if (!
|
|
47
|
+
if (!isCtagsSourcePath(input.path))
|
|
104
48
|
return { state: 'READY' };
|
|
105
49
|
if (!(await this.available()))
|
|
106
50
|
return { state: 'UNAVAILABLE' };
|
|
@@ -143,17 +87,23 @@ export class WorkspaceCodeNavigationService {
|
|
|
143
87
|
async definition(workspace, input) {
|
|
144
88
|
const path = input.path;
|
|
145
89
|
const symbol = input.symbol;
|
|
146
|
-
const
|
|
90
|
+
const reference = input.reference === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: workspaceFileReferenceSchema.safeParse(input.reference);
|
|
147
93
|
if (typeof path !== 'string' ||
|
|
148
94
|
path.length === 0 ||
|
|
149
95
|
path.length > 4096 ||
|
|
150
|
-
(typeof symbol === 'string' &&
|
|
151
|
-
(typeof symbol !== 'string' &&
|
|
96
|
+
(typeof symbol === 'string' && reference !== undefined) ||
|
|
97
|
+
(typeof symbol !== 'string' && reference === undefined) ||
|
|
152
98
|
(typeof symbol === 'string' && !isSymbol(symbol)) ||
|
|
153
|
-
(
|
|
99
|
+
(reference !== undefined && !reference.success))
|
|
154
100
|
throw new Error('SYMBOL_DEFINITION_INVALID');
|
|
155
|
-
if (
|
|
156
|
-
|
|
101
|
+
if (reference !== undefined) {
|
|
102
|
+
const parsedReference = reparseFileReference(path, reference.data);
|
|
103
|
+
if (parsedReference === undefined)
|
|
104
|
+
return emptyDefinitionResult({ state: 'READY' });
|
|
105
|
+
return this.resolvePath(workspace, path, parsedReference);
|
|
106
|
+
}
|
|
157
107
|
const requestedSymbol = symbol;
|
|
158
108
|
if (!(await this.available()))
|
|
159
109
|
return emptyDefinitionResult({ state: 'UNAVAILABLE' });
|
|
@@ -247,12 +197,22 @@ export class WorkspaceCodeNavigationService {
|
|
|
247
197
|
this.forceFreshCandidates.set(workspaceId, (this.forceFreshCandidates.get(workspaceId) ?? 0) + 1);
|
|
248
198
|
}
|
|
249
199
|
async available() {
|
|
200
|
+
const executable = this.executable();
|
|
250
201
|
try {
|
|
251
|
-
await access(
|
|
202
|
+
await access(executable, constants.X_OK);
|
|
252
203
|
return true;
|
|
253
204
|
}
|
|
254
205
|
catch {
|
|
255
|
-
|
|
206
|
+
if (this.options.ctagsExecutable !== undefined)
|
|
207
|
+
return false;
|
|
208
|
+
try {
|
|
209
|
+
await repairBundledCtagsExecutable(this.options.platform ?? process.platform, this.options.architecture ?? process.arch);
|
|
210
|
+
await access(executable, constants.X_OK);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
256
216
|
}
|
|
257
217
|
}
|
|
258
218
|
executable() {
|
|
@@ -276,7 +236,7 @@ export class WorkspaceCodeNavigationService {
|
|
|
276
236
|
const entries = candidates
|
|
277
237
|
.filter((entry) => entry.size !== null &&
|
|
278
238
|
entry.size <= MAX_SOURCE_BYTES &&
|
|
279
|
-
|
|
239
|
+
isCtagsSourcePath(entry.path) &&
|
|
280
240
|
!entry.path.includes('\n') &&
|
|
281
241
|
!entry.path.includes('\r'))
|
|
282
242
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
@@ -494,7 +454,7 @@ export class WorkspaceCodeNavigationService {
|
|
|
494
454
|
}
|
|
495
455
|
const valid = await Promise.all(records.map(async (record) => ({
|
|
496
456
|
record,
|
|
497
|
-
preview: await
|
|
457
|
+
preview: await validateSymbolTarget(input.root, record, symbol, input.revalidateSourceFingerprint ? input.sourceFiles[record.path] : undefined)
|
|
498
458
|
})));
|
|
499
459
|
return valid
|
|
500
460
|
.flatMap(({ record, preview }) => (preview === undefined ? [] : [{ ...record, preview }]))
|
|
@@ -525,7 +485,17 @@ export class WorkspaceCodeNavigationService {
|
|
|
525
485
|
return { matches: [], truncated: false };
|
|
526
486
|
}
|
|
527
487
|
}
|
|
528
|
-
async resolvePath(workspace, currentPath,
|
|
488
|
+
async resolvePath(workspace, currentPath, reference) {
|
|
489
|
+
const direct = await this.resolveDirectPathTargets(workspace, currentPath, reference);
|
|
490
|
+
if (direct.length > 0)
|
|
491
|
+
return {
|
|
492
|
+
state: 'READY',
|
|
493
|
+
targets: [],
|
|
494
|
+
references: [],
|
|
495
|
+
referenceTruncated: false,
|
|
496
|
+
paths: direct,
|
|
497
|
+
truncated: false
|
|
498
|
+
};
|
|
529
499
|
const location = await this.indexLocation(workspace);
|
|
530
500
|
this.locations.set(workspace.id, location);
|
|
531
501
|
this.forgotten.delete(location.directory);
|
|
@@ -556,7 +526,7 @@ export class WorkspaceCodeNavigationService {
|
|
|
556
526
|
mustBuild: !current || forceRefreshGeneration !== undefined,
|
|
557
527
|
forceRefreshGeneration
|
|
558
528
|
});
|
|
559
|
-
const resolved = await this.resolvePathTargets(input, currentPath,
|
|
529
|
+
const resolved = await this.resolvePathTargets(input, currentPath, reference);
|
|
560
530
|
return {
|
|
561
531
|
state: stale ? 'STALE' : 'READY',
|
|
562
532
|
targets: [],
|
|
@@ -566,22 +536,42 @@ export class WorkspaceCodeNavigationService {
|
|
|
566
536
|
truncated: input.truncated || resolved.truncated
|
|
567
537
|
};
|
|
568
538
|
}
|
|
569
|
-
async
|
|
539
|
+
async resolveDirectPathTargets(workspace, currentPath, reference) {
|
|
540
|
+
const handler = pathSpecifierResolver(reference.languageId, reference.value);
|
|
541
|
+
const candidates = handler?.directCandidates?.(currentPath, reference.value) ?? [];
|
|
542
|
+
const targets = await Promise.all(candidates.slice(0, MAX_RESULTS).map(async (path) => {
|
|
543
|
+
const target = await validatePathTarget(workspace.path, path, undefined, reference.anchor, handler?.anchorLine);
|
|
544
|
+
return target === undefined
|
|
545
|
+
? undefined
|
|
546
|
+
: {
|
|
547
|
+
path,
|
|
548
|
+
line: target.line,
|
|
549
|
+
kind: 'file',
|
|
550
|
+
...(target.preview.length === 0 ? {} : { preview: targetPreview(target.preview) }),
|
|
551
|
+
scope: undefined
|
|
552
|
+
};
|
|
553
|
+
}));
|
|
554
|
+
return targets.flatMap((target) => (target === undefined ? [] : [target]));
|
|
555
|
+
}
|
|
556
|
+
async resolvePathTargets(input, currentPath, reference) {
|
|
557
|
+
const handler = pathSpecifierResolver(reference.languageId, reference.value);
|
|
558
|
+
if (handler === undefined)
|
|
559
|
+
return { targets: [], truncated: false };
|
|
570
560
|
const targets = input.entries
|
|
571
561
|
.map((entry) => entry.path)
|
|
572
|
-
.filter((path) =>
|
|
573
|
-
.sort((left, right) =>
|
|
574
|
-
|
|
562
|
+
.filter((path) => handler.matches(path, currentPath, reference.value))
|
|
563
|
+
.sort((left, right) => handler.score(left, currentPath, reference.value) -
|
|
564
|
+
handler.score(right, currentPath, reference.value))
|
|
575
565
|
.slice(0, MAX_RESULTS);
|
|
576
566
|
const resolved = await Promise.all(targets.map(async (path) => {
|
|
577
|
-
const
|
|
578
|
-
return
|
|
567
|
+
const target = await validatePathTarget(input.root, path, input.revalidateSourceFingerprint ? input.sourceFiles[path] : undefined, reference.anchor, handler.anchorLine);
|
|
568
|
+
return target === undefined
|
|
579
569
|
? undefined
|
|
580
570
|
: {
|
|
581
571
|
path,
|
|
582
|
-
line:
|
|
572
|
+
line: target.line,
|
|
583
573
|
kind: 'file',
|
|
584
|
-
...(preview.length === 0 ? {} : { preview: targetPreview(preview) }),
|
|
574
|
+
...(target.preview.length === 0 ? {} : { preview: targetPreview(target.preview) }),
|
|
585
575
|
scope: undefined
|
|
586
576
|
};
|
|
587
577
|
}));
|
|
@@ -602,616 +592,12 @@ function emptyDefinitionResult(input) {
|
|
|
602
592
|
...(input.failureCode === undefined ? {} : { failureCode: input.failureCode })
|
|
603
593
|
};
|
|
604
594
|
}
|
|
605
|
-
function
|
|
606
|
-
|
|
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());
|
|
595
|
+
function isCtagsSourcePath(path) {
|
|
596
|
+
return supportsSymbolNavigationPath(path);
|
|
887
597
|
}
|
|
888
598
|
function isSymbol(value) {
|
|
889
599
|
return value.length > 0 && value.length <= 512 && /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(value);
|
|
890
600
|
}
|
|
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
601
|
function buildFailure(error) {
|
|
1216
602
|
return error instanceof Error && error.message === 'SYMBOL_INDEX_LIMIT_EXCEEDED'
|
|
1217
603
|
? 'SYMBOL_INDEX_LIMIT_EXCEEDED'
|