@@ -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+
13911394ipcMain . 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+
15111549ipcMain . handle ( 'clear-filter' , async ( ) => {
15121550 if ( currentFilePath ) {
15131551 filterState . delete ( currentFilePath ) ;
0 commit comments