@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.
Files changed (35) hide show
  1. package/dist/codex-app-server.js +4 -15
  2. package/dist/connector.js +0 -18
  3. package/dist/ctags-binary.js +10 -0
  4. package/dist/runner/claude/managed-run-controller.js +1 -1
  5. package/dist/runner/claude-code-runner.js +14 -48
  6. package/dist/runner/codex/managed-run-controller.js +1 -1
  7. package/dist/runner/codex-runner.js +6 -13
  8. package/dist/runner/mar-agent-runner.js +3 -10
  9. package/dist/runner/opencode-runner.js +7 -19
  10. package/dist/service/code-navigation/c-family-path-specifier-resolver.js +40 -0
  11. package/dist/service/code-navigation/csharp-path-specifier-resolver.js +32 -0
  12. package/dist/service/code-navigation/ctags-index-storage.js +290 -0
  13. package/dist/service/code-navigation/ctags-query.js +140 -0
  14. package/dist/service/code-navigation/ecmascript-path-specifier-resolver.js +85 -0
  15. package/dist/service/code-navigation/file-reference-parser.js +28 -0
  16. package/dist/service/code-navigation/go-path-specifier-resolver.js +31 -0
  17. package/dist/service/code-navigation/jvm-path-specifier-resolver.js +28 -0
  18. package/dist/service/code-navigation/markdown-path-specifier-resolver.js +80 -0
  19. package/dist/service/code-navigation/navigation-resolution-definition.js +1 -0
  20. package/dist/service/code-navigation/navigation-resolution-registry.js +36 -0
  21. package/dist/service/code-navigation/navigation-target-validator.js +121 -0
  22. package/dist/service/code-navigation/path-specifier-resolver.js +6 -0
  23. package/dist/service/code-navigation/python-path-specifier-resolver.js +41 -0
  24. package/dist/service/code-navigation/rust-path-specifier-resolver.js +31 -0
  25. package/dist/service/code-navigation/stylesheet-path-specifier-resolver.js +55 -0
  26. package/dist/service/conversation-history-service.js +1 -1
  27. package/dist/service/external-session-resume-service.js +5 -22
  28. package/dist/service/node-control-message-service.js +1 -1
  29. package/dist/service/node-upgrade-coordinator.js +0 -1
  30. package/dist/service/session-catalog-service.js +2 -27
  31. package/dist/service/session-command-service.js +3 -3
  32. package/dist/service/session-message-service.js +3 -15
  33. package/dist/service/session-presentation-service.js +0 -1
  34. package/dist/service/workspace-code-navigation-service.js +80 -694
  35. 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, createReadStream, createWriteStream } from 'node:fs';
4
- import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
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 { posix, relative, resolve, sep } from 'node:path';
7
- import { createInterface } from 'node:readline';
8
- import { bundledCtagsPath, BUNDLED_CTAGS_VERSION } from '../ctags-binary.js';
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 (!isNavigablePath(input.path))
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 specifier = input.specifier;
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' && typeof specifier === 'string') ||
151
- (typeof symbol !== 'string' && typeof specifier !== 'string') ||
96
+ (typeof symbol === 'string' && reference !== undefined) ||
97
+ (typeof symbol !== 'string' && reference === undefined) ||
152
98
  (typeof symbol === 'string' && !isSymbol(symbol)) ||
153
- (typeof specifier === 'string' && !isNavigableSpecifier(specifier)))
99
+ (reference !== undefined && !reference.success))
154
100
  throw new Error('SYMBOL_DEFINITION_INVALID');
155
- if (typeof specifier === 'string')
156
- return this.resolvePath(workspace, path, specifier);
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(this.executable(), constants.X_OK);
202
+ await access(executable, constants.X_OK);
252
203
  return true;
253
204
  }
254
205
  catch {
255
- return false;
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
- isNavigablePath(entry.path) &&
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 validateTarget(input.root, record, symbol, input.revalidateSourceFingerprint ? input.sourceFiles[record.path] : undefined)
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, specifier) {
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, specifier);
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 resolvePathTargets(input, currentPath, specifier) {
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) => isNavigablePath(path) && matchesSpecifier(path, currentPath, specifier))
573
- .sort((left, right) => pathTargetScore(left, currentPath, specifier) -
574
- pathTargetScore(right, currentPath, specifier))
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 preview = await validatePathTarget(input.root, path, input.revalidateSourceFingerprint ? input.sourceFiles[path] : undefined);
578
- return preview === undefined
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: 1,
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 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());
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'