@@ -165,6 +165,9 @@ interface AppState {
165165 columnConfig : ColumnConfig | null ;
166166 // Notes file tracking
167167 currentNotesFile : string | null ;
168+ // Folder search
169+ folderSearchResults : FolderSearchMatch [ ] ;
170+ isFolderSearching : boolean ;
168171}
169172
170173const state : AppState = {
@@ -191,6 +194,8 @@ const state: AppState = {
191194 folders : [ ] ,
192195 columnConfig : null ,
193196 currentNotesFile : null ,
197+ folderSearchResults : [ ] ,
198+ isFolderSearching : false ,
194199} ;
195200
196201// Constants
@@ -247,6 +252,10 @@ const elements = {
247252 welcomeMessage : document . getElementById ( 'welcome-message' ) as HTMLDivElement ,
248253 foldersList : document . getElementById ( 'folders-list' ) as HTMLDivElement ,
249254 btnAddFolder : document . getElementById ( 'btn-add-folder' ) as HTMLButtonElement ,
255+ folderSearchInput : document . getElementById ( 'folder-search-input' ) as HTMLInputElement ,
256+ btnFolderSearch : document . getElementById ( 'btn-folder-search' ) as HTMLButtonElement ,
257+ btnFolderSearchCancel : document . getElementById ( 'btn-folder-search-cancel' ) as HTMLButtonElement ,
258+ folderSearchResults : document . getElementById ( 'folder-search-results' ) as HTMLDivElement ,
250259 fileStats : document . getElementById ( 'file-stats' ) as HTMLDivElement ,
251260 analysisResults : document . getElementById ( 'analysis-results' ) as HTMLDivElement ,
252261 analysisProgress : document . getElementById ( 'analysis-progress' ) as HTMLDivElement ,
@@ -1757,12 +1766,17 @@ async function openFolder(): Promise<void> {
17571766 collapsed : false ,
17581767 } ) ;
17591768 renderFolderTree ( ) ;
1769+ updateFolderSearchState ( ) ;
17601770 }
17611771}
17621772
17631773function removeFolder ( folderPath : string ) : void {
17641774 state . folders = state . folders . filter ( ( f ) => f . path !== folderPath ) ;
17651775 renderFolderTree ( ) ;
1776+ updateFolderSearchState ( ) ;
1777+ if ( state . folders . length === 0 ) {
1778+ closeFolderSearchResults ( ) ;
1779+ }
17661780}
17671781
17681782function toggleFolder ( folderPath : string ) : void {
@@ -1849,6 +1863,114 @@ function renderFolderTree(): void {
18491863 } ) ;
18501864}
18511865
1866+ // === Folder Search ===
1867+
1868+ function updateFolderSearchState ( ) : void {
1869+ const hasFolders = state . folders . length > 0 ;
1870+ elements . folderSearchInput . disabled = ! hasFolders ;
1871+ elements . btnFolderSearch . disabled = ! hasFolders ;
1872+ }
1873+
1874+ async function performFolderSearch ( ) : Promise < void > {
1875+ const pattern = elements . folderSearchInput . value . trim ( ) ;
1876+ if ( ! pattern || state . folders . length === 0 ) return ;
1877+
1878+ state . isFolderSearching = true ;
1879+ state . folderSearchResults = [ ] ;
1880+
1881+ elements . btnFolderSearch . classList . add ( 'hidden' ) ;
1882+ elements . btnFolderSearchCancel . classList . remove ( 'hidden' ) ;
1883+ elements . folderSearchResults . classList . remove ( 'hidden' ) ;
1884+ elements . folderSearchResults . innerHTML = '<div class="folder-search-searching">Searching...</div>' ;
1885+
1886+ const unsubscribe = window . api . onFolderSearchProgress ( ( data ) => {
1887+ if ( state . isFolderSearching ) {
1888+ elements . folderSearchResults . innerHTML = `<div class="folder-search-searching">Searching... ${ data . matchCount } matches found</div>` ;
1889+ }
1890+ } ) ;
1891+
1892+ try {
1893+ const folderPaths = state . folders . map ( f => f . path ) ;
1894+ const result = await window . api . folderSearch ( folderPaths , pattern , { isRegex : false , matchCase : false } ) ;
1895+
1896+ if ( result . success && result . matches ) {
1897+ state . folderSearchResults = result . matches ;
1898+ renderFolderSearchResults ( pattern , result . cancelled ) ;
1899+ } else {
1900+ elements . folderSearchResults . innerHTML = `<div class="folder-search-searching">${ result . error || 'Search failed' } </div>` ;
1901+ }
1902+ } catch ( error ) {
1903+ elements . folderSearchResults . innerHTML = `<div class="folder-search-searching">Error: ${ error } </div>` ;
1904+ } finally {
1905+ unsubscribe ( ) ;
1906+ state . isFolderSearching = false ;
1907+ elements . btnFolderSearch . classList . remove ( 'hidden' ) ;
1908+ elements . btnFolderSearchCancel . classList . add ( 'hidden' ) ;
1909+ }
1910+ }
1911+
1912+ function cancelFolderSearch ( ) : void {
1913+ if ( state . isFolderSearching ) {
1914+ window . api . cancelFolderSearch ( ) ;
1915+ }
1916+ }
1917+
1918+ function closeFolderSearchResults ( ) : void {
1919+ state . folderSearchResults = [ ] ;
1920+ elements . folderSearchResults . classList . add ( 'hidden' ) ;
1921+ elements . folderSearchResults . innerHTML = '' ;
1922+ }
1923+
1924+ function highlightMatch ( text : string , pattern : string ) : string {
1925+ const escaped = escapeHtml ( text ) ;
1926+ const patternEscaped = pattern . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
1927+ const regex = new RegExp ( `(${ patternEscaped } )` , 'gi' ) ;
1928+ return escaped . replace ( regex , '<span class="folder-search-match">$1</span>' ) ;
1929+ }
1930+
1931+ function renderFolderSearchResults ( pattern : string , cancelled ?: boolean ) : void {
1932+ const matches = state . folderSearchResults ;
1933+
1934+ if ( matches . length === 0 ) {
1935+ elements . folderSearchResults . innerHTML = '<div class="folder-search-searching">No matches found</div>' ;
1936+ return ;
1937+ }
1938+
1939+ const header = `
1940+ <div class="folder-search-header">
1941+ <span>${ matches . length } ${ cancelled ? '+' : '' } match${ matches . length !== 1 ? 'es' : '' } </span>
1942+ <button class="folder-search-close" title="Close">×</button>
1943+ </div>
1944+ ` ;
1945+
1946+ const items = matches . map ( ( match , index ) => {
1947+ const relPath = match . filePath ;
1948+ const lineText = match . lineText . length > 200 ? match . lineText . substring ( 0 , 200 ) + '...' : match . lineText ;
1949+
1950+ return `
1951+ <div class="folder-search-item" data-index="${ index } ">
1952+ <span class="folder-search-file">${ escapeHtml ( match . fileName ) } </span>:<span class="folder-search-line">${ match . lineNumber } </span>: <span class="folder-search-text">${ highlightMatch ( lineText , pattern ) } </span>
1953+ </div>
1954+ ` ;
1955+ } ) . join ( '' ) ;
1956+
1957+ elements . folderSearchResults . innerHTML = header + items ;
1958+
1959+ // Add event listeners
1960+ elements . folderSearchResults . querySelector ( '.folder-search-close' ) ?. addEventListener ( 'click' , closeFolderSearchResults ) ;
1961+
1962+ elements . folderSearchResults . querySelectorAll ( '.folder-search-item' ) . forEach ( ( item ) => {
1963+ item . addEventListener ( 'click' , async ( ) => {
1964+ const index = parseInt ( ( item as HTMLElement ) . dataset . index || '0' , 10 ) ;
1965+ const match = state . folderSearchResults [ index ] ;
1966+ if ( match ) {
1967+ await loadFile ( match . filePath ) ;
1968+ goToLine ( match . lineNumber - 1 ) ; // Convert to 0-based
1969+ }
1970+ } ) ;
1971+ } ) ;
1972+ }
1973+
18521974async function loadFile ( filePath : string , createNewTab : boolean = true ) : Promise < void > {
18531975 // Check if file is already open in a tab BEFORE calling backend
18541976 const existingTab = findTabByFilePath ( filePath ) ;
@@ -3374,6 +3496,17 @@ function init(): void {
33743496 // Folder operations
33753497 elements . btnAddFolder . addEventListener ( 'click' , openFolder ) ;
33763498
3499+ // Folder search
3500+ elements . btnFolderSearch . addEventListener ( 'click' , performFolderSearch ) ;
3501+ elements . btnFolderSearchCancel . addEventListener ( 'click' , cancelFolderSearch ) ;
3502+ elements . folderSearchInput . addEventListener ( 'keydown' , ( e ) => {
3503+ if ( e . key === 'Enter' ) {
3504+ performFolderSearch ( ) ;
3505+ } else if ( e . key === 'Escape' ) {
3506+ closeFolderSearchResults ( ) ;
3507+ }
3508+ } ) ;
3509+
33773510 // Search
33783511 elements . btnSearch . addEventListener ( 'click' , performSearch ) ;
33793512 elements . btnPrevResult . addEventListener ( 'click' , ( ) => {
0 commit comments