@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
|
@@ -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,85 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
const importExtensionMappings = [
|
|
3
|
+
{ importExtension: '.js', sourceExtensions: ['.ts', '.tsx'] },
|
|
4
|
+
{ importExtension: '.jsx', sourceExtensions: ['.tsx'] },
|
|
5
|
+
{ importExtension: '.mjs', sourceExtensions: ['.mts'] },
|
|
6
|
+
{ importExtension: '.cjs', sourceExtensions: ['.cts'] }
|
|
7
|
+
];
|
|
8
|
+
const extensionlessSourceExtensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
9
|
+
function accepts(value) {
|
|
10
|
+
return (/^(?:\.\.?\/)[A-Za-z0-9_./$-]+$/u.test(value) ||
|
|
11
|
+
/^(?:@?[A-Za-z0-9_$-]+)(?:\/[A-Za-z0-9_.$-]+)+$/u.test(value));
|
|
12
|
+
}
|
|
13
|
+
function matches(path, currentPath, specifier) {
|
|
14
|
+
if (!specifier.startsWith('.'))
|
|
15
|
+
return sourceFileStem(path) === sourceFileStem(specifier);
|
|
16
|
+
const resolved = resolveRelative(currentPath, specifier);
|
|
17
|
+
return (resolved !== undefined &&
|
|
18
|
+
candidates(resolved).some((candidate) => path === candidate ||
|
|
19
|
+
path === `${candidate}/index.ts` ||
|
|
20
|
+
path === `${candidate}/index.tsx` ||
|
|
21
|
+
path === `${candidate}/index.js` ||
|
|
22
|
+
path === `${candidate}/index.jsx`));
|
|
23
|
+
}
|
|
24
|
+
function score(path, currentPath, specifier) {
|
|
25
|
+
if (!specifier.startsWith('.'))
|
|
26
|
+
return path.length;
|
|
27
|
+
const resolved = resolveRelative(currentPath, specifier);
|
|
28
|
+
if (resolved === undefined)
|
|
29
|
+
return Number.MAX_SAFE_INTEGER;
|
|
30
|
+
const resolvedCandidates = candidates(resolved);
|
|
31
|
+
const exact = resolvedCandidates.indexOf(path);
|
|
32
|
+
if (exact >= 0)
|
|
33
|
+
return exact;
|
|
34
|
+
const index = resolvedCandidates.findIndex((candidate) => path === `${candidate}/index.ts` ||
|
|
35
|
+
path === `${candidate}/index.tsx` ||
|
|
36
|
+
path === `${candidate}/index.js` ||
|
|
37
|
+
path === `${candidate}/index.jsx`);
|
|
38
|
+
return index >= 0 ? resolvedCandidates.length + index : Number.MAX_SAFE_INTEGER;
|
|
39
|
+
}
|
|
40
|
+
function directCandidates(currentPath, specifier) {
|
|
41
|
+
if (!specifier.startsWith('.'))
|
|
42
|
+
return [];
|
|
43
|
+
const resolved = resolveRelative(currentPath, specifier);
|
|
44
|
+
if (resolved === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
const resolvedCandidates = candidates(resolved);
|
|
47
|
+
return resolvedCandidates.flatMap((candidate) => [
|
|
48
|
+
candidate,
|
|
49
|
+
`${candidate}/index.ts`,
|
|
50
|
+
`${candidate}/index.tsx`,
|
|
51
|
+
`${candidate}/index.js`,
|
|
52
|
+
`${candidate}/index.jsx`
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
function resolveRelative(currentPath, specifier) {
|
|
56
|
+
const resolved = posix.normalize(posix.join(posix.dirname(currentPath), specifier));
|
|
57
|
+
if (resolved.startsWith('../') || resolved === '..')
|
|
58
|
+
return undefined;
|
|
59
|
+
return resolved.replace(/^\.\//u, '');
|
|
60
|
+
}
|
|
61
|
+
function candidates(path) {
|
|
62
|
+
const mapping = importExtensionMappings.find(({ importExtension }) => path.toLowerCase().endsWith(importExtension));
|
|
63
|
+
if (mapping !== undefined) {
|
|
64
|
+
const sourceBase = path.slice(0, -mapping.importExtension.length);
|
|
65
|
+
return [path, ...mapping.sourceExtensions.map((extension) => `${sourceBase}${extension}`)];
|
|
66
|
+
}
|
|
67
|
+
return path.slice(path.lastIndexOf('/') + 1).includes('.')
|
|
68
|
+
? [path]
|
|
69
|
+
: [path, ...extensionlessSourceExtensions.map((extension) => `${path}${extension}`)];
|
|
70
|
+
}
|
|
71
|
+
function sourceFileStem(path) {
|
|
72
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
73
|
+
return name.replace(/(?:\.d)?\.[^.]+$/u, '');
|
|
74
|
+
}
|
|
75
|
+
export const ecmascriptPathSpecifierResolver = {
|
|
76
|
+
id: 'ecmascript.path-specifier',
|
|
77
|
+
accepts,
|
|
78
|
+
directCandidates,
|
|
79
|
+
matches,
|
|
80
|
+
score
|
|
81
|
+
};
|
|
82
|
+
export const ecmascriptNavigationResolutionDefinition = {
|
|
83
|
+
languageId: 'ecmascript',
|
|
84
|
+
resolver: ecmascriptPathSpecifierResolver
|
|
85
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { matchesPathSuffix } from './path-specifier-resolver.js';
|
|
2
|
+
function accepts(value) {
|
|
3
|
+
return /^[\p{L}_$][\p{L}\p{N}_$]*(?:\.[\p{L}_$][\p{L}\p{N}_$]*)+$/u.test(value);
|
|
4
|
+
}
|
|
5
|
+
function matches(path, _currentPath, specifier) {
|
|
6
|
+
return candidates(specifier).some((candidate) => matchesPathSuffix(path, candidate));
|
|
7
|
+
}
|
|
8
|
+
function score(path, _currentPath, specifier) {
|
|
9
|
+
const exact = candidates(specifier).findIndex((candidate) => matchesPathSuffix(path, candidate));
|
|
10
|
+
return exact < 0 ? Number.MAX_SAFE_INTEGER : exact * 10_000 + path.length;
|
|
11
|
+
}
|
|
12
|
+
function candidates(specifier) {
|
|
13
|
+
const segments = specifier.split('.');
|
|
14
|
+
return segments.flatMap((_, index) => {
|
|
15
|
+
const value = segments.slice(0, segments.length - index).join('/');
|
|
16
|
+
return ['.java', '.kt', '.kts'].map((suffix) => `${value}${suffix}`);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export const jvmPathSpecifierResolver = {
|
|
20
|
+
id: 'jvm.path-specifier',
|
|
21
|
+
accepts,
|
|
22
|
+
matches,
|
|
23
|
+
score
|
|
24
|
+
};
|
|
25
|
+
export const jvmNavigationResolutionDefinition = {
|
|
26
|
+
languageId: 'jvm',
|
|
27
|
+
resolver: jvmPathSpecifierResolver
|
|
28
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
function accepts(value) {
|
|
3
|
+
return value === '.' || /^(?:\.\.?\/)?[A-Za-z0-9_./$+ -]+$/u.test(value);
|
|
4
|
+
}
|
|
5
|
+
function matches(path, currentPath, specifier) {
|
|
6
|
+
return candidates(currentPath, specifier).includes(path);
|
|
7
|
+
}
|
|
8
|
+
function score(path, currentPath, specifier) {
|
|
9
|
+
const index = candidates(currentPath, specifier).indexOf(path);
|
|
10
|
+
return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
|
|
11
|
+
}
|
|
12
|
+
function candidates(currentPath, specifier) {
|
|
13
|
+
if (specifier === '.')
|
|
14
|
+
return [currentPath];
|
|
15
|
+
const resolved = posix.normalize(posix.join(posix.dirname(currentPath), specifier));
|
|
16
|
+
if (resolved === '..' || resolved.startsWith('../'))
|
|
17
|
+
return [];
|
|
18
|
+
const path = resolved.replace(/^\.\//u, '');
|
|
19
|
+
return path.includes('.')
|
|
20
|
+
? [path]
|
|
21
|
+
: [path, `${path}.md`, `${path}.markdown`, `${path}/README.md`, `${path}/readme.md`];
|
|
22
|
+
}
|
|
23
|
+
export function markdownAnchorLine(source, anchor) {
|
|
24
|
+
const requested = markdownAnchorSlug(anchor);
|
|
25
|
+
if (requested === '')
|
|
26
|
+
return undefined;
|
|
27
|
+
const counts = new Map();
|
|
28
|
+
const lines = source.split(/\r?\n/u);
|
|
29
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
30
|
+
const heading = markdownHeading(lines, index);
|
|
31
|
+
if (heading === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
const base = markdownAnchorSlug(heading.text);
|
|
34
|
+
if (base === '')
|
|
35
|
+
continue;
|
|
36
|
+
const count = counts.get(base) ?? 0;
|
|
37
|
+
counts.set(base, count + 1);
|
|
38
|
+
const slug = count === 0 ? base : `${base}-${count}`;
|
|
39
|
+
if (slug === requested)
|
|
40
|
+
return heading.line;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function markdownHeading(lines, index) {
|
|
45
|
+
const atx = /^[ \t]{0,3}#{1,6}[ \t]+(.+?)[ \t]*#*[ \t]*$/u.exec(lines[index] ?? '');
|
|
46
|
+
if (atx !== null)
|
|
47
|
+
return { text: atx[1], line: index + 1 };
|
|
48
|
+
const underline = /^[ \t]{0,3}(?:=+|-+)[ \t]*$/u.test(lines[index + 1] ?? '');
|
|
49
|
+
return underline && (lines[index] ?? '').trim().length > 0
|
|
50
|
+
? { text: lines[index], line: index + 1 }
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
function markdownAnchorSlug(value) {
|
|
54
|
+
return decodeURIComponentSafe(value)
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[`*_~[\]()]/gu, '')
|
|
58
|
+
.replace(/[^\p{L}\p{N}_ -]/gu, '')
|
|
59
|
+
.replace(/[\s-]+/gu, '-');
|
|
60
|
+
}
|
|
61
|
+
function decodeURIComponentSafe(value) {
|
|
62
|
+
try {
|
|
63
|
+
return decodeURIComponent(value);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const markdownPathSpecifierResolver = {
|
|
70
|
+
id: 'markdown.path-specifier',
|
|
71
|
+
accepts,
|
|
72
|
+
directCandidates: candidates,
|
|
73
|
+
matches,
|
|
74
|
+
score,
|
|
75
|
+
anchorLine: markdownAnchorLine
|
|
76
|
+
};
|
|
77
|
+
export const markdownNavigationResolutionDefinition = {
|
|
78
|
+
languageId: 'markdown',
|
|
79
|
+
resolver: markdownPathSpecifierResolver
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|