use crate::data::data_view::DataView;
use crate::ui::viewport_manager::ViewportManager;
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone)]
pub struct SearchMatch {
pub row: usize,
pub col: usize,
pub value: String,
}
#[derive(Debug, Clone)]
pub enum VimSearchState {
Inactive,
Typing { pattern: String },
Navigating {
pattern: String,
matches: Vec<SearchMatch>,
current_index: usize,
},
}
pub struct VimSearchManager {
state: VimSearchState,
case_sensitive: bool,
last_search_pattern: Option<String>,
}
impl Default for VimSearchManager {
fn default() -> Self {
Self::new()
}
}
impl VimSearchManager {
#[must_use]
pub fn new() -> Self {
Self {
state: VimSearchState::Inactive,
case_sensitive: false,
last_search_pattern: None,
}
}
pub fn start_search(&mut self) {
info!(target: "vim_search", "Starting vim search mode");
self.state = VimSearchState::Typing {
pattern: String::new(),
};
}
pub fn update_pattern(
&mut self,
pattern: String,
dataview: &DataView,
viewport: &mut ViewportManager,
) -> Option<SearchMatch> {
debug!(target: "vim_search", "Updating pattern to: '{}'", pattern);
self.state = VimSearchState::Typing {
pattern: pattern.clone(),
};
if pattern.is_empty() {
return None;
}
let matches = self.find_matches(&pattern, dataview);
if let Some(first_match) = matches.first() {
debug!(target: "vim_search",
"Found {} matches, navigating to first at ({}, {})",
matches.len(), first_match.row, first_match.col);
self.navigate_to_match(first_match, viewport);
Some(first_match.clone())
} else {
debug!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
None
}
}
pub fn confirm_search(&mut self, dataview: &DataView, viewport: &mut ViewportManager) -> bool {
if let VimSearchState::Typing { pattern } = &self.state {
if pattern.is_empty() {
info!(target: "vim_search", "Empty pattern, canceling search");
self.cancel_search();
return false;
}
let pattern = pattern.clone();
let matches = self.find_matches(&pattern, dataview);
if matches.is_empty() {
warn!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
self.cancel_search();
return false;
}
info!(target: "vim_search",
"Confirming search with {} matches for pattern: '{}'",
matches.len(), pattern);
if let Some(first_match) = matches.first() {
self.navigate_to_match(first_match, viewport);
}
self.state = VimSearchState::Navigating {
pattern: pattern.clone(),
matches,
current_index: 0,
};
self.last_search_pattern = Some(pattern);
true
} else {
warn!(target: "vim_search", "confirm_search called in wrong state: {:?}", self.state);
false
}
}
pub fn next_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
let match_to_navigate = if let VimSearchState::Navigating {
matches,
current_index,
pattern,
} = &mut self.state
{
if matches.is_empty() {
return None;
}
info!(target: "vim_search",
"=== 'n' KEY PRESSED - BEFORE NAVIGATION ===");
info!(target: "vim_search",
"Current match index: {}/{}, Pattern: '{}'",
*current_index + 1, matches.len(), pattern);
info!(target: "vim_search",
"Current viewport - rows: {:?}, cols: {:?}",
viewport.get_viewport_rows(), viewport.viewport_cols());
info!(target: "vim_search",
"Current crosshair position: row={}, col={}",
viewport.get_crosshair_row(), viewport.get_crosshair_col());
*current_index = (*current_index + 1) % matches.len();
let match_item = matches[*current_index].clone();
info!(target: "vim_search",
"=== NEXT MATCH DETAILS ===");
info!(target: "vim_search",
"Match {}/{}: row={}, visual_col={}, stored_value='{}'",
*current_index + 1, matches.len(),
match_item.row, match_item.col, match_item.value);
if !match_item
.value
.to_lowercase()
.contains(&pattern.to_lowercase())
{
error!(target: "vim_search",
"CRITICAL ERROR: Match value '{}' does NOT contain search pattern '{}'!",
match_item.value, pattern);
error!(target: "vim_search",
"This indicates the search index is corrupted or stale!");
}
info!(target: "vim_search",
"Expected: Cell at row {} col {} should contain substring '{}'",
match_item.row, match_item.col, pattern);
let stored_contains = match_item
.value
.to_lowercase()
.contains(&pattern.to_lowercase());
if stored_contains {
info!(target: "vim_search",
"✓ Stored match '{}' contains pattern '{}'",
match_item.value, pattern);
} else {
warn!(target: "vim_search",
"CRITICAL: Stored match '{}' does NOT contain pattern '{}'!",
match_item.value, pattern);
}
Some(match_item)
} else {
debug!(target: "vim_search", "next_match called but not in navigation mode");
None
};
if let Some(ref match_item) = match_to_navigate {
info!(target: "vim_search",
"=== NAVIGATING TO MATCH ===");
self.navigate_to_match(match_item, viewport);
info!(target: "vim_search",
"=== AFTER NAVIGATION ===");
info!(target: "vim_search",
"New viewport - rows: {:?}, cols: {:?}",
viewport.get_viewport_rows(), viewport.viewport_cols());
info!(target: "vim_search",
"New crosshair position: row={}, col={}",
viewport.get_crosshair_row(), viewport.get_crosshair_col());
info!(target: "vim_search",
"Crosshair should be at: row={}, col={} (visual coordinates)",
match_item.row, match_item.col);
}
match_to_navigate
}
pub fn previous_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
let match_to_navigate = if let VimSearchState::Navigating {
matches,
current_index,
pattern: _,
} = &mut self.state
{
if matches.is_empty() {
return None;
}
*current_index = if *current_index == 0 {
matches.len() - 1
} else {
*current_index - 1
};
let match_item = matches[*current_index].clone();
info!(target: "vim_search",
"Navigating to previous match {}/{} at ({}, {})",
*current_index + 1, matches.len(), match_item.row, match_item.col);
Some(match_item)
} else {
debug!(target: "vim_search", "previous_match called but not in navigation mode");
None
};
if let Some(ref match_item) = match_to_navigate {
self.navigate_to_match(match_item, viewport);
}
match_to_navigate
}
pub fn cancel_search(&mut self) {
info!(target: "vim_search", "Canceling search, returning to inactive state");
self.state = VimSearchState::Inactive;
}
pub fn clear(&mut self) {
info!(target: "vim_search", "Clearing all search state");
self.state = VimSearchState::Inactive;
self.last_search_pattern = None;
}
pub fn exit_navigation(&mut self) {
if let VimSearchState::Navigating { pattern, .. } = &self.state {
self.last_search_pattern = Some(pattern.clone());
}
self.state = VimSearchState::Inactive;
}
pub fn resume_last_search(
&mut self,
dataview: &DataView,
viewport: &mut ViewportManager,
) -> bool {
if let Some(pattern) = &self.last_search_pattern {
let pattern = pattern.clone();
let matches = self.find_matches(&pattern, dataview);
if matches.is_empty() {
false
} else {
info!(target: "vim_search",
"Resuming search with pattern '{}', found {} matches",
pattern, matches.len());
if let Some(first_match) = matches.first() {
self.navigate_to_match(first_match, viewport);
}
self.state = VimSearchState::Navigating {
pattern,
matches,
current_index: 0,
};
true
}
} else {
false
}
}
#[must_use]
pub fn is_active(&self) -> bool {
!matches!(self.state, VimSearchState::Inactive)
}
#[must_use]
pub fn is_typing(&self) -> bool {
matches!(self.state, VimSearchState::Typing { .. })
}
#[must_use]
pub fn is_navigating(&self) -> bool {
matches!(self.state, VimSearchState::Navigating { .. })
}
#[must_use]
pub fn get_pattern(&self) -> Option<String> {
match &self.state {
VimSearchState::Typing { pattern } => Some(pattern.clone()),
VimSearchState::Navigating { pattern, .. } => Some(pattern.clone()),
VimSearchState::Inactive => None,
}
}
#[must_use]
pub fn get_match_info(&self) -> Option<(usize, usize)> {
match &self.state {
VimSearchState::Navigating {
matches,
current_index,
..
} => Some((*current_index + 1, matches.len())),
_ => None,
}
}
pub fn reset_to_first_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
if let VimSearchState::Navigating {
matches,
current_index,
..
} = &mut self.state
{
if matches.is_empty() {
return None;
}
*current_index = 0;
let first_match = matches[0].clone();
info!(target: "vim_search",
"Resetting to first match at ({}, {})",
first_match.row, first_match.col);
self.navigate_to_match(&first_match, viewport);
Some(first_match)
} else {
debug!(target: "vim_search", "reset_to_first_match called but not in navigation mode");
None
}
}
fn find_matches(&self, pattern: &str, dataview: &DataView) -> Vec<SearchMatch> {
let mut matches = Vec::new();
let pattern_lower = if self.case_sensitive {
pattern.to_string()
} else {
pattern.to_lowercase()
};
info!(target: "vim_search",
"=== FIND_MATCHES CALLED ===");
info!(target: "vim_search",
"Pattern passed in: '{}', pattern_lower: '{}', case_sensitive: {}",
pattern, pattern_lower, self.case_sensitive);
let display_columns = dataview.get_display_columns();
debug!(target: "vim_search",
"Display columns mapping: {:?} (count: {})",
display_columns, display_columns.len());
for row_idx in 0..dataview.row_count() {
if let Some(row) = dataview.get_row(row_idx) {
let mut first_match_in_row: Option<SearchMatch> = None;
for (enum_idx, value) in row.values.iter().enumerate() {
let value_str = value.to_string();
let search_value = if self.case_sensitive {
value_str.clone()
} else {
value_str.to_lowercase()
};
if search_value.contains(&pattern_lower) {
if first_match_in_row.is_none() {
let actual_col = if enum_idx < display_columns.len() {
display_columns[enum_idx]
} else {
enum_idx };
info!(target: "vim_search",
"Found first match in row {} at visual col {} (DataTable col {}, value '{}')",
row_idx, enum_idx, actual_col, value_str);
if value_str.contains("Futures Trading") {
warn!(target: "vim_search",
"SUSPICIOUS: Found 'Futures Trading' as a match for pattern '{}' (search_value='{}', pattern_lower='{}')",
pattern, search_value, pattern_lower);
}
first_match_in_row = Some(SearchMatch {
row: row_idx,
col: enum_idx, value: value_str,
});
} else {
debug!(target: "vim_search",
"Skipping additional match in row {} at visual col {} (enum_idx {}): '{}'",
row_idx, enum_idx, enum_idx, value_str);
}
}
}
if let Some(match_item) = first_match_in_row {
matches.push(match_item);
}
}
}
debug!(target: "vim_search", "Found {} total matches", matches.len());
matches
}
fn navigate_to_match(&self, match_item: &SearchMatch, viewport: &mut ViewportManager) {
info!(target: "vim_search",
"=== NAVIGATE_TO_MATCH START ===");
info!(target: "vim_search",
"Target match: row={} (absolute), col={} (visual), value='{}'",
match_item.row, match_item.col, match_item.value);
let terminal_width = viewport.get_terminal_width();
let terminal_height = viewport.get_terminal_height();
info!(target: "vim_search",
"Terminal dimensions: width={}, height={}",
terminal_width, terminal_height);
let viewport_rows = viewport.get_viewport_rows();
let viewport_cols = viewport.viewport_cols();
let viewport_height = viewport_rows.end - viewport_rows.start;
let viewport_width = viewport_cols.end - viewport_cols.start;
info!(target: "vim_search",
"Current viewport BEFORE changes:");
info!(target: "vim_search",
" Rows: {:?} (height={})", viewport_rows, viewport_height);
info!(target: "vim_search",
" Cols: {:?} (width={})", viewport_cols, viewport_width);
info!(target: "vim_search",
" Current crosshair: row={}, col={}",
viewport.get_crosshair_row(), viewport.get_crosshair_col());
let new_row_start = match_item.row.saturating_sub(viewport_height / 2);
info!(target: "vim_search",
"Centering row {} in viewport (height={}), new viewport start row={}",
match_item.row, viewport_height, new_row_start);
let new_col_start = match_item.col.saturating_sub(3); info!(target: "vim_search",
"Positioning column {} in viewport, new viewport start col={}",
match_item.col, new_col_start);
info!(target: "vim_search",
"=== VIEWPORT UPDATE ===");
info!(target: "vim_search",
"Will call set_viewport with: row_start={}, col_start={}, width={}, height={}",
new_row_start, new_col_start, terminal_width, terminal_height);
viewport.set_viewport(
new_row_start,
new_col_start,
terminal_width, terminal_height as u16,
);
let final_viewport_rows = viewport.get_viewport_rows();
let final_viewport_cols = viewport.viewport_cols();
info!(target: "vim_search",
"Viewport AFTER set_viewport: rows {:?}, cols {:?}",
final_viewport_rows, final_viewport_cols);
if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
error!(target: "vim_search",
"CRITICAL ERROR: Target column {} is NOT in viewport {:?} after set_viewport!",
match_item.col, final_viewport_cols);
error!(target: "vim_search",
"We asked for col_start={}, but viewport gave us {:?}",
new_col_start, final_viewport_cols);
}
info!(target: "vim_search",
"=== CROSSHAIR POSITIONING ===");
info!(target: "vim_search",
"Setting crosshair to ABSOLUTE position: row={}, col={}",
match_item.row, match_item.col);
viewport.set_crosshair(match_item.row, match_item.col);
let center_row =
final_viewport_rows.start + (final_viewport_rows.end - final_viewport_rows.start) / 2;
let center_col =
final_viewport_cols.start + (final_viewport_cols.end - final_viewport_cols.start) / 2;
info!(target: "vim_search",
"Viewport center is at: row={}, col={}",
center_row, center_col);
info!(target: "vim_search",
"Match is at: row={}, col={}",
match_item.row, match_item.col);
info!(target: "vim_search",
"Distance from center: row_diff={}, col_diff={}",
(match_item.row as i32 - center_row as i32).abs(),
(match_item.col as i32 - center_col as i32).abs());
if let Some((vp_row, vp_col)) = viewport.get_crosshair_viewport_position() {
info!(target: "vim_search",
"Crosshair appears at viewport position: ({}, {})",
vp_row, vp_col);
info!(target: "vim_search",
"Viewport dimensions: {} rows x {} cols",
final_viewport_rows.end - final_viewport_rows.start,
final_viewport_cols.end - final_viewport_cols.start);
info!(target: "vim_search",
"Expected center position: ({}, {})",
(final_viewport_rows.end - final_viewport_rows.start) / 2,
(final_viewport_cols.end - final_viewport_cols.start) / 2);
} else {
error!(target: "vim_search",
"CRITICAL: Crosshair is NOT visible in viewport after centering!");
}
info!(target: "vim_search",
"=== VERIFICATION ===");
if match_item.row < final_viewport_rows.start || match_item.row >= final_viewport_rows.end {
error!(target: "vim_search",
"ERROR: Match row {} is OUTSIDE viewport {:?} after scrolling!",
match_item.row, final_viewport_rows);
} else {
info!(target: "vim_search",
"✓ Match row {} is within viewport {:?}",
match_item.row, final_viewport_rows);
}
if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
error!(target: "vim_search",
"ERROR: Match column {} is OUTSIDE viewport {:?} after scrolling!",
match_item.col, final_viewport_cols);
} else {
info!(target: "vim_search",
"✓ Match column {} is within viewport {:?}",
match_item.col, final_viewport_cols);
}
info!(target: "vim_search",
"=== NAVIGATE_TO_MATCH COMPLETE ===");
info!(target: "vim_search",
"Match at absolute ({}, {}), crosshair at ({}, {}), viewport rows {:?} cols {:?}",
match_item.row, match_item.col,
viewport.get_crosshair_row(), viewport.get_crosshair_col(),
final_viewport_rows, final_viewport_cols);
}
pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
self.case_sensitive = case_sensitive;
debug!(target: "vim_search", "Case sensitivity set to: {}", case_sensitive);
}
pub fn set_search_state_from_external(
&mut self,
pattern: String,
matches: Vec<(usize, usize)>,
dataview: &DataView,
) {
info!(target: "vim_search",
"Setting search state from external search: pattern='{}', {} matches",
pattern, matches.len());
let search_matches: Vec<SearchMatch> = matches
.into_iter()
.filter_map(|(row, col)| {
if let Some(row_data) = dataview.get_row(row) {
if col < row_data.values.len() {
Some(SearchMatch {
row,
col,
value: row_data.values[col].to_string(),
})
} else {
None
}
} else {
None
}
})
.collect();
if search_matches.is_empty() {
warn!(target: "vim_search", "No valid matches to set in vim search state");
} else {
let match_count = search_matches.len();
self.state = VimSearchState::Navigating {
pattern: pattern.clone(),
matches: search_matches,
current_index: 0,
};
self.last_search_pattern = Some(pattern);
info!(target: "vim_search",
"Vim search state updated: {} matches ready for navigation",
match_count);
}
}
}