n8n
Version:
n8n Workflow Automation Tool
332 lines • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.runInternalCommand = runInternalCommand;
exports.runSearchOperation = runSearchOperation;
const file_references_1 = require("./file-references");
const DEFAULT_READ_RANGE_CONTEXT = 6;
const MAX_SEARCH_MATCH_TEXT_LENGTH = 500;
const MULTI_QUERY_WINDOW_LINES = 3;
async function runInternalCommand(commandService, workspaceRoot, request) {
const result = await commandService.run(workspaceRoot, request);
return { ...result, command: request.command };
}
async function runSearchOperation(input, workspaceRoot, files, commandService) {
if (input.query === undefined && input.queries === undefined) {
return {
operation: 'search',
files,
error: 'Either query or queries must be provided for search.',
};
}
const requestedFiles = (0, file_references_1.mapFileReferences)(files, input.files);
const primaryPattern = getPrimarySearchPattern(input);
const commandPattern = getSearchCommandPattern(input);
const commandFixedStrings = getSearchCommandFixedStrings(input);
let contentResult;
const countResult = await runInternalCommand(commandService, workspaceRoot, {
command: 'git_grep',
pattern: commandPattern,
outputMode: 'count',
caseInsensitive: input.caseInsensitive,
fixedStrings: commandFixedStrings,
files: requestedFiles,
});
let counts = parseCountOutput(countResult.stdout, files);
let multiQueryMatches;
if (input.queries) {
contentResult = await runInternalCommand(commandService, workspaceRoot, {
command: 'git_grep',
pattern: commandPattern,
caseInsensitive: input.caseInsensitive,
fixedStrings: commandFixedStrings,
context: input.context,
files: requestedFiles,
});
multiQueryMatches = filterMultiQueryMatches(parseSearchMatches(contentResult.stdout, files), input.queries, input.match_mode, input.caseInsensitive);
counts = buildCountsFromMatches(multiQueryMatches, files);
}
if (input.output_mode === 'files_with_matches') {
const slicedCounts = sliceResults(counts, input.offset, input.head_limit);
return {
operation: 'search',
files,
result: toDisplayResult(countResult, formatSearchFiles(counts, input.offset, input.head_limit), slicedCounts.truncated),
search: buildSearchResult({
mode: input.output_mode,
query: primaryPattern,
queries: input.queries,
matchMode: input.queries ? input.match_mode : undefined,
counts,
matches: [],
offset: input.offset,
headLimit: input.head_limit,
hint: buildSearchHint('files_with_matches', slicedCounts, input.head_limit),
}),
};
}
if (input.output_mode === 'count') {
const slicedCounts = sliceResults(counts, input.offset, input.head_limit);
return {
operation: 'search',
files,
result: toDisplayResult(countResult, formatSearchCounts(counts, input.offset, input.head_limit), slicedCounts.truncated),
search: buildSearchResult({
mode: input.output_mode,
query: primaryPattern,
queries: input.queries,
matchMode: input.queries ? input.match_mode : undefined,
counts,
matches: [],
offset: input.offset,
headLimit: input.head_limit,
hint: buildSearchHint('count', slicedCounts, input.head_limit),
}),
};
}
contentResult ??= await runInternalCommand(commandService, workspaceRoot, {
command: 'git_grep',
pattern: commandPattern,
caseInsensitive: input.caseInsensitive,
fixedStrings: commandFixedStrings,
context: input.context,
files: requestedFiles,
});
const parsedMatches = parseSearchMatches(contentResult.stdout, files);
const matches = multiQueryMatches ?? parsedMatches;
const slicedMatches = sliceResults(matches, input.offset, input.head_limit);
const displayMatches = slicedMatches.items.map(toSearchMatchOutput);
const search = buildSearchResult({
mode: input.output_mode,
query: primaryPattern,
queries: input.queries,
matchMode: input.queries ? input.match_mode : undefined,
counts,
matches: displayMatches,
offset: input.offset,
headLimit: input.head_limit,
nextOffset: slicedMatches.nextOffset,
hint: buildSearchHint('content', slicedMatches, input.head_limit),
});
return {
operation: 'search',
files,
result: toDisplayResult(contentResult, formatSearchMatches(displayMatches, slicedMatches, input.head_limit), search.truncated || contentResult.truncated),
search,
};
}
function toDisplayResult(result, stdout, truncated = false) {
return {
...result,
stdout,
truncated: result.truncated || truncated,
};
}
function parseCountOutput(stdout, files) {
const byRelativePath = new Map(files.map((file) => [file.relativePath, file]));
const counts = stdout
.split('\n')
.flatMap((line) => {
if (line.trim() === '')
return [];
const separatorIndex = line.lastIndexOf(':');
if (separatorIndex === -1)
return [];
const relativePath = normaliseGrepPath(line.slice(0, separatorIndex));
const matchCount = Number(line.slice(separatorIndex + 1));
const file = byRelativePath.get(relativePath);
if (!file || !Number.isFinite(matchCount) || matchCount <= 0)
return [];
return [
{
id: file.id,
fileName: file.fileName,
relativePath: file.relativePath,
matchCount,
},
];
})
.sort((left, right) => right.matchCount - left.matchCount);
return counts;
}
function parseSearchMatches(stdout, files) {
const byRelativePath = new Map(files.map((file) => [file.relativePath, file]));
return stdout.split('\n').flatMap((line) => {
const parsed = parseGrepLine(line);
if (!parsed?.isMatch)
return [];
const file = byRelativePath.get(normaliseGrepPath(parsed.filePath));
if (!file || parsed.lineNumber === undefined)
return [];
const fullText = line.slice(parsed.contentStartIndex);
const { text, truncated } = truncateMatchText(fullText);
return [
{
fileId: file.id,
fileName: file.fileName,
relativePath: file.relativePath,
lineNumber: parsed.lineNumber,
fullText,
text,
readRange: toReadRange(parsed.lineNumber),
truncated,
},
];
});
}
function toSearchMatchOutput({ fullText: _fullText, ...match }) {
return match;
}
function truncateMatchText(text) {
if (text.length <= MAX_SEARCH_MATCH_TEXT_LENGTH)
return { text };
return {
text: `${text.slice(0, MAX_SEARCH_MATCH_TEXT_LENGTH)}... [line truncated; use read for full text]`,
truncated: true,
};
}
function filterMultiQueryMatches(matches, queries, matchMode, caseInsensitive) {
const normalizedQueries = queries.map((query) => normalizeSearchText(query, caseInsensitive));
if (matchMode === 'any') {
return matches.filter((match) => normalizedQueries.some((query) => normalizeSearchText(match.fullText, caseInsensitive).includes(query)));
}
if (matchMode === 'all_on_same_line') {
return matches.filter((match) => {
const text = normalizeSearchText(match.fullText, caseInsensitive);
return normalizedQueries.every((query) => text.includes(query));
});
}
return matches.filter((match) => hasAllQueriesInNearbyWindow(matches, match.relativePath, match.lineNumber, normalizedQueries, caseInsensitive));
}
function buildCountsFromMatches(matches, files) {
const countByRelativePath = new Map();
for (const match of matches) {
countByRelativePath.set(match.relativePath, (countByRelativePath.get(match.relativePath) ?? 0) + 1);
}
return files
.flatMap((file) => {
const matchCount = countByRelativePath.get(file.relativePath) ?? 0;
if (matchCount === 0)
return [];
return [
{
id: file.id,
fileName: file.fileName,
relativePath: file.relativePath,
matchCount,
},
];
})
.sort((left, right) => right.matchCount - left.matchCount);
}
function hasAllQueriesInNearbyWindow(matches, relativePath, lineNumber, queries, caseInsensitive) {
const sameFileMatches = matches.filter((match) => match.relativePath === relativePath);
return sameFileMatches.some((windowStart) => {
const start = windowStart.lineNumber;
const end = start + MULTI_QUERY_WINDOW_LINES - 1;
if (lineNumber < start || lineNumber > end)
return false;
const windowText = sameFileMatches
.filter((match) => match.lineNumber >= start && match.lineNumber <= end)
.map((match) => normalizeSearchText(match.fullText, caseInsensitive))
.join('\n');
return queries.every((query) => windowText.includes(query));
});
}
function normalizeSearchText(text, caseInsensitive) {
return caseInsensitive ? text.toLowerCase() : text;
}
function toReadRange(lineNumber) {
return {
start: Math.max(1, lineNumber - DEFAULT_READ_RANGE_CONTEXT),
end: lineNumber + DEFAULT_READ_RANGE_CONTEXT,
};
}
function getPrimarySearchPattern(input) {
return input.query ?? input.queries?.[0] ?? '';
}
function getSearchCommandPattern(input) {
if (!input.queries)
return input.query ?? '';
return input.queries.map(escapeExtendedRegex).join('|');
}
function getSearchCommandFixedStrings(input) {
return input.queries ? false : (input.fixedStrings ?? true);
}
function escapeExtendedRegex(pattern) {
return pattern.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}
function buildSearchResult({ mode, query, queries, matchMode, counts, matches, offset, headLimit, nextOffset, hint, }) {
const slicedCounts = sliceResults(counts, offset, headLimit);
const totalMatchingLines = counts.reduce((total, count) => total + count.matchCount, 0);
const effectiveNextOffset = mode === 'content' ? nextOffset : slicedCounts.nextOffset;
return {
mode,
query,
queries,
matchMode,
totalMatchingFiles: counts.length,
totalMatchingLines,
files: slicedCounts.items,
matches,
truncated: slicedCounts.truncated || effectiveNextOffset !== undefined,
appliedLimit: (mode === 'content' && effectiveNextOffset !== undefined) || slicedCounts.truncated
? headLimit
: undefined,
appliedOffset: offset > 0 ? offset : undefined,
nextOffset: effectiveNextOffset,
hint,
};
}
function sliceResults(items, offset, headLimit) {
const sliced = headLimit === 0 ? items.slice(offset) : items.slice(offset, offset + headLimit);
return {
items: sliced,
truncated: offset + sliced.length < items.length,
nextOffset: offset + sliced.length < items.length ? offset + sliced.length : undefined,
};
}
function buildSearchHint(mode, sliced, headLimit) {
if (sliced.nextOffset !== undefined) {
return `Additional ${mode === 'files_with_matches' ? 'files' : mode === 'count' ? 'counts' : 'matches'} omitted. Continue with offset=${sliced.nextOffset} and head_limit=${headLimit}, or ${mode === 'content' ? 'read one of the returned ranges' : 'switch to output_mode=content after choosing a file'}.`;
}
if (mode === 'content')
return 'Use read with the suggested line ranges for grounded citations.';
if (mode === 'count')
return 'Use output_mode=content after choosing a file or exact phrase.';
return 'Use read on a matching file or switch to output_mode=content for line anchors.';
}
function formatSearchFiles(counts, offset, headLimit) {
const sliced = sliceResults(counts, offset, headLimit);
const lines = sliced.items.map((file) => file.fileName);
if (sliced.truncated)
lines.push(buildSearchHint('files_with_matches', sliced, headLimit));
return lines.length > 0 ? `${lines.join('\n')}\n` : '';
}
function formatSearchCounts(counts, offset, headLimit) {
const sliced = sliceResults(counts, offset, headLimit);
const lines = sliced.items.map((file) => `${file.fileName}: ${file.matchCount}`);
if (sliced.truncated)
lines.push(buildSearchHint('count', sliced, headLimit));
return lines.length > 0 ? `${lines.join('\n')}\n` : '';
}
function formatSearchMatches(matches, sliced, headLimit) {
const lines = matches.map((match) => `${match.fileName}:${match.lineNumber}:${match.text} (read ${match.readRange.start}-${match.readRange.end})`);
if (sliced.truncated)
lines.push(buildSearchHint('content', sliced, headLimit));
return lines.length > 0 ? `${lines.join('\n')}\n` : '';
}
function parseGrepLine(line) {
const match = /^(?<filePath>.*)(?<separator>[:-])(?<lineNumber>\d+)(?<contentSeparator>[:-])/.exec(line);
if (!match?.groups)
return undefined;
return {
filePath: normaliseGrepPath(match.groups.filePath),
isMatch: match.groups.separator === ':' && match.groups.contentSeparator === ':',
lineNumber: Number(match.groups.lineNumber),
contentStartIndex: match[0].length,
};
}
function normaliseGrepPath(filePath) {
return filePath.startsWith('./') ? filePath.slice(2) : filePath;
}
//# sourceMappingURL=search.operation.js.map