Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/main/fd.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,31 +138,33 @@ class FdSearchSession {
const onDebug = (msg) => this.emit('debug', { msg });
const t0 = Date.now();

const perKwSets = [];
const union = new Set();
const stderrAny = [];
let stopped = false;

for (const kw of keywords) {
if (this.stopRequested) {
stopped = true;
break;
}
// eslint-disable-next-line no-await-in-loop
const r = await runSingleFd(exe, folder, kw, caseSensitive, respectIgnore, inc, exdList, this, onDebug);
// Run every keyword concurrently - fd spends most of its time walking the
// filesystem, so launching the scans in parallel overlaps that I/O instead
// of paying for it once per keyword.
const results = await Promise.all(
keywords.map((kw) =>
runSingleFd(exe, folder, kw, caseSensitive, respectIgnore, inc, exdList, this, onDebug)
.then((r) => ({ kw, r }))),
);

for (const { r } of results) {
if (r.error && r.error.code === 'ENOENT') {
this.emit('error', { msg: `fd not found: ${exe}` });
return;
}
}

const perKwSets = [];
const union = new Set();
for (const { kw, r } of results) {
if (r.stderr) stderrAny.push(`[${kw}] ${r.stderr}`);
for (const f of r.files) union.add(f);
perKwSets.push(r.files);
onDebug(`fd done kw='${kw}' | files=${r.files.size}`);
if (this.stopRequested) {
stopped = true;
break;
}
}
const stopped = this.stopRequested;

let final = union;
if (mode === 'AND' && perKwSets.length) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,11 @@ function registerIpc({ openCscopeWindow, getInitialFolder, getStartupLogs }) {
});

// ---- preview ----
ipcMain.handle('preview:build', (_e, { filePath, keywords, caseSensitive, contextLines }) => {
ipcMain.handle('preview:build', async (_e, { filePath, keywords, caseSensitive, contextLines }) => {
try {
const kws = Array.isArray(keywords) ? keywords : parseKeywords(keywords);
const lines = (typeof contextLines === 'number' && contextLines > 0) ? contextLines : PreviewConfig.CONTEXT_LINES;
const text = buildPreviewText(filePath, kws, !!caseSensitive, lines);
const text = await buildPreviewText(filePath, kws, !!caseSensitive, lines);
return { ok: true, text };
} catch (err) {
return { ok: false, text: `(Preview build failed)\n${err}\n` };
Expand Down
42 changes: 25 additions & 17 deletions src/main/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,6 @@
const fs = require('fs');
const { PreviewConfig } = require('./config');

function getMatchLineNumbers(allLines, keyword, caseSensitive) {
const out = [];
const needle = caseSensitive ? keyword : keyword.toLowerCase();
if (!needle) return out;
for (let i = 0; i < allLines.length; i += 1) {
const hay = caseSensitive ? allLines[i] : allLines[i].toLowerCase();
if (hay.includes(needle)) out.push(i + 1);
}
return out;
}

function mergeRanges(ranges) {
if (!ranges.length) return [];
const sorted = ranges.slice().sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]));
Expand All @@ -47,23 +36,42 @@ function mergeRanges(ranges) {

// Build the preview text for a file given keywords. Returns a string with
// "N: line" prefixes and optional block separators.
function buildPreviewText(filePath, keywords, caseSensitive, contextLines) {
async function buildPreviewText(filePath, keywords, caseSensitive, contextLines) {
let raw;
try {
raw = fs.readFileSync(filePath, 'utf8');
raw = await fs.promises.readFile(filePath, 'utf8');
} catch (e) {
return `(Failed to open file)\n${e}\n`;
}
const allLines = raw.split(/\r?\n/);
const total = allLines.length;
if (total === 0) return '(Empty file)\n';

const matchUnion = new Set();
// Normalise the keyword needles once. For case-insensitive matching we also
// lowercase each line a single time (instead of once per keyword) below.
const needles = [];
for (const kw of keywords) {
for (const ln of getMatchLineNumbers(allLines, kw, caseSensitive)) matchUnion.add(ln);
const n = caseSensitive ? kw : (kw || '').toLowerCase();
if (n) needles.push(n);
}

// Single pass over the file, testing every keyword per line. Pushing line
// numbers in ascending order yields an already-sorted, duplicate-free list,
// so no Set or sort is needed afterwards.
const matchLines = [];
if (needles.length) {
for (let i = 0; i < total; i += 1) {
const hay = caseSensitive ? allLines[i] : allLines[i].toLowerCase();
for (let k = 0; k < needles.length; k += 1) {
if (hay.includes(needles[k])) {
matchLines.push(i + 1);
break;
}
}
}
}

if (matchUnion.size === 0) {
if (matchLines.length === 0) {
const headN = 10;
const n = Math.min(headN, total);
const out = ['(No matches in this file with current keyword(s). Show file head)', ''];
Expand All @@ -73,7 +81,7 @@ function buildPreviewText(filePath, keywords, caseSensitive, contextLines) {
}

let ranges = [];
for (const ln of [...matchUnion].sort((a, b) => a - b)) {
for (const ln of matchLines) {
const s = Math.max(1, ln - contextLines);
const e = Math.min(total, ln + contextLines);
ranges.push([s, e]);
Expand Down
12 changes: 10 additions & 2 deletions src/main/rg.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const { SearchConfig } = require('./config');

// Build the ripgrep argv (without the exe). Emits --json --stats and uses
// fixed-strings literal matching, matching M2_SEEK behavior.
function rgSearchArgs(folder, keyword, inc, exd, exf, caseSensitive, respectIgnoreFiles) {
//
// `keywords` may be a single string or an array. When several patterns are
// passed they are emitted as multiple `-e` patterns so ripgrep matches their
// union (OR) in a SINGLE filesystem scan instead of one process per keyword.
function rgSearchArgs(folder, keywords, inc, exd, exf, caseSensitive, respectIgnoreFiles) {
const args = ['--json', '--stats', '--fixed-strings'];

if (!respectIgnoreFiles) {
Expand All @@ -34,7 +38,11 @@ function rgSearchArgs(folder, keyword, inc, exd, exf, caseSensitive, respectIgno
args.push('-g', g);
}

args.push(keyword, folder);
// `-e` also protects patterns that start with '-' from being parsed as flags.
const kws = Array.isArray(keywords) ? keywords : [keywords];
for (const kw of kws) args.push('-e', kw);

args.push(folder);
return args;
}

Expand Down
27 changes: 21 additions & 6 deletions src/main/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ const { SearchConfig, LiveUpdateConfig } = require('./config');

const WINDOWS = process.platform === 'win32';

// Run a single ripgrep keyword search, streaming match counts.
// onMatch(path) is called for every match line. Returns a promise resolving to
// { counts: Map, filesSearched, stderr, stopped }.
function runSingleRg(rgExe, folder, keyword, inc, exd, exf, caseSensitive, respectIgnore, ctx, onMatch, onDebug) {
// Run a single ripgrep invocation, streaming match counts. `keywords` may be a
// single string or an array; passing several keywords matches their union (OR)
// in one scan. onMatch(path) is called for every match line. Returns a promise
// resolving to { counts: Map, filesSearched, stderr, stopped }.
function runSingleRg(rgExe, folder, keywords, inc, exd, exf, caseSensitive, respectIgnore, ctx, onMatch, onDebug) {
return new Promise((resolve) => {
const args = rgSearchArgs(folder, keyword, inc, exd, exf, caseSensitive, respectIgnore);
const args = rgSearchArgs(folder, keywords, inc, exd, exf, caseSensitive, respectIgnore);
if (onDebug) onDebug(`RUN (cmdline): ${formatCmdline([rgExe, ...args])}`);

const counts = new Map();
Expand Down Expand Up @@ -197,7 +198,21 @@ class SearchSession {
const filesSearchedSeen = [];
let stopped = false;

if (useParallelAnd) {
if (mode !== 'AND') {
// OR: match the union of all keywords in ONE ripgrep process, so the
// filesystem is traversed a single time instead of once per keyword.
const r = await runSingleRg(exe, folder, keywords, inc, exd, exf, caseSensitive, respectIgnore, this, onMatch, onDebug);
if (r.error && r.error.code === 'ENOENT') {
this.emit('error', { msg: `rg not found: ${exe}` });
return;
}
if (r.filesSearched !== null && r.filesSearched !== undefined) filesSearchedSeen.push(r.filesSearched);
if (r.stderr) stderrAny.push(r.stderr);
if (r.stopped) stopped = true;
perKwCounts.push(r.counts);
perKwFiles.push(new Set(r.counts.keys()));
onDebug(`[OR] done | files=${r.counts.size} matches=${sumMap(r.counts)}`);
} else if (useParallelAnd) {
const results = await Promise.all(
keywords.map((kw) =>
runSingleRg(exe, folder, kw, inc, exd, exf, caseSensitive, respectIgnore, this, onMatch, onDebug)
Expand Down
Loading