@thegitai/cli 1.0.0-preview.27 → 1.0.0-preview.29
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/README.md +2 -2
- package/dist/bin/ai.js +0 -15
- package/dist/src/agent-mode.js +0 -5
- package/dist/src/api/browser-login.js +2 -2
- package/dist/src/api/chat.js +4 -7
- package/dist/src/help-text.js +4 -4
- package/dist/src/session-safety.js +0 -12
- package/dist/src/signin.js +5 -3
- package/dist/src/tool-executor.js +9 -44
- package/dist/src/tools/delete-file.js +1 -3
- package/dist/src/tools/index.js +0 -10
- package/dist/src/tools/patch-file.js +1 -11
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +2 -45
- package/dist/src/tools/run-node-script.js +1 -55
- package/dist/src/tools/str-replace.js +1 -11
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +1 -11
- package/dist/src/ui/repl.js +1 -10
- package/dist/src/ui/tui/build-frame.js +1 -4
- package/package.json +6 -6
- package/dist/src/project-index.js +0 -233
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import { ALWAYS_IGNORE_DIRS, listProjectFiles as listScannerProjectFiles, shouldIgnorePath, } from '../scanner.js';
|
|
6
|
-
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
7
|
-
import { getRipgrepPathCandidates, isRipgrepExecutionUnavailable, } from './ripgrep.js';
|
|
8
|
-
import { getCommandExitCode } from './exec-utils.js';
|
|
9
|
-
const MAX_RIPGREP_BUFFER = 2 * 1024 * 1024;
|
|
10
|
-
const MAX_RIPGREP_TIMEOUT_MS = 10000;
|
|
11
|
-
const DEFAULT_PROJECT_FILE_LIMIT = 400;
|
|
12
|
-
const MAX_FALLBACK_FILES_TO_SCAN = 2000;
|
|
13
|
-
const execFileAsync = promisify(execFile);
|
|
14
|
-
const SOURCE_FILE_EXTENSIONS = new Set([
|
|
15
|
-
'.c',
|
|
16
|
-
'.cc',
|
|
17
|
-
'.cpp',
|
|
18
|
-
'.cs',
|
|
19
|
-
'.css',
|
|
20
|
-
'.cts',
|
|
21
|
-
'.cxx',
|
|
22
|
-
'.go',
|
|
23
|
-
'.h',
|
|
24
|
-
'.hpp',
|
|
25
|
-
'.html',
|
|
26
|
-
'.java',
|
|
27
|
-
'.js',
|
|
28
|
-
'.jsx',
|
|
29
|
-
'.kt',
|
|
30
|
-
'.kts',
|
|
31
|
-
'.m',
|
|
32
|
-
'.mjml',
|
|
33
|
-
'.mm',
|
|
34
|
-
'.mts',
|
|
35
|
-
'.php',
|
|
36
|
-
'.py',
|
|
37
|
-
'.rb',
|
|
38
|
-
'.rs',
|
|
39
|
-
'.swift',
|
|
40
|
-
'.ts',
|
|
41
|
-
'.tsx',
|
|
42
|
-
'.vue',
|
|
43
|
-
]);
|
|
44
|
-
const DEF_KEYWORDS = [
|
|
45
|
-
'def ',
|
|
46
|
-
'async def ',
|
|
47
|
-
'function ',
|
|
48
|
-
'async function ',
|
|
49
|
-
'class ',
|
|
50
|
-
'const ',
|
|
51
|
-
'let ',
|
|
52
|
-
'var ',
|
|
53
|
-
'interface ',
|
|
54
|
-
'type ',
|
|
55
|
-
'fn ',
|
|
56
|
-
'func ',
|
|
57
|
-
'pub fn ',
|
|
58
|
-
'pub async fn ',
|
|
59
|
-
'export function ',
|
|
60
|
-
'export async function ',
|
|
61
|
-
'export const ',
|
|
62
|
-
'export class ',
|
|
63
|
-
'export interface ',
|
|
64
|
-
'export type ',
|
|
65
|
-
'export default function ',
|
|
66
|
-
'export default class ',
|
|
67
|
-
'struct ',
|
|
68
|
-
'pub struct ',
|
|
69
|
-
'enum ',
|
|
70
|
-
'pub enum ',
|
|
71
|
-
'trait ',
|
|
72
|
-
'pub trait ',
|
|
73
|
-
'impl ',
|
|
74
|
-
'pub mod ',
|
|
75
|
-
'mod ',
|
|
76
|
-
];
|
|
77
|
-
const SYMBOL_PATTERNS = [
|
|
78
|
-
{
|
|
79
|
-
regex: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/,
|
|
80
|
-
kind: 'function',
|
|
81
|
-
nameGroup: 1,
|
|
82
|
-
},
|
|
83
|
-
{ regex: /^\s*(?:pub\s+)?struct\s+(\w+)/, kind: 'struct', nameGroup: 1 },
|
|
84
|
-
{ regex: /^\s*(?:pub\s+)?enum\s+(\w+)/, kind: 'enum', nameGroup: 1 },
|
|
85
|
-
{ regex: /^\s*(?:pub\s+)?trait\s+(\w+)/, kind: 'interface', nameGroup: 1 },
|
|
86
|
-
{ regex: /^\s*(?:pub\s+)?type\s+(\w+)/, kind: 'type', nameGroup: 1 },
|
|
87
|
-
{
|
|
88
|
-
regex: /^\s*(?:pub\s+)?(?:static|const)\s+(\w+)\s*:/,
|
|
89
|
-
kind: 'constant',
|
|
90
|
-
nameGroup: 1,
|
|
91
|
-
},
|
|
92
|
-
{ regex: /^\s*impl(?:<[^>]*>)?\s+(\w+)/, kind: 'class', nameGroup: 1 },
|
|
93
|
-
{ regex: /^\s*(?:pub\s+)?mod\s+(\w+)/, kind: 'module', nameGroup: 1 },
|
|
94
|
-
{ regex: /^func\s+(?:\([^)]+\)\s+)?(\w+)/, kind: 'function', nameGroup: 1 },
|
|
95
|
-
{ regex: /^type\s+(\w+)\s+struct\b/, kind: 'struct', nameGroup: 1 },
|
|
96
|
-
{ regex: /^type\s+(\w+)\s+interface\b/, kind: 'interface', nameGroup: 1 },
|
|
97
|
-
{ regex: /^type\s+(\w+)\b/, kind: 'type', nameGroup: 1 },
|
|
98
|
-
{ regex: /^var\s+(\w+)\b/, kind: 'variable', nameGroup: 1 },
|
|
99
|
-
{ regex: /^const\s+(\w+)\b/, kind: 'constant', nameGroup: 1 },
|
|
100
|
-
{ regex: /^\s*(?:async\s+)?def\s+(\w+)/, kind: 'function', nameGroup: 1 },
|
|
101
|
-
{ regex: /^\s*class\s+(\w+)/, kind: 'class', nameGroup: 1 },
|
|
102
|
-
{
|
|
103
|
-
regex: /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
104
|
-
kind: 'class',
|
|
105
|
-
nameGroup: 1,
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
regex: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s+(\w+)/,
|
|
109
|
-
kind: 'function',
|
|
110
|
-
nameGroup: 1,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
regex: /^\s*(?:export\s+)?interface\s+(\w+)/,
|
|
114
|
-
kind: 'interface',
|
|
115
|
-
nameGroup: 1,
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
regex: /^\s*(?:export\s+)?type\s+(\w+)\s*[<=]/,
|
|
119
|
-
kind: 'type',
|
|
120
|
-
nameGroup: 1,
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
regex: /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::\s*\S+\s*)?=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/,
|
|
124
|
-
kind: 'function',
|
|
125
|
-
nameGroup: 1,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
regex: /^\s*(?:export\s+)?const\s+(\w+)\s*[=:]/,
|
|
129
|
-
kind: 'constant',
|
|
130
|
-
nameGroup: 1,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
regex: /^\s*(?:export\s+)?(?:let|var)\s+(\w+)\b/,
|
|
134
|
-
kind: 'variable',
|
|
135
|
-
nameGroup: 1,
|
|
136
|
-
},
|
|
137
|
-
{ regex: /^\s*(?:export\s+)?enum\s+(\w+)/, kind: 'enum', nameGroup: 1 },
|
|
138
|
-
];
|
|
139
|
-
function escapeRegExp(value) {
|
|
140
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
141
|
-
}
|
|
142
|
-
function isLikelySourceFile(filePath) {
|
|
143
|
-
return SOURCE_FILE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
144
|
-
}
|
|
145
|
-
export function normalizeProjectFilePath(rootDir, filePath) {
|
|
146
|
-
if (!filePath)
|
|
147
|
-
return filePath;
|
|
148
|
-
const relativePath = normalizeProjectRelativePath(rootDir, filePath);
|
|
149
|
-
return relativePath ?? filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
150
|
-
}
|
|
151
|
-
export function readProjectText(rootDir, filePath) {
|
|
152
|
-
const normalizedPath = normalizeProjectRelativePath(rootDir, filePath);
|
|
153
|
-
if (!normalizedPath) {
|
|
154
|
-
throw Error(`Refusing to access path outside the project root: ${filePath}`);
|
|
155
|
-
}
|
|
156
|
-
if (shouldIgnorePath(normalizedPath)) {
|
|
157
|
-
throw Error(`Ignored artifact paths are off-limits: ${normalizedPath}`);
|
|
158
|
-
}
|
|
159
|
-
const fullPath = path.join(rootDir, normalizedPath);
|
|
160
|
-
return readFileSync(fullPath, 'utf8');
|
|
161
|
-
}
|
|
162
|
-
export function extractIdentifierAtPosition(content, line, column) {
|
|
163
|
-
const lines = content.split(/\r?\n/);
|
|
164
|
-
if (line < 1 || line > lines.length)
|
|
165
|
-
return null;
|
|
166
|
-
const text = (lines[line - 1] ?? '').replace(/\r$/, '');
|
|
167
|
-
if (column < 1 || column > text.length + 1)
|
|
168
|
-
return null;
|
|
169
|
-
const index = column - 1;
|
|
170
|
-
const identifierChar = /[\w$]/;
|
|
171
|
-
if (!identifierChar.test(text[index] ?? ''))
|
|
172
|
-
return null;
|
|
173
|
-
let start = index;
|
|
174
|
-
let end = index;
|
|
175
|
-
while (start > 0 && identifierChar.test(text[start - 1] ?? ''))
|
|
176
|
-
start--;
|
|
177
|
-
while (end < text.length - 1 && identifierChar.test(text[end + 1] ?? ''))
|
|
178
|
-
end++;
|
|
179
|
-
const symbol = text.slice(start, end + 1).trim();
|
|
180
|
-
return symbol || null;
|
|
181
|
-
}
|
|
182
|
-
export function isDefinitionLine(line, symbol) {
|
|
183
|
-
const trimmed = line.trim();
|
|
184
|
-
if (/^(\/\/|#|\/\*|\*)/.test(trimmed))
|
|
185
|
-
return false;
|
|
186
|
-
for (const keyword of DEF_KEYWORDS) {
|
|
187
|
-
const index = trimmed.indexOf(keyword);
|
|
188
|
-
if (index === -1)
|
|
189
|
-
continue;
|
|
190
|
-
const before = trimmed.slice(0, index);
|
|
191
|
-
if (before.length > 0 && !/^[a-zA-Z0-9_$\s]*$/.test(before))
|
|
192
|
-
continue;
|
|
193
|
-
const after = trimmed.slice(index + keyword.length).trimStart();
|
|
194
|
-
if (!after.startsWith(symbol))
|
|
195
|
-
continue;
|
|
196
|
-
const nextChar = after[symbol.length];
|
|
197
|
-
if (!nextChar || /[\s(<:{=[]/.test(nextChar)) {
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
export function buildIgnoreArgs() {
|
|
204
|
-
const args = [];
|
|
205
|
-
for (const dir of ALWAYS_IGNORE_DIRS) {
|
|
206
|
-
args.push('--glob', `!${dir}`, '--glob', `!**/${dir}/**`);
|
|
207
|
-
}
|
|
208
|
-
return args;
|
|
209
|
-
}
|
|
210
|
-
async function runRipgrep(rootDir, args) {
|
|
211
|
-
let unavailableError = null;
|
|
212
|
-
for (const candidate of getRipgrepPathCandidates()) {
|
|
213
|
-
try {
|
|
214
|
-
const { stdout } = await execFileAsync(candidate, args, {
|
|
215
|
-
cwd: rootDir,
|
|
216
|
-
encoding: 'utf-8',
|
|
217
|
-
maxBuffer: MAX_RIPGREP_BUFFER,
|
|
218
|
-
timeout: MAX_RIPGREP_TIMEOUT_MS,
|
|
219
|
-
});
|
|
220
|
-
return String(stdout ?? '');
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
if (getCommandExitCode(error) === 1) {
|
|
224
|
-
return '';
|
|
225
|
-
}
|
|
226
|
-
if (!isRipgrepExecutionUnavailable(error)) {
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
unavailableError = error;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (unavailableError) {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
function collectRelativeFilesUnder(rootDir, dirRel, out, maxFiles) {
|
|
238
|
-
if (out.length >= maxFiles)
|
|
239
|
-
return;
|
|
240
|
-
const absDir = path.join(rootDir, dirRel);
|
|
241
|
-
let entries;
|
|
242
|
-
try {
|
|
243
|
-
entries = readdirSync(absDir, { withFileTypes: true });
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
for (const ent of entries) {
|
|
249
|
-
if (out.length >= maxFiles)
|
|
250
|
-
return;
|
|
251
|
-
const name = String(ent.name);
|
|
252
|
-
const rel = dirRel ? path.join(dirRel, name) : name;
|
|
253
|
-
const relPosix = rel.split(path.sep).join('/');
|
|
254
|
-
if (shouldIgnorePath(relPosix))
|
|
255
|
-
continue;
|
|
256
|
-
if (ent.isDirectory()) {
|
|
257
|
-
collectRelativeFilesUnder(rootDir, rel, out, maxFiles);
|
|
258
|
-
}
|
|
259
|
-
else if (ent.isFile()) {
|
|
260
|
-
out.push(relPosix);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function listRelativePathsForTextSearch(rootDir, targetRel) {
|
|
265
|
-
const absTarget = path.join(rootDir, targetRel);
|
|
266
|
-
let stat;
|
|
267
|
-
try {
|
|
268
|
-
stat = statSync(absTarget);
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
const out = [];
|
|
274
|
-
if (stat.isFile()) {
|
|
275
|
-
if (!shouldIgnorePath(normalizeProjectFilePath(rootDir, targetRel))) {
|
|
276
|
-
out.push(normalizeProjectFilePath(rootDir, targetRel));
|
|
277
|
-
}
|
|
278
|
-
return out;
|
|
279
|
-
}
|
|
280
|
-
if (stat.isDirectory()) {
|
|
281
|
-
collectRelativeFilesUnder(rootDir, targetRel === '.' ? '' : targetRel, out, MAX_FALLBACK_FILES_TO_SCAN);
|
|
282
|
-
}
|
|
283
|
-
return out;
|
|
284
|
-
}
|
|
285
|
-
function searchTextMatchesWithoutRipgrep(rootDir, searchTerm, options = {}) {
|
|
286
|
-
const target = options.filePath
|
|
287
|
-
? normalizeProjectFilePath(rootDir, options.filePath)
|
|
288
|
-
: '.';
|
|
289
|
-
const limit = Math.max(1, Math.min(Number(options.limit ?? 80), 200));
|
|
290
|
-
const wordPattern = new RegExp(`\\b${escapeRegExp(searchTerm)}\\b`);
|
|
291
|
-
const files = listRelativePathsForTextSearch(rootDir, target);
|
|
292
|
-
const results = [];
|
|
293
|
-
for (const rel of files) {
|
|
294
|
-
if (results.length >= limit)
|
|
295
|
-
break;
|
|
296
|
-
let content;
|
|
297
|
-
try {
|
|
298
|
-
content = readFileSync(path.join(rootDir, rel), 'utf8');
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
const lines = content.split(/\r?\n/);
|
|
304
|
-
for (let i = 0; i < lines.length; i++) {
|
|
305
|
-
if (results.length >= limit)
|
|
306
|
-
break;
|
|
307
|
-
const line = lines[i] ?? '';
|
|
308
|
-
if (wordPattern.test(line)) {
|
|
309
|
-
results.push({
|
|
310
|
-
filePath: normalizeProjectFilePath(rootDir, rel),
|
|
311
|
-
line: i + 1,
|
|
312
|
-
content: line,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return results.slice(0, limit);
|
|
318
|
-
}
|
|
319
|
-
function parseTextSearchMatch(rootDir, line) {
|
|
320
|
-
const match = line.match(/^(.+?):(\d+):(.*)$/);
|
|
321
|
-
if (!match)
|
|
322
|
-
return null;
|
|
323
|
-
return {
|
|
324
|
-
filePath: normalizeProjectFilePath(rootDir, match[1] ?? ''),
|
|
325
|
-
line: parseInt(match[2] ?? '1', 10),
|
|
326
|
-
content: match[3] ?? '',
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
export async function searchTextMatches(rootDir, searchTerm, options = {}) {
|
|
330
|
-
const target = options.filePath
|
|
331
|
-
? normalizeProjectFilePath(rootDir, options.filePath)
|
|
332
|
-
: '.';
|
|
333
|
-
const limit = Math.max(1, Math.min(Number(options.limit ?? 80), 200));
|
|
334
|
-
const raw = await runRipgrep(rootDir, [
|
|
335
|
-
'-n',
|
|
336
|
-
'--no-heading',
|
|
337
|
-
'--color',
|
|
338
|
-
'never',
|
|
339
|
-
'--word-regexp',
|
|
340
|
-
'--fixed-strings',
|
|
341
|
-
...buildIgnoreArgs(),
|
|
342
|
-
'-m',
|
|
343
|
-
String(limit),
|
|
344
|
-
'--',
|
|
345
|
-
searchTerm,
|
|
346
|
-
target,
|
|
347
|
-
]);
|
|
348
|
-
if (raw === null) {
|
|
349
|
-
return searchTextMatchesWithoutRipgrep(rootDir, searchTerm, options);
|
|
350
|
-
}
|
|
351
|
-
return raw
|
|
352
|
-
.trim()
|
|
353
|
-
.split('\n')
|
|
354
|
-
.filter(Boolean)
|
|
355
|
-
.map((line) => parseTextSearchMatch(rootDir, line))
|
|
356
|
-
.filter((match) => match != null)
|
|
357
|
-
.slice(0, limit);
|
|
358
|
-
}
|
|
359
|
-
export async function searchSymbolOccurrences(rootDir, symbol, options = {}) {
|
|
360
|
-
const limit = Math.max(1, Math.min(Number(options.limit ?? 80), 200));
|
|
361
|
-
const matches = await searchTextMatches(rootDir, symbol, options);
|
|
362
|
-
const wordPattern = new RegExp(`\\b${escapeRegExp(symbol)}\\b`);
|
|
363
|
-
return matches
|
|
364
|
-
.map((match) => {
|
|
365
|
-
const filePath = match.filePath;
|
|
366
|
-
const startLine = match.line;
|
|
367
|
-
const content = match.content;
|
|
368
|
-
const columnMatch = content.match(wordPattern);
|
|
369
|
-
const startColumn = columnMatch?.index != null ? columnMatch.index + 1 : 1;
|
|
370
|
-
return {
|
|
371
|
-
filePath,
|
|
372
|
-
content,
|
|
373
|
-
startLine,
|
|
374
|
-
startColumn,
|
|
375
|
-
endLine: startLine,
|
|
376
|
-
endColumn: startColumn + symbol.length,
|
|
377
|
-
};
|
|
378
|
-
})
|
|
379
|
-
.slice(0, limit);
|
|
380
|
-
}
|
|
381
|
-
export function expandContext(rootDir, filePath, startLine, maxLines = 35) {
|
|
382
|
-
try {
|
|
383
|
-
const lines = readProjectText(rootDir, filePath).split('\n');
|
|
384
|
-
const start = Math.max(0, startLine - 1);
|
|
385
|
-
const end = Math.min(lines.length, start + maxLines);
|
|
386
|
-
return lines
|
|
387
|
-
.slice(start, end)
|
|
388
|
-
.map((line, index) => `${start + index + 1}: ${line}`)
|
|
389
|
-
.join('\n');
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
return '';
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
export function extractSymbolsFromFile(rootDir, filePath, query, limit = 40) {
|
|
396
|
-
let content;
|
|
397
|
-
try {
|
|
398
|
-
content = readProjectText(rootDir, filePath);
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
const lines = content.split('\n');
|
|
404
|
-
const symbols = [];
|
|
405
|
-
const seen = new Set();
|
|
406
|
-
const normalizedPath = normalizeProjectFilePath(rootDir, filePath);
|
|
407
|
-
for (let index = 0; index < lines.length; index++) {
|
|
408
|
-
const line = lines[index] ?? '';
|
|
409
|
-
const trimmed = line.trim();
|
|
410
|
-
if (trimmed.startsWith('//') ||
|
|
411
|
-
trimmed.startsWith('#') ||
|
|
412
|
-
trimmed.startsWith('/*') ||
|
|
413
|
-
trimmed.startsWith('*')) {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
for (const pattern of SYMBOL_PATTERNS) {
|
|
417
|
-
const match = line.match(pattern.regex);
|
|
418
|
-
if (!match)
|
|
419
|
-
continue;
|
|
420
|
-
const name = match[pattern.nameGroup];
|
|
421
|
-
if (!name || name === '_' || name === 'self')
|
|
422
|
-
continue;
|
|
423
|
-
if (query && !name.toLowerCase().includes(query.toLowerCase()))
|
|
424
|
-
continue;
|
|
425
|
-
const key = `${pattern.kind}:${name}:${index + 1}`;
|
|
426
|
-
if (seen.has(key))
|
|
427
|
-
continue;
|
|
428
|
-
seen.add(key);
|
|
429
|
-
const startColumn = line.indexOf(name) + 1;
|
|
430
|
-
symbols.push({
|
|
431
|
-
name,
|
|
432
|
-
kind: pattern.kind,
|
|
433
|
-
filePath: normalizedPath,
|
|
434
|
-
startLine: index + 1,
|
|
435
|
-
startColumn,
|
|
436
|
-
endLine: index + 1,
|
|
437
|
-
endColumn: startColumn + name.length,
|
|
438
|
-
selectionLine: index + 1,
|
|
439
|
-
selectionColumn: startColumn,
|
|
440
|
-
searchSource: 'regex',
|
|
441
|
-
});
|
|
442
|
-
if (symbols.length >= limit) {
|
|
443
|
-
return symbols;
|
|
444
|
-
}
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return symbols;
|
|
449
|
-
}
|
|
450
|
-
export async function listProjectFiles(rootDir, options = {}) {
|
|
451
|
-
const limit = Math.max(1, Math.min(Number(options.limit ?? DEFAULT_PROJECT_FILE_LIMIT), 2000));
|
|
452
|
-
try {
|
|
453
|
-
const raw = await runRipgrep(rootDir, ['--files', ...buildIgnoreArgs()]);
|
|
454
|
-
if (raw === null) {
|
|
455
|
-
const scanned = listScannerProjectFiles(rootDir, { limit });
|
|
456
|
-
return scanned
|
|
457
|
-
.map((filePath) => filePath.split(path.sep).join('/'))
|
|
458
|
-
.filter((filePath) => !options.sourceOnly || isLikelySourceFile(filePath))
|
|
459
|
-
.slice(0, limit);
|
|
460
|
-
}
|
|
461
|
-
const files = raw
|
|
462
|
-
.split('\n')
|
|
463
|
-
.map((line) => line.trim())
|
|
464
|
-
.filter(Boolean)
|
|
465
|
-
.filter((filePath) => !options.sourceOnly || isLikelySourceFile(filePath));
|
|
466
|
-
return files.slice(0, limit);
|
|
467
|
-
}
|
|
468
|
-
catch (error) {
|
|
469
|
-
console.warn(`[code-intel] Failed to list project files: ${error?.message ?? String(error)}`);
|
|
470
|
-
return [];
|
|
471
|
-
}
|
|
472
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { expandContext, extractIdentifierAtPosition, isDefinitionLine, readProjectText, searchSymbolOccurrences, } from './code-intel.js';
|
|
2
|
-
const MAX_CONTEXT_LINES = 35;
|
|
3
|
-
const MAX_REFS = 30;
|
|
4
|
-
const MAX_OUTPUT_CHARS = 8000;
|
|
5
|
-
async function fallbackFindSymbol(rootDir, args) {
|
|
6
|
-
const symbol = resolveRequestedSymbol(rootDir, args);
|
|
7
|
-
if (!symbol)
|
|
8
|
-
return {
|
|
9
|
-
ok: false,
|
|
10
|
-
error: 'symbol or filePath + line + column is required',
|
|
11
|
-
};
|
|
12
|
-
const all = await searchSymbolOccurrences(rootDir, symbol, { limit: 80 });
|
|
13
|
-
const defs = all.filter((hit) => isDefinitionLine(hit.content, symbol));
|
|
14
|
-
const defKeys = new Set(defs.map((definition) => `${definition.filePath}:${definition.startLine}`));
|
|
15
|
-
const refs = args.includeReferences !== false
|
|
16
|
-
? all.filter((hit) => !defKeys.has(`${hit.filePath}:${hit.startLine}`))
|
|
17
|
-
: [];
|
|
18
|
-
const sections = [];
|
|
19
|
-
if (defs.length === 0) {
|
|
20
|
-
sections.push(`No definition found for "${symbol}". Showing all occurrences as references.`);
|
|
21
|
-
for (const r of all.slice(0, MAX_REFS)) {
|
|
22
|
-
sections.push(`- ${r.filePath}:${r.startLine}: ${r.content.trim()}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
sections.push(`## Definitions (${defs.length})`);
|
|
27
|
-
for (const def of defs) {
|
|
28
|
-
sections.push(`\n### ${def.filePath}:${def.startLine}\n\`\`\`\n${expandContext(rootDir, def.filePath, def.startLine, MAX_CONTEXT_LINES)}\n\`\`\``);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (refs.length > 0) {
|
|
32
|
-
const shown = refs.slice(0, MAX_REFS);
|
|
33
|
-
sections.push(`\n## References (${refs.length} total, showing ${shown.length})`);
|
|
34
|
-
for (const r of shown)
|
|
35
|
-
sections.push(`- ${r.filePath}:${r.startLine}: ${r.content.trim()}`);
|
|
36
|
-
}
|
|
37
|
-
const output = sections.join('\n');
|
|
38
|
-
return {
|
|
39
|
-
ok: true,
|
|
40
|
-
provider: 'text_search',
|
|
41
|
-
symbol,
|
|
42
|
-
definitions: defs,
|
|
43
|
-
references: refs,
|
|
44
|
-
definitionCount: defs.length,
|
|
45
|
-
referenceCount: refs.length,
|
|
46
|
-
output: output.length > MAX_OUTPUT_CHARS
|
|
47
|
-
? `${output.slice(0, MAX_OUTPUT_CHARS)}\n...(truncated)`
|
|
48
|
-
: output,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
export async function findSymbol(context, args) {
|
|
52
|
-
return fallbackFindSymbol(context.rootDir, args);
|
|
53
|
-
}
|
|
54
|
-
function resolveRequestedSymbol(rootDir, args) {
|
|
55
|
-
const symbol = String(args.symbol ?? '').trim();
|
|
56
|
-
if (symbol)
|
|
57
|
-
return symbol;
|
|
58
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
59
|
-
const line = Number(args.line ?? 0);
|
|
60
|
-
const column = Number(args.column ?? 0);
|
|
61
|
-
if (!filePath || !line || !column)
|
|
62
|
-
return '';
|
|
63
|
-
try {
|
|
64
|
-
const content = readProjectText(rootDir, filePath);
|
|
65
|
-
return extractIdentifierAtPosition(content, line, column) ?? '';
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return '';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { extractIdentifierAtPosition, isDefinitionLine, readProjectText, searchTextMatches, } from './code-intel.js';
|
|
2
|
-
const MAX_CONTEXT_LINES = 15;
|
|
3
|
-
const MAX_OUTPUT_CHARS = 4000;
|
|
4
|
-
async function findDefinitionContext(rootDir, symbol) {
|
|
5
|
-
const matches = await searchTextMatches(rootDir, symbol, { limit: 40 });
|
|
6
|
-
for (const match of matches) {
|
|
7
|
-
if (!isDefinitionLine(match.content, symbol))
|
|
8
|
-
continue;
|
|
9
|
-
return extractDefinitionBlock(rootDir, match.filePath, match.line, symbol);
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
function extractDefinitionBlock(rootDir, filePath, lineNum, symbol) {
|
|
14
|
-
let fileContent;
|
|
15
|
-
try {
|
|
16
|
-
fileContent = readProjectText(rootDir, filePath);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const lines = fileContent.split('\n');
|
|
22
|
-
const defIdx = lineNum - 1;
|
|
23
|
-
let start = defIdx;
|
|
24
|
-
while (start > 0) {
|
|
25
|
-
const prev = lines[start - 1].trim();
|
|
26
|
-
if (prev.startsWith('///') ||
|
|
27
|
-
prev.startsWith('//!') ||
|
|
28
|
-
prev.startsWith('#[') ||
|
|
29
|
-
prev.startsWith('#![') ||
|
|
30
|
-
prev.startsWith('#') ||
|
|
31
|
-
prev.startsWith('@') ||
|
|
32
|
-
prev.startsWith('/**') ||
|
|
33
|
-
prev.startsWith('*') ||
|
|
34
|
-
prev.startsWith('//') ||
|
|
35
|
-
prev === '') {
|
|
36
|
-
if (prev === '' && start > 1) {
|
|
37
|
-
const prevPrev = lines[start - 2].trim();
|
|
38
|
-
if (!prevPrev.startsWith('//') &&
|
|
39
|
-
!prevPrev.startsWith('*') &&
|
|
40
|
-
!prevPrev.startsWith('#') &&
|
|
41
|
-
!prevPrev.startsWith('@'))
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
start--;
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const end = Math.min(lines.length, defIdx + MAX_CONTEXT_LINES);
|
|
51
|
-
const block = lines.slice(start, end);
|
|
52
|
-
return `${symbol} — ${filePath}:${lineNum}\n\`\`\`\n${block.join('\n')}\n\`\`\``;
|
|
53
|
-
}
|
|
54
|
-
export async function hoverSymbol(context, args) {
|
|
55
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
56
|
-
const line = Number(args.line ?? 0);
|
|
57
|
-
const column = Number(args.column ?? 0);
|
|
58
|
-
if (!filePath || !line || !column) {
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
error: 'hover_symbol requires filePath, line, and column.',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
let content;
|
|
65
|
-
try {
|
|
66
|
-
content = readProjectText(context.rootDir, filePath);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return { ok: false, error: `Could not read ${filePath}.` };
|
|
70
|
-
}
|
|
71
|
-
const symbol = extractIdentifierAtPosition(content, line, column);
|
|
72
|
-
if (!symbol) {
|
|
73
|
-
return { ok: false, error: 'No identifier found at the given position.' };
|
|
74
|
-
}
|
|
75
|
-
const hoverContent = await findDefinitionContext(context.rootDir, symbol);
|
|
76
|
-
if (!hoverContent) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
error: `Could not find definition for "${symbol}". Use find_symbol or grep_code.`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
const output = hoverContent.length > MAX_OUTPUT_CHARS
|
|
83
|
-
? `${hoverContent.slice(0, MAX_OUTPUT_CHARS)}\n...(truncated)`
|
|
84
|
-
: hoverContent;
|
|
85
|
-
return {
|
|
86
|
-
ok: true,
|
|
87
|
-
provider: 'text_search',
|
|
88
|
-
available: true,
|
|
89
|
-
filePath,
|
|
90
|
-
line,
|
|
91
|
-
column,
|
|
92
|
-
contents: output,
|
|
93
|
-
output,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { shouldIgnoreArtifactPath } from '../artifact-policy.js';
|
|
2
|
-
import { extractSymbolsFromFile, listProjectFiles, normalizeProjectFilePath, } from './code-intel.js';
|
|
3
|
-
function formatRegexSymbolsOutput(symbols) {
|
|
4
|
-
if (!symbols.length)
|
|
5
|
-
return 'No symbols found.';
|
|
6
|
-
const lines = symbols.map((s) => `- ${s.kind} ${s.name} @ ${s.filePath}:${s.selectionLine}:${s.selectionColumn}`);
|
|
7
|
-
return lines.join('\n');
|
|
8
|
-
}
|
|
9
|
-
export async function listSymbols(context, args) {
|
|
10
|
-
const scope = args.scope === 'workspace' ? 'workspace' : 'document';
|
|
11
|
-
const filePath = String(args.filePath ?? '').trim();
|
|
12
|
-
if (scope === 'document' && !filePath) {
|
|
13
|
-
return {
|
|
14
|
-
ok: false,
|
|
15
|
-
error: 'filePath is required for document symbol queries.',
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
if (scope === 'document' &&
|
|
19
|
-
shouldIgnoreArtifactPath(normalizeProjectFilePath(context.rootDir, filePath))) {
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
error: 'This path is not permitted.',
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const limit = Math.max(1, Math.min(Number(args.limit ?? 40), 100));
|
|
26
|
-
const query = String(args.query ?? '').trim() || undefined;
|
|
27
|
-
const symbols = scope === 'workspace'
|
|
28
|
-
? await listWorkspaceSymbols(context.rootDir, query, limit)
|
|
29
|
-
: extractSymbolsFromFile(context.rootDir, filePath, query, limit);
|
|
30
|
-
return {
|
|
31
|
-
ok: true,
|
|
32
|
-
provider: 'regex',
|
|
33
|
-
available: true,
|
|
34
|
-
scope,
|
|
35
|
-
filePath: scope === 'document' ? filePath : undefined,
|
|
36
|
-
total: symbols.length,
|
|
37
|
-
symbols,
|
|
38
|
-
output: formatRegexSymbolsOutput(symbols),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
async function listWorkspaceSymbols(rootDir, query, limit) {
|
|
42
|
-
const files = await listProjectFiles(rootDir, {
|
|
43
|
-
limit: 400,
|
|
44
|
-
sourceOnly: true,
|
|
45
|
-
});
|
|
46
|
-
const symbols = [];
|
|
47
|
-
for (const projectFile of files) {
|
|
48
|
-
const remaining = limit - symbols.length;
|
|
49
|
-
if (remaining <= 0)
|
|
50
|
-
break;
|
|
51
|
-
const fileSymbols = extractSymbolsFromFile(rootDir, projectFile, query, remaining);
|
|
52
|
-
symbols.push(...fileSymbols);
|
|
53
|
-
}
|
|
54
|
-
return symbols;
|
|
55
|
-
}
|