diff --git a/sql-cli/src/data/datavalue_compare.rs b/sql-cli/src/data/datavalue_compare.rs index b7db5015..2714a669 100644 --- a/sql-cli/src/data/datavalue_compare.rs +++ b/sql-cli/src/data/datavalue_compare.rs @@ -165,15 +165,26 @@ mod tests { #[test] fn test_cross_type_comparison() { - // Test the type ordering + // Test the type ordering (except Integer/Float which compare by value) assert_eq!( compare_datavalues(&DataValue::Boolean(true), &DataValue::Integer(1)), Ordering::Less ); + + // Integer and Float now compare by numeric value, not type assert_eq!( compare_datavalues(&DataValue::Integer(1), &DataValue::Float(1.0)), - Ordering::Less + Ordering::Equal // 1 == 1.0 + ); + assert_eq!( + compare_datavalues(&DataValue::Integer(1), &DataValue::Float(1.5)), + Ordering::Less // 1 < 1.5 + ); + assert_eq!( + compare_datavalues(&DataValue::Integer(2), &DataValue::Float(1.5)), + Ordering::Greater // 2 > 1.5 ); + assert_eq!( compare_datavalues(&DataValue::Float(1.0), &DataValue::String("a".to_string())), Ordering::Less diff --git a/sql-cli/src/ui/action_handlers.rs b/sql-cli/src/ui/action_handlers.rs index 70a1b3bf..ca547821 100644 --- a/sql-cli/src/ui/action_handlers.rs +++ b/sql-cli/src/ui/action_handlers.rs @@ -105,6 +105,14 @@ pub trait ActionHandlerContext { fn delete(&mut self); fn undo(&mut self); fn redo(&mut self); + + // Jump-to-row operations + fn start_jump_to_row(&mut self); + fn clear_jump_to_row_input(&mut self); + + // Viewport lock operations + fn toggle_cursor_lock(&mut self); + fn toggle_viewport_lock(&mut self); } /// Handler for navigation actions (Up, Down, Left, Right, PageUp, etc.) @@ -615,6 +623,42 @@ impl ActionHandler for InputCursorActionHandler { } } +/// Handler for debug and viewport control operations +pub struct DebugViewportActionHandler; + +impl ActionHandler for DebugViewportActionHandler { + fn handle_action( + &self, + action: &Action, + _context: &ActionContext, + tui: &mut dyn ActionHandlerContext, + ) -> Option> { + match action { + Action::ShowDebugInfo => { + tui.toggle_debug_mode(); + Some(Ok(ActionResult::Handled)) + } + Action::StartJumpToRow => { + tui.start_jump_to_row(); + Some(Ok(ActionResult::Handled)) + } + Action::ToggleCursorLock => { + tui.toggle_cursor_lock(); + Some(Ok(ActionResult::Handled)) + } + Action::ToggleViewportLock => { + tui.toggle_viewport_lock(); + Some(Ok(ActionResult::Handled)) + } + _ => None, + } + } + + fn name(&self) -> &'static str { + "DebugViewport" + } +} + /// Handler for text editing actions in Command mode pub struct TextEditActionHandler; @@ -679,6 +723,7 @@ impl ActionDispatcher { Box::new(InputCursorActionHandler), Box::new(TextEditActionHandler), Box::new(ViewportNavigationHandler), + Box::new(DebugViewportActionHandler), ]; Self { handlers } @@ -895,6 +940,22 @@ mod tests { self.last_action = "navigate_to_viewport_bottom".to_string(); } + // Jump-to-row operations + fn start_jump_to_row(&mut self) { + self.last_action = "start_jump_to_row".to_string(); + } + fn clear_jump_to_row_input(&mut self) { + self.last_action = "clear_jump_to_row_input".to_string(); + } + + // Viewport lock operations + fn toggle_cursor_lock(&mut self) { + self.last_action = "toggle_cursor_lock".to_string(); + } + fn toggle_viewport_lock(&mut self) { + self.last_action = "toggle_viewport_lock".to_string(); + } + // Input and text editing fn move_input_cursor_left(&mut self) { self.last_action = "move_input_cursor_left".to_string(); @@ -922,6 +983,55 @@ mod tests { } } + #[test] + fn test_debug_viewport_handler() { + let mut tui = MockTui::new(); + let handler = DebugViewportActionHandler; + let context = ActionContext { + mode: AppMode::Results, + selection_mode: crate::app_state_container::SelectionMode::Cell, + has_results: true, + has_filter: false, + has_search: false, + row_count: 100, + column_count: 10, + current_row: 0, + current_column: 0, + }; + + // Test ShowDebugInfo + let result = handler + .handle_action(&Action::ShowDebugInfo, &context, &mut tui) + .unwrap(); + assert!(matches!(result, Ok(ActionResult::Handled))); + assert_eq!(tui.last_action, "toggle_debug_mode"); + + // Test StartJumpToRow + let result = handler + .handle_action(&Action::StartJumpToRow, &context, &mut tui) + .unwrap(); + assert!(matches!(result, Ok(ActionResult::Handled))); + assert_eq!(tui.last_action, "start_jump_to_row"); + + // Test ToggleCursorLock + let result = handler + .handle_action(&Action::ToggleCursorLock, &context, &mut tui) + .unwrap(); + assert!(matches!(result, Ok(ActionResult::Handled))); + assert_eq!(tui.last_action, "toggle_cursor_lock"); + + // Test ToggleViewportLock + let result = handler + .handle_action(&Action::ToggleViewportLock, &context, &mut tui) + .unwrap(); + assert!(matches!(result, Ok(ActionResult::Handled))); + assert_eq!(tui.last_action, "toggle_viewport_lock"); + + // Test unhandled action + let result = handler.handle_action(&Action::Quit, &context, &mut tui); + assert!(result.is_none()); + } + #[test] fn test_navigation_handler() { let handler = NavigationActionHandler; diff --git a/sql-cli/src/ui/enhanced_tui.rs b/sql-cli/src/ui/enhanced_tui.rs index 395890e2..45eaceba 100644 --- a/sql-cli/src/ui/enhanced_tui.rs +++ b/sql-cli/src/ui/enhanced_tui.rs @@ -19,14 +19,13 @@ use crate::data::data_view::DataView; use crate::debug::{DebugRegistry, MemoryTracker}; use crate::debug_service::DebugService; use crate::help_text::HelpText; -use crate::services::{QueryExecutionService, QueryOrchestrator}; +use crate::services::QueryOrchestrator; use crate::sql::hybrid_parser::HybridParser; use crate::sql_highlighter::SqlHighlighter; use crate::state::StateDispatcher; use crate::ui::action_handlers::ActionHandlerContext; use crate::ui::actions::{Action, ActionContext, ActionResult}; use crate::ui::debug_context::DebugContext; -use crate::ui::enhanced_tui_helpers; use crate::ui::key_chord_handler::{ChordResult, KeyChordHandler}; use crate::ui::key_dispatcher::KeyDispatcher; use crate::ui::key_indicator::{format_key_for_display, KeyPressIndicator}; @@ -320,14 +319,6 @@ impl DebugContext for EnhancedTuiApp { EnhancedTuiApp::debug_generate_parser_info(self, query) } - // debug_generate_buffer_state now uses default implementation from trait - // debug_generate_results_state now uses default implementation from trait - // debug_generate_memory_info now uses default implementation from trait - // debug_generate_datatable_schema now uses default implementation from trait - // debug_generate_dataview_state now uses default implementation from trait - - // debug_generate_viewport_state now uses default implementation from trait - fn debug_generate_navigation_state(&self) -> String { // Call the actual implementation method on self (defined below in impl EnhancedTuiApp) Self::debug_generate_navigation_state(self) @@ -462,83 +453,8 @@ impl EnhancedTuiApp { // - Export operations: ExportActionHandler // - Yank operations: YankActionHandler // - UI operations: UIActionHandler (ShowHelp) - ShowDebugInfo => { - // Use the existing toggle_debug_mode which generates all debug info - self.toggle_debug_mode(); - Ok(ActionResult::Handled) - } - StartJumpToRow => { - self.state_container.set_mode(AppMode::JumpToRow); - self.shadow_state - .borrow_mut() - .observe_mode_change(AppMode::JumpToRow, "jump_to_row_requested"); - self.clear_jump_to_row_input(); - - // Set jump-to-row state as active (can mutate directly now) - self.state_container.jump_to_row_mut().is_active = true; - - self.state_container - .set_status_message("Enter row number (1-based):".to_string()); - Ok(ActionResult::Handled) - } - ToggleCursorLock => { - // Toggle cursor lock in ViewportManager - let is_locked = { - let mut viewport_manager_borrow = self.viewport_manager.borrow_mut(); - if let Some(ref mut viewport_manager) = *viewport_manager_borrow { - viewport_manager.toggle_cursor_lock(); - Some(viewport_manager.is_cursor_locked()) - } else { - None - } - }; // Borrow is dropped here - - if let Some(is_locked) = is_locked { - let msg = if is_locked { - "Cursor lock ON - cursor stays in viewport position while scrolling" - } else { - "Cursor lock OFF" - }; - self.state_container.set_status_message(msg.to_string()); - - // Log for shadow state learning (not tracking as state change yet) - info!(target: "shadow_state", - "Cursor lock toggled: {} (in {:?} mode)", - if is_locked { "ON" } else { "OFF" }, - self.shadow_state.borrow().get_mode() - ); - } - Ok(ActionResult::Handled) - } - ToggleViewportLock => { - // Toggle viewport lock in ViewportManager - let is_locked = { - let mut viewport_manager_borrow = self.viewport_manager.borrow_mut(); - if let Some(ref mut viewport_manager) = *viewport_manager_borrow { - viewport_manager.toggle_viewport_lock(); - Some(viewport_manager.is_viewport_locked()) - } else { - None - } - }; // Borrow is dropped here + // - Debug/Viewport operations: DebugViewportActionHandler (ShowDebugInfo, StartJumpToRow, ToggleCursorLock, ToggleViewportLock) - if let Some(is_locked) = is_locked { - let msg = if is_locked { - "Viewport lock ON - navigation constrained to current viewport" - } else { - "Viewport lock OFF" - }; - self.state_container.set_status_message(msg.to_string()); - - // Log for shadow state learning (not tracking as state change yet) - info!(target: "shadow_state", - "Viewport lock toggled: {} (in {:?} mode)", - if is_locked { "ON" } else { "OFF" }, - self.shadow_state.borrow().get_mode() - ); - } - Ok(ActionResult::Handled) - } // NextColumn and PreviousColumn are now handled by NavigationActionHandler in visitor pattern Sort(_column_idx) => { // For now, always sort by current column (like 's' key does) @@ -4123,7 +4039,7 @@ impl EnhancedTuiApp { info!(target: "search", "Setting column to visual index {}", search_match.col); - + // Update all column-related state to the visual column index self.state_container .set_current_column_buffer(search_match.col); @@ -4133,7 +4049,7 @@ impl EnhancedTuiApp { self.state_container.select_column(search_match.col); info!(target: "search", "Updated SelectionState column to: {}", search_match.col); - + // Log the current state of all column-related fields info!(target: "search", "Column state after update: nav.selected_column={}, buffer.current_column={}, selection.selected_column={}", @@ -4143,22 +4059,24 @@ impl EnhancedTuiApp { // CRITICAL: Also update navigation's selected_row to trigger proper rendering self.state_container.navigation_mut().selected_row = search_match.row; - + // CRITICAL: Update navigation scroll offset to match the viewport! // The viewport manager has scrolled, but we need to sync that back to navigation state if let Some(ref viewport) = *self.viewport_manager.borrow() { let viewport_rows = viewport.get_viewport_rows(); let viewport_cols = viewport.viewport_cols(); - + info!(target: "search", "Syncing navigation scroll to viewport: row_offset={}, col_offset={}", viewport_rows.start, viewport_cols.start); - + // Update the navigation scroll offset to match viewport - self.state_container.navigation_mut().scroll_offset = (viewport_rows.start, viewport_cols.start); - + self.state_container.navigation_mut().scroll_offset = + (viewport_rows.start, viewport_cols.start); + // Also update the buffer scroll offset - self.state_container.set_scroll_offset((viewport_rows.start, viewport_cols.start)); + self.state_container + .set_scroll_offset((viewport_rows.start, viewport_cols.start)); } // CRITICAL: Update TableWidgetManager to trigger re-render @@ -4167,7 +4085,7 @@ impl EnhancedTuiApp { self.table_widget_manager .borrow_mut() .navigate_to(search_match.row, search_match.col); - + // Log column state after TableWidgetManager update info!(target: "search", "After TableWidgetManager update: nav.selected_column={}, buffer.current_column={}, selection.selected_column={}", @@ -4215,28 +4133,36 @@ impl EnhancedTuiApp { search_match.col + 1 )); } - + // Final column state logging info!(target: "search", "FINAL vim_search_next state: nav.selected_column={}, buffer.current_column={}, selection.selected_column={}", self.state_container.navigation().selected_column, self.state_container.get_current_column(), self.state_container.selection().selected_column); - + // CRITICAL: Verify what's actually at the final position if let Some(dataview) = self.state_container.get_buffer_dataview() { let final_row = self.state_container.navigation().selected_row; let final_col = self.state_container.navigation().selected_column; - + if let Some(row_data) = dataview.get_row(final_row) { if final_col < row_data.values.len() { let actual_value = &row_data.values[final_col]; info!(target: "search", "VERIFICATION: Cell at final position ({}, {}) contains: '{}'", final_row, final_col, actual_value); - - let pattern = self.vim_search_adapter.borrow().get_pattern().unwrap_or_default(); - if !actual_value.to_string().to_lowercase().contains(&pattern.to_lowercase()) { + + let pattern = self + .vim_search_adapter + .borrow() + .get_pattern() + .unwrap_or_default(); + if !actual_value + .to_string() + .to_lowercase() + .contains(&pattern.to_lowercase()) + { error!(target: "search", "ERROR: Final cell '{}' does NOT contain search pattern '{}'!", actual_value, pattern); @@ -4304,7 +4230,7 @@ impl EnhancedTuiApp { info!(target: "search", "Setting column to visual index {}", search_match.col); - + // Update all column-related state to the visual column index self.state_container .set_current_column_buffer(search_match.col); @@ -4314,7 +4240,7 @@ impl EnhancedTuiApp { self.state_container.select_column(search_match.col); info!(target: "search", "Updated SelectionState column to: {}", search_match.col); - + // Log the current state of all column-related fields info!(target: "search", "Column state after update: nav.selected_column={}, buffer.current_column={}, selection.selected_column={}", @@ -4324,22 +4250,24 @@ impl EnhancedTuiApp { // CRITICAL: Also update navigation's selected_row to trigger proper rendering self.state_container.navigation_mut().selected_row = search_match.row; - + // CRITICAL: Update navigation scroll offset to match the viewport! // The viewport manager has scrolled, but we need to sync that back to navigation state if let Some(ref viewport) = *self.viewport_manager.borrow() { let viewport_rows = viewport.get_viewport_rows(); let viewport_cols = viewport.viewport_cols(); - + info!(target: "search", "Syncing navigation scroll to viewport: row_offset={}, col_offset={}", viewport_rows.start, viewport_cols.start); - + // Update the navigation scroll offset to match viewport - self.state_container.navigation_mut().scroll_offset = (viewport_rows.start, viewport_cols.start); - + self.state_container.navigation_mut().scroll_offset = + (viewport_rows.start, viewport_cols.start); + // Also update the buffer scroll offset - self.state_container.set_scroll_offset((viewport_rows.start, viewport_cols.start)); + self.state_container + .set_scroll_offset((viewport_rows.start, viewport_cols.start)); } // CRITICAL: Update TableWidgetManager to trigger re-render @@ -4348,7 +4276,7 @@ impl EnhancedTuiApp { self.table_widget_manager .borrow_mut() .navigate_to(search_match.row, search_match.col); - + // Log column state after TableWidgetManager update info!(target: "search", "After TableWidgetManager update: nav.selected_column={}, buffer.current_column={}, selection.selected_column={}", @@ -5723,13 +5651,14 @@ impl EnhancedTuiApp { // Get the crosshair's viewport-relative position for rendering // The viewport manager stores crosshair in absolute coordinates // but we need viewport-relative for rendering - let crosshair_column_position = if let Some((_, col_pos)) = viewport_manager.get_crosshair_viewport_position() { - col_pos - } else { - // Crosshair is outside viewport, default to 0 - 0 - }; - + let crosshair_column_position = + if let Some((_, col_pos)) = viewport_manager.get_crosshair_viewport_position() { + col_pos + } else { + // Crosshair is outside viewport, default to 0 + 0 + }; + let crosshair_visual = viewport_manager.get_crosshair_col(); (info.1, crosshair_column_position, crosshair_visual) @@ -6251,11 +6180,6 @@ impl EnhancedTuiApp { .get_detailed_debug_info(query, query.len()) } - // debug_generate_buffer_state and debug_generate_results_state moved to DebugContext trait defaults - - // debug_generate_memory_info moved to DebugContext trait default implementation - // debug_generate_datatable_schema moved to DebugContext trait default implementation - fn debug_generate_navigation_state(&self) -> String { let mut debug_info = String::new(); debug_info.push_str("\n========== NAVIGATION DEBUG ==========\n"); @@ -6960,7 +6884,7 @@ impl ActionHandlerContext for EnhancedTuiApp { } AppMode::JumpToRow => { self.state_container.set_mode(AppMode::Results); - self.clear_jump_to_row_input(); + ::clear_jump_to_row_input(self); // Clear jump-to-row state (can mutate directly now) self.state_container.jump_to_row_mut().is_active = false; self.state_container @@ -7126,6 +7050,82 @@ impl ActionHandlerContext for EnhancedTuiApp { fn redo(&mut self) { self.state_container.perform_redo(); } + + fn start_jump_to_row(&mut self) { + self.state_container.set_mode(AppMode::JumpToRow); + self.shadow_state + .borrow_mut() + .observe_mode_change(AppMode::JumpToRow, "jump_to_row_requested"); + ::clear_jump_to_row_input(self); + + // Set jump-to-row state as active (can mutate directly now) + self.state_container.jump_to_row_mut().is_active = true; + + self.state_container + .set_status_message("Enter row number (1-based):".to_string()); + } + + fn clear_jump_to_row_input(&mut self) { + ::clear_jump_to_row_input(self); + } + + fn toggle_cursor_lock(&mut self) { + // Toggle cursor lock in ViewportManager + let is_locked = { + let mut viewport_manager_borrow = self.viewport_manager.borrow_mut(); + if let Some(ref mut viewport_manager) = *viewport_manager_borrow { + viewport_manager.toggle_cursor_lock(); + Some(viewport_manager.is_cursor_locked()) + } else { + None + } + }; + + if let Some(is_locked) = is_locked { + let msg = if is_locked { + "Cursor lock ON - cursor stays in viewport position while scrolling" + } else { + "Cursor lock OFF" + }; + self.state_container.set_status_message(msg.to_string()); + + // Log for shadow state learning (not tracking as state change yet) + info!(target: "shadow_state", + "Cursor lock toggled: {} (in {:?} mode)", + if is_locked { "ON" } else { "OFF" }, + self.shadow_state.borrow().get_mode() + ); + } + } + + fn toggle_viewport_lock(&mut self) { + // Toggle viewport lock in ViewportManager + let is_locked = { + let mut viewport_manager_borrow = self.viewport_manager.borrow_mut(); + if let Some(ref mut viewport_manager) = *viewport_manager_borrow { + viewport_manager.toggle_viewport_lock(); + Some(viewport_manager.is_viewport_locked()) + } else { + None + } + }; + + if let Some(is_locked) = is_locked { + let msg = if is_locked { + "Viewport lock ON - navigation constrained to current viewport" + } else { + "Viewport lock OFF" + }; + self.state_container.set_status_message(msg.to_string()); + + // Log for shadow state learning (not tracking as state change yet) + info!(target: "shadow_state", + "Viewport lock toggled: {} (in {:?} mode)", + if is_locked { "ON" } else { "OFF" }, + self.shadow_state.borrow().get_mode() + ); + } + } } // Implement NavigationBehavior trait for EnhancedTuiApp diff --git a/sql-cli/src/ui/vim_search_manager.rs b/sql-cli/src/ui/vim_search_manager.rs index 1371523f..1a7c5fe7 100644 --- a/sql-cli/src/ui/vim_search_manager.rs +++ b/sql-cli/src/ui/vim_search_manager.rs @@ -164,9 +164,13 @@ impl VimSearchManager { "Match {}/{}: row={}, visual_col={}, stored_value='{}'", *current_index + 1, matches.len(), match_item.row, match_item.col, match_item.value); - + // Double-check: Does this value actually contain our pattern? - if !match_item.value.to_lowercase().contains(&pattern.to_lowercase()) { + 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); @@ -495,7 +499,7 @@ impl VimSearchManager { 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", @@ -527,7 +531,7 @@ impl VimSearchManager { 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); - + // Update viewport with preserved terminal dimensions viewport.set_viewport( new_row_start, @@ -539,11 +543,11 @@ impl VimSearchManager { // Get the updated viewport state 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); - + // CRITICAL: Check if our target column is actually in the viewport! if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end { error!(target: "vim_search", @@ -561,13 +565,15 @@ impl VimSearchManager { 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); - + // Verify the match is centered in the viewport - 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; - + 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); @@ -578,7 +584,7 @@ impl VimSearchManager { "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()); - + // Get the viewport-relative position for verification if let Some((vp_row, vp_col)) = viewport.get_crosshair_viewport_position() { info!(target: "vim_search", @@ -600,7 +606,7 @@ impl VimSearchManager { // Verify the match is actually visible in the viewport after scrolling 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!",