@myagentroam/node 0.9.97 → 0.9.99
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/ctags-binary.js +10 -0
- 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 +100 -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 +26 -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 +61 -0
- package/dist/service/workspace-code-navigation-service.js +82 -698
- package/package.json +4 -3
package/dist/ctags-binary.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chmod } from 'node:fs/promises';
|
|
1
2
|
import { dirname, resolve } from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
4
|
export const BUNDLED_CTAGS_VERSION = '6.2.1';
|
|
@@ -16,3 +17,12 @@ export function bundledCtagsPath(platform = process.platform, architecture = pro
|
|
|
16
17
|
throw new Error('CTAGS_PLATFORM_UNSUPPORTED');
|
|
17
18
|
return resolve(bundledCtagsDirectory(packageRoot), ...target);
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Restores the execute bit when an installation mechanism lost the bundled
|
|
22
|
+
* Linux binary's archive mode. Explicit Ctags overrides are never modified.
|
|
23
|
+
*/
|
|
24
|
+
export async function repairBundledCtagsExecutable(platform = process.platform, architecture = process.arch, packageRoot) {
|
|
25
|
+
if (platform !== 'linux')
|
|
26
|
+
return;
|
|
27
|
+
await chmod(bundledCtagsPath(platform, architecture, packageRoot), 0o755);
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { matchesPathSuffix } from './path-specifier-resolver.js';
|
|
3
|
+
function accepts(value) {
|
|
4
|
+
return /^(?:\.\.?\/)?(?:[A-Za-z0-9_$+.-]+\/)*[A-Za-z0-9_$+.-]+$/u.test(value);
|
|
5
|
+
}
|
|
6
|
+
function matches(path, currentPath, specifier) {
|
|
7
|
+
return candidates(currentPath, specifier).some((candidate) => path === candidate || matchesPathSuffix(path, candidate));
|
|
8
|
+
}
|
|
9
|
+
function score(path, currentPath, specifier) {
|
|
10
|
+
const resolvedCandidates = candidates(currentPath, specifier);
|
|
11
|
+
const exact = resolvedCandidates.indexOf(path);
|
|
12
|
+
if (exact >= 0)
|
|
13
|
+
return exact;
|
|
14
|
+
const suffix = resolvedCandidates.findIndex((candidate) => matchesPathSuffix(path, candidate));
|
|
15
|
+
return suffix >= 0 ? resolvedCandidates.length + suffix + path.length : Number.MAX_SAFE_INTEGER;
|
|
16
|
+
}
|
|
17
|
+
function candidates(currentPath, specifier) {
|
|
18
|
+
const relative = posix.normalize(posix.join(posix.dirname(currentPath), specifier));
|
|
19
|
+
if (relative.startsWith('../') || relative === '..' || relative === specifier)
|
|
20
|
+
return [specifier];
|
|
21
|
+
return [relative.replace(/^\.\//u, ''), specifier];
|
|
22
|
+
}
|
|
23
|
+
export const cFamilyPathSpecifierResolver = {
|
|
24
|
+
id: 'c-family.path-specifier',
|
|
25
|
+
accepts,
|
|
26
|
+
directCandidates: candidates,
|
|
27
|
+
matches,
|
|
28
|
+
score
|
|
29
|
+
};
|
|
30
|
+
function createCFamilyNavigationResolutionDefinition(languageId) {
|
|
31
|
+
return {
|
|
32
|
+
languageId,
|
|
33
|
+
resolver: { ...cFamilyPathSpecifierResolver, id: `${languageId}.path-specifier` }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export const cNavigationResolutionDefinition = createCFamilyNavigationResolutionDefinition('c');
|
|
37
|
+
export const cppNavigationResolutionDefinition = createCFamilyNavigationResolutionDefinition('cpp');
|
|
38
|
+
export const objectiveCNavigationResolutionDefinition = createCFamilyNavigationResolutionDefinition('objective-c');
|
|
39
|
+
export const objectiveCppNavigationResolutionDefinition = createCFamilyNavigationResolutionDefinition('objective-cpp');
|
|
40
|
+
export const cFamilyNavigationResolutionDefinition = createCFamilyNavigationResolutionDefinition('c-family');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { matchesPathSuffix } from './path-specifier-resolver.js';
|
|
2
|
+
function accepts(value) {
|
|
3
|
+
return /^[\p{L}_\p{Nl}][\p{L}\p{Nl}\p{Nd}\p{Pc}\p{Mn}\p{Mc}]*(?:\.[\p{L}_\p{Nl}][\p{L}\p{Nl}\p{Nd}\p{Pc}\p{Mn}\p{Mc}]*)*$/u.test(value);
|
|
4
|
+
}
|
|
5
|
+
function matches(path, _currentPath, specifier) {
|
|
6
|
+
if (!path.endsWith('.cs'))
|
|
7
|
+
return false;
|
|
8
|
+
return namespaceSuffixes(specifier).some((suffix) => matchesPathSuffix(path, `${suffix}.cs`) ||
|
|
9
|
+
path.startsWith(`${suffix}/`) ||
|
|
10
|
+
path.includes(`/${suffix}/`));
|
|
11
|
+
}
|
|
12
|
+
function score(path, _currentPath, specifier) {
|
|
13
|
+
const suffixes = namespaceSuffixes(specifier);
|
|
14
|
+
const index = suffixes.findIndex((suffix) => matchesPathSuffix(path, `${suffix}.cs`) ||
|
|
15
|
+
path.startsWith(`${suffix}/`) ||
|
|
16
|
+
path.includes(`/${suffix}/`));
|
|
17
|
+
return index < 0 ? Number.MAX_SAFE_INTEGER : index * 10_000 + path.length;
|
|
18
|
+
}
|
|
19
|
+
function namespaceSuffixes(specifier) {
|
|
20
|
+
const segments = specifier.split('.');
|
|
21
|
+
return segments.map((_, index) => segments.slice(index).join('/'));
|
|
22
|
+
}
|
|
23
|
+
export const csharpPathSpecifierResolver = {
|
|
24
|
+
id: 'csharp.path-specifier',
|
|
25
|
+
accepts,
|
|
26
|
+
matches,
|
|
27
|
+
score
|
|
28
|
+
};
|
|
29
|
+
export const csharpNavigationResolutionDefinition = {
|
|
30
|
+
languageId: 'csharp',
|
|
31
|
+
resolver: csharpPathSpecifierResolver
|
|
32
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { compareTagNames } from './ctags-query.js';
|
|
8
|
+
import { sourceFileFingerprint } from './navigation-target-validator.js';
|
|
9
|
+
const BUILD_TIMEOUT_MS = 120_000;
|
|
10
|
+
const BUILD_QUEUE_LIMIT = 4;
|
|
11
|
+
const BUILD_QUEUE_TIMEOUT_MS = 120_000;
|
|
12
|
+
const MAX_TAGS_BYTES = 512 * 1024 * 1024;
|
|
13
|
+
let activeBuilds = 0;
|
|
14
|
+
const buildWaiters = [];
|
|
15
|
+
export function sourceDelta(previous, current) {
|
|
16
|
+
const changed = Object.entries(current)
|
|
17
|
+
.filter(([path, fingerprint]) => previous[path] !== fingerprint)
|
|
18
|
+
.map(([path]) => path);
|
|
19
|
+
const removed = Object.keys(previous).filter((path) => current[path] === undefined);
|
|
20
|
+
return { changed, removed };
|
|
21
|
+
}
|
|
22
|
+
export function manifestEntry(path, fingerprint) {
|
|
23
|
+
const match = /^([0-9]+):(-1|[0-9]+(?:\.[0-9]+)?)$/u.exec(fingerprint);
|
|
24
|
+
if (match === null)
|
|
25
|
+
return undefined;
|
|
26
|
+
const size = Number(match[1]);
|
|
27
|
+
const modifiedAt = match[2] === '-1' ? null : Number(match[2]);
|
|
28
|
+
if (!Number.isSafeInteger(size) ||
|
|
29
|
+
size < 0 ||
|
|
30
|
+
(modifiedAt !== null && !Number.isFinite(modifiedAt)))
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
path,
|
|
34
|
+
name: path.slice(path.lastIndexOf('/') + 1),
|
|
35
|
+
kind: 'FILE',
|
|
36
|
+
size,
|
|
37
|
+
modifiedAt
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function sourceFingerprint(entries) {
|
|
41
|
+
const hash = createHash('sha256');
|
|
42
|
+
for (const entry of entries)
|
|
43
|
+
hash.update(`${entry.path}\0${sourceFileFingerprint(entry.size ?? -1, entry.modifiedAt)}\0`, 'utf8');
|
|
44
|
+
return hash.digest('hex');
|
|
45
|
+
}
|
|
46
|
+
export async function buildIncrementalTags(executable, root, previousTags, delta, staging, destination) {
|
|
47
|
+
const changedTags = resolve(staging, 'changed.tags');
|
|
48
|
+
if (delta.changed.length > 0) {
|
|
49
|
+
const list = resolve(staging, 'changed.files.list');
|
|
50
|
+
await writeFile(list, delta.changed.join('\n'), { encoding: 'utf8', mode: 0o600 });
|
|
51
|
+
const release = await acquireBuild();
|
|
52
|
+
try {
|
|
53
|
+
const ctagsMs = await runCtags(executable, root, list, changedTags);
|
|
54
|
+
await mergeTags(previousTags, changedTags, new Set([...delta.changed, ...delta.removed]), destination);
|
|
55
|
+
return ctagsMs;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
release();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
await writeFile(changedTags, '', { encoding: 'utf8', mode: 0o600 });
|
|
63
|
+
await mergeTags(previousTags, changedTags, new Set([...delta.changed, ...delta.removed]), destination);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
async function mergeTags(previous, changed, replacedPaths, destination) {
|
|
67
|
+
const output = createWriteStream(destination, { encoding: 'utf8', mode: 0o600 });
|
|
68
|
+
const oldLines = tagLines(previous)[Symbol.asyncIterator]();
|
|
69
|
+
const changedLines = tagLines(changed)[Symbol.asyncIterator]();
|
|
70
|
+
const write = async (line) => {
|
|
71
|
+
if (!output.write(`${line}\n`))
|
|
72
|
+
await new Promise((resolvePromise) => output.once('drain', resolvePromise));
|
|
73
|
+
};
|
|
74
|
+
let old = await oldLines.next();
|
|
75
|
+
while (!old.done && old.value.startsWith('!_TAG_')) {
|
|
76
|
+
await write(old.value);
|
|
77
|
+
old = await oldLines.next();
|
|
78
|
+
}
|
|
79
|
+
let nextChanged = await nextDataTag(changedLines);
|
|
80
|
+
while (!old.done || nextChanged !== undefined) {
|
|
81
|
+
while (!old.done && tagPath(old.value) !== undefined && replacedPaths.has(tagPath(old.value)))
|
|
82
|
+
old = await oldLines.next();
|
|
83
|
+
if (old.done) {
|
|
84
|
+
await write(nextChanged);
|
|
85
|
+
nextChanged = await nextDataTag(changedLines);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (nextChanged === undefined || compareTagLines(old.value, nextChanged) <= 0) {
|
|
89
|
+
await write(old.value);
|
|
90
|
+
old = await oldLines.next();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await write(nextChanged);
|
|
94
|
+
nextChanged = await nextDataTag(changedLines);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await new Promise((resolvePromise, reject) => {
|
|
98
|
+
output.once('error', reject);
|
|
99
|
+
output.end(resolvePromise);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function* tagLines(path) {
|
|
103
|
+
const input = createInterface({
|
|
104
|
+
input: createReadStream(path, { encoding: 'utf8' }),
|
|
105
|
+
crlfDelay: Infinity
|
|
106
|
+
});
|
|
107
|
+
for await (const line of input)
|
|
108
|
+
yield line;
|
|
109
|
+
}
|
|
110
|
+
async function nextDataTag(iterator) {
|
|
111
|
+
for (;;) {
|
|
112
|
+
const next = await iterator.next();
|
|
113
|
+
if (next.done)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (!next.value.startsWith('!_TAG_'))
|
|
116
|
+
return next.value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function tagPath(line) {
|
|
120
|
+
const second = line.indexOf('\t');
|
|
121
|
+
if (second < 0)
|
|
122
|
+
return undefined;
|
|
123
|
+
const third = line.indexOf('\t', second + 1);
|
|
124
|
+
return third < 0 ? undefined : line.slice(second + 1, third);
|
|
125
|
+
}
|
|
126
|
+
function compareTagLines(left, right) {
|
|
127
|
+
const leftName = left.slice(0, left.indexOf('\t'));
|
|
128
|
+
const rightName = right.slice(0, right.indexOf('\t'));
|
|
129
|
+
return (compareTagNames(leftName, rightName) || Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
130
|
+
}
|
|
131
|
+
export async function acquireBuild() {
|
|
132
|
+
if (activeBuilds >= 1) {
|
|
133
|
+
if (buildWaiters.length >= BUILD_QUEUE_LIMIT)
|
|
134
|
+
throw new Error('SYMBOL_INDEX_BUILD_FAILED');
|
|
135
|
+
await new Promise((resolvePromise, reject) => {
|
|
136
|
+
const resume = () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
resolvePromise();
|
|
139
|
+
};
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
const index = buildWaiters.indexOf(resume);
|
|
142
|
+
if (index >= 0)
|
|
143
|
+
buildWaiters.splice(index, 1);
|
|
144
|
+
reject(new Error('SYMBOL_INDEX_BUILD_FAILED'));
|
|
145
|
+
}, BUILD_QUEUE_TIMEOUT_MS);
|
|
146
|
+
timer.unref();
|
|
147
|
+
buildWaiters.push(resume);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
activeBuilds += 1;
|
|
151
|
+
return () => {
|
|
152
|
+
activeBuilds -= 1;
|
|
153
|
+
buildWaiters.shift()?.();
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export async function runCtags(executable, cwd, fileList, destination) {
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
await new Promise((resolvePromise, reject) => {
|
|
159
|
+
const child = spawn(executable, [
|
|
160
|
+
'--options=NONE',
|
|
161
|
+
'--output-format=u-ctags',
|
|
162
|
+
'--excmd=number',
|
|
163
|
+
'--sort=yes',
|
|
164
|
+
'--tag-relative=no',
|
|
165
|
+
'--pseudo-tags=-TAG_PROC_CWD',
|
|
166
|
+
'--fields=+nKS',
|
|
167
|
+
'--extras=+q',
|
|
168
|
+
'-L',
|
|
169
|
+
fileList,
|
|
170
|
+
'-f',
|
|
171
|
+
destination
|
|
172
|
+
], {
|
|
173
|
+
cwd,
|
|
174
|
+
detached: process.platform !== 'win32',
|
|
175
|
+
windowsHide: true,
|
|
176
|
+
stdio: ['ignore', 'ignore', 'pipe']
|
|
177
|
+
});
|
|
178
|
+
let settled = false;
|
|
179
|
+
const settle = (callback) => {
|
|
180
|
+
if (settled)
|
|
181
|
+
return;
|
|
182
|
+
settled = true;
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
callback();
|
|
185
|
+
};
|
|
186
|
+
const terminate = () => {
|
|
187
|
+
if (child.pid !== undefined && process.platform !== 'win32') {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Fall through to the direct process kill.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
child.kill('SIGKILL');
|
|
197
|
+
};
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
terminate();
|
|
200
|
+
settle(() => reject(new Error('SYMBOL_INDEX_BUILD_FAILED')));
|
|
201
|
+
}, BUILD_TIMEOUT_MS);
|
|
202
|
+
timer.unref();
|
|
203
|
+
child.stderr.on('data', () => undefined);
|
|
204
|
+
child.once('error', () => settle(() => reject(new Error('SYMBOL_INDEX_BUILD_FAILED'))));
|
|
205
|
+
child.once('exit', (code) => settle(() => (code === 0 ? resolvePromise() : reject(new Error('SYMBOL_INDEX_BUILD_FAILED')))));
|
|
206
|
+
});
|
|
207
|
+
return Date.now() - startedAt;
|
|
208
|
+
}
|
|
209
|
+
export async function ensurePrivateDirectory(path) {
|
|
210
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
211
|
+
const metadata = await lstat(path);
|
|
212
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
213
|
+
throw new Error('SYMBOL_INDEX_UNAVAILABLE');
|
|
214
|
+
}
|
|
215
|
+
export async function removeInactiveGenerations(directory, activeGeneration) {
|
|
216
|
+
const generations = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
217
|
+
await Promise.all(generations
|
|
218
|
+
.filter((entry) => entry.isDirectory() &&
|
|
219
|
+
entry.name !== activeGeneration &&
|
|
220
|
+
/^[0-9a-f-]{36}(?:\.staging)?$/u.test(entry.name))
|
|
221
|
+
.map((entry) => rm(resolve(directory, entry.name), { recursive: true, force: true })));
|
|
222
|
+
}
|
|
223
|
+
export async function readManifest(path) {
|
|
224
|
+
try {
|
|
225
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
226
|
+
const sourceFiles = validManifestSourceFiles(value.sourceFiles) ? value.sourceFiles : undefined;
|
|
227
|
+
const schemaVersion = value.schemaVersion;
|
|
228
|
+
const optionsVersion = value.optionsVersion;
|
|
229
|
+
return typeof value === 'object' &&
|
|
230
|
+
value !== null &&
|
|
231
|
+
typeof schemaVersion === 'number' &&
|
|
232
|
+
Number.isInteger(schemaVersion) &&
|
|
233
|
+
schemaVersion >= 0 &&
|
|
234
|
+
typeof optionsVersion === 'number' &&
|
|
235
|
+
Number.isInteger(optionsVersion) &&
|
|
236
|
+
optionsVersion >= 0 &&
|
|
237
|
+
typeof value.ctagsVersion === 'string' &&
|
|
238
|
+
typeof value.sourceFingerprint === 'string' &&
|
|
239
|
+
/^[a-f0-9]{64}$/u.test(value.sourceFingerprint) &&
|
|
240
|
+
typeof value.truncated === 'boolean' &&
|
|
241
|
+
typeof value.generation === 'string' &&
|
|
242
|
+
/^[0-9a-f-]{36}$/u.test(value.generation)
|
|
243
|
+
? {
|
|
244
|
+
schemaVersion,
|
|
245
|
+
optionsVersion,
|
|
246
|
+
ctagsVersion: value.ctagsVersion,
|
|
247
|
+
sourceFingerprint: value.sourceFingerprint,
|
|
248
|
+
...(sourceFiles === undefined ? {} : { sourceFiles }),
|
|
249
|
+
truncated: value.truncated,
|
|
250
|
+
generation: value.generation
|
|
251
|
+
}
|
|
252
|
+
: undefined;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function validManifestSourceFiles(value) {
|
|
259
|
+
if (value === undefined)
|
|
260
|
+
return false;
|
|
261
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
262
|
+
return false;
|
|
263
|
+
const entries = Object.entries(value);
|
|
264
|
+
return (entries.length <= 100_000 &&
|
|
265
|
+
entries.every(([path, fingerprint]) => path.length > 0 &&
|
|
266
|
+
path.length <= 4096 &&
|
|
267
|
+
!path.includes('\0') &&
|
|
268
|
+
!path.includes('\\') &&
|
|
269
|
+
!path.startsWith('/') &&
|
|
270
|
+
!path.split('/').some((part) => part.length === 0 || part === '.' || part === '..') &&
|
|
271
|
+
typeof fingerprint === 'string' &&
|
|
272
|
+
/^[0-9]+:(?:-1|[0-9]+(?:\.[0-9]+)?)$/u.test(fingerprint)));
|
|
273
|
+
}
|
|
274
|
+
export async function tagsPath(location, manifest) {
|
|
275
|
+
const path = resolve(location.generations, manifest.generation, 'tags');
|
|
276
|
+
if (relative(location.generations, path)
|
|
277
|
+
.split(sep)
|
|
278
|
+
.some((part) => part === '..'))
|
|
279
|
+
throw new Error('SYMBOL_INDEX_UNAVAILABLE');
|
|
280
|
+
return path;
|
|
281
|
+
}
|
|
282
|
+
export async function tagsAvailable(path) {
|
|
283
|
+
try {
|
|
284
|
+
const metadata = await stat(path);
|
|
285
|
+
return metadata.isFile() && metadata.size <= MAX_TAGS_BYTES;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const MAX_TAG_LINE_BYTES = 16 * 1024;
|
|
2
|
+
const READ_BLOCK_BYTES = 4096;
|
|
3
|
+
export async function firstTagOffset(handle, size, symbol) {
|
|
4
|
+
let low = 0;
|
|
5
|
+
let high = size;
|
|
6
|
+
while (low < high) {
|
|
7
|
+
const middle = Math.floor((low + high) / 2);
|
|
8
|
+
const start = await findTagLineStart(handle, middle);
|
|
9
|
+
if (start === undefined) {
|
|
10
|
+
high = middle;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const line = await readTagLine(handle, start, size);
|
|
14
|
+
if (line === undefined) {
|
|
15
|
+
high = middle;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const tag = parseTag(line.text);
|
|
19
|
+
if (tag === undefined || compareTagNames(tag.name, symbol) < 0)
|
|
20
|
+
low = line.nextOffset;
|
|
21
|
+
else
|
|
22
|
+
high = line.start;
|
|
23
|
+
}
|
|
24
|
+
return low;
|
|
25
|
+
}
|
|
26
|
+
async function findTagLineStart(handle, offset) {
|
|
27
|
+
let cursor = offset;
|
|
28
|
+
let scanned = 0;
|
|
29
|
+
while (cursor > 0 && scanned < MAX_TAG_LINE_BYTES) {
|
|
30
|
+
const start = Math.max(0, cursor - Math.min(READ_BLOCK_BYTES, MAX_TAG_LINE_BYTES - scanned));
|
|
31
|
+
const chunk = Buffer.alloc(cursor - start);
|
|
32
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, start);
|
|
33
|
+
if (bytesRead === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
const newline = chunk.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
36
|
+
if (newline >= 0)
|
|
37
|
+
return start + newline + 1;
|
|
38
|
+
cursor = start;
|
|
39
|
+
scanned += bytesRead;
|
|
40
|
+
}
|
|
41
|
+
return cursor === 0 ? 0 : undefined;
|
|
42
|
+
}
|
|
43
|
+
export async function readTagLine(handle, offset, size) {
|
|
44
|
+
if (offset >= size)
|
|
45
|
+
return undefined;
|
|
46
|
+
let start = offset;
|
|
47
|
+
if (start > 0) {
|
|
48
|
+
const before = Buffer.alloc(1);
|
|
49
|
+
await handle.read(before, 0, 1, start - 1);
|
|
50
|
+
if (before[0] !== 0x0a) {
|
|
51
|
+
const aligned = await findNewline(handle, start, size, MAX_TAG_LINE_BYTES);
|
|
52
|
+
if (aligned === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
start = aligned + 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (start >= size)
|
|
58
|
+
return undefined;
|
|
59
|
+
const bytes = [];
|
|
60
|
+
let total = 0;
|
|
61
|
+
let cursor = start;
|
|
62
|
+
while (cursor < size && total <= MAX_TAG_LINE_BYTES) {
|
|
63
|
+
const chunk = Buffer.alloc(Math.min(READ_BLOCK_BYTES, size - cursor));
|
|
64
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, cursor);
|
|
65
|
+
if (bytesRead === 0)
|
|
66
|
+
break;
|
|
67
|
+
const content = chunk.subarray(0, bytesRead);
|
|
68
|
+
const newline = content.indexOf(0x0a);
|
|
69
|
+
const part = newline >= 0 ? content.subarray(0, newline) : content;
|
|
70
|
+
total += part.length;
|
|
71
|
+
if (total > MAX_TAG_LINE_BYTES)
|
|
72
|
+
return undefined;
|
|
73
|
+
bytes.push(part);
|
|
74
|
+
if (newline >= 0)
|
|
75
|
+
return {
|
|
76
|
+
start,
|
|
77
|
+
nextOffset: cursor + newline + 1,
|
|
78
|
+
text: Buffer.concat(bytes, total).toString('utf8')
|
|
79
|
+
};
|
|
80
|
+
cursor += bytesRead;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
async function findNewline(handle, offset, size, limit) {
|
|
85
|
+
let cursor = offset;
|
|
86
|
+
let remaining = limit;
|
|
87
|
+
while (cursor < size && remaining > 0) {
|
|
88
|
+
const chunk = Buffer.alloc(Math.min(READ_BLOCK_BYTES, size - cursor, remaining));
|
|
89
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, cursor);
|
|
90
|
+
if (bytesRead === 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
const newline = chunk.subarray(0, bytesRead).indexOf(0x0a);
|
|
93
|
+
if (newline >= 0)
|
|
94
|
+
return cursor + newline;
|
|
95
|
+
cursor += bytesRead;
|
|
96
|
+
remaining -= bytesRead;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
export function parseTag(text) {
|
|
101
|
+
if (text.length === 0 ||
|
|
102
|
+
text.startsWith('!_TAG_') ||
|
|
103
|
+
Buffer.byteLength(text) > MAX_TAG_LINE_BYTES)
|
|
104
|
+
return undefined;
|
|
105
|
+
const fields = text.split('\t');
|
|
106
|
+
if (fields.length < 3)
|
|
107
|
+
return undefined;
|
|
108
|
+
const [name, path, excmd, ...extensions] = fields;
|
|
109
|
+
if (name === undefined ||
|
|
110
|
+
path === undefined ||
|
|
111
|
+
excmd === undefined ||
|
|
112
|
+
name.length === 0 ||
|
|
113
|
+
path.length === 0 ||
|
|
114
|
+
path.startsWith('/') ||
|
|
115
|
+
path.includes('\\') ||
|
|
116
|
+
path.split('/').some((part) => part === '' || part === '.' || part === '..'))
|
|
117
|
+
return undefined;
|
|
118
|
+
const match = /^([1-9][0-9]*);?"?$/u.exec(excmd);
|
|
119
|
+
if (match === null)
|
|
120
|
+
return undefined;
|
|
121
|
+
const line = Number(match[1]);
|
|
122
|
+
if (!Number.isSafeInteger(line) || line < 1)
|
|
123
|
+
return undefined;
|
|
124
|
+
let kind;
|
|
125
|
+
let scope;
|
|
126
|
+
for (const extension of extensions) {
|
|
127
|
+
if (extension.length === 1)
|
|
128
|
+
kind ??= extension;
|
|
129
|
+
else if (extension.startsWith('kind:'))
|
|
130
|
+
kind ??= extension.slice('kind:'.length);
|
|
131
|
+
else if (extension.startsWith('scope:') ||
|
|
132
|
+
extension.startsWith('class:') ||
|
|
133
|
+
extension.startsWith('namespace:'))
|
|
134
|
+
scope ??= extension.slice(extension.indexOf(':') + 1);
|
|
135
|
+
}
|
|
136
|
+
return { name, path, line, kind, scope };
|
|
137
|
+
}
|
|
138
|
+
export function compareTagNames(left, right) {
|
|
139
|
+
return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
|
|
140
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { trailingPathCandidateScore } from './path-specifier-resolver.js';
|
|
3
|
+
const importExtensionMappings = [
|
|
4
|
+
{ importExtension: '.js', sourceExtensions: ['.ts', '.tsx'] },
|
|
5
|
+
{ importExtension: '.jsx', sourceExtensions: ['.tsx'] },
|
|
6
|
+
{ importExtension: '.mjs', sourceExtensions: ['.mts'] },
|
|
7
|
+
{ importExtension: '.cjs', sourceExtensions: ['.cts'] }
|
|
8
|
+
];
|
|
9
|
+
const extensionlessSourceExtensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
10
|
+
function accepts(value) {
|
|
11
|
+
return (/^(?:\.\.?\/)[A-Za-z0-9_./$-]+$/u.test(value) ||
|
|
12
|
+
/^@\/[A-Za-z0-9_./$-]+$/u.test(value) ||
|
|
13
|
+
/^(?:@?[A-Za-z0-9_$-]+)(?:\/[A-Za-z0-9_.$-]+)+$/u.test(value));
|
|
14
|
+
}
|
|
15
|
+
function matches(path, currentPath, specifier) {
|
|
16
|
+
if (!specifier.startsWith('.'))
|
|
17
|
+
return unresolvedSpecifierScore(path, specifier) !== undefined;
|
|
18
|
+
const resolved = resolveRelative(currentPath, specifier);
|
|
19
|
+
return (resolved !== undefined &&
|
|
20
|
+
candidates(resolved).some((candidate) => path === candidate ||
|
|
21
|
+
path === `${candidate}/index.ts` ||
|
|
22
|
+
path === `${candidate}/index.tsx` ||
|
|
23
|
+
path === `${candidate}/index.js` ||
|
|
24
|
+
path === `${candidate}/index.jsx`));
|
|
25
|
+
}
|
|
26
|
+
function score(path, currentPath, specifier) {
|
|
27
|
+
if (!specifier.startsWith('.'))
|
|
28
|
+
return unresolvedSpecifierScore(path, specifier) ?? Number.MAX_SAFE_INTEGER;
|
|
29
|
+
const resolved = resolveRelative(currentPath, specifier);
|
|
30
|
+
if (resolved === undefined)
|
|
31
|
+
return Number.MAX_SAFE_INTEGER;
|
|
32
|
+
const resolvedCandidates = candidates(resolved);
|
|
33
|
+
const exact = resolvedCandidates.indexOf(path);
|
|
34
|
+
if (exact >= 0)
|
|
35
|
+
return exact;
|
|
36
|
+
const index = resolvedCandidates.findIndex((candidate) => path === `${candidate}/index.ts` ||
|
|
37
|
+
path === `${candidate}/index.tsx` ||
|
|
38
|
+
path === `${candidate}/index.js` ||
|
|
39
|
+
path === `${candidate}/index.jsx`);
|
|
40
|
+
return index >= 0 ? resolvedCandidates.length + index : Number.MAX_SAFE_INTEGER;
|
|
41
|
+
}
|
|
42
|
+
function directCandidates(currentPath, specifier) {
|
|
43
|
+
if (!specifier.startsWith('.'))
|
|
44
|
+
return [];
|
|
45
|
+
const resolved = resolveRelative(currentPath, specifier);
|
|
46
|
+
if (resolved === undefined)
|
|
47
|
+
return [];
|
|
48
|
+
const resolvedCandidates = candidates(resolved);
|
|
49
|
+
return resolvedCandidates.flatMap((candidate) => [
|
|
50
|
+
candidate,
|
|
51
|
+
`${candidate}/index.ts`,
|
|
52
|
+
`${candidate}/index.tsx`,
|
|
53
|
+
`${candidate}/index.js`,
|
|
54
|
+
`${candidate}/index.jsx`
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
function resolveRelative(currentPath, specifier) {
|
|
58
|
+
const resolved = posix.normalize(posix.join(posix.dirname(currentPath), specifier));
|
|
59
|
+
if (resolved.startsWith('../') || resolved === '..')
|
|
60
|
+
return undefined;
|
|
61
|
+
return resolved.replace(/^\.\//u, '');
|
|
62
|
+
}
|
|
63
|
+
function candidates(path) {
|
|
64
|
+
const mapping = importExtensionMappings.find(({ importExtension }) => path.toLowerCase().endsWith(importExtension));
|
|
65
|
+
if (mapping !== undefined) {
|
|
66
|
+
const sourceBase = path.slice(0, -mapping.importExtension.length);
|
|
67
|
+
return [path, ...mapping.sourceExtensions.map((extension) => `${sourceBase}${extension}`)];
|
|
68
|
+
}
|
|
69
|
+
return path.slice(path.lastIndexOf('/') + 1).includes('.')
|
|
70
|
+
? [path]
|
|
71
|
+
: [path, ...extensionlessSourceExtensions.map((extension) => `${path}${extension}`)];
|
|
72
|
+
}
|
|
73
|
+
function sourceFileStem(path) {
|
|
74
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
75
|
+
return name.replace(/(?:\.d)?\.[^.]+$/u, '');
|
|
76
|
+
}
|
|
77
|
+
function unresolvedSpecifierScore(path, specifier) {
|
|
78
|
+
const normalizedSpecifier = specifier.startsWith('@/') ? specifier.slice(2) : specifier;
|
|
79
|
+
return trailingPathCandidateScore(path, unresolvedCandidates(normalizedSpecifier));
|
|
80
|
+
}
|
|
81
|
+
function unresolvedCandidates(specifier) {
|
|
82
|
+
return candidates(specifier).flatMap((candidate) => [
|
|
83
|
+
candidate,
|
|
84
|
+
`${candidate}/index.ts`,
|
|
85
|
+
`${candidate}/index.tsx`,
|
|
86
|
+
`${candidate}/index.js`,
|
|
87
|
+
`${candidate}/index.jsx`
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
export const ecmascriptPathSpecifierResolver = {
|
|
91
|
+
id: 'ecmascript.path-specifier',
|
|
92
|
+
accepts,
|
|
93
|
+
directCandidates,
|
|
94
|
+
matches,
|
|
95
|
+
score
|
|
96
|
+
};
|
|
97
|
+
export const ecmascriptNavigationResolutionDefinition = {
|
|
98
|
+
languageId: 'ecmascript',
|
|
99
|
+
resolver: ecmascriptPathSpecifierResolver
|
|
100
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fileSpecifierParser } from '@myagentroam/code-navigation';
|
|
2
|
+
const MAX_REFERENCE_REPARSE_POSITIONS = 8;
|
|
3
|
+
export function reparseFileReference(path, requested) {
|
|
4
|
+
const parser = fileSpecifierParser(path, requested.sourceContext.text);
|
|
5
|
+
if (parser === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
for (const position of referenceCandidatePositions(requested)) {
|
|
8
|
+
const parsed = parser.find(requested.sourceContext.text, position);
|
|
9
|
+
if (parsed?.value === requested.value && parsed.anchor === requested.anchor)
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
export function referenceCandidatePositions(requested) {
|
|
15
|
+
const source = requested.sourceContext.text;
|
|
16
|
+
const requestedPosition = requested.sourceContext.position;
|
|
17
|
+
const positions = new Set([requestedPosition, requested.from, requested.to - 1]);
|
|
18
|
+
const matches = [];
|
|
19
|
+
for (let offset = source.indexOf(requested.value); offset >= 0;) {
|
|
20
|
+
matches.push(offset);
|
|
21
|
+
offset = source.indexOf(requested.value, offset + Math.max(1, requested.value.length));
|
|
22
|
+
}
|
|
23
|
+
matches
|
|
24
|
+
.sort((left, right) => Math.abs(left - requestedPosition) - Math.abs(right - requestedPosition) || left - right)
|
|
25
|
+
.slice(0, MAX_REFERENCE_REPARSE_POSITIONS - positions.size)
|
|
26
|
+
.forEach((position) => positions.add(position));
|
|
27
|
+
return [...positions].filter((position) => position >= 0 && position < source.length);
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { matchesPathSuffix } from './path-specifier-resolver.js';
|
|
2
|
+
function accepts(value) {
|
|
3
|
+
return /^(?:[A-Za-z0-9][A-Za-z0-9._-]*)(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/u.test(value);
|
|
4
|
+
}
|
|
5
|
+
function matches(path, _currentPath, specifier) {
|
|
6
|
+
if (!path.endsWith('.go'))
|
|
7
|
+
return false;
|
|
8
|
+
const directory = path.slice(0, path.lastIndexOf('/'));
|
|
9
|
+
return packageSuffixes(specifier).some((suffix) => matchesPathSuffix(directory, suffix));
|
|
10
|
+
}
|
|
11
|
+
function score(path, _currentPath, specifier) {
|
|
12
|
+
const directory = path.slice(0, path.lastIndexOf('/'));
|
|
13
|
+
const index = packageSuffixes(specifier).findIndex((suffix) => matchesPathSuffix(directory, suffix));
|
|
14
|
+
return index < 0 ? Number.MAX_SAFE_INTEGER : index * 10_000 + path.length;
|
|
15
|
+
}
|
|
16
|
+
function packageSuffixes(specifier) {
|
|
17
|
+
const segments = specifier.split('/');
|
|
18
|
+
return segments.length < 2
|
|
19
|
+
? []
|
|
20
|
+
: segments.slice(0, -1).map((_, index) => segments.slice(index).join('/'));
|
|
21
|
+
}
|
|
22
|
+
export const goPathSpecifierResolver = {
|
|
23
|
+
id: 'go.path-specifier',
|
|
24
|
+
accepts,
|
|
25
|
+
matches,
|
|
26
|
+
score
|
|
27
|
+
};
|
|
28
|
+
export const goNavigationResolutionDefinition = {
|
|
29
|
+
languageId: 'go',
|
|
30
|
+
resolver: goPathSpecifierResolver
|
|
31
|
+
};
|