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 Default for VimSearchManager {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl VimSearchManager {
42 #[must_use]
43 pub fn new() -> Self {
44 Self {
45 state: VimSearchState::Inactive,
46 case_sensitive: false,
47 last_search_pattern: None,
48 }
49 }
50
51 pub fn start_search(&mut self) {
53 info!(target: "vim_search", "Starting vim search mode");
54 self.state = VimSearchState::Typing {
55 pattern: String::new(),
56 };
57 }
58
59 pub fn update_pattern(
61 &mut self,
62 pattern: String,
63 dataview: &DataView,
64 viewport: &mut ViewportManager,
65 ) -> Option<SearchMatch> {
66 debug!(target: "vim_search", "Updating pattern to: '{}'", pattern);
67
68 self.state = VimSearchState::Typing {
70 pattern: pattern.clone(),
71 };
72
73 if pattern.is_empty() {
74 return None;
75 }
76
77 let matches = self.find_matches(&pattern, dataview);
79
80 if let Some(first_match) = matches.first() {
81 debug!(target: "vim_search",
82 "Found {} matches, navigating to first at ({}, {})",
83 matches.len(), first_match.row, first_match.col);
84
85 self.navigate_to_match(first_match, viewport);
87 Some(first_match.clone())
88 } else {
89 debug!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
90 None
91 }
92 }
93
94 pub fn confirm_search(&mut self, dataview: &DataView, viewport: &mut ViewportManager) -> bool {
96 if let VimSearchState::Typing { pattern } = &self.state {
97 if pattern.is_empty() {
98 info!(target: "vim_search", "Empty pattern, canceling search");
99 self.cancel_search();
100 return false;
101 }
102
103 let pattern = pattern.clone();
104 let matches = self.find_matches(&pattern, dataview);
105
106 if matches.is_empty() {
107 warn!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
108 self.cancel_search();
109 return false;
110 }
111
112 info!(target: "vim_search",
113 "Confirming search with {} matches for pattern: '{}'",
114 matches.len(), pattern);
115
116 if let Some(first_match) = matches.first() {
118 self.navigate_to_match(first_match, viewport);
119 }
120
121 self.state = VimSearchState::Navigating {
123 pattern: pattern.clone(),
124 matches,
125 current_index: 0,
126 };
127 self.last_search_pattern = Some(pattern);
128 true
129 } else {
130 warn!(target: "vim_search", "confirm_search called in wrong state: {:?}", self.state);
131 false
132 }
133 }
134
135 pub fn next_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
137 let match_to_navigate = if let VimSearchState::Navigating {
139 matches,
140 current_index,
141 pattern,
142 } = &mut self.state
143 {
144 if matches.is_empty() {
145 return None;
146 }
147
148 info!(target: "vim_search",
150 "=== 'n' KEY PRESSED - BEFORE NAVIGATION ===");
151 info!(target: "vim_search",
152 "Current match index: {}/{}, Pattern: '{}'",
153 *current_index + 1, matches.len(), pattern);
154 info!(target: "vim_search",
155 "Current viewport - rows: {:?}, cols: {:?}",
156 viewport.get_viewport_rows(), viewport.viewport_cols());
157 info!(target: "vim_search",
158 "Current crosshair position: row={}, col={}",
159 viewport.get_crosshair_row(), viewport.get_crosshair_col());
160
161 *current_index = (*current_index + 1) % matches.len();
163 let match_item = matches[*current_index].clone();
164
165 info!(target: "vim_search",
166 "=== NEXT MATCH DETAILS ===");
167 info!(target: "vim_search",
168 "Match {}/{}: row={}, visual_col={}, stored_value='{}'",
169 *current_index + 1, matches.len(),
170 match_item.row, match_item.col, match_item.value);
171
172 if !match_item
174 .value
175 .to_lowercase()
176 .contains(&pattern.to_lowercase())
177 {
178 error!(target: "vim_search",
179 "CRITICAL ERROR: Match value '{}' does NOT contain search pattern '{}'!",
180 match_item.value, pattern);
181 error!(target: "vim_search",
182 "This indicates the search index is corrupted or stale!");
183 }
184
185 info!(target: "vim_search",
187 "Expected: Cell at row {} col {} should contain substring '{}'",
188 match_item.row, match_item.col, pattern);
189
190 let stored_contains = match_item
192 .value
193 .to_lowercase()
194 .contains(&pattern.to_lowercase());
195 if stored_contains {
196 info!(target: "vim_search",
197 "✓ Stored match '{}' contains pattern '{}'",
198 match_item.value, pattern);
199 } else {
200 warn!(target: "vim_search",
201 "CRITICAL: Stored match '{}' does NOT contain pattern '{}'!",
202 match_item.value, pattern);
203 }
204
205 Some(match_item)
206 } else {
207 debug!(target: "vim_search", "next_match called but not in navigation mode");
208 None
209 };
210
211 if let Some(ref match_item) = match_to_navigate {
213 info!(target: "vim_search",
214 "=== NAVIGATING TO MATCH ===");
215 self.navigate_to_match(match_item, viewport);
216
217 info!(target: "vim_search",
219 "=== AFTER NAVIGATION ===");
220 info!(target: "vim_search",
221 "New viewport - rows: {:?}, cols: {:?}",
222 viewport.get_viewport_rows(), viewport.viewport_cols());
223 info!(target: "vim_search",
224 "New crosshair position: row={}, col={}",
225 viewport.get_crosshair_row(), viewport.get_crosshair_col());
226 info!(target: "vim_search",
227 "Crosshair should be at: row={}, col={} (visual coordinates)",
228 match_item.row, match_item.col);
229 }
230
231 match_to_navigate
232 }
233
234 pub fn previous_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
236 let match_to_navigate = if let VimSearchState::Navigating {
238 matches,
239 current_index,
240 pattern,
241 } = &mut self.state
242 {
243 if matches.is_empty() {
244 return None;
245 }
246
247 *current_index = if *current_index == 0 {
249 matches.len() - 1
250 } else {
251 *current_index - 1
252 };
253
254 let match_item = matches[*current_index].clone();
255
256 info!(target: "vim_search",
257 "Navigating to previous match {}/{} at ({}, {})",
258 *current_index + 1, matches.len(), match_item.row, match_item.col);
259
260 Some(match_item)
261 } else {
262 debug!(target: "vim_search", "previous_match called but not in navigation mode");
263 None
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 false
307 } else {
308 info!(target: "vim_search",
309 "Resuming search with pattern '{}', found {} matches",
310 pattern, matches.len());
311
312 if let Some(first_match) = matches.first() {
314 self.navigate_to_match(first_match, viewport);
315 }
316
317 self.state = VimSearchState::Navigating {
318 pattern,
319 matches,
320 current_index: 0,
321 };
322 true
323 }
324 } else {
325 false
326 }
327 }
328
329 #[must_use]
331 pub fn is_active(&self) -> bool {
332 !matches!(self.state, VimSearchState::Inactive)
333 }
334
335 #[must_use]
337 pub fn is_typing(&self) -> bool {
338 matches!(self.state, VimSearchState::Typing { .. })
339 }
340
341 #[must_use]
343 pub fn is_navigating(&self) -> bool {
344 matches!(self.state, VimSearchState::Navigating { .. })
345 }
346
347 #[must_use]
349 pub fn get_pattern(&self) -> Option<String> {
350 match &self.state {
351 VimSearchState::Typing { pattern } => Some(pattern.clone()),
352 VimSearchState::Navigating { pattern, .. } => Some(pattern.clone()),
353 VimSearchState::Inactive => None,
354 }
355 }
356
357 #[must_use]
359 pub fn get_match_info(&self) -> Option<(usize, usize)> {
360 match &self.state {
361 VimSearchState::Navigating {
362 matches,
363 current_index,
364 ..
365 } => Some((*current_index + 1, matches.len())),
366 _ => None,
367 }
368 }
369
370 pub fn reset_to_first_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
372 if let VimSearchState::Navigating {
373 matches,
374 current_index,
375 ..
376 } = &mut self.state
377 {
378 if matches.is_empty() {
379 return None;
380 }
381
382 *current_index = 0;
384 let first_match = matches[0].clone();
385
386 info!(target: "vim_search",
387 "Resetting to first match at ({}, {})",
388 first_match.row, first_match.col);
389
390 self.navigate_to_match(&first_match, viewport);
392 Some(first_match)
393 } else {
394 debug!(target: "vim_search", "reset_to_first_match called but not in navigation mode");
395 None
396 }
397 }
398
399 fn find_matches(&self, pattern: &str, dataview: &DataView) -> Vec<SearchMatch> {
401 let mut matches = Vec::new();
402 let pattern_lower = if self.case_sensitive {
403 pattern.to_string()
404 } else {
405 pattern.to_lowercase()
406 };
407
408 info!(target: "vim_search",
409 "=== FIND_MATCHES CALLED ===");
410 info!(target: "vim_search",
411 "Pattern passed in: '{}', pattern_lower: '{}', case_sensitive: {}",
412 pattern, pattern_lower, self.case_sensitive);
413
414 let display_columns = dataview.get_display_columns();
416 debug!(target: "vim_search",
417 "Display columns mapping: {:?} (count: {})",
418 display_columns, display_columns.len());
419
420 for row_idx in 0..dataview.row_count() {
422 if let Some(row) = dataview.get_row(row_idx) {
423 let mut first_match_in_row: Option<SearchMatch> = None;
424
425 for (enum_idx, value) in row.values.iter().enumerate() {
427 let value_str = value.to_string();
428 let search_value = if self.case_sensitive {
429 value_str.clone()
430 } else {
431 value_str.to_lowercase()
432 };
433
434 if search_value.contains(&pattern_lower) {
435 if first_match_in_row.is_none() {
438 let actual_col = if enum_idx < display_columns.len() {
445 display_columns[enum_idx]
446 } else {
447 enum_idx };
449
450 info!(target: "vim_search",
451 "Found first match in row {} at visual col {} (DataTable col {}, value '{}')",
452 row_idx, enum_idx, actual_col, value_str);
453
454 if value_str.contains("Futures Trading") {
456 warn!(target: "vim_search",
457 "SUSPICIOUS: Found 'Futures Trading' as a match for pattern '{}' (search_value='{}', pattern_lower='{}')",
458 pattern, search_value, pattern_lower);
459 }
460
461 first_match_in_row = Some(SearchMatch {
462 row: row_idx,
463 col: enum_idx, value: value_str,
465 });
466 } else {
467 debug!(target: "vim_search",
468 "Skipping additional match in row {} at visual col {} (enum_idx {}): '{}'",
469 row_idx, enum_idx, enum_idx, value_str);
470 }
471 }
472 }
473
474 if let Some(match_item) = first_match_in_row {
476 matches.push(match_item);
477 }
478 }
479 }
480
481 debug!(target: "vim_search", "Found {} total matches", matches.len());
482 matches
483 }
484
485 fn navigate_to_match(&self, match_item: &SearchMatch, viewport: &mut ViewportManager) {
487 info!(target: "vim_search",
488 "=== NAVIGATE_TO_MATCH START ===");
489 info!(target: "vim_search",
490 "Target match: row={} (absolute), col={} (visual), value='{}'",
491 match_item.row, match_item.col, match_item.value);
492
493 let terminal_width = viewport.get_terminal_width();
495 let terminal_height = viewport.get_terminal_height();
496 info!(target: "vim_search",
497 "Terminal dimensions: width={}, height={}",
498 terminal_width, terminal_height);
499
500 let viewport_rows = viewport.get_viewport_rows();
502 let viewport_cols = viewport.viewport_cols();
503 let viewport_height = viewport_rows.end - viewport_rows.start;
504 let viewport_width = viewport_cols.end - viewport_cols.start;
505
506 info!(target: "vim_search",
507 "Current viewport BEFORE changes:");
508 info!(target: "vim_search",
509 " Rows: {:?} (height={})", viewport_rows, viewport_height);
510 info!(target: "vim_search",
511 " Cols: {:?} (width={})", viewport_cols, viewport_width);
512 info!(target: "vim_search",
513 " Current crosshair: row={}, col={}",
514 viewport.get_crosshair_row(), viewport.get_crosshair_col());
515
516 let new_row_start = match_item.row.saturating_sub(viewport_height / 2);
519 info!(target: "vim_search",
520 "Centering row {} in viewport (height={}), new viewport start row={}",
521 match_item.row, viewport_height, new_row_start);
522
523 let new_col_start = match_item.col.saturating_sub(3); info!(target: "vim_search",
528 "Positioning column {} in viewport, new viewport start col={}",
529 match_item.col, new_col_start);
530
531 info!(target: "vim_search",
533 "=== VIEWPORT UPDATE ===");
534 info!(target: "vim_search",
535 "Will call set_viewport with: row_start={}, col_start={}, width={}, height={}",
536 new_row_start, new_col_start, terminal_width, terminal_height);
537
538 viewport.set_viewport(
540 new_row_start,
541 new_col_start,
542 terminal_width, terminal_height as u16,
544 );
545
546 let final_viewport_rows = viewport.get_viewport_rows();
548 let final_viewport_cols = viewport.viewport_cols();
549
550 info!(target: "vim_search",
551 "Viewport AFTER set_viewport: rows {:?}, cols {:?}",
552 final_viewport_rows, final_viewport_cols);
553
554 if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
556 error!(target: "vim_search",
557 "CRITICAL ERROR: Target column {} is NOT in viewport {:?} after set_viewport!",
558 match_item.col, final_viewport_cols);
559 error!(target: "vim_search",
560 "We asked for col_start={}, but viewport gave us {:?}",
561 new_col_start, final_viewport_cols);
562 }
563
564 info!(target: "vim_search",
567 "=== CROSSHAIR POSITIONING ===");
568 info!(target: "vim_search",
569 "Setting crosshair to ABSOLUTE position: row={}, col={}",
570 match_item.row, match_item.col);
571
572 viewport.set_crosshair(match_item.row, match_item.col);
573
574 let center_row =
576 final_viewport_rows.start + (final_viewport_rows.end - final_viewport_rows.start) / 2;
577 let center_col =
578 final_viewport_cols.start + (final_viewport_cols.end - final_viewport_cols.start) / 2;
579
580 info!(target: "vim_search",
581 "Viewport center is at: row={}, col={}",
582 center_row, center_col);
583 info!(target: "vim_search",
584 "Match is at: row={}, col={}",
585 match_item.row, match_item.col);
586 info!(target: "vim_search",
587 "Distance from center: row_diff={}, col_diff={}",
588 (match_item.row as i32 - center_row as i32).abs(),
589 (match_item.col as i32 - center_col as i32).abs());
590
591 if let Some((vp_row, vp_col)) = viewport.get_crosshair_viewport_position() {
593 info!(target: "vim_search",
594 "Crosshair appears at viewport position: ({}, {})",
595 vp_row, vp_col);
596 info!(target: "vim_search",
597 "Viewport dimensions: {} rows x {} cols",
598 final_viewport_rows.end - final_viewport_rows.start,
599 final_viewport_cols.end - final_viewport_cols.start);
600 info!(target: "vim_search",
601 "Expected center position: ({}, {})",
602 (final_viewport_rows.end - final_viewport_rows.start) / 2,
603 (final_viewport_cols.end - final_viewport_cols.start) / 2);
604 } else {
605 error!(target: "vim_search",
606 "CRITICAL: Crosshair is NOT visible in viewport after centering!");
607 }
608
609 info!(target: "vim_search",
611 "=== VERIFICATION ===");
612
613 if match_item.row < final_viewport_rows.start || match_item.row >= final_viewport_rows.end {
614 error!(target: "vim_search",
615 "ERROR: Match row {} is OUTSIDE viewport {:?} after scrolling!",
616 match_item.row, final_viewport_rows);
617 } else {
618 info!(target: "vim_search",
619 "✓ Match row {} is within viewport {:?}",
620 match_item.row, final_viewport_rows);
621 }
622
623 if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
624 error!(target: "vim_search",
625 "ERROR: Match column {} is OUTSIDE viewport {:?} after scrolling!",
626 match_item.col, final_viewport_cols);
627 } else {
628 info!(target: "vim_search",
629 "✓ Match column {} is within viewport {:?}",
630 match_item.col, final_viewport_cols);
631 }
632
633 info!(target: "vim_search",
635 "=== NAVIGATE_TO_MATCH COMPLETE ===");
636 info!(target: "vim_search",
637 "Match at absolute ({}, {}), crosshair at ({}, {}), viewport rows {:?} cols {:?}",
638 match_item.row, match_item.col,
639 viewport.get_crosshair_row(), viewport.get_crosshair_col(),
640 final_viewport_rows, final_viewport_cols);
641 }
642
643 pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
645 self.case_sensitive = case_sensitive;
646 debug!(target: "vim_search", "Case sensitivity set to: {}", case_sensitive);
647 }
648
649 pub fn set_search_state_from_external(
652 &mut self,
653 pattern: String,
654 matches: Vec<(usize, usize)>,
655 dataview: &DataView,
656 ) {
657 info!(target: "vim_search",
658 "Setting search state from external search: pattern='{}', {} matches",
659 pattern, matches.len());
660
661 let search_matches: Vec<SearchMatch> = matches
663 .into_iter()
664 .filter_map(|(row, col)| {
665 if let Some(row_data) = dataview.get_row(row) {
666 if col < row_data.values.len() {
667 Some(SearchMatch {
668 row,
669 col,
670 value: row_data.values[col].to_string(),
671 })
672 } else {
673 None
674 }
675 } else {
676 None
677 }
678 })
679 .collect();
680
681 if search_matches.is_empty() {
682 warn!(target: "vim_search", "No valid matches to set in vim search state");
683 } else {
684 let match_count = search_matches.len();
685
686 self.state = VimSearchState::Navigating {
688 pattern: pattern.clone(),
689 matches: search_matches,
690 current_index: 0,
691 };
692 self.last_search_pattern = Some(pattern);
693
694 info!(target: "vim_search",
695 "Vim search state updated: {} matches ready for navigation",
696 match_count);
697 }
698 }
699}