Skip to content

Commit 5a85687

Browse files
ozgesolidkeyclaude
andcommitted
Add non-blocking filter, search direction/start line, and tab state preservation
- Make filter non-blocking: yield to event loop every 50ms with progress updates, add cancellation support, pre-compile regex patterns once - Add search direction (top-to-bottom / bottom-to-top) and start line options via gear popup, with "Search from here" in context menu - Preserve analysis results, filter state, and highlights across tab switches so content and sidebar state are not lost Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a3f2db1 commit 5a85687

6 files changed

Lines changed: 281 additions & 42 deletions

File tree

src/main/index.ts

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,12 +1388,17 @@ function compileAdvancedFilter(config: AdvancedFilterConfig): CompiledMatcher {
13881388
return (text: string, level: string) => compiledGroups.every(fn => fn(text, level));
13891389
}
13901390

1391+
// Cancellation signal for filter
1392+
let filterSignal = { cancelled: false };
1393+
13911394
ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
13921395
const handler = getFileHandler();
13931396
if (!handler || !currentFilePath) {
13941397
return { success: false, error: 'No file open' };
13951398
}
13961399

1400+
filterSignal = { cancelled: false };
1401+
13971402
try {
13981403
const totalLines = handler.getTotalLines();
13991404
const matchingLines: Set<number> = new Set();
@@ -1417,26 +1422,43 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
14171422
// Pattern matching helper respecting matchCase and exactMatch options
14181423
const caseSensitive = config.matchCase || false;
14191424
const exactMatch = config.exactMatch || false;
1420-
const matchPattern = (text: string, pattern: string): boolean => {
1425+
1426+
// Pre-compile regex patterns once for performance (avoid re-creating RegExp per line)
1427+
type CompiledPattern = { regex: RegExp } | { literal: string; lowerLiteral: string };
1428+
const compilePattern = (pattern: string): CompiledPattern => {
14211429
if (exactMatch) {
1422-
// Literal substring match
1423-
return caseSensitive
1424-
? text.includes(pattern)
1425-
: text.toLowerCase().includes(pattern.toLowerCase());
1430+
return { literal: pattern, lowerLiteral: pattern.toLowerCase() };
14261431
}
1427-
// Regex match with fallback to substring
14281432
try {
1429-
return new RegExp(pattern, caseSensitive ? '' : 'i').test(text);
1433+
return { regex: new RegExp(pattern, caseSensitive ? '' : 'i') };
14301434
} catch {
1431-
return caseSensitive
1432-
? text.includes(pattern)
1433-
: text.toLowerCase().includes(pattern.toLowerCase());
1435+
return { literal: pattern, lowerLiteral: pattern.toLowerCase() };
1436+
}
1437+
};
1438+
1439+
const compiledIncludePatterns = config.includePatterns.map(compilePattern);
1440+
const compiledExcludePatterns = config.excludePatterns.map(compilePattern);
1441+
1442+
const matchCompiled = (text: string, compiled: CompiledPattern): boolean => {
1443+
if ('regex' in compiled) {
1444+
return compiled.regex.test(text);
14341445
}
1446+
return caseSensitive
1447+
? text.includes(compiled.literal)
1448+
: text.toLowerCase().includes(compiled.lowerLiteral);
14351449
};
14361450

14371451
// Process in batches for performance
14381452
const batchSize = 10000;
1453+
let processedLines = 0;
1454+
let lastProgressUpdate = Date.now();
1455+
14391456
for (let start = 0; start < totalLines; start += batchSize) {
1457+
// Check for cancellation
1458+
if (filterSignal.cancelled) {
1459+
return { success: false, error: 'Cancelled' };
1460+
}
1461+
14401462
const count = Math.min(batchSize, totalLines - start);
14411463
const lines = handler.getLines(start, count);
14421464

@@ -1456,13 +1478,13 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
14561478
}
14571479

14581480
// Include patterns (OR logic)
1459-
if (matches && config.includePatterns.length > 0) {
1460-
matches = config.includePatterns.some(pattern => matchPattern(line.text, pattern));
1481+
if (matches && compiledIncludePatterns.length > 0) {
1482+
matches = compiledIncludePatterns.some(cp => matchCompiled(line.text, cp));
14611483
}
14621484

14631485
// Track exclude matches separately (exact lines only)
14641486
if (hasBasicExclude) {
1465-
const excluded = config.excludePatterns.some(pattern => matchPattern(line.text, pattern));
1487+
const excluded = compiledExcludePatterns.some(cp => matchCompiled(line.text, cp));
14661488
if (excluded) {
14671489
excludeLines.add(line.lineNumber);
14681490
}
@@ -1473,6 +1495,17 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
14731495
matchingLines.add(line.lineNumber);
14741496
}
14751497
}
1498+
1499+
processedLines += count;
1500+
1501+
// Yield to event loop and send progress every 50ms to keep UI responsive
1502+
const now = Date.now();
1503+
if (now - lastProgressUpdate > 50) {
1504+
await yieldToEventLoop();
1505+
const progress = Math.round((processedLines / totalLines) * 100);
1506+
mainWindow?.webContents.send('filter-progress', { percent: Math.min(progress, 99) });
1507+
lastProgressUpdate = Date.now();
1508+
}
14761509
}
14771510

14781511
// Add context lines around include matches (before exclude removal)
@@ -1508,6 +1541,11 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
15081541
}
15091542
});
15101543

1544+
ipcMain.handle('cancel-filter', async () => {
1545+
filterSignal.cancelled = true;
1546+
return { success: true };
1547+
});
1548+
15111549
ipcMain.handle('clear-filter', async () => {
15121550
if (currentFilePath) {
15131551
filterState.delete(currentFilePath);

src/preload/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,15 @@ const api = {
166166
applyFilter: (config: any): Promise<{ success: boolean; stats?: { filteredLines: number }; error?: string }> =>
167167
ipcRenderer.invoke('apply-filter', config),
168168

169+
cancelFilter: (): Promise<{ success: boolean }> =>
170+
ipcRenderer.invoke('cancel-filter'),
171+
172+
onFilterProgress: (callback: (data: { percent: number }) => void): (() => void) => {
173+
const handler = (_: any, data: { percent: number }) => callback(data);
174+
ipcRenderer.on('filter-progress', handler);
175+
return () => ipcRenderer.removeListener('filter-progress', handler);
176+
},
177+
169178
clearFilter: (): Promise<{ success: boolean }> =>
170179
ipcRenderer.invoke('clear-filter'),
171180

src/renderer/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@
4343
<input type="checkbox" id="search-whole-word"> Whole Word
4444
</label>
4545
<button id="btn-search" class="toolbar-btn" title="Search">Search</button>
46+
<div class="search-options-wrapper">
47+
<button id="btn-search-options" class="toolbar-btn small" title="Search options">&#9881;</button>
48+
<div id="search-options-popup" class="search-options-popup hidden">
49+
<div class="search-option-row">
50+
<label class="search-option-label">Direction</label>
51+
<button id="search-direction" class="toolbar-btn small search-direction-btn" title="Toggle search direction">&#8595; Top to Bottom</button>
52+
</div>
53+
<div class="search-option-row">
54+
<label class="search-option-label" for="search-start-line">Start from line</label>
55+
<input type="number" id="search-start-line" class="search-start-line-input" placeholder="1" min="1">
56+
</div>
57+
</div>
58+
</div>
4659
<button id="btn-prev-result" class="toolbar-btn small" title="Previous result" disabled>&lt;</button>
4760
<span id="search-result-count" class="result-count"></span>
4861
<span id="search-engine-badge" class="search-engine-badge" title="Search engine"></span>

0 commit comments

Comments
 (0)