@@ -74,7 +74,7 @@ impl ContextIndexer {
7474 if let Ok ( event) = res {
7575 let _ = tx. blocking_send ( event) ;
7676 }
77- } , notify:: Config :: default ( ) ) ;
77+ } , notify:: Config :: default ( ) . with_poll_interval ( std :: time :: Duration :: from_secs ( 2 ) ) ) ;
7878
7979 if let Ok ( mut watcher) = watcher_res {
8080 if let Err ( e) = watcher. watch ( & root, RecursiveMode :: Recursive ) {
@@ -85,28 +85,66 @@ impl ContextIndexer {
8585 // Keep watcher alive in this thread
8686 let _watcher = watcher;
8787
88- while let Some ( event) = rx. recv ( ) . await {
89- match event. kind {
90- EventKind :: Modify ( _) | EventKind :: Create ( _) => {
91- // If the user edited `.cursorignore`, refresh
92- // the cached matcher so subsequent events
93- // honor the new rules immediately.
94- if event. paths . iter ( ) . any ( |p| p. file_name ( ) . map ( |n| n == ".hadesignore" || n == ".cursorignore" || n == ".cursorindexignore" || n == ".gitignore" ) . unwrap_or ( false ) ) {
95- let fresh = IgnoreSet :: load ( & root) ;
96- if let Ok ( mut w) = ignore_set. write ( ) {
97- * w = fresh;
88+ // Debounce: batch rapid-fire events (e.g. during builds)
89+ let mut pending_paths: Vec < std:: path:: PathBuf > = Vec :: new ( ) ;
90+ let mut debounce_deadline = None ;
91+
92+ loop {
93+ // If we have pending events and the debounce window expired, process them
94+ if !pending_paths. is_empty ( ) {
95+ if let Some ( deadline) = debounce_deadline {
96+ if tokio:: time:: Instant :: now ( ) >= deadline {
97+ let paths: Vec < _ > = pending_paths. drain ( ..) . collect ( ) ;
98+ debounce_deadline = None ;
99+ let snapshot = ignore_set. read ( ) . ok ( ) . map ( |g| g. clone ( ) ) ;
100+ for path in paths {
101+ if Self :: is_indexable_with ( & path, snapshot. as_ref ( ) ) {
102+ if let Err ( e) = Self :: index_single_file ( & ms, & root, & path) . await {
103+ eprintln ! ( "[CONTEXT] Error indexing file {:?}: {:?}" , path, e) ;
104+ }
105+ }
98106 }
107+ continue ;
99108 }
100- let snapshot = ignore_set. read ( ) . ok ( ) . map ( |g| g. clone ( ) ) ;
101- for path in event. paths {
102- if Self :: is_indexable_with ( & path, snapshot. as_ref ( ) ) {
103- if let Err ( e) = Self :: index_single_file ( & ms, & root, & path) . await {
104- eprintln ! ( "[CONTEXT] Error indexing file {:?}: {:?}" , path, e) ;
109+ }
110+ }
111+
112+ // Wait for next event or timeout
113+ let timeout = debounce_deadline. map ( |d| {
114+ let remaining = d. saturating_duration_since ( tokio:: time:: Instant :: now ( ) ) ;
115+ remaining. min ( std:: time:: Duration :: from_millis ( 500 ) )
116+ } ) . unwrap_or ( std:: time:: Duration :: from_millis ( 500 ) ) ;
117+
118+ match tokio:: time:: timeout ( timeout, rx. recv ( ) ) . await {
119+ Ok ( Some ( event) ) => {
120+ match event. kind {
121+ EventKind :: Modify ( _) | EventKind :: Create ( _) => {
122+ // Refresh ignore files
123+ if event. paths . iter ( ) . any ( |p| p. file_name ( ) . map ( |n| n == ".hadesignore" || n == ".cursorignore" || n == ".cursorindexignore" || n == ".gitignore" ) . unwrap_or ( false ) ) {
124+ let fresh = IgnoreSet :: load ( & root) ;
125+ if let Ok ( mut w) = ignore_set. write ( ) {
126+ * w = fresh;
127+ }
105128 }
129+ // Collect paths for debounced processing
130+ for path in event. paths {
131+ if path. is_file ( ) {
132+ pending_paths. push ( path) ;
133+ }
134+ }
135+ // Set/reset debounce deadline (300ms window)
136+ debounce_deadline = Some ( tokio:: time:: Instant :: now ( ) + std:: time:: Duration :: from_millis ( 300 ) ) ;
106137 }
138+ _ => { }
139+ }
140+ }
141+ Ok ( None ) => break , // Channel closed
142+ Err ( _) => {
143+ // Timeout — process any pending events
144+ if !pending_paths. is_empty ( ) {
145+ debounce_deadline = Some ( tokio:: time:: Instant :: now ( ) ) ;
107146 }
108147 }
109- _ => { }
110148 }
111149 }
112150 } ) ;
@@ -422,42 +460,51 @@ impl ContextIndexer {
422460
423461 fn extract_symbols_detailed ( content : & str , ext : & str , path : & str ) -> Vec < crate :: memory_store:: SymbolDefinition > {
424462 let mut symbols = Vec :: new ( ) ;
425- let mut parser = Parser :: new ( ) ;
426463
427- let language = match ext {
428- "rs" => tree_sitter_rust:: LANGUAGE ,
429- "ts" | "tsx" => tree_sitter_typescript:: LANGUAGE_TYPESCRIPT ,
430- "js" | "jsx" => tree_sitter_typescript:: LANGUAGE_TSX ,
431- "py" => tree_sitter_python:: LANGUAGE ,
464+ let ( language, query_str) = match ext {
465+ "rs" => ( tree_sitter_rust:: LANGUAGE . into ( ) ,
466+ "(function_item name: (identifier) @name) @kind_func
467+ (struct_item name: (type_identifier) @name) @kind_struct
468+ (enum_item name: (type_identifier) @name) @kind_enum
469+ (trait_item name: (type_identifier) @name) @kind_trait
470+ (impl_item type: (type_identifier) @name) @kind_impl" ) ,
471+ "ts" | "tsx" | "js" | "jsx" => ( tree_sitter_typescript:: LANGUAGE_TYPESCRIPT . into ( ) ,
472+ "(function_declaration name: (identifier) @name) @kind_func
473+ (variable_declarator name: (identifier) @name value: (arrow_function)) @kind_func
474+ (method_definition name: (property_identifier) @name) @kind_func
475+ (class_declaration name: (type_identifier) @name) @kind_class
476+ (interface_declaration name: (type_identifier) @name) @kind_interface
477+ (type_alias_declaration name: (type_identifier) @name) @kind_type" ) ,
478+ "py" => ( tree_sitter_python:: LANGUAGE . into ( ) ,
479+ "(function_definition name: (identifier) @name) @kind_func
480+ (class_definition name: (identifier) @name) @kind_class" ) ,
432481 _ => return Vec :: new ( ) ,
433482 } ;
434483
435- parser. set_language ( & language. into ( ) ) . expect ( "Error loading language" ) ;
436- let tree = parser. parse ( content, None ) . expect ( "Error parsing code" ) ;
437-
438- let query_str = match ext {
439- "rs" => "(function_item name: (identifier) @name) @kind_func
440- (struct_item name: (type_identifier) @name) @kind_struct
441- (enum_item name: (type_identifier) @name) @kind_enum
442- (trait_item name: (type_identifier) @name) @kind_trait
443- (impl_item type: (type_identifier) @name) @kind_impl" ,
444- "ts" | "tsx" | "js" | "jsx" => "(function_declaration name: (identifier) @name) @kind_func
445- (variable_declarator name: (identifier) @name value: (arrow_function)) @kind_func
446- (method_definition name: (property_identifier) @name) @kind_func
447- (class_declaration name: (type_identifier) @name) @kind_class
448- (interface_declaration name: (type_identifier) @name) @kind_interface
449- (type_alias_declaration name: (type_identifier) @name) @kind_type" ,
450- "py" => "(function_definition name: (identifier) @name) @kind_func
451- (class_definition name: (identifier) @name) @kind_class" ,
452- _ => "" ,
453- } ;
454-
455- if query_str. is_empty ( ) {
456- return Vec :: new ( ) ;
484+ // Cache parser per language using thread_local to avoid re-creating 2000x per cycle
485+ use std:: cell:: RefCell ;
486+ thread_local ! {
487+ static PARSER_CACHE : RefCell <HashMap <String , Parser >> = RefCell :: new( HashMap :: new( ) ) ;
457488 }
458-
459- let query = Query :: new ( & language. into ( ) , query_str) . expect ( "Error creating query" ) ;
460- let mut cursor = QueryCursor :: new ( ) ;
489+
490+ PARSER_CACHE . with ( |cache| {
491+ let mut cache = cache. borrow_mut ( ) ;
492+ let parser = cache. entry ( ext. to_string ( ) ) . or_insert_with ( || {
493+ let mut p = Parser :: new ( ) ;
494+ p. set_language ( & language) . expect ( "Error loading language" ) ;
495+ p
496+ } ) ;
497+
498+ let tree = match parser. parse ( content, None ) {
499+ Some ( t) => t,
500+ None => return Vec :: new ( ) ,
501+ } ;
502+
503+ let query = match Query :: new ( & language, query_str) {
504+ Ok ( q) => q,
505+ Err ( _) => return Vec :: new ( ) ,
506+ } ;
507+ let mut cursor = QueryCursor :: new ( ) ;
461508 let mut matches = cursor. matches ( & query, tree. root_node ( ) , content. as_bytes ( ) ) ;
462509 while let Some ( m) = StreamingIterator :: next ( & mut matches) {
463510 let mut name = String :: new ( ) ;
@@ -487,6 +534,7 @@ impl ContextIndexer {
487534 }
488535 }
489536 symbols
537+ } )
490538 }
491539
492540 /// Returns just the symbol names for a file — useful for quick context summaries.
0 commit comments