141 lines
5.2 KiB
JavaScript
141 lines
5.2 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const fg = require('fast-glob');
|
|
const { execSync } = require('child_process');
|
|
const { getContainerContext, resolveContainerPath, execContainerAction } = require('./container_bridge');
|
|
|
|
function resolvePath(workspace, p) {
|
|
if (!p) return workspace;
|
|
if (path.isAbsolute(p)) return p;
|
|
return path.join(workspace, p);
|
|
}
|
|
|
|
function hasRg() {
|
|
try {
|
|
execSync('rg --version', { stdio: 'ignore' });
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function searchWorkspaceTool(workspace, args) {
|
|
const root = resolvePath(workspace, args.root || '.');
|
|
const mode = args.mode;
|
|
const query = args.query || '';
|
|
const useRegex = !!args.use_regex;
|
|
const caseSensitive = !!args.case_sensitive;
|
|
const maxResults = args.max_results || 20;
|
|
const maxMatchesPerFile = args.max_matches_per_file || 3;
|
|
const includeGlob = Array.isArray(args.include_glob) && args.include_glob.length ? args.include_glob : ['**/*'];
|
|
const excludeGlob = Array.isArray(args.exclude_glob) ? args.exclude_glob : [];
|
|
const maxFileSize = args.max_file_size || null;
|
|
const ctx = getContainerContext();
|
|
|
|
if (ctx) {
|
|
const resolved = resolveContainerPath(workspace, args.root || '.', ctx);
|
|
if (resolved.error) return { success: false, error: resolved.error };
|
|
const resp = execContainerAction(ctx, 'search_workspace', {
|
|
mode,
|
|
query,
|
|
use_regex: useRegex,
|
|
case_sensitive: caseSensitive,
|
|
max_results: maxResults,
|
|
max_matches_per_file: maxMatchesPerFile,
|
|
include_glob: includeGlob,
|
|
exclude_glob: excludeGlob,
|
|
max_file_size: maxFileSize,
|
|
root: resolved.containerPath,
|
|
}, null, resolved.containerPath);
|
|
if (!resp.success) return { success: false, error: resp.error || '搜索失败' };
|
|
return resp;
|
|
}
|
|
|
|
if (mode === 'file') {
|
|
const files = await fg(includeGlob, { cwd: root, dot: true, ignore: excludeGlob, onlyFiles: true, absolute: true });
|
|
const matcher = useRegex ? new RegExp(query, caseSensitive ? '' : 'i') : null;
|
|
const matches = [];
|
|
for (const file of files) {
|
|
const name = path.basename(file);
|
|
const hay = caseSensitive ? name : name.toLowerCase();
|
|
const needle = caseSensitive ? query : query.toLowerCase();
|
|
const ok = useRegex ? matcher.test(name) : hay.includes(needle);
|
|
if (ok) {
|
|
matches.push(file);
|
|
if (matches.length >= maxResults) break;
|
|
}
|
|
}
|
|
return { success: true, mode: 'file', root, query, matches };
|
|
}
|
|
|
|
if (mode === 'content') {
|
|
if (hasRg()) {
|
|
const argsList = ['--line-number', '--no-heading', '--color=never'];
|
|
if (!useRegex) argsList.push('--fixed-strings');
|
|
if (!caseSensitive) argsList.push('-i');
|
|
if (maxMatchesPerFile) argsList.push(`--max-count=${maxMatchesPerFile}`);
|
|
if (maxFileSize) argsList.push(`--max-filesize=${maxFileSize}`);
|
|
for (const g of includeGlob) argsList.push('-g', g);
|
|
for (const g of excludeGlob) argsList.push('-g', `!${g}`);
|
|
argsList.push(query);
|
|
argsList.push(root);
|
|
let output = '';
|
|
try {
|
|
output = execSync(`rg ${argsList.map((a) => JSON.stringify(a)).join(' ')}`, { maxBuffer: 10 * 1024 * 1024 }).toString();
|
|
} catch (err) {
|
|
output = err.stdout ? err.stdout.toString() : '';
|
|
}
|
|
const lines = output.split(/\r?\n/).filter(Boolean);
|
|
const files = new Map();
|
|
for (const line of lines) {
|
|
const [filePath, lineNum, ...rest] = line.split(':');
|
|
const snippet = rest.join(':').trim();
|
|
if (!files.has(filePath)) files.set(filePath, []);
|
|
if (files.get(filePath).length < maxMatchesPerFile) {
|
|
files.get(filePath).push({ line: Number(lineNum), snippet });
|
|
}
|
|
if (files.size >= maxResults) break;
|
|
}
|
|
const results = Array.from(files.entries()).map(([file, matches]) => ({ file, matches }));
|
|
return { success: true, mode: 'content', root, query, results };
|
|
}
|
|
|
|
// fallback
|
|
const files = await fg(includeGlob, { cwd: root, dot: true, ignore: excludeGlob, onlyFiles: true, absolute: true });
|
|
const matcher = useRegex ? new RegExp(query, caseSensitive ? '' : 'i') : null;
|
|
const results = [];
|
|
for (const file of files) {
|
|
if (maxFileSize) {
|
|
try {
|
|
const stat = fs.statSync(file);
|
|
if (stat.size > maxFileSize) continue;
|
|
} catch (_) {}
|
|
}
|
|
const content = fs.readFileSync(file, 'utf8');
|
|
const lines = content.split(/\r?\n/);
|
|
const matches = [];
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const hay = caseSensitive ? line : line.toLowerCase();
|
|
const needle = caseSensitive ? query : query.toLowerCase();
|
|
const ok = useRegex ? matcher.test(line) : hay.includes(needle);
|
|
if (ok) {
|
|
matches.push({ line: i + 1, snippet: line.trim() });
|
|
if (matches.length >= maxMatchesPerFile) break;
|
|
}
|
|
}
|
|
if (matches.length) {
|
|
results.push({ file, matches });
|
|
if (results.length >= maxResults) break;
|
|
}
|
|
}
|
|
return { success: true, mode: 'content', root, query, results };
|
|
}
|
|
|
|
return { success: false, error: '未知 search_workspace mode' };
|
|
}
|
|
|
|
module.exports = { searchWorkspaceTool };
|