1use crate::data::data_view::DataView;
2use crate::ui::viewport_manager::ViewportManager;
3use tracing::{debug, error, info, warn};
4
5#[derive(Debug, Clone)]
7pub struct SearchMatch {
8 pub row: usize,
9 pub col: usize,
10 pub value: String,
11}
12
13#[derive(Debug, Clone)]
15pub enum VimSearchState {
16 Inactive,
18 Typing { pattern: String },
20 Navigating {
22 pattern: String,
23 matches: Vec<SearchMatch>,
24 current_index: usize,
25 },
26}
27
28pub struct VimSearchManager {
30 state: VimSearchState,
31 case_sensitive: bool,
32 last_search_pattern: Option<String>,
33}
34
35impl VimSearchManager {
36 pub fn new() -> Self {
37 Self {
38 state: VimSearchState::Inactive,
39 case_sensitive: false,
40 last_search_pattern: None,
41 }
42 }
43
44 pub fn start_search(&mut self) {
46 info!(target: "vim_search", "Starting vim search mode");
47 self.state = VimSearchState::Typing {
48 pattern: String::new(),
49 };
50 }
51
52 pub fn update_pattern(
54 &mut self,
55 pattern: String,
56 dataview: &DataView,
57 viewport: &mut ViewportManager,
58 ) -> Option<SearchMatch> {
59 debug!(target: "vim_search", "Updating pattern to: '{}'", pattern);
60
61 self.state = VimSearchState::Typing {
63 pattern: pattern.clone(),
64 };
65
66 if pattern.is_empty() {
67 return None;
68 }
69
70 let matches = self.find_matches(&pattern, dataview);
72
73 if let Some(first_match) = matches.first() {
74 debug!(target: "vim_search",
75 "Found {} matches, navigating to first at ({}, {})",
76 matches.len(), first_match.row, first_match.col);
77
78 self.navigate_to_match(first_match, viewport);
80 Some(first_match.clone())
81 } else {
82 debug!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
83 None
84 }
85 }
86
87 pub fn confirm_search(&mut self, dataview: &DataView, viewport: &mut ViewportManager) -> bool {
89 match &self.state {
90 VimSearchState::Typing { pattern } => {
91 if pattern.is_empty() {
92 info!(target: "vim_search", "Empty pattern, canceling search");
93 self.cancel_search();
94 return false;
95 }
96
97 let pattern = pattern.clone();
98 let matches = self.find_matches(&pattern, dataview);
99
100 if matches.is_empty() {
101 warn!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
102 self.cancel_search();
103 return false;
104 }
105
106 info!(target: "vim_search",
107 "Confirming search with {} matches for pattern: '{}'",
108 matches.len(), pattern);
109
110 if let Some(first_match) = matches.first() {
112 self.navigate_to_match(first_match, viewport);
113 }
114
115 self.state = VimSearchState::Navigating {
117 pattern: pattern.clone(),
118 matches,
119 current_index: 0,
120 };
121 self.last_search_pattern = Some(pattern);
122 true
123 }
124 _ => {
125 warn!(target: "vim_search", "confirm_search called in wrong state: {:?}", self.state);
126 false
127 }
128 }
129 }
130
131 pub fn next_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
133 let match_to_navigate = match &mut self.state {
135 VimSearchState::Navigating {
136 matches,
137 current_index,
138 pattern,
139 } => {
140 if matches.is_empty() {
141 return None;
142 }
143
144 info!(target: "vim_search",
146 "=== 'n' KEY PRESSED - BEFORE NAVIGATION ===");
147 info!(target: "vim_search",
148 "Current match index: {}/{}, Pattern: '{}'",
149 *current_index + 1, matches.len(), pattern);
150 info!(target: "vim_search",
151 "Current viewport - rows: {:?}, cols: {:?}",
152 viewport.get_viewport_rows(), viewport.viewport_cols());
153 info!(target: "vim_search",
154 "Current crosshair position: row={}, col={}",
155 viewport.get_crosshair_row(), viewport.get_crosshair_col());
156
157 *current_index = (*current_index + 1) % matches.len();
159 let match_item = matches[*current_index].clone();
160
161 info!(target: "vim_search",
162 "=== NEXT MATCH DETAILS ===");
163 info!(target: "vim_search",
164 "Match {}/{}: row={}, visual_col={}, stored_value='{}'",
165 *current_index + 1, matches.len(),
166 match_item.row, match_item.col, match_item.value);
167
168 if !match_item
170 .value
171 .to_lowercase()
172 .contains(&pattern.to_lowercase())
173 {
174 error!(target: "vim_search",
175 "CRITICAL ERROR: Match value '{}' does NOT contain search pattern '{}'!",
176 match_item.value, pattern);
177 error!(target: "vim_search",
178 "This indicates the search index is corrupted or stale!");
179 }
180
181 info!(target: "vim_search",
183 "Expected: Cell at row {} col {} should contain substring '{}'",
184 match_item.row, match_item.col, pattern);
185
186 let stored_contains = match_item
188 .value
189 .to_lowercase()
190 .contains(&pattern.to_lowercase());
191 if !stored_contains {
192 warn!(target: "vim_search",
193 "CRITICAL: Stored match '{}' does NOT contain pattern '{}'!",
194 match_item.value, pattern);
195 } else {
196 info!(target: "vim_search",
197 "✓ Stored match '{}' contains pattern '{}'",
198 match_item.value, pattern);
199 }
200
201 Some(match_item)
202 }
203 _ => {
204 debug!(target: "vim_search", "next_match called but not in navigation mode");
205 None
206 }
207 };
208
209 if let Some(ref match_item) = match_to_navigate {
211 info!(target: "vim_search",
212 "=== NAVIGATING TO MATCH ===");
213 self.navigate_to_match(match_item, viewport);
214
215 info!(target: "vim_search",
217 "=== AFTER NAVIGATION ===");
218 info!(target: "vim_search",
219 "New viewport - rows: {:?}, cols: {:?}",
220 viewport.get_viewport_rows(), viewport.viewport_cols());
221 info!(target: "vim_search",
222 "New crosshair position: row={}, col={}",
223 viewport.get_crosshair_row(), viewport.get_crosshair_col());
224 info!(target: "vim_search",
225 "Crosshair should be at: row={}, col={} (visual coordinates)",
226 match_item.row, match_item.col);
227 }
228
229 match_to_navigate
230 }
231
232 pub fn previous_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
234 let match_to_navigate = match &mut self.state {
236 VimSearchState::Navigating {
237 matches,
238 current_index,
239 pattern,
240 } => {
241 if matches.is_empty() {
242 return None;
243 }
244
245 *current_index = if *current_index == 0 {
247 matches.len() - 1
248 } else {
249 *current_index - 1
250 };
251
252 let match_item = matches[*current_index].clone();
253
254 info!(target: "vim_search",
255 "Navigating to previous match {}/{} at ({}, {})",
256 *current_index + 1, matches.len(), match_item.row, match_item.col);
257
258 Some(match_item)
259 }
260 _ => {
261 debug!(target: "vim_search", "previous_match called but not in navigation mode");
262 None
263 }
264 };
265
266 if let Some(ref match_item) = match_to_navigate {
268 self.navigate_to_match(match_item, viewport);
269 }
270
271 match_to_navigate
272 }
273
274 pub fn cancel_search(&mut self) {
276 info!(target: "vim_search", "Canceling search, returning to inactive state");
277 self.state = VimSearchState::Inactive;
278 }
279
280 pub fn clear(&mut self) {
282 info!(target: "vim_search", "Clearing all search state");
283 self.state = VimSearchState::Inactive;
284 self.last_search_pattern = None;
285 }
286
287 pub fn exit_navigation(&mut self) {
289 if let VimSearchState::Navigating { pattern, .. } = &self.state {
290 self.last_search_pattern = Some(pattern.clone());
291 }
292 self.state = VimSearchState::Inactive;
293 }
294
295 pub fn resume_last_search(
297 &mut self,
298 dataview: &DataView,
299 viewport: &mut ViewportManager,
300 ) -> bool {
301 if let Some(pattern) = &self.last_search_pattern {
302 let pattern = pattern.clone();
303 let matches = self.find_matches(&pattern, dataview);
304
305 if !matches.is_empty() {
306 info!(target: "vim_search",
307 "Resuming search with pattern '{}', found {} matches",
308 pattern, matches.len());
309
310 if let Some(first_match) = matches.first() {
312 self.navigate_to_match(first_match, viewport);
313 }
314
315 self.state = VimSearchState::Navigating {
316 pattern,
317 matches,
318 current_index: 0,
319 };
320 true
321 } else {
322 false
323 }
324 } else {
325 false
326 }
327 }
328
329 pub fn is_active(&self) -> bool {
331 !matches!(self.state, VimSearchState::Inactive)
332 }
333
334 pub fn is_typing(&self) -> bool {
336 matches!(self.state, VimSearchState::Typing { .. })
337 }
338
339 pub fn is_navigating(&self) -> bool {
341 matches!(self.state, VimSearchState::Navigating { .. })
342 }
343
344 pub fn get_pattern(&self) -> Option<String> {
346 match &self.state {
347 VimSearchState::Typing { pattern } => Some(pattern.clone()),
348 VimSearchState::Navigating { pattern, .. } => Some(pattern.clone()),
349 VimSearchState::Inactive => None,
350 }
351 }
352
353 pub fn get_match_info(&self) -> Option<(usize, usize)> {
355 match &self.state {
356 VimSearchState::Navigating {
357 matches,
358 current_index,
359 ..
360 } => Some((*current_index + 1, matches.len())),
361 _ => None,
362 }
363 }
364
365 pub fn reset_to_first_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
367 match &mut self.state {
368 VimSearchState::Navigating {
369 matches,
370 current_index,
371 ..
372 } => {
373 if matches.is_empty() {
374 return None;
375 }
376
377 *current_index = 0;
379 let first_match = matches[0].clone();
380
381 info!(target: "vim_search",
382 "Resetting to first match at ({}, {})",
383 first_match.row, first_match.col);
384
385 self.navigate_to_match(&first_match, viewport);
387 Some(first_match)
388 }
389 _ => {
390 debug!(target: "vim_search", "reset_to_first_match called but not in navigation mode");
391 None
392 }
393 }
394 }
395
396 fn find_matches(&self, pattern: &str, dataview: &DataView) -> Vec<SearchMatch> {
398 let mut matches = Vec::new();
399 let pattern_lower = if !self.case_sensitive {
400 pattern.to_lowercase()
401 } else {
402 pattern.to_string()
403 };
404
405 info!(target: "vim_search",
406 "=== FIND_MATCHES CALLED ===");
407 info!(target: "vim_search",
408 "Pattern passed in: '{}', pattern_lower: '{}', case_sensitive: {}",
409 pattern, pattern_lower, self.case_sensitive);
410
411 let display_columns = dataview.get_display_columns();
413 debug!(target: "vim_search",
414 "Display columns mapping: {:?} (count: {})",
415 display_columns, display_columns.len());
416
417 for row_idx in 0..dataview.row_count() {
419 if let Some(row) = dataview.get_row(row_idx) {
420 let mut first_match_in_row: Option<SearchMatch> = None;
421
422 for (enum_idx, value) in row.values.iter().enumerate() {
424 let value_str = value.to_string();
425 let search_value = if !self.case_sensitive {
426 value_str.to_lowercase()
427 } else {
428 value_str.clone()
429 };
430
431 if search_value.contains(&pattern_lower) {
432 if first_match_in_row.is_none() {
435 let actual_col = if enum_idx < display_columns.len() {
442 display_columns[enum_idx]
443 } else {
444 enum_idx };
446
447 info!(target: "vim_search",
448 "Found first match in row {} at visual col {} (DataTable col {}, value '{}')",
449 row_idx, enum_idx, actual_col, value_str);
450
451 if value_str.contains("Futures Trading") {
453 warn!(target: "vim_search",
454 "SUSPICIOUS: Found 'Futures Trading' as a match for pattern '{}' (search_value='{}', pattern_lower='{}')",
455 pattern, search_value, pattern_lower);
456 }
457
458 first_match_in_row = Some(SearchMatch {
459 row: row_idx,
460 col: enum_idx, value: value_str,
462 });
463 } else {
464 debug!(target: "vim_search",
465 "Skipping additional match in row {} at visual col {} (enum_idx {}): '{}'",
466 row_idx, enum_idx, enum_idx, value_str);
467 }
468 }
469 }
470
471 if let Some(match_item) = first_match_in_row {
473 matches.push(match_item);
474 }
475 }
476 }
477
478 debug!(target: "vim_search", "Found {} total matches", matches.len());
479 matches
480 }
481
482 fn navigate_to_match(&self, match_item: &SearchMatch, viewport: &mut ViewportManager) {
484 info!(target: "vim_search",
485 "=== NAVIGATE_TO_MATCH START ===");
486 info!(target: "vim_search",
487 "Target match: row={} (absolute), col={} (visual), value='{}'",
488 match_item.row, match_item.col, match_item.value);
489
490 let terminal_width = viewport.get_terminal_width();
492 let terminal_height = viewport.get_terminal_height();
493 info!(target: "vim_search",
494 "Terminal dimensions: width={}, height={}",
495 terminal_width, terminal_height);
496
497 let viewport_rows = viewport.get_viewport_rows();
499 let viewport_cols = viewport.viewport_cols();
500 let viewport_height = viewport_rows.end - viewport_rows.start;
501 let viewport_width = viewport_cols.end - viewport_cols.start;
502
503 info!(target: "vim_search",
504 "Current viewport BEFORE changes:");
505 info!(target: "vim_search",
506 " Rows: {:?} (height={})", viewport_rows, viewport_height);
507 info!(target: "vim_search",
508 " Cols: {:?} (width={})", viewport_cols, viewport_width);
509 info!(target: "vim_search",
510 " Current crosshair: row={}, col={}",
511 viewport.get_crosshair_row(), viewport.get_crosshair_col());
512
513 let new_row_start = match_item.row.saturating_sub(viewport_height / 2);
516 info!(target: "vim_search",
517 "Centering row {} in viewport (height={}), new viewport start row={}",
518 match_item.row, viewport_height, new_row_start);
519
520 let new_col_start = match_item.col.saturating_sub(3); info!(target: "vim_search",
525 "Positioning column {} in viewport, new viewport start col={}",
526 match_item.col, new_col_start);
527
528 info!(target: "vim_search",
530 "=== VIEWPORT UPDATE ===");
531 info!(target: "vim_search",
532 "Will call set_viewport with: row_start={}, col_start={}, width={}, height={}",
533 new_row_start, new_col_start, terminal_width, terminal_height);
534
535 viewport.set_viewport(
537 new_row_start,
538 new_col_start,
539 terminal_width, terminal_height as u16,
541 );
542
543 let final_viewport_rows = viewport.get_viewport_rows();
545 let final_viewport_cols = viewport.viewport_cols();
546
547 info!(target: "vim_search",
548 "Viewport AFTER set_viewport: rows {:?}, cols {:?}",
549 final_viewport_rows, final_viewport_cols);
550
551 if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
553 error!(target: "vim_search",
554 "CRITICAL ERROR: Target column {} is NOT in viewport {:?} after set_viewport!",
555 match_item.col, final_viewport_cols);
556 error!(target: "vim_search",
557 "We asked for col_start={}, but viewport gave us {:?}",
558 new_col_start, final_viewport_cols);
559 }
560
561 info!(target: "vim_search",
564 "=== CROSSHAIR POSITIONING ===");
565 info!(target: "vim_search",
566 "Setting crosshair to ABSOLUTE position: row={}, col={}",
567 match_item.row, match_item.col);
568
569 viewport.set_crosshair(match_item.row, match_item.col);
570
571 let center_row =
573 final_viewport_rows.start + (final_viewport_rows.end - final_viewport_rows.start) / 2;
574 let center_col =
575 final_viewport_cols.start + (final_viewport_cols.end - final_viewport_cols.start) / 2;
576
577 info!(target: "vim_search",
578 "Viewport center is at: row={}, col={}",
579 center_row, center_col);
580 info!(target: "vim_search",
581 "Match is at: row={}, col={}",
582 match_item.row, match_item.col);
583 info!(target: "vim_search",
584 "Distance from center: row_diff={}, col_diff={}",
585 (match_item.row as i32 - center_row as i32).abs(),
586 (match_item.col as i32 - center_col as i32).abs());
587
588 if let Some((vp_row, vp_col)) = viewport.get_crosshair_viewport_position() {
590 info!(target: "vim_search",
591 "Crosshair appears at viewport position: ({}, {})",
592 vp_row, vp_col);
593 info!(target: "vim_search",
594 "Viewport dimensions: {} rows x {} cols",
595 final_viewport_rows.end - final_viewport_rows.start,
596 final_viewport_cols.end - final_viewport_cols.start);
597 info!(target: "vim_search",
598 "Expected center position: ({}, {})",
599 (final_viewport_rows.end - final_viewport_rows.start) / 2,
600 (final_viewport_cols.end - final_viewport_cols.start) / 2);
601 } else {
602 error!(target: "vim_search",
603 "CRITICAL: Crosshair is NOT visible in viewport after centering!");
604 }
605
606 info!(target: "vim_search",
608 "=== VERIFICATION ===");
609
610 if match_item.row < final_viewport_rows.start || match_item.row >= final_viewport_rows.end {
611 error!(target: "vim_search",
612 "ERROR: Match row {} is OUTSIDE viewport {:?} after scrolling!",
613 match_item.row, final_viewport_rows);
614 } else {
615 info!(target: "vim_search",
616 "✓ Match row {} is within viewport {:?}",
617 match_item.row, final_viewport_rows);
618 }
619
620 if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
621 error!(target: "vim_search",
622 "ERROR: Match column {} is OUTSIDE viewport {:?} after scrolling!",
623 match_item.col, final_viewport_cols);
624 } else {
625 info!(target: "vim_search",
626 "✓ Match column {} is within viewport {:?}",
627 match_item.col, final_viewport_cols);
628 }
629
630 info!(target: "vim_search",
632 "=== NAVIGATE_TO_MATCH COMPLETE ===");
633 info!(target: "vim_search",
634 "Match at absolute ({}, {}), crosshair at ({}, {}), viewport rows {:?} cols {:?}",
635 match_item.row, match_item.col,
636 viewport.get_crosshair_row(), viewport.get_crosshair_col(),
637 final_viewport_rows, final_viewport_cols);
638 }
639
640 pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
642 self.case_sensitive = case_sensitive;
643 debug!(target: "vim_search", "Case sensitivity set to: {}", case_sensitive);
644 }
645
646 pub fn set_search_state_from_external(
649 &mut self,
650 pattern: String,
651 matches: Vec<(usize, usize)>,
652 dataview: &DataView,
653 ) {
654 info!(target: "vim_search",
655 "Setting search state from external search: pattern='{}', {} matches",
656 pattern, matches.len());
657
658 let search_matches: Vec<SearchMatch> = matches
660 .into_iter()
661 .filter_map(|(row, col)| {
662 if let Some(row_data) = dataview.get_row(row) {
663 if col < row_data.values.len() {
664 Some(SearchMatch {
665 row,
666 col,
667 value: row_data.values[col].to_string(),
668 })
669 } else {
670 None
671 }
672 } else {
673 None
674 }
675 })
676 .collect();
677
678 if !search_matches.is_empty() {
679 let match_count = search_matches.len();
680
681 self.state = VimSearchState::Navigating {
683 pattern: pattern.clone(),
684 matches: search_matches,
685 current_index: 0,
686 };
687 self.last_search_pattern = Some(pattern);
688
689 info!(target: "vim_search",
690 "Vim search state updated: {} matches ready for navigation",
691 match_count);
692 } else {
693 warn!(target: "vim_search", "No valid matches to set in vim search state");
694 }
695 }
696}