@@ -5,12 +5,17 @@ use anyhow::Result;
55use std:: path:: PathBuf ;
66use std:: sync:: mpsc:: { self , Receiver , Sender } ;
77use std:: thread;
8+ use std:: time:: { Duration , Instant } ;
9+
10+ /// Debounce delay for search (avoid searching on every keystroke during fast typing/paste)
11+ const SEARCH_DEBOUNCE : Duration = Duration :: from_millis ( 50 ) ;
812
913/// Messages from the indexing thread
1014pub enum IndexMsg {
1115 Progress { indexed : usize , total : usize } ,
1216 Done { total_sessions : usize } ,
1317 NeedsReload ,
18+ Error ( String ) ,
1419}
1520
1621/// Search scope
@@ -25,6 +30,8 @@ pub enum SearchScope {
2530pub struct App {
2631 /// Current search query
2732 pub query : String ,
33+ /// Cursor position in query (char index)
34+ pub cursor : usize ,
2835 /// Search results
2936 pub results : Vec < SearchResult > ,
3037 /// Selected result index
@@ -57,6 +64,12 @@ pub struct App {
5764 pub search_scope : SearchScope ,
5865 /// Launch directory (for folder-scoped search)
5966 pub launch_cwd : String ,
67+ /// Whether a search is pending (for debouncing)
68+ search_pending : bool ,
69+ /// When the last input occurred (for debouncing)
70+ last_input : Instant ,
71+ /// Error from indexing thread (shown on exit)
72+ pub index_error : Option < String > ,
6073}
6174
6275impl App {
@@ -89,8 +102,10 @@ impl App {
89102 background_index ( index_path_clone, state_path, tx) ;
90103 } ) ;
91104
105+ let initial_cursor = initial_query. chars ( ) . count ( ) ;
92106 let mut app = Self {
93107 query : initial_query,
108+ cursor : initial_cursor,
94109 results : Vec :: new ( ) ,
95110 selected : 0 ,
96111 list_scroll : 0 ,
@@ -107,6 +122,9 @@ impl App {
107122 indexing : true ,
108123 search_scope : SearchScope :: Folder ( launch_cwd. clone ( ) ) ,
109124 launch_cwd,
125+ search_pending : false ,
126+ last_input : Instant :: now ( ) ,
127+ index_error : None ,
110128 } ;
111129
112130 // If there's an initial query, run the search immediately
@@ -119,16 +137,26 @@ impl App {
119137
120138 /// Check for indexing updates (call this in the main loop)
121139 pub fn poll_index_updates ( & mut self ) {
122- if self . index_rx . is_none ( ) {
123- return ;
124- }
140+ use std:: sync:: mpsc:: TryRecvError ;
125141
126- // Collect messages first to avoid borrow issues
127- let messages: Vec < _ > = {
128- let rx = self . index_rx . as_ref ( ) . unwrap ( ) ;
129- std:: iter:: from_fn ( || rx. try_recv ( ) . ok ( ) ) . collect ( )
142+ let Some ( rx) = & self . index_rx else {
143+ return ;
130144 } ;
131145
146+ // Collect messages, tracking if channel was disconnected
147+ let mut messages = Vec :: new ( ) ;
148+ let mut channel_disconnected = false ;
149+ loop {
150+ match rx. try_recv ( ) {
151+ Ok ( msg) => messages. push ( msg) ,
152+ Err ( TryRecvError :: Empty ) => break ,
153+ Err ( TryRecvError :: Disconnected ) => {
154+ channel_disconnected = true ;
155+ break ;
156+ }
157+ }
158+ }
159+
132160 let mut should_close_rx = false ;
133161 let mut needs_reload = false ;
134162 let mut needs_search = false ;
@@ -151,9 +179,23 @@ impl App {
151179 needs_reload = true ;
152180 needs_search = true ;
153181 }
182+ IndexMsg :: Error ( err) => {
183+ self . index_error = Some ( err) ;
184+ self . status = Some ( "Index error • Ctrl+C for details" . to_string ( ) ) ;
185+ self . indexing = false ;
186+ should_close_rx = true ;
187+ }
154188 }
155189 }
156190
191+ // Detect unexpected indexer death (channel closed without Done/Error)
192+ if channel_disconnected && self . indexing {
193+ self . index_error = Some ( "Indexer stopped unexpectedly (possible crash)" . to_string ( ) ) ;
194+ self . status = Some ( "Index error • Ctrl+C for details" . to_string ( ) ) ;
195+ self . indexing = false ;
196+ should_close_rx = true ;
197+ }
198+
157199 if needs_reload {
158200 let _ = self . index . reload ( ) ;
159201 }
@@ -167,6 +209,9 @@ impl App {
167209
168210 /// Perform a search (or show recent sessions if query is empty)
169211 pub fn search ( & mut self ) -> Result < ( ) > {
212+ // Remember currently selected session to preserve selection
213+ let selected_session_id = self . results . get ( self . selected ) . map ( |r| r. session . id . clone ( ) ) ;
214+
170215 let mut results = if self . query . is_empty ( ) {
171216 self . index . recent ( 50 ) ?
172217 } else {
@@ -179,8 +224,21 @@ impl App {
179224 }
180225
181226 self . results = results;
182- self . selected = 0 ;
183- self . list_scroll = 0 ;
227+
228+ // Try to preserve selection on the same session
229+ if let Some ( ref id) = selected_session_id {
230+ if let Some ( pos) = self . results . iter ( ) . position ( |r| & r. session . id == id) {
231+ self . selected = pos;
232+ // Scroll to keep selection visible (at top of list area)
233+ self . list_scroll = pos;
234+ } else {
235+ self . selected = 0 ;
236+ self . list_scroll = 0 ;
237+ }
238+ } else {
239+ self . selected = 0 ;
240+ self . list_scroll = 0 ;
241+ }
184242 self . update_preview_scroll ( ) ;
185243
186244 Ok ( ( ) )
@@ -239,14 +297,31 @@ impl App {
239297
240298 /// Handle character input
241299 pub fn on_char ( & mut self , c : char ) {
242- self . query . push ( c) ;
243- let _ = self . search ( ) ;
300+ // Insert at cursor position
301+ let byte_pos = self . cursor_byte_pos ( ) ;
302+ self . query . insert ( byte_pos, c) ;
303+ self . cursor += 1 ;
304+ self . mark_search_pending ( ) ;
244305 }
245306
246307 /// Handle backspace
247308 pub fn on_backspace ( & mut self ) {
248- self . query . pop ( ) ;
249- let _ = self . search ( ) ;
309+ if self . cursor > 0 {
310+ self . cursor -= 1 ;
311+ let byte_pos = self . cursor_byte_pos ( ) ;
312+ self . query . remove ( byte_pos) ;
313+ self . mark_search_pending ( ) ;
314+ }
315+ }
316+
317+ /// Handle delete key
318+ pub fn on_delete ( & mut self ) {
319+ let char_count = self . query . chars ( ) . count ( ) ;
320+ if self . cursor < char_count {
321+ let byte_pos = self . cursor_byte_pos ( ) ;
322+ self . query . remove ( byte_pos) ;
323+ self . mark_search_pending ( ) ;
324+ }
250325 }
251326
252327 /// Clear search
@@ -255,6 +330,60 @@ impl App {
255330 self . should_quit = true ;
256331 } else {
257332 self . query . clear ( ) ;
333+ self . cursor = 0 ;
334+ self . mark_search_pending ( ) ;
335+ }
336+ }
337+
338+ /// Move cursor left
339+ pub fn on_left ( & mut self ) {
340+ self . cursor = self . cursor . saturating_sub ( 1 ) ;
341+ }
342+
343+ /// Move cursor right
344+ pub fn on_right ( & mut self ) {
345+ let char_count = self . query . chars ( ) . count ( ) ;
346+ if self . cursor < char_count {
347+ self . cursor += 1 ;
348+ }
349+ }
350+
351+ /// Move cursor to start
352+ pub fn on_home ( & mut self ) {
353+ self . cursor = 0 ;
354+ }
355+
356+ /// Move cursor to end
357+ pub fn on_end ( & mut self ) {
358+ self . cursor = self . query . chars ( ) . count ( ) ;
359+ }
360+
361+ /// Convert cursor (char index) to byte position
362+ fn cursor_byte_pos ( & self ) -> usize {
363+ self . query . char_indices ( )
364+ . nth ( self . cursor )
365+ . map ( |( i, _) | i)
366+ . unwrap_or ( self . query . len ( ) )
367+ }
368+
369+ /// Mark that a search is needed (debounced)
370+ fn mark_search_pending ( & mut self ) {
371+ self . search_pending = true ;
372+ self . last_input = Instant :: now ( ) ;
373+ }
374+
375+ /// Check if debounce period has elapsed and trigger search if needed
376+ pub fn maybe_search ( & mut self ) {
377+ if self . search_pending && self . last_input . elapsed ( ) >= SEARCH_DEBOUNCE {
378+ self . search_pending = false ;
379+ let _ = self . search ( ) ;
380+ }
381+ }
382+
383+ /// Force any pending search to run immediately (for tests)
384+ pub fn flush_pending_search ( & mut self ) {
385+ if self . search_pending {
386+ self . search_pending = false ;
258387 let _ = self . search ( ) ;
259388 }
260389 }
@@ -318,11 +447,19 @@ impl App {
318447
319448/// Background indexing function
320449fn background_index ( index_path : PathBuf , state_path : PathBuf , tx : Sender < IndexMsg > ) {
321- let Ok ( index) = SessionIndex :: open_or_create ( & index_path) else {
322- return ;
450+ let index = match SessionIndex :: open_or_create ( & index_path) {
451+ Ok ( idx) => idx,
452+ Err ( e) => {
453+ let _ = tx. send ( IndexMsg :: Error ( format ! ( "Failed to open index: {}" , e) ) ) ;
454+ return ;
455+ }
323456 } ;
324- let Ok ( mut state) = IndexState :: load ( & state_path) else {
325- return ;
457+ let mut state = match IndexState :: load ( & state_path) {
458+ Ok ( s) => s,
459+ Err ( e) => {
460+ let _ = tx. send ( IndexMsg :: Error ( format ! ( "Failed to load index state: {}" , e) ) ) ;
461+ return ;
462+ }
326463 } ;
327464
328465 // Discover and sort files by mtime (most recent first)
@@ -351,8 +488,12 @@ fn background_index(index_path: PathBuf, state_path: PathBuf, tx: Sender<IndexMs
351488 return ;
352489 }
353490
354- let Ok ( mut writer) = index. writer ( ) else {
355- return ;
491+ let mut writer = match index. writer ( ) {
492+ Ok ( w) => w,
493+ Err ( e) => {
494+ let _ = tx. send ( IndexMsg :: Error ( format ! ( "Failed to create index writer: {}" , e) ) ) ;
495+ return ;
496+ }
356497 } ;
357498
358499 for ( i, file_path) in files_to_index. iter ( ) . enumerate ( ) {
0 commit comments