@@ -5,7 +5,7 @@ import * as os from 'os';
55import { spawn } from 'child_process' ;
66import * as pty from 'node-pty' ;
77import { FileHandler , filterLineToVisibleColumns , ColumnConfig } from './fileHandler' ;
8- import { IPC , SearchOptions , Bookmark , Highlight , HighlightGroup , ActivityEntry , LocalFileData } from '../shared/types' ;
8+ import { IPC , SearchOptions , Bookmark , Highlight , HighlightGroup , SearchConfig , ActivityEntry , LocalFileData } from '../shared/types' ;
99import * as Diff from 'diff' ;
1010import { analyzerRegistry , AnalyzerOptions } from './analyzers' ;
1111import { loadDatadogConfig , saveDatadogConfig , clearDatadogConfig , fetchDatadogLogs , DatadogConfig , DatadogFetchParams } from './datadogClient' ;
@@ -1570,6 +1570,181 @@ ipcMain.handle('highlight-group-delete', async (_, groupId: string) => {
15701570 return { success : true } ;
15711571} ) ;
15721572
1573+ // === Search Configs ===
1574+
1575+ const getSearchConfigsPath = ( ) => path . join ( getConfigDir ( ) , 'search-configs.json' ) ;
1576+ const GLOBAL_SEARCH_CONFIGS_KEY = '_global' ;
1577+
1578+ interface SearchConfigsStore {
1579+ [ key : string ] : SearchConfig [ ] ;
1580+ }
1581+
1582+ function loadSearchConfigsStore ( ) : SearchConfigsStore {
1583+ try {
1584+ ensureConfigDir ( ) ;
1585+ const configPath = getSearchConfigsPath ( ) ;
1586+ if ( fs . existsSync ( configPath ) ) {
1587+ const data = fs . readFileSync ( configPath , 'utf-8' ) ;
1588+ return JSON . parse ( data ) ;
1589+ }
1590+ } catch ( error ) {
1591+ console . error ( 'Failed to load search configs:' , error ) ;
1592+ }
1593+ return { } ;
1594+ }
1595+
1596+ function saveSearchConfigsStore ( store : SearchConfigsStore ) : void {
1597+ try {
1598+ ensureConfigDir ( ) ;
1599+ const configPath = getSearchConfigsPath ( ) ;
1600+ const cleanStore : SearchConfigsStore = { } ;
1601+ for ( const [ key , value ] of Object . entries ( store ) ) {
1602+ if ( value . length > 0 ) {
1603+ cleanStore [ key ] = value ;
1604+ }
1605+ }
1606+ fs . writeFileSync ( configPath , JSON . stringify ( cleanStore , null , 2 ) , 'utf-8' ) ;
1607+ } catch ( error ) {
1608+ console . error ( 'Failed to save search configs:' , error ) ;
1609+ }
1610+ }
1611+
1612+ function loadSearchConfigsForFile ( filePath : string ) : SearchConfig [ ] {
1613+ const store = loadSearchConfigsStore ( ) ;
1614+ const configs : SearchConfig [ ] = [ ] ;
1615+
1616+ // Load global configs
1617+ const globalConfigs = store [ GLOBAL_SEARCH_CONFIGS_KEY ] || [ ] ;
1618+ for ( const c of globalConfigs ) {
1619+ configs . push ( { ...c , isGlobal : true } ) ;
1620+ }
1621+
1622+ // Load file-specific configs
1623+ if ( filePath ) {
1624+ const fileConfigs = store [ filePath ] || [ ] ;
1625+ for ( const c of fileConfigs ) {
1626+ configs . push ( { ...c , isGlobal : false } ) ;
1627+ }
1628+ }
1629+
1630+ return configs ;
1631+ }
1632+
1633+ function saveSearchConfig ( config : SearchConfig ) : void {
1634+ const store = loadSearchConfigsStore ( ) ;
1635+
1636+ // Remove from all keys first
1637+ for ( const k of Object . keys ( store ) ) {
1638+ store [ k ] = store [ k ] . filter ( c => c . id !== config . id ) ;
1639+ }
1640+
1641+ const key = config . isGlobal ? GLOBAL_SEARCH_CONFIGS_KEY : ( currentFilePath || GLOBAL_SEARCH_CONFIGS_KEY ) ;
1642+ if ( ! store [ key ] ) store [ key ] = [ ] ;
1643+ store [ key ] . push ( config ) ;
1644+ saveSearchConfigsStore ( store ) ;
1645+ }
1646+
1647+ function removeSearchConfigFromStore ( id : string ) : void {
1648+ const store = loadSearchConfigsStore ( ) ;
1649+ let changed = false ;
1650+ for ( const k of Object . keys ( store ) ) {
1651+ const before = store [ k ] . length ;
1652+ store [ k ] = store [ k ] . filter ( c => c . id !== id ) ;
1653+ if ( store [ k ] . length !== before ) changed = true ;
1654+ }
1655+ if ( changed ) saveSearchConfigsStore ( store ) ;
1656+ }
1657+
1658+ ipcMain . handle ( IPC . SEARCH_CONFIG_SAVE , async ( _ , config : SearchConfig ) => {
1659+ saveSearchConfig ( config ) ;
1660+ return { success : true } ;
1661+ } ) ;
1662+
1663+ ipcMain . handle ( IPC . SEARCH_CONFIG_LOAD , async ( ) => {
1664+ const configs = loadSearchConfigsForFile ( currentFilePath || '' ) ;
1665+ return { success : true , configs } ;
1666+ } ) ;
1667+
1668+ ipcMain . handle ( IPC . SEARCH_CONFIG_DELETE , async ( _ , id : string ) => {
1669+ removeSearchConfigFromStore ( id ) ;
1670+ return { success : true } ;
1671+ } ) ;
1672+
1673+ ipcMain . handle ( IPC . SEARCH_CONFIG_BATCH , async ( _ , configs : Array < { id : string ; pattern : string ; isRegex : boolean ; matchCase : boolean ; wholeWord : boolean } > ) => {
1674+ const handler = getFileHandler ( ) ;
1675+ if ( ! handler ) return { success : false , error : 'No file open' } ;
1676+
1677+ const results : Record < string , Array < { lineNumber : number ; column : number ; length : number ; lineText : string } > > = { } ;
1678+ const totalConfigs = configs . length ;
1679+
1680+ for ( let i = 0 ; i < configs . length ; i ++ ) {
1681+ const cfg = configs [ i ] ;
1682+ try {
1683+ const searchOpts : SearchOptions = {
1684+ pattern : cfg . pattern ,
1685+ isRegex : cfg . isRegex ,
1686+ isWildcard : false ,
1687+ matchCase : cfg . matchCase ,
1688+ wholeWord : cfg . wholeWord ,
1689+ } ;
1690+
1691+ // Add filtered lines if filter is active
1692+ const filteredIndices = getFilteredLines ( ) ;
1693+ if ( filteredIndices ) {
1694+ searchOpts . filteredLineIndices = filteredIndices ;
1695+ }
1696+
1697+ const matches = await handler . search ( searchOpts , ( percent ) => {
1698+ const overallPercent = Math . round ( ( ( i + percent / 100 ) / totalConfigs ) * 100 ) ;
1699+ mainWindow ?. webContents . send ( IPC . SEARCH_CONFIG_BATCH_PROGRESS , { percent : overallPercent , configId : cfg . id } ) ;
1700+ } , { cancelled : false } ) ;
1701+
1702+ // If filter is active, remap line numbers
1703+ if ( filteredIndices && filteredIndices . length > 0 ) {
1704+ const filteredSet = new Set ( filteredIndices ) ;
1705+ const lineToFilteredIndex = new Map < number , number > ( ) ;
1706+ filteredIndices . forEach ( ( lineNum , idx ) => lineToFilteredIndex . set ( lineNum , idx ) ) ;
1707+
1708+ results [ cfg . id ] = matches
1709+ . filter ( m => filteredSet . has ( m . lineNumber ) )
1710+ . map ( m => ( {
1711+ lineNumber : lineToFilteredIndex . get ( m . lineNumber ) ?? m . lineNumber ,
1712+ column : m . column ,
1713+ length : m . length ,
1714+ lineText : m . lineText ,
1715+ } ) ) ;
1716+ } else {
1717+ results [ cfg . id ] = matches . map ( m => ( {
1718+ lineNumber : m . lineNumber ,
1719+ column : m . column ,
1720+ length : m . length ,
1721+ lineText : m . lineText ,
1722+ } ) ) ;
1723+ }
1724+ } catch ( error ) {
1725+ console . error ( `Search config batch error for ${ cfg . id } :` , error ) ;
1726+ results [ cfg . id ] = [ ] ;
1727+ }
1728+ }
1729+
1730+ return { success : true , results } ;
1731+ } ) ;
1732+
1733+ ipcMain . handle ( IPC . SEARCH_CONFIG_EXPORT , async ( _ , configId : string , lines : string [ ] ) => {
1734+ if ( ! currentFilePath ) return { success : false , error : 'No file open' } ;
1735+
1736+ try {
1737+ const baseName = path . basename ( currentFilePath , path . extname ( currentFilePath ) ) ;
1738+ const date = new Date ( ) . toISOString ( ) . replace ( / [: .] / g, '-' ) . substring ( 0 , 19 ) ;
1739+ const exportName = `${ baseName } _search_${ configId . substring ( 0 , 8 ) } _${ date } .txt` ;
1740+ const exportPath = path . join ( path . dirname ( currentFilePath ) , exportName ) ;
1741+ fs . writeFileSync ( exportPath , lines . join ( '\n' ) , 'utf-8' ) ;
1742+ return { success : true , filePath : exportPath } ;
1743+ } catch ( error ) {
1744+ return { success : false , error : String ( error ) } ;
1745+ }
1746+ } ) ;
1747+
15731748// === Utility ===
15741749
15751750ipcMain . handle ( 'get-file-info' , async ( ) => {
0 commit comments