diff --git a/sql-cli/ADD_TO_GITHUB_WORKFLOWS.md b/sql-cli/docs/ADD_TO_GITHUB_WORKFLOWS.md similarity index 100% rename from sql-cli/ADD_TO_GITHUB_WORKFLOWS.md rename to sql-cli/docs/ADD_TO_GITHUB_WORKFLOWS.md diff --git a/sql-cli/COLUMN_SEARCH_COMPLETE_FIX.md b/sql-cli/docs/COLUMN_SEARCH_COMPLETE_FIX.md similarity index 100% rename from sql-cli/COLUMN_SEARCH_COMPLETE_FIX.md rename to sql-cli/docs/COLUMN_SEARCH_COMPLETE_FIX.md diff --git a/sql-cli/COLUMN_SEARCH_FIX.md b/sql-cli/docs/COLUMN_SEARCH_FIX.md similarity index 100% rename from sql-cli/COLUMN_SEARCH_FIX.md rename to sql-cli/docs/COLUMN_SEARCH_FIX.md diff --git a/sql-cli/CONFIG.md b/sql-cli/docs/CONFIG.md similarity index 100% rename from sql-cli/CONFIG.md rename to sql-cli/docs/CONFIG.md diff --git a/sql-cli/docs/DATATABLE_IMPLEMENTATION_STRATEGY.md b/sql-cli/docs/DATATABLE_IMPLEMENTATION_STRATEGY.md index 0e4381e3..9682e966 100644 --- a/sql-cli/docs/DATATABLE_IMPLEMENTATION_STRATEGY.md +++ b/sql-cli/docs/DATATABLE_IMPLEMENTATION_STRATEGY.md @@ -22,7 +22,6 @@ Currently: - Data and view logic are intertwined - Row counts are incorrect due to multiple filter states - The TUI knows too much about data implementation - Goal: - TUI only knows about views (what to display) - DataTable holds immutable source data diff --git a/sql-cli/FINAL_COLUMN_SEARCH_FIX.md b/sql-cli/docs/FINAL_COLUMN_SEARCH_FIX.md similarity index 100% rename from sql-cli/FINAL_COLUMN_SEARCH_FIX.md rename to sql-cli/docs/FINAL_COLUMN_SEARCH_FIX.md diff --git a/sql-cli/MERGE_SUMMARY.md b/sql-cli/docs/MERGE_SUMMARY.md similarity index 100% rename from sql-cli/MERGE_SUMMARY.md rename to sql-cli/docs/MERGE_SUMMARY.md diff --git a/sql-cli/docs/MODULE_STRUCTURE.md b/sql-cli/docs/MODULE_STRUCTURE.md new file mode 100644 index 00000000..f5cf9722 --- /dev/null +++ b/sql-cli/docs/MODULE_STRUCTURE.md @@ -0,0 +1,188 @@ +# Module Structure + +## Overview +Organizing the codebase into logical modules for better maintainability and clarity. + +## Proposed Structure + +``` +src/ +├── lib.rs # Public API exports +├── main.rs # Binary entry point +│ +├── core/ # Core business logic +│ ├── mod.rs +│ ├── app_state_container.rs +│ ├── buffer_manager.rs +│ ├── service_container.rs +│ └── global_state.rs +│ +├── data/ # Data layer (DataTable/DataView) +│ ├── mod.rs +│ ├── provider.rs # DataProvider traits +│ ├── table.rs # DataTable implementation +│ ├── view.rs # DataView implementation +│ ├── adapters/ # Adapters for existing data sources +│ │ ├── mod.rs +│ │ ├── buffer_adapter.rs +│ │ ├── csv_adapter.rs +│ │ └── api_adapter.rs +│ └── converters/ # Data format converters +│ ├── mod.rs +│ ├── csv_converter.rs +│ └── json_converter.rs +│ +├── ui/ # UI layer +│ ├── mod.rs +│ ├── enhanced_tui.rs # Main TUI application +│ ├── classic_cli.rs # Classic CLI mode +│ ├── key_dispatcher.rs # Key event handling +│ └── renderer.rs # Rendering logic +│ +├── widgets/ # UI widgets +│ ├── mod.rs +│ ├── debug_widget.rs +│ ├── editor_widget.rs +│ ├── help_widget.rs +│ ├── stats_widget.rs +│ ├── search_modes_widget.rs +│ ├── history_widget.rs +│ └── table_widget.rs +│ +├── state/ # State management +│ ├── mod.rs +│ ├── selection_state.rs +│ ├── filter_state.rs +│ ├── sort_state.rs +│ ├── search_state.rs +│ ├── column_search_state.rs +│ ├── clipboard_state.rs +│ ├── chord_state.rs +│ └── undo_redo_state.rs +│ +├── sql/ # SQL parsing and execution +│ ├── mod.rs +│ ├── parser.rs +│ ├── executor.rs +│ ├── optimizer.rs +│ └── cache.rs +│ +├── api/ # External API interactions +│ ├── mod.rs +│ ├── client.rs +│ ├── models.rs +│ └── endpoints.rs +│ +├── utils/ # Utility functions +│ ├── mod.rs +│ ├── debouncer.rs +│ ├── formatter.rs +│ ├── logger.rs +│ └── paths.rs +│ +├── config/ # Configuration +│ ├── mod.rs +│ ├── settings.rs +│ ├── themes.rs +│ └── keybindings.rs +│ +└── tests/ # Integration tests + ├── mod.rs + └── ... +``` + +## Migration Strategy + +### Phase 1: Create Directory Structure (V35) +- Create directories +- Add mod.rs files with re-exports +- No code moves yet + +### Phase 2: Move Widgets (V36) +- Move all *_widget.rs files to widgets/ +- Update imports + +### Phase 3: Move Data Layer (V37) +- Move DataProvider trait to data/provider.rs +- Move datatable* files to data/ +- Move converters and adapters + +### Phase 4: Move State Components (V38) +- Extract state structs from app_state_container.rs +- Create separate files in state/ +- Keep AppStateContainer as orchestrator + +### Phase 5: Move UI Components (V39) +- Move enhanced_tui.rs to ui/ +- Move classic_cli.rs to ui/ +- Move key_dispatcher.rs to ui/ + +### Phase 6: Move SQL Components (V40) +- Move SQL-related files to sql/ +- Organize parser, executor, cache + +### Phase 7: Move Utils and Config (V41) +- Move utility files to utils/ +- Move config files to config/ + +## Benefits + +1. **Clearer Organization**: Related files grouped together +2. **Easier Navigation**: Find files by functionality +3. **Better Encapsulation**: Modules can have private internals +4. **Scalability**: Easy to add new features in appropriate modules +5. **Testing**: Can test modules in isolation +6. **Documentation**: Each module can have its own README + +## Module Visibility Rules + +- Each module has a `mod.rs` that controls what's public +- Internal implementation details stay private +- Public API is explicitly exported +- Cross-module dependencies are minimized + +## Example: widgets/mod.rs + +```rust +// Re-export public widgets +pub mod debug_widget; +pub mod editor_widget; +pub mod help_widget; +pub mod stats_widget; + +// Common widget traits (if any) +pub trait Widget { + fn render(&self, area: Rect, buf: &mut Buffer); +} + +// Widget utilities +mod utils; // Private to widgets module +``` + +## Example: data/mod.rs + +```rust +// Public API for data layer +pub mod provider; +pub use provider::{DataProvider, DataViewProvider}; + +// DataTable and DataView will be public +pub mod table; +pub mod view; + +// Adapters are public for gradual migration +pub mod adapters; + +// Internal converters +mod converters; +``` + +## Gradual Migration + +Each phase is a separate PR that: +1. Moves specific files +2. Updates imports +3. Ensures tests pass +4. Maintains backward compatibility + +No breaking changes - just reorganization! \ No newline at end of file diff --git a/sql-cli/RELEASE_NOTES.md b/sql-cli/docs/RELEASE_NOTES.md similarity index 100% rename from sql-cli/RELEASE_NOTES.md rename to sql-cli/docs/RELEASE_NOTES.md diff --git a/sql-cli/RELEASE_NOTES_v1.16.0.md b/sql-cli/docs/RELEASE_NOTES_v1.16.0.md similarity index 100% rename from sql-cli/RELEASE_NOTES_v1.16.0.md rename to sql-cli/docs/RELEASE_NOTES_v1.16.0.md diff --git a/sql-cli/TEST_COMPLETION_MIGRATION.md b/sql-cli/docs/TEST_COMPLETION_MIGRATION.md similarity index 100% rename from sql-cli/TEST_COMPLETION_MIGRATION.md rename to sql-cli/docs/TEST_COMPLETION_MIGRATION.md diff --git a/sql-cli/V10_SEARCH_MODES_COMPLETE.md b/sql-cli/docs/V10_SEARCH_MODES_COMPLETE.md similarity index 100% rename from sql-cli/V10_SEARCH_MODES_COMPLETE.md rename to sql-cli/docs/V10_SEARCH_MODES_COMPLETE.md diff --git a/sql-cli/test_display_improvements.md b/sql-cli/docs/test_display_improvements.md similarity index 100% rename from sql-cli/test_display_improvements.md rename to sql-cli/docs/test_display_improvements.md diff --git a/sql-cli/update_release_action.md b/sql-cli/docs/update_release_action.md similarity index 100% rename from sql-cli/update_release_action.md rename to sql-cli/docs/update_release_action.md diff --git a/sql-cli/verify_column_search_fix.md b/sql-cli/docs/verify_column_search_fix.md similarity index 100% rename from sql-cli/verify_column_search_fix.md rename to sql-cli/docs/verify_column_search_fix.md diff --git a/sql-cli/src/api/mod.rs b/sql-cli/src/api/mod.rs new file mode 100644 index 00000000..7700f238 --- /dev/null +++ b/sql-cli/src/api/mod.rs @@ -0,0 +1,9 @@ +//! External API client and models +//! +//! This module handles communication with external APIs +//! and defines the data models for API requests/responses. + +// API components to be moved here: +// - api_client.rs → client.rs +// - API response models +// - Endpoint definitions diff --git a/sql-cli/src/buffer_handler.rs b/sql-cli/src/buffer_handler.rs index cc1bdc94..206bb07d 100644 --- a/sql-cli/src/buffer_handler.rs +++ b/sql-cli/src/buffer_handler.rs @@ -1,5 +1,5 @@ use crate::buffer::{Buffer, BufferAPI, BufferManager}; -use crate::config::Config; +use crate::config::config::Config; use tracing::{debug, info}; /// Handles all buffer-related operations diff --git a/sql-cli/src/cell_renderer.rs b/sql-cli/src/cell_renderer.rs index 8a25e17e..1a3f0823 100644 --- a/sql-cli/src/cell_renderer.rs +++ b/sql-cli/src/cell_renderer.rs @@ -4,7 +4,7 @@ use ratatui::{ widgets::{Block, Borders, Cell}, }; -use crate::config::CellSelectionStyle; +use crate::config::config::CellSelectionStyle; /// Different visual styles for rendering selected cells #[derive(Debug, Clone)] diff --git a/sql-cli/src/completer.rs b/sql-cli/src/completer.rs index 21655c80..492605b1 100644 --- a/sql-cli/src/completer.rs +++ b/sql-cli/src/completer.rs @@ -1,7 +1,7 @@ use reedline::{Completer, Span, Suggestion}; use std::sync::{Arc, Mutex}; -use crate::parser::{ParseState, Schema, SqlParser}; +use sql_cli::sql::parser::{ParseState, Schema, SqlParser}; pub struct SqlCompleter { parser: Arc>, diff --git a/sql-cli/src/config.rs b/sql-cli/src/config/config.rs similarity index 100% rename from sql-cli/src/config.rs rename to sql-cli/src/config/config.rs diff --git a/sql-cli/src/key_bindings.rs b/sql-cli/src/config/key_bindings.rs similarity index 100% rename from sql-cli/src/key_bindings.rs rename to sql-cli/src/config/key_bindings.rs diff --git a/sql-cli/src/config/mod.rs b/sql-cli/src/config/mod.rs new file mode 100644 index 00000000..268e0a8f --- /dev/null +++ b/sql-cli/src/config/mod.rs @@ -0,0 +1,8 @@ +//! Configuration module +//! +//! This module contains all configuration-related functionality +//! including settings, key bindings, and schema configuration. + +pub mod config; +pub mod key_bindings; +pub mod schema_config; diff --git a/sql-cli/src/schema_config.rs b/sql-cli/src/config/schema_config.rs similarity index 100% rename from sql-cli/src/schema_config.rs rename to sql-cli/src/config/schema_config.rs diff --git a/sql-cli/src/core/mod.rs b/sql-cli/src/core/mod.rs new file mode 100644 index 00000000..0b58c964 --- /dev/null +++ b/sql-cli/src/core/mod.rs @@ -0,0 +1,10 @@ +//! Core business logic and application state management +//! +//! This module contains the central components that manage application state, +//! buffer management, and service orchestration. + +// Core components will be moved here: +// - app_state_container.rs +// - buffer_manager.rs +// - service_container.rs +// - global_state.rs diff --git a/sql-cli/src/data/adapters/mod.rs b/sql-cli/src/data/adapters/mod.rs new file mode 100644 index 00000000..5ee74897 --- /dev/null +++ b/sql-cli/src/data/adapters/mod.rs @@ -0,0 +1,9 @@ +//! Adapters for existing data sources +//! +//! These adapters implement the DataProvider trait for existing data sources, +//! allowing gradual migration to the new architecture. + +// Future adapters: +// - buffer_adapter.rs - Makes Buffer implement DataProvider +// - csv_adapter.rs - Makes CSVClient implement DataProvider +// - api_adapter.rs - Makes API responses implement DataProvider diff --git a/sql-cli/src/data/converters/mod.rs b/sql-cli/src/data/converters/mod.rs new file mode 100644 index 00000000..f1061e5d --- /dev/null +++ b/sql-cli/src/data/converters/mod.rs @@ -0,0 +1,9 @@ +//! Data format converters +//! +//! Converters transform data between different formats +//! (CSV, JSON, DataTable, etc.) + +// Will contain: +// - csv_converter.rs +// - json_converter.rs +// - datatable_converter.rs diff --git a/sql-cli/src/csv_datasource.rs b/sql-cli/src/data/csv_datasource.rs similarity index 100% rename from sql-cli/src/csv_datasource.rs rename to sql-cli/src/data/csv_datasource.rs diff --git a/sql-cli/src/csv_fixes.rs b/sql-cli/src/data/csv_fixes.rs similarity index 100% rename from sql-cli/src/csv_fixes.rs rename to sql-cli/src/data/csv_fixes.rs diff --git a/sql-cli/src/data_analyzer.rs b/sql-cli/src/data/data_analyzer.rs similarity index 100% rename from sql-cli/src/data_analyzer.rs rename to sql-cli/src/data/data_analyzer.rs diff --git a/sql-cli/src/data_exporter.rs b/sql-cli/src/data/data_exporter.rs similarity index 100% rename from sql-cli/src/data_exporter.rs rename to sql-cli/src/data/data_exporter.rs diff --git a/sql-cli/src/data_provider.rs b/sql-cli/src/data/data_provider.rs similarity index 87% rename from sql-cli/src/data_provider.rs rename to sql-cli/src/data/data_provider.rs index 948c5db8..83696763 100644 --- a/sql-cli/src/data_provider.rs +++ b/sql-cli/src/data/data_provider.rs @@ -5,6 +5,47 @@ use std::fmt::Debug; +/// Filter specification for DataView +#[derive(Debug, Clone)] +pub enum FilterSpec { + /// SQL WHERE clause filter + WhereClause(String), + /// Fuzzy text search across all columns + FuzzySearch(String), + /// Column-specific filter + ColumnFilter { column: usize, pattern: String }, + /// Custom filter function + Custom(String), +} + +/// Sort order for columns +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortOrder { + Ascending, + Descending, +} + +/// Data type for columns +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataType { + Text, + Number, + Date, + Boolean, + Json, + Unknown, +} + +/// Column statistics +#[derive(Debug, Clone)] +pub struct ColumnStats { + pub null_count: usize, + pub unique_count: usize, + pub min_value: Option, + pub max_value: Option, + pub mean_value: Option, +} + /// Core trait for read-only data access /// /// This trait defines the minimal interface that any data source must provide diff --git a/sql-cli/src/datasource_adapter.rs b/sql-cli/src/data/datasource_adapter.rs similarity index 100% rename from sql-cli/src/datasource_adapter.rs rename to sql-cli/src/data/datasource_adapter.rs diff --git a/sql-cli/src/datasource_trait.rs b/sql-cli/src/data/datasource_trait.rs similarity index 100% rename from sql-cli/src/datasource_trait.rs rename to sql-cli/src/data/datasource_trait.rs diff --git a/sql-cli/src/datatable.rs b/sql-cli/src/data/datatable.rs similarity index 100% rename from sql-cli/src/datatable.rs rename to sql-cli/src/data/datatable.rs diff --git a/sql-cli/src/datatable_buffer.rs b/sql-cli/src/data/datatable_buffer.rs similarity index 100% rename from sql-cli/src/datatable_buffer.rs rename to sql-cli/src/data/datatable_buffer.rs diff --git a/sql-cli/src/datatable_converter.rs b/sql-cli/src/data/datatable_converter.rs similarity index 100% rename from sql-cli/src/datatable_converter.rs rename to sql-cli/src/data/datatable_converter.rs diff --git a/sql-cli/src/datatable_loaders.rs b/sql-cli/src/data/datatable_loaders.rs similarity index 100% rename from sql-cli/src/datatable_loaders.rs rename to sql-cli/src/data/datatable_loaders.rs diff --git a/sql-cli/src/datatable_view.rs b/sql-cli/src/data/datatable_view.rs similarity index 100% rename from sql-cli/src/datatable_view.rs rename to sql-cli/src/data/datatable_view.rs diff --git a/sql-cli/src/data/mod.rs b/sql-cli/src/data/mod.rs new file mode 100644 index 00000000..95fa747b --- /dev/null +++ b/sql-cli/src/data/mod.rs @@ -0,0 +1,23 @@ +//! Data layer for DataTable/DataView architecture +//! +//! This module provides the data abstraction layer that separates +//! data storage from presentation. + +pub mod adapters; +pub mod converters; + +// Core data modules +pub mod data_provider; +pub mod datatable; +pub mod datatable_buffer; +pub mod datatable_converter; +pub mod datatable_loaders; +pub mod datatable_view; + +// Data source modules +pub mod csv_datasource; +pub mod csv_fixes; +pub mod data_analyzer; +pub mod data_exporter; +pub mod datasource_adapter; +pub mod datasource_trait; diff --git a/sql-cli/src/enhanced_tui.rs.backup b/sql-cli/src/enhanced_tui.rs.backup deleted file mode 100644 index a6246a3a..00000000 --- a/sql-cli/src/enhanced_tui.rs.backup +++ /dev/null @@ -1,7937 +0,0 @@ -use crate::hybrid_parser::HybridParser; -use crate::parser::SqlParser; -use crate::sql_highlighter::SqlHighlighter; -use anyhow::Result; -use chrono::Local; -use crossterm::{ - event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, - }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Wrap}, - Frame, Terminal, -}; -use regex::Regex; -use serde_json::Value; -use sql_cli::api_client::{ApiClient, QueryResponse}; -use sql_cli::buffer::{BufferAPI, BufferManager}; -use sql_cli::cache::QueryCache; -use sql_cli::config::Config; -use sql_cli::csv_datasource::CsvApiClient; -use sql_cli::history::{CommandHistory, HistoryMatch}; -use sql_cli::logging::{get_log_buffer, LogRingBuffer}; -use sql_cli::where_ast::format_where_ast; -use sql_cli::where_parser::WhereParser; -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::fs::File; -use std::io; -use std::io::Write; -use tracing::{debug, error, info, trace, warn}; -use tui_input::{backend::crossterm::EventHandler, Input}; -use tui_textarea::{CursorMove, TextArea}; - -#[derive(Clone, PartialEq, Debug)] -enum AppMode { - Command, - Results, - Search, - Filter, - FuzzyFilter, - ColumnSearch, - Help, - History, - Debug, - PrettyQuery, - CacheList, - JumpToRow, - ColumnStats, -} - -#[derive(Clone, PartialEq)] -enum EditMode { - SingleLine, - MultiLine, -} - -#[derive(Clone, PartialEq, Debug)] -enum SelectionMode { - Row, - Cell, -} - -#[derive(Clone, PartialEq, Copy)] -enum SortOrder { - Ascending, - Descending, - None, -} - -#[derive(Clone)] -struct SortState { - column: Option, - order: SortOrder, -} - -#[derive(Clone)] -struct FilterState { - pattern: String, - regex: Option, - active: bool, -} - -struct FuzzyFilterState { - pattern: String, - active: bool, - matcher: SkimMatcherV2, - filtered_indices: Vec, // Indices of rows that match -} - -impl Clone for FuzzyFilterState { - fn clone(&self) -> Self { - Self { - pattern: self.pattern.clone(), - active: self.active, - matcher: SkimMatcherV2::default(), // Create new matcher - filtered_indices: self.filtered_indices.clone(), - } - } -} - -#[derive(Clone)] -struct ColumnSearchState { - pattern: String, - matching_columns: Vec<(usize, String)>, // (index, column_name) - current_match: usize, // Index into matching_columns -} - -#[derive(Clone, Debug)] -struct ColumnStatistics { - column_name: String, - column_type: ColumnType, - // For all columns - total_count: usize, - null_count: usize, - unique_count: usize, - // For categorical/string columns - frequency_map: Option>, - // For numeric columns - min: Option, - max: Option, - sum: Option, - mean: Option, - median: Option, -} - -#[derive(Clone, Debug)] -enum ColumnType { - String, - Numeric, - Mixed, -} - -#[derive(Clone)] -struct SearchState { - pattern: String, - current_match: Option<(usize, usize)>, // (row, col) - matches: Vec<(usize, usize)>, - match_index: usize, -} - -#[derive(Clone)] -struct CompletionState { - suggestions: Vec, - current_index: usize, - last_query: String, - last_cursor_pos: usize, -} - -#[derive(Clone)] -struct HistoryState { - search_query: String, - matches: Vec, - selected_index: usize, -} - -pub struct EnhancedTuiApp { - api_client: ApiClient, - input: Input, - textarea: TextArea<'static>, - edit_mode: EditMode, - mode: AppMode, - results: Option, - table_state: TableState, - last_results_row: Option, // Preserve row position when switching modes - last_scroll_offset: (usize, usize), // Preserve scroll offset when switching modes - show_help: bool, - sql_parser: SqlParser, - hybrid_parser: HybridParser, - - // Configuration - config: Config, - - // Enhanced features - sort_state: SortState, - filter_state: FilterState, - fuzzy_filter_state: FuzzyFilterState, - search_state: SearchState, - column_search_state: ColumnSearchState, - completion_state: CompletionState, - history_state: HistoryState, - command_history: CommandHistory, - filtered_data: Option>>, - column_widths: Vec, - scroll_offset: (usize, usize), // (row, col) - current_column: usize, // For column-based operations - pinned_columns: Vec, // Indices of pinned columns - column_stats: Option, // Current column statistics - sql_highlighter: SqlHighlighter, - debug_text: String, - debug_scroll: u16, - key_history: Vec, // Track key presses for debugging - help_scroll: u16, // Scroll offset for help page - input_scroll_offset: u16, // Horizontal scroll offset for input - case_insensitive: bool, // Toggle for case-insensitive string comparisons - - // Selection and clipboard - selection_mode: SelectionMode, // Row or Cell mode - yank_mode: Option, // Track multi-key yank commands (e.g., 'yy', 'yc') - last_yanked: Option<(String, String)>, // (description, value) of last yanked item - - // CSV mode - csv_client: Option, - csv_mode: bool, - csv_table_name: String, - - // Buffer management (new - for supporting multiple files) - buffer_manager: BufferManager, - current_buffer_name: Option, // Name of current buffer/table - - // Cache - query_cache: Option, - cache_mode: bool, - cached_data: Option>, - - // Data source tracking - last_query_source: Option, - - // Undo/redo and kill ring - undo_stack: Vec<(String, usize)>, // (text, cursor_pos) - redo_stack: Vec<(String, usize)>, - kill_ring: String, - - // Viewport tracking - last_visible_rows: usize, // Track the last calculated viewport height - - // Display options - compact_mode: bool, // Compact display mode with reduced padding - viewport_lock: bool, // Lock viewport position for anchor scrolling - viewport_lock_row: Option, // The row position to lock to in viewport - show_row_numbers: bool, // Show row numbers in results view - jump_to_row_input: String, // Input buffer for jump to row command - log_buffer: Option, // Ring buffer for debug logs -} - -fn escape_csv_field(field: &str) -> String { - if field.contains(',') || field.contains('"') || field.contains('\n') { - // Escape quotes by doubling them and wrap field in quotes - format!("\"{}\"", field.replace('"', "\"\"")) - } else { - field.to_string() - } -} - -fn is_sql_delimiter(ch: char) -> bool { - matches!( - ch, - ',' | '(' | ')' | '=' | '<' | '>' | '.' | '"' | '\'' | ';' - ) -} - -impl EnhancedTuiApp { - // --- Buffer Compatibility Layer --- - // These methods provide a gradual migration path from direct field access to BufferAPI - - /// Get current buffer (panics if no buffer - which should never happen) - fn current_buffer(&self) -> &dyn sql_cli::buffer::BufferAPI { - self.buffer_manager - .current() - .map(|b| b as &dyn sql_cli::buffer::BufferAPI) - .expect("Buffer manager should always have a current buffer") - } - - /// Get current buffer for writing (panics if no buffer - which should never happen) - fn current_buffer_mut(&mut self) -> &mut dyn sql_cli::buffer::BufferAPI { - self.buffer_manager - .current_mut() - .map(|b| b as &mut dyn sql_cli::buffer::BufferAPI) - .expect("Buffer manager should always have a current buffer") - } - - // Compatibility wrapper for edit_mode - fn get_edit_mode(&self) -> EditMode { - let buffer = self.current_buffer(); - // Convert from buffer::EditMode to local EditMode - match buffer.get_edit_mode() { - sql_cli::buffer::EditMode::SingleLine => EditMode::SingleLine, - sql_cli::buffer::EditMode::MultiLine => EditMode::MultiLine, - } - } - - fn set_edit_mode(&mut self, mode: EditMode) { - // Update local field (will be removed later) - self.edit_mode = mode.clone(); - - // Also update in buffer if available - if let Some(buffer) = self.current_buffer_mut() { - let buffer_mode = match mode { - EditMode::SingleLine => sql_cli::buffer::EditMode::SingleLine, - EditMode::MultiLine => sql_cli::buffer::EditMode::MultiLine, - }; - buffer.set_edit_mode(buffer_mode); - } - } - - // Compatibility wrapper for case_insensitive - fn get_case_insensitive(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_case_insensitive() - } else { - self.case_insensitive - } - } - - // Helper to get input text from buffer or fallback to direct input - fn get_input_text(&self) -> String { - // For special modes that use the input field for their own purposes - match self.get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes temporarily use the input field for their patterns - self.input.value().to_string() - } - _ => { - // All other modes use the buffer - if let Some(buffer) = self.current_buffer() { - buffer.get_input_text() - } else { - // Fallback to direct input access during migration - match self.get_edit_mode() { - EditMode::SingleLine => self.input.value().to_string(), - EditMode::MultiLine => self.textarea.lines().join("\n"), - } - } - } - } - } - - // Helper to get cursor position from buffer or fallback to direct input - fn get_input_cursor(&self) -> usize { - // For special modes that use the input field directly - match self.get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes use the input field for their patterns - self.input.cursor() - } - _ => { - // All other modes use the buffer - if let Some(buffer) = self.current_buffer() { - buffer.get_input_cursor_position() - } else { - // Fallback to direct input access during migration - match self.get_edit_mode() { - EditMode::SingleLine => self.input.cursor(), - EditMode::MultiLine => { - // For multi-line, calculate absolute position - let (row, col) = self.textarea.cursor(); - let mut pos = 0; - let text = self.get_input_text(); - for (i, line) in text.lines().enumerate() { - if i < row { - pos += line.len() + 1; // +1 for newline - } else if i == row { - pos += col; - break; - } - } - pos - } - } - } - } - } - } - - // Helper to set input text through buffer or fallback to direct input - fn set_input_text(&mut self, text: String) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_text(text.clone()); - // Also sync cursor position to end of text - buffer.set_input_cursor_position(text.len()); - } - - // For special modes that use the input field directly - match self.get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes still need the input field updated - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - _ => { - // Command mode and others use the buffer exclusively - } - } - } - - // Helper to handle key events in the input - fn handle_input_key(&mut self, key: KeyEvent) -> bool { - if let Some(buffer) = self.current_buffer_mut() { - // Route to buffer's input handling - buffer.handle_input_key(key) - } else { - // For special modes that handle input directly - match self.get_mode() { - AppMode::Search - | AppMode::Filter - | AppMode::FuzzyFilter - | AppMode::ColumnSearch => { - self.input.handle_event(&Event::Key(key)); - false - } - _ => false, - } - } - } - - // Helper to get visual cursor position (for rendering) - fn get_visual_cursor(&self) -> (usize, usize) { - if let Some(buffer) = self.current_buffer() { - // Buffer should provide visual cursor for rendering - // For now, use a simple calculation - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - let lines: Vec<&str> = text.split('\n').collect(); - - let mut current_pos = 0; - for (row, line) in lines.iter().enumerate() { - if current_pos + line.len() >= cursor { - return (row, cursor - current_pos); - } - current_pos += line.len() + 1; // +1 for newline - } - (0, cursor) - } else { - // Fallback to direct input access - match self.get_edit_mode() { - EditMode::SingleLine => (0, self.input.visual_cursor()), - EditMode::MultiLine => self.textarea.cursor(), - } - } - } - - fn set_case_insensitive(&mut self, case_insensitive: bool) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_case_insensitive(case_insensitive); - } else { - self.case_insensitive = case_insensitive; - } - } - - // Compatibility wrapper for last_results_row - fn get_last_results_row(&self) -> Option { - if let Some(buffer) = self.current_buffer() { - buffer.get_last_results_row() - } else { - self.last_results_row - } - } - - fn set_last_results_row(&mut self, row: Option) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_last_results_row(row); - } else { - self.last_results_row = row; - } - } - - // Compatibility wrapper for last_scroll_offset - fn get_last_scroll_offset(&self) -> (usize, usize) { - if let Some(buffer) = self.current_buffer() { - buffer.get_last_scroll_offset() - } else { - self.last_scroll_offset - } - } - - fn set_last_scroll_offset(&mut self, offset: (usize, usize)) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_last_scroll_offset(offset); - } else { - self.last_scroll_offset = offset; - } - } - - // Compatibility wrapper for last_query_source - fn get_last_query_source(&self) -> Option { - if let Some(buffer) = self.current_buffer() { - buffer.get_last_query_source() - } else { - self.last_query_source.clone() - } - } - - fn set_last_query_source(&mut self, source: Option) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_last_query_source(source); - } else { - self.last_query_source = source; - } - } - - // Compatibility wrapper for input - fn get_input(&self) -> &tui_input::Input { - if let Some(buffer) = self.current_buffer() { - // TODO: Need to get input from buffer - for now use TUI field - &self.input - } else { - &self.input - } - } - - fn get_input_mut(&mut self) -> &mut tui_input::Input { - // For now, always use TUI field since Buffer input access is more complex - &mut self.input - } - - // Helper functions to convert between buffer AppMode and local AppMode - fn buffer_mode_to_local(buffer_mode: sql_cli::buffer::AppMode) -> AppMode { - match buffer_mode { - sql_cli::buffer::AppMode::Command => AppMode::Command, - sql_cli::buffer::AppMode::Results => AppMode::Results, - sql_cli::buffer::AppMode::Search => AppMode::Search, - sql_cli::buffer::AppMode::Filter => AppMode::Filter, - sql_cli::buffer::AppMode::FuzzyFilter => AppMode::FuzzyFilter, - sql_cli::buffer::AppMode::ColumnSearch => AppMode::ColumnSearch, - sql_cli::buffer::AppMode::Help => AppMode::Help, - sql_cli::buffer::AppMode::History => AppMode::History, - sql_cli::buffer::AppMode::Debug => AppMode::Debug, - sql_cli::buffer::AppMode::PrettyQuery => AppMode::PrettyQuery, - sql_cli::buffer::AppMode::CacheList => AppMode::CacheList, - sql_cli::buffer::AppMode::JumpToRow => AppMode::JumpToRow, - sql_cli::buffer::AppMode::ColumnStats => AppMode::ColumnStats, - } - } - - fn local_mode_to_buffer(local_mode: &AppMode) -> sql_cli::buffer::AppMode { - match local_mode { - AppMode::Command => sql_cli::buffer::AppMode::Command, - AppMode::Results => sql_cli::buffer::AppMode::Results, - AppMode::Search => sql_cli::buffer::AppMode::Search, - AppMode::Filter => sql_cli::buffer::AppMode::Filter, - AppMode::FuzzyFilter => sql_cli::buffer::AppMode::FuzzyFilter, - AppMode::ColumnSearch => sql_cli::buffer::AppMode::ColumnSearch, - AppMode::Help => sql_cli::buffer::AppMode::Help, - AppMode::History => sql_cli::buffer::AppMode::History, - AppMode::Debug => sql_cli::buffer::AppMode::Debug, - AppMode::PrettyQuery => sql_cli::buffer::AppMode::PrettyQuery, - AppMode::CacheList => sql_cli::buffer::AppMode::CacheList, - AppMode::JumpToRow => sql_cli::buffer::AppMode::JumpToRow, - AppMode::ColumnStats => sql_cli::buffer::AppMode::ColumnStats, - } - } - - // Compatibility wrapper for mode - fn get_mode(&self) -> AppMode { - if let Some(buffer) = self.current_buffer() { - Self::buffer_mode_to_local(buffer.get_mode()) - } else { - self.mode.clone() - } - } - - fn set_mode(&mut self, mode: AppMode) { - // Update local field (will be removed later) - self.mode = mode.clone(); - - // Also update in buffer if available - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_mode(Self::local_mode_to_buffer(&mode)); - } - } - - // Compatibility wrapper for results - fn get_results(&self) -> Option<&QueryResponse> { - // For now, always use TUI field due to type conflicts - // TODO: Resolve QueryResponse type mismatch between crate::api_client and sql_cli::api_client - self.results.as_ref() - } - - fn set_results(&mut self, results: Option) { - // Update local field - self.results = results; - - // TODO: Also update in buffer when type conflicts are resolved - } - - // Compatibility wrapper for table_state - fn get_table_state(&self) -> &TableState { - // For now, always use TUI field since TableState access is complex - &self.table_state - } - - fn get_table_state_mut(&mut self) -> &mut TableState { - &mut self.table_state - } - - // Wrapper methods for status_message (uses buffer system) - fn get_status_message(&self) -> String { - if let Some(buffer) = self.current_buffer() { - buffer.get_status_message() - } else { - "Ready".to_string() - } - } - - fn set_status_message(&mut self, message: String) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_status_message(message); - } - } - - // Wrapper methods for scroll_offset (uses buffer system) - fn get_scroll_offset(&self) -> (usize, usize) { - if let Some(buffer) = self.current_buffer() { - buffer.get_scroll_offset() - } else { - self.scroll_offset - } - } - - fn set_scroll_offset(&mut self, offset: (usize, usize)) { - // Update local field (will be removed later) - self.scroll_offset = offset; - - // Also update in buffer if available - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_scroll_offset(offset); - } - } - - // Wrapper methods for current_column (uses buffer system) - fn get_current_column(&self) -> usize { - if let Some(buffer) = self.current_buffer() { - buffer.get_current_column() - } else { - self.current_column - } - } - - fn set_current_column(&mut self, col: usize) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_current_column(col); - } else { - self.current_column = col; - } - } - - fn get_compact_mode(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_compact_mode() - } else { - self.compact_mode - } - } - - fn set_compact_mode(&mut self, compact: bool) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_compact_mode(compact); - } else { - self.compact_mode = compact; - } - } - - fn get_show_row_numbers(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_show_row_numbers() - } else { - self.show_row_numbers - } - } - - fn set_show_row_numbers(&mut self, show: bool) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_show_row_numbers(show); - } else { - self.show_row_numbers = show; - } - } - - fn is_viewport_lock(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_viewport_lock() - } else { - self.viewport_lock - } - } - - fn set_viewport_lock(&mut self, locked: bool) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_viewport_lock(locked); - } else { - self.viewport_lock = locked; - } - } - - fn get_viewport_lock_row(&self) -> Option { - if let Some(buffer) = self.current_buffer() { - buffer.get_viewport_lock_row() - } else { - self.viewport_lock_row - } - } - - fn set_viewport_lock_row(&mut self, row: Option) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_viewport_lock_row(row); - } else { - self.viewport_lock_row = row; - } - } - - fn get_column_widths(&self) -> Vec { - if let Some(buffer) = self.current_buffer() { - buffer.get_column_widths().clone() - } else { - self.column_widths.clone() - } - } - - fn set_column_widths(&mut self, widths: Vec) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_column_widths(widths.clone()); - } else { - self.column_widths = widths; - } - } - - fn is_csv_mode(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_csv_mode() - } else { - self.csv_mode - } - } - - fn set_csv_mode(&mut self, csv_mode: bool) { - // Note: csv_mode is stored in the buffer, but we also need to update local field for now - self.csv_mode = csv_mode; - // TODO: Update buffer when buffer system is fully integrated - } - - fn get_csv_table_name(&self) -> String { - if let Some(buffer) = self.current_buffer() { - buffer.get_table_name() - } else { - self.csv_table_name.clone() - } - } - - fn set_csv_table_name(&mut self, table_name: String) { - // Note: csv_table_name is stored in the buffer, but we also need to update local field for now - self.csv_table_name = table_name; - // TODO: Update buffer when buffer system is fully integrated - } - - fn is_cache_mode(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_cache_mode() - } else { - self.cache_mode - } - } - - fn set_cache_mode(&mut self, cache_mode: bool) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_cache_mode(cache_mode); - } else { - self.cache_mode = cache_mode; - } - } - - // Wrapper methods for undo/redo/kill ring (uses buffer system) - fn get_undo_stack(&self) -> &Vec<(String, usize)> { - if let Some(buffer) = self.current_buffer() { - buffer.get_undo_stack() - } else { - &self.undo_stack - } - } - - fn push_undo(&mut self, state: (String, usize)) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.push_undo(state); - } else { - self.undo_stack.push(state); - if self.undo_stack.len() > 100 { - self.undo_stack.remove(0); - } - } - } - - fn pop_undo(&mut self) -> Option<(String, usize)> { - if let Some(buffer) = self.current_buffer_mut() { - buffer.pop_undo() - } else { - self.undo_stack.pop() - } - } - - fn push_redo(&mut self, state: (String, usize)) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.push_redo(state); - } else { - self.redo_stack.push(state); - } - } - - fn pop_redo(&mut self) -> Option<(String, usize)> { - if let Some(buffer) = self.current_buffer_mut() { - buffer.pop_redo() - } else { - self.redo_stack.pop() - } - } - - fn clear_redo(&mut self) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.clear_redo(); - } else { - self.redo_stack.clear(); - } - } - - fn get_kill_ring(&self) -> String { - if let Some(buffer) = self.current_buffer() { - buffer.get_kill_ring() - } else { - self.kill_ring.clone() - } - } - - fn set_kill_ring(&mut self, text: String) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_kill_ring(text); - } else { - self.kill_ring = text; - } - } - - fn is_kill_ring_empty(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.is_kill_ring_empty() - } else { - self.kill_ring.is_empty() - } - } - - // Wrapper methods for last_visible_rows (uses buffer system) - fn get_last_visible_rows(&self) -> usize { - if let Some(buffer) = self.current_buffer() { - buffer.get_last_visible_rows() - } else { - self.last_visible_rows - } - } - - fn set_last_visible_rows(&mut self, rows: usize) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_last_visible_rows(rows); - } else { - self.last_visible_rows = rows; - } - } - - // Wrapper methods for cached_data (uses buffer system) - fn get_cached_data(&self) -> Option<&Vec> { - if let Some(buffer) = self.current_buffer() { - buffer.get_cached_data() - } else { - self.cached_data.as_ref() - } - } - - fn set_cached_data(&mut self, data: Option>) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_cached_data(data); - } else { - self.cached_data = data; - } - } - - fn has_cached_data(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.has_cached_data() - } else { - self.cached_data.is_some() - } - } - - // Wrapper methods for csv_client (uses buffer system) - fn get_csv_client(&self) -> Option<&CsvApiClient> { - if let Some(buffer) = self.current_buffer() { - buffer.get_csv_client() - } else { - self.csv_client.as_ref() - } - } - - fn set_csv_client(&mut self, client: Option) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_csv_client(client); - } else { - self.csv_client = client; - } - } - - fn set_csv_client_case_insensitive(&mut self, case_insensitive: bool) { - if let Some(buffer) = self.current_buffer_mut() { - if let Some(csv_client) = buffer.get_csv_client_mut() { - csv_client.set_case_insensitive(case_insensitive); - } - } else { - if let Some(csv_client) = self.csv_client.as_mut() { - csv_client.set_case_insensitive(case_insensitive); - } - } - } - - // Wrapper methods for filtered_data (uses buffer system) - fn get_filtered_data(&self) -> Option<&Vec>> { - if let Some(buffer) = self.current_buffer() { - buffer.get_filtered_data() - } else { - self.filtered_data.as_ref() - } - } - - fn set_filtered_data(&mut self, data: Option>>) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_filtered_data(data); - } else { - self.filtered_data = data; - } - } - - fn has_filtered_data(&self) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.get_filtered_data().is_some() - } else { - self.filtered_data.is_some() - } - } - - // Wrapper methods for search_state (uses buffer system) - fn get_search_pattern(&self) -> String { - if let Some(buffer) = self.current_buffer() { - buffer.get_search_pattern() - } else { - self.search_state.pattern.clone() - } - } - - fn set_search_pattern(&mut self, pattern: String) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_search_pattern(pattern); - } else { - self.search_state.pattern = pattern; - } - } - - fn push_search_pattern_char(&mut self, c: char) { - let mut pattern = self.get_search_pattern(); - pattern.push(c); - self.set_search_pattern(pattern); - } - - fn pop_search_pattern_char(&mut self) { - let mut pattern = self.get_search_pattern(); - pattern.pop(); - self.set_search_pattern(pattern); - } - - fn clear_search_pattern(&mut self) { - self.set_search_pattern(String::new()); - } - - fn is_search_pattern_empty(&self) -> bool { - self.get_search_pattern().is_empty() - } - - fn get_search_matches(&self) -> Vec<(usize, usize)> { - if let Some(buffer) = self.current_buffer() { - buffer.get_search_matches() - } else { - self.search_state.matches.clone() - } - } - - fn set_search_matches(&mut self, matches: Vec<(usize, usize)>) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_search_matches(matches); - } else { - self.search_state.matches = matches; - } - } - - fn clear_search_matches(&mut self) { - self.set_search_matches(Vec::new()); - } - - fn get_search_match_index(&self) -> usize { - if let Some(buffer) = self.current_buffer() { - buffer.get_search_match_index() - } else { - self.search_state.match_index - } - } - - fn set_search_match_index(&mut self, index: usize) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_search_match_index(index); - } else { - self.search_state.match_index = index; - } - } - - fn get_current_search_match(&self) -> Option<(usize, usize)> { - if let Some(buffer) = self.current_buffer() { - buffer.get_current_match() - } else { - self.search_state.current_match - } - } - - fn set_current_search_match(&mut self, match_pos: Option<(usize, usize)>) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_current_match(match_pos); - } else { - self.search_state.current_match = match_pos; - } - } - - fn clear_search_state(&mut self) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.clear_search_state(); - } else { - self.search_state.pattern.clear(); - self.search_state.matches.clear(); - self.search_state.current_match = None; - self.search_state.match_index = 0; - } - } - - // Wrapper methods for pinned_columns (uses buffer system) - fn get_pinned_columns(&self) -> Vec { - if let Some(buffer) = self.current_buffer() { - buffer.get_pinned_columns().clone() - } else { - self.pinned_columns.clone() - } - } - - fn add_pinned_column(&mut self, col: usize) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.add_pinned_column(col); - } else { - if !self.pinned_columns.contains(&col) { - self.pinned_columns.push(col); - self.pinned_columns.sort(); - } - } - } - - fn remove_pinned_column(&mut self, col: usize) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.remove_pinned_column(col); - } else { - self.pinned_columns.retain(|&c| c != col); - } - } - - fn clear_pinned_columns(&mut self) { - if let Some(buffer) = self.current_buffer_mut() { - buffer.clear_pinned_columns(); - } else { - self.pinned_columns.clear(); - } - } - - fn contains_pinned_column(&self, col: usize) -> bool { - if let Some(buffer) = self.current_buffer() { - buffer.get_pinned_columns().contains(&col) - } else { - self.pinned_columns.contains(&col) - } - } - - fn get_filter_state(&self) -> &FilterState { - &self.filter_state - } - - fn get_filter_state_mut(&mut self) -> &mut FilterState { - &mut self.filter_state - } - - fn sanitize_table_name(name: &str) -> String { - // Replace spaces and other problematic characters with underscores - // to create SQL-friendly table names - // Examples: "Business Crime Borough Level" -> "Business_Crime_Borough_Level" - name.trim() - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect() - } - - pub fn has_results(&self) -> bool { - self.get_results().is_some() - } - - pub fn new(api_url: &str) -> Self { - let mut textarea = TextArea::default(); - textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED)); - - // Load configuration - let config = Config::load().unwrap_or_else(|e| { - eprintln!("Warning: Could not load config: {}. Using defaults.", e); - Config::default() - }); - - Self { - api_client: ApiClient::new(api_url), - input: Input::default(), - textarea, - edit_mode: EditMode::SingleLine, - mode: AppMode::Command, - results: None, - table_state: TableState::default(), - last_results_row: None, - last_scroll_offset: (0, 0), - show_help: false, - sql_parser: SqlParser::new(), - hybrid_parser: HybridParser::new(), - config: config.clone(), - - sort_state: SortState { - column: None, - order: SortOrder::None, - }, - filter_state: FilterState { - pattern: String::new(), - regex: None, - active: false, - }, - fuzzy_filter_state: FuzzyFilterState { - pattern: String::new(), - active: false, - matcher: SkimMatcherV2::default(), - filtered_indices: Vec::new(), - }, - search_state: SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }, - column_search_state: ColumnSearchState { - pattern: String::new(), - matching_columns: Vec::new(), - current_match: 0, - }, - completion_state: CompletionState { - suggestions: Vec::new(), - current_index: 0, - last_query: String::new(), - last_cursor_pos: 0, - }, - history_state: HistoryState { - search_query: String::new(), - matches: Vec::new(), - selected_index: 0, - }, - command_history: CommandHistory::new().unwrap_or_default(), - filtered_data: None, - column_widths: Vec::new(), - scroll_offset: (0, 0), - current_column: 0, - pinned_columns: Vec::new(), - column_stats: None, - sql_highlighter: SqlHighlighter::new(), - debug_text: String::new(), - debug_scroll: 0, - key_history: Vec::new(), - help_scroll: 0, - input_scroll_offset: 0, - case_insensitive: config.behavior.case_insensitive_default, - selection_mode: SelectionMode::Row, // Default to row mode - yank_mode: None, - last_yanked: None, - csv_client: None, - csv_mode: false, - csv_table_name: String::new(), - buffer_manager: { - // Initialize buffer manager with a default buffer - let mut manager = BufferManager::new(); - let mut buffer = sql_cli::buffer::Buffer::new(1); - // Sync initial settings from config - buffer.set_case_insensitive(config.behavior.case_insensitive_default); - buffer.set_compact_mode(config.display.compact_mode); - buffer.set_show_row_numbers(config.display.show_row_numbers); - manager.add_buffer(buffer); - manager - }, - current_buffer_name: None, - query_cache: QueryCache::new().ok(), - cache_mode: false, - cached_data: None, - last_query_source: None, - undo_stack: Vec::new(), - redo_stack: Vec::new(), - kill_ring: String::new(), - last_visible_rows: 30, // Default estimate - compact_mode: config.display.compact_mode, - viewport_lock: false, - viewport_lock_row: None, - show_row_numbers: config.display.show_row_numbers, - jump_to_row_input: String::new(), - log_buffer: get_log_buffer(), - } - } - - pub fn new_with_csv(csv_path: &str) -> Result { - let mut csv_client = CsvApiClient::new(); - - // First create the app to get its config - let mut app = Self::new(""); // Empty API URL for CSV mode - - // Use the app's config for consistency - csv_client.set_case_insensitive(app.config.behavior.case_insensitive_default); - - let raw_name = std::path::Path::new(csv_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - // Sanitize the table name to be SQL-friendly - let table_name = Self::sanitize_table_name(&raw_name); - - csv_client.load_csv(csv_path, &table_name)?; - - // Get schema from CSV - let schema = csv_client - .get_schema() - .ok_or_else(|| anyhow::anyhow!("Failed to get CSV schema"))?; - - // Configure the app for CSV mode - app.set_csv_client(Some(csv_client.clone())); - app.set_csv_mode(true); - app.set_csv_table_name(table_name.clone()); - app.current_buffer_name = Some(format!("{}", raw_name)); - - // Replace the default buffer with a CSV buffer - { - // Clear all buffers and add a CSV buffer - app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_csv( - 1, - std::path::PathBuf::from(csv_path), - csv_client, - table_name.clone(), - ); - // Apply config settings to the buffer - use app's config - buffer.set_case_insensitive(app.config.behavior.case_insensitive_default); - buffer.set_compact_mode(app.config.display.compact_mode); - buffer.set_show_row_numbers(app.config.display.show_row_numbers); - - info!(target: "buffer", "Configured CSV buffer with: compact_mode={}, case_insensitive={}, show_row_numbers={}", - app.config.display.compact_mode, - app.config.behavior.case_insensitive_default, - app.config.display.show_row_numbers); - app.buffer_manager.add_buffer(buffer); - - // Sync app-level state from the buffer to ensure status line renders correctly - if let Some(current_buffer) = app.buffer_manager.current() { - app.case_insensitive = current_buffer.is_case_insensitive(); - } - } - - // Update parser with CSV columns - if let Some(columns) = schema.get(&table_name) { - // Update the parser with CSV columns - app.hybrid_parser - .update_single_table(table_name.clone(), columns.clone()); - let display_msg = if raw_name != table_name { - format!( - "CSV loaded: '{}' as table '{}' with {} columns", - raw_name, - table_name, - columns.len() - ) - } else { - format!( - "CSV loaded: table '{}' with {} columns", - table_name, - columns.len() - ) - }; - app.set_status_message(display_msg); - } - - // Auto-execute SELECT * FROM table_name to show data immediately (if configured) - let auto_query = format!("SELECT * FROM {}", table_name); - - // Populate the input field with the query for easy editing - app.set_input_text(auto_query.clone()); - - if app.config.behavior.auto_execute_on_load { - if let Err(e) = app.execute_query(&auto_query) { - // If auto-query fails, just log it in status but don't fail the load - app.set_status_message(format!( - "CSV loaded: table '{}' ({} columns) - Note: {}", - table_name, - schema.get(&table_name).map(|c| c.len()).unwrap_or(0), - e - )); - } - } - - Ok(app) - } - - pub fn new_with_json(json_path: &str) -> Result { - let mut csv_client = CsvApiClient::new(); - - // First create the app to get its config - let mut app = Self::new(""); // Empty API URL for JSON mode - - // Use the app's config for consistency - csv_client.set_case_insensitive(app.config.behavior.case_insensitive_default); - - let raw_name = std::path::Path::new(json_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - // Sanitize the table name to be SQL-friendly - let table_name = Self::sanitize_table_name(&raw_name); - - csv_client.load_json(json_path, &table_name)?; - - // Get schema from JSON data - let schema = csv_client - .get_schema() - .ok_or_else(|| anyhow::anyhow!("Failed to get JSON schema"))?; - - // Configure the app for JSON mode - app.set_csv_client(Some(csv_client.clone())); - app.set_csv_mode(true); // Reuse CSV mode since the data structure is the same - app.set_csv_table_name(table_name.clone()); - app.current_buffer_name = Some(format!("{}", raw_name)); - - // Replace the default buffer with a JSON buffer - { - // Clear all buffers and add a JSON buffer - app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_json( - 1, - std::path::PathBuf::from(json_path), - csv_client, - table_name.clone(), - ); - // Apply config settings to the buffer - use app's config - buffer.set_case_insensitive(app.config.behavior.case_insensitive_default); - buffer.set_compact_mode(app.config.display.compact_mode); - buffer.set_show_row_numbers(app.config.display.show_row_numbers); - - info!(target: "buffer", "Configured CSV buffer with: compact_mode={}, case_insensitive={}, show_row_numbers={}", - app.config.display.compact_mode, - app.config.behavior.case_insensitive_default, - app.config.display.show_row_numbers); - app.buffer_manager.add_buffer(buffer); - - // Sync app-level state from the buffer to ensure status line renders correctly - if let Some(current_buffer) = app.buffer_manager.current() { - app.case_insensitive = current_buffer.is_case_insensitive(); - } - } - - // Update parser with JSON columns - if let Some(columns) = schema.get(&table_name) { - app.hybrid_parser - .update_single_table(table_name.clone(), columns.clone()); - let display_msg = if raw_name != table_name { - format!( - "JSON loaded: '{}' as table '{}' with {} columns", - raw_name, - table_name, - columns.len() - ) - } else { - format!( - "JSON loaded: table '{}' with {} columns", - table_name, - columns.len() - ) - }; - app.set_status_message(display_msg); - } - - // Auto-execute SELECT * FROM table_name to show data immediately (if configured) - let auto_query = format!("SELECT * FROM {}", table_name); - - // Populate the input field with the query for easy editing - app.set_input_text(auto_query.clone()); - - if app.config.behavior.auto_execute_on_load { - if let Err(e) = app.execute_query(&auto_query) { - // If auto-query fails, just log it in status but don't fail the load - app.set_status_message(format!( - "JSON loaded: table '{}' ({} columns) - Note: {}", - table_name, - schema.get(&table_name).map(|c| c.len()).unwrap_or(0), - e - )); - } - } - - Ok(app) - } - - pub fn run(mut self) -> Result<()> { - // Setup terminal with error handling - if let Err(e) = enable_raw_mode() { - return Err(anyhow::anyhow!( - "Failed to enable raw mode: {}. Try running with --classic flag.", - e - )); - } - - let mut stdout = io::stdout(); - if let Err(e) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) { - let _ = disable_raw_mode(); - return Err(anyhow::anyhow!( - "Failed to setup terminal: {}. Try running with --classic flag.", - e - )); - } - - let backend = CrosstermBackend::new(stdout); - let mut terminal = match Terminal::new(backend) { - Ok(t) => t, - Err(e) => { - let _ = disable_raw_mode(); - return Err(anyhow::anyhow!( - "Failed to create terminal: {}. Try running with --classic flag.", - e - )); - } - }; - - let res = self.run_app(&mut terminal); - - // Always restore terminal, even on error - let _ = disable_raw_mode(); - let _ = execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ); - let _ = terminal.show_cursor(); - - match res { - Ok(_) => Ok(()), - Err(e) => Err(anyhow::anyhow!("TUI error: {}", e)), - } - } - - fn run_app(&mut self, terminal: &mut Terminal) -> Result<()> { - // Initial draw - terminal.draw(|f| self.ui(f))?; - - loop { - // Use blocking read for better performance - only process when there's an actual event - match event::read()? { - Event::Key(key) => { - // On Windows, filter out key release events - only handle key press - // This prevents double-triggering of toggles - if key.kind != crossterm::event::KeyEventKind::Press { - continue; - } - - let should_exit = match self.get_mode() { - AppMode::Command => self.handle_command_input(key)?, - AppMode::Results => self.handle_results_input(key)?, - AppMode::Search => self.handle_search_input(key)?, - AppMode::Filter => self.handle_filter_input(key)?, - AppMode::FuzzyFilter => self.handle_fuzzy_filter_input(key)?, - AppMode::ColumnSearch => self.handle_column_search_input(key)?, - AppMode::Help => self.handle_help_input(key)?, - AppMode::History => self.handle_history_input(key)?, - AppMode::Debug => self.handle_debug_input(key)?, - AppMode::PrettyQuery => self.handle_pretty_query_input(key)?, - AppMode::CacheList => self.handle_cache_list_input(key)?, - AppMode::JumpToRow => self.handle_jump_to_row_input(key)?, - AppMode::ColumnStats => self.handle_column_stats_input(key)?, - }; - - if should_exit { - break; - } - - // Only redraw after handling a key event - terminal.draw(|f| self.ui(f))?; - } - _ => { - // Ignore other events (mouse, resize, etc.) to reduce CPU - } - } - } - Ok(()) - } - - fn handle_command_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // Store old cursor position - let old_cursor = self.get_input_cursor(); - - // Debug: Log key presses to help diagnose input issues - // Keep last 50 key presses - if self.key_history.len() > 50 { - self.key_history.remove(0); - } - self.key_history.push(format!( - "[{}] Key: {:?} Mods: {:?}", - Local::now().format("%H:%M:%S.%3f"), - key.code, - key.modifiers - )); - - // Also log to tracing - trace!(target: "input", "Key: {:?} Modifiers: {:?}", key.code, key.modifiers); - - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Expand SELECT * to all column names - self.expand_asterisk(); - } - // Buffer navigation keys - KeyCode::Tab if key.modifiers.contains(KeyModifiers::ALT) => { - // Alt+Tab - next buffer - self.next_buffer(); - } - KeyCode::BackTab - if key - .modifiers - .contains(KeyModifiers::ALT | KeyModifiers::SHIFT) => - { - // Alt+Shift+Tab - previous buffer - self.prev_buffer(); - } - // Alternative buffer navigation keys (for Windows users) - KeyCode::F(11) => { - // F11 - previous buffer - self.prev_buffer(); - } - KeyCode::F(12) => { - // F12 - next buffer - self.next_buffer(); - } - // Ctrl+PageDown for next buffer (more standard) - KeyCode::PageDown if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+PageDown - next buffer - self.next_buffer(); - } - // Ctrl+PageUp for previous buffer - KeyCode::PageUp if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+PageUp - previous buffer - self.prev_buffer(); - } - // Also support Ctrl+Tab-like behavior with Ctrl+6 - KeyCode::Char('6') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+6 - toggle between last two buffers (like vim) - self.next_buffer(); - } - KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alt+N - new buffer - self.new_buffer(); - } - KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alt+W - close current buffer - self.close_buffer(); - } - KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alt+B - list buffers - let buffer_list = self.list_buffers(); - if !buffer_list.is_empty() { - let message = format!("Buffers:\n{}", buffer_list.join("\n")); - self.set_status_message(message); - } - } - KeyCode::F(1) | KeyCode::Char('?') => { - self.show_help = !self.show_help; - self.set_mode(if self.show_help { - AppMode::Help - } else { - AppMode::Command - }); - } - KeyCode::F(3) => { - // Toggle between single-line and multi-line mode - match self.get_edit_mode() { - EditMode::SingleLine => { - self.set_edit_mode(EditMode::MultiLine); - let current_text = self.get_input_text(); - - // Pretty format the query for multi-line editing - let formatted_lines = if !current_text.trim().is_empty() { - crate::recursive_parser::format_sql_pretty_compact(¤t_text, 5) - // 5 columns per line for compact multi-line - } else { - vec![current_text] - }; - - self.textarea = TextArea::from(formatted_lines); - self.textarea.set_cursor_line_style( - Style::default().add_modifier(Modifier::UNDERLINED), - ); - // Move cursor to the beginning - self.textarea.move_cursor(CursorMove::Top); - self.textarea.move_cursor(CursorMove::Head); - self.set_status_message("Multi-line mode (F3 to toggle, Tab for completion, Ctrl+Enter to execute)".to_string()); - } - EditMode::MultiLine => { - self.set_edit_mode(EditMode::SingleLine); - // Join lines with single space to create compact query - let text = self - .textarea - .lines() - .iter() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - .collect::>() - .join(" "); - self.input = tui_input::Input::new(text); - self.set_status_message( - "Single-line mode enabled (F3 to toggle multi-line)".to_string(), - ); - } - } - } - KeyCode::F(7) => { - // F7 - Toggle cache mode or show cache list - if self.is_cache_mode() { - self.set_mode(AppMode::CacheList); - } else { - self.set_mode(AppMode::CacheList); - } - } - KeyCode::Enter => { - let query = match self.get_edit_mode() { - EditMode::SingleLine => { - let q = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query from single-line mode: {}", q); - q - } - EditMode::MultiLine => { - if key.modifiers.contains(KeyModifiers::CONTROL) { - // Ctrl+Enter executes the query in multi-line mode - let q = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query from multi-line mode: {}", q); - q - } else { - // Regular Enter adds a new line - self.textarea.input(key); - return Ok(false); - } - } - }; - - if !query.is_empty() { - // Check for special commands - if query == ":help" { - self.show_help = true; - self.set_mode(AppMode::Help); - self.set_status_message("Help Mode - Press ESC to return".to_string()); - } else if query == ":exit" || query == ":quit" { - return Ok(true); - } else if query == ":tui" { - // Already in TUI mode - self.set_status_message("Already in TUI mode".to_string()); - } else if query.starts_with(":cache ") { - self.handle_cache_command(&query)?; - } else { - self.set_status_message(format!("Processing query: '{}'", query)); - self.execute_query(&query)?; - } - } else { - self.set_status_message("Empty query - please enter a SQL command".to_string()); - } - } - KeyCode::Tab => { - // Tab completion works in both modes - match self.edit_mode { - EditMode::SingleLine => self.apply_completion(), - EditMode::MultiLine => { - // In vim normal mode, Tab should also trigger completion - self.apply_completion_multiline(); - } - } - } - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.set_mode(AppMode::History); - self.history_state.search_query.clear(); - self.update_history_matches(); - } - // History navigation - Ctrl+P or Alt+Up - KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to previous command in history - // Get history entries first, before mutable borrow - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.current_buffer_mut() { - if buffer.navigate_history_up(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Debug: show what we got from history - let debug_msg = if text.is_empty() { - "History navigation returned empty text!".to_string() - } else { - format!( - "History: {}", - if text.len() > 50 { - format!("{}...", &text[..50]) - } else { - text.clone() - } - ) - }; - - // Update the appropriate input field based on edit mode - match self.edit_mode { - EditMode::SingleLine => { - self.input = - tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - EditMode::MultiLine => { - let lines: Vec = - text.lines().map(|s| s.to_string()).collect(); - self.textarea = tui_textarea::TextArea::from(lines); - self.textarea.move_cursor(tui_textarea::CursorMove::End); - } - } - self.set_status_message(debug_msg); - } - } - } - // History navigation - Ctrl+N or Alt+Down - KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to next command in history - // Get history entries first, before mutable borrow - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.current_buffer_mut() { - if buffer.navigate_history_down(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Update the appropriate input field based on edit mode - match self.edit_mode { - EditMode::SingleLine => { - self.input = - tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - EditMode::MultiLine => { - let lines: Vec = - text.lines().map(|s| s.to_string()).collect(); - self.textarea = tui_textarea::TextArea::from(lines); - self.textarea.move_cursor(tui_textarea::CursorMove::End); - } - } - self.set_status_message("Next command from history".to_string()); - } - } - } - // Alternative: Alt+Up for history previous (in case Ctrl+P is intercepted) - KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.current_buffer_mut() { - if buffer.navigate_history_up(&history_commands) { - let text = buffer.get_input_text(); - match self.edit_mode { - EditMode::SingleLine => { - self.input = - tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - EditMode::MultiLine => { - let lines: Vec = - text.lines().map(|s| s.to_string()).collect(); - self.textarea = tui_textarea::TextArea::from(lines); - self.textarea.move_cursor(tui_textarea::CursorMove::End); - } - } - self.set_status_message("Previous command (Alt+Up)".to_string()); - } - } - } - // Alternative: Alt+Down for history next - KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.current_buffer_mut() { - if buffer.navigate_history_down(&history_commands) { - let text = buffer.get_input_text(); - match self.edit_mode { - EditMode::SingleLine => { - self.input = - tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - EditMode::MultiLine => { - let lines: Vec = - text.lines().map(|s| s.to_string()).collect(); - self.textarea = tui_textarea::TextArea::from(lines); - self.textarea.move_cursor(tui_textarea::CursorMove::End); - } - } - self.set_status_message("Next command (Alt+Down)".to_string()); - } - } - } - KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Jump to beginning of line (like bash/zsh) - self.handle_input_key(KeyEvent::new(KeyCode::Home, KeyModifiers::empty())); - } - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Jump to end of line (like bash/zsh) - self.handle_input_key(KeyEvent::new(KeyCode::End, KeyModifiers::empty())); - } - KeyCode::F(8) => { - // Toggle case-insensitive string comparisons - let current = self.get_case_insensitive(); - self.set_case_insensitive(!current); - - // Update CSV client if in CSV mode - self.set_csv_client_case_insensitive(!current); - - self.set_status_message(format!( - "Case-insensitive string comparisons: {}", - if !current { "ON" } else { "OFF" } - )); - } - KeyCode::F(9) => { - // F9 as alternative for kill line (for terminals that intercept Ctrl+K) - self.kill_line(); - self.set_status_message(format!( - "Killed to end of line{}", - if !self.is_kill_ring_empty() { - format!(" ('{}' saved to kill ring)", self.get_kill_ring()) - } else { - "".to_string() - } - )); - } - KeyCode::F(10) => { - // F10 as alternative for kill line backward (for consistency with F9) - self.kill_line_backward(); - self.set_status_message(format!( - "Killed to beginning of line{}", - if !self.is_kill_ring_empty() { - format!(" ('{}' saved to kill ring)", self.get_kill_ring()) - } else { - "".to_string() - } - )); - } - KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Delete word backward (like bash/zsh) - self.delete_word_backward(); - } - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::ALT) => { - // Delete word forward (like bash/zsh) - self.delete_word_forward(); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line - delete from cursor to end of line - self.set_status_message("Ctrl+K pressed - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alternative: Alt+K for kill line (for terminals that intercept Ctrl+K) - self.set_status_message("Alt+K - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line backward - delete from cursor to beginning of line - self.kill_line_backward(); - } - KeyCode::Char('z') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Undo - self.undo(); - } - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Yank - paste from kill ring - self.yank(); - } - KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Paste from system clipboard - self.paste_from_clipboard(); - } - KeyCode::Char('[') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to previous SQL token - self.jump_to_prev_token(); - } - KeyCode::Char(']') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to next SQL token - self.jump_to_next_token(); - } - KeyCode::Left if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move backward one word - self.move_cursor_word_backward(); - } - KeyCode::Right if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move forward one word - self.move_cursor_word_forward(); - } - KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move backward one word (alt+b like in bash) - self.move_cursor_word_backward(); - } - KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move forward one word (alt+f like in bash) - self.move_cursor_word_forward(); - } - KeyCode::Down if self.results.is_some() && self.edit_mode == EditMode::SingleLine => { - self.set_mode(AppMode::Results); - // Restore previous position or default to 0 - let row = self.get_last_results_row().unwrap_or(0); - self.get_table_state_mut().select(Some(row)); - - // Restore the exact scroll offset from when we left - self.set_scroll_offset(self.get_last_scroll_offset()); - } - KeyCode::F(5) => { - // Debug command - show detailed parser information - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; // Get column position for single-line - let query = self.get_input_text(); - let mut debug_info = self - .hybrid_parser - .get_detailed_debug_info(&query, cursor_pos); - - // Add input state information - let input_state = format!( - "\n========== INPUT STATE ==========\n\ - Input Value Length: {}\n\ - Cursor Position: {}\n\ - Visual Cursor: {}\n\ - Input Mode: Command\n", - query.len(), - cursor_pos, - visual_cursor - ); - debug_info.push_str(&input_state); - - // Add dataset information - let dataset_info = if self.is_csv_mode() { - if let Some(csv_client) = self.get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - let (table_name, columns) = schema - .iter() - .next() - .map(|(t, c)| (t.as_str(), c.clone())) - .unwrap_or(("unknown", vec![])); - format!( - "\n========== DATASET INFO ==========\n\ - Mode: CSV\n\ - Table Name: {}\n\ - Columns ({}): {}\n", - table_name, - columns.len(), - columns.join(", ") - ) - } else { - "\n========== DATASET INFO ==========\nMode: CSV\nNo schema available\n" - .to_string() - } - } else { - "\n========== DATASET INFO ==========\nMode: CSV\nNo CSV client initialized\n".to_string() - } - } else { - format!( - "\n========== DATASET INFO ==========\n\ - Mode: API ({})\n\ - Table: trade_deal\n\ - Default Columns: {}\n", - self.api_client.base_url, - "id, platformOrderId, tradeDate, executionSide, quantity, price, counterparty, ..." - ) - }; - debug_info.push_str(&dataset_info); - - // Add current data statistics - let data_stats = format!( - "\n========== CURRENT DATA ==========\n\ - Total Rows Loaded: {}\n\ - Filtered Rows: {}\n\ - Current Column: {}\n\ - Sort State: {}\n", - self.results.as_ref().map(|r| r.data.len()).unwrap_or(0), - self.get_filtered_data().map(|d| d.len()).unwrap_or(0), - self.get_current_column(), - match &self.sort_state { - SortState { - column: Some(col), - order, - } => format!( - "Column {} - {}", - col, - match order { - SortOrder::Ascending => "Ascending", - SortOrder::Descending => "Descending", - SortOrder::None => "None", - } - ), - _ => "None".to_string(), - } - ); - debug_info.push_str(&data_stats); - - // Add WHERE clause AST if query contains WHERE - if query.to_lowercase().contains(" where ") { - let where_ast_info = match self.parse_where_clause_ast(&query) { - Ok(ast_str) => ast_str, - Err(e) => format!("\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {}\n", e) - }; - debug_info.push_str(&where_ast_info); - } - - // Add status line info - let status_line_info = format!( - "\n========== STATUS LINE INFO ==========\n\ - Current Mode: {:?}\n\ - Case Insensitive: {}\n\ - Compact Mode: {}\n\ - Viewport Lock: {}\n\ - CSV Mode: {}\n\ - Cache Mode: {}\n\ - Data Source: {}\n\ - Active Filters: {}\n", - self.get_mode(), - self.get_case_insensitive(), - self.get_compact_mode(), - self.is_viewport_lock(), - self.is_csv_mode(), - self.is_cache_mode(), - &self.get_last_query_source().unwrap_or("None".to_string()), - if self.fuzzy_filter_state.active { - format!("Fuzzy: {}", self.fuzzy_filter_state.pattern) - } else if self.get_filter_state().active { - format!("Filter: {}", self.get_filter_state().pattern) - } else { - "None".to_string() - } - ); - debug_info.push_str(&status_line_info); - - // Add buffer manager debug info - debug_info.push_str("\n========== BUFFER MANAGER STATE ==========\n"); - debug_info.push_str(&format!("Buffer Manager: INITIALIZED\n")); - debug_info.push_str(&format!( - "Number of Buffers: {}\n", - self.buffer_manager.all_buffers().len() - )); - debug_info.push_str(&format!( - "Current Buffer Index: {}\n", - self.buffer_manager.current_index() - )); - debug_info.push_str(&format!( - "Has Multiple Buffers: {}\n", - self.buffer_manager.has_multiple() - )); - - // Add info about all buffers - for (i, buffer) in self.buffer_manager.all_buffers().iter().enumerate() { - debug_info.push_str(&format!("\nBuffer [{}]: {}\n", i, buffer.display_name())); - debug_info.push_str(&format!(" ID: {}\n", buffer.get_id())); - debug_info.push_str(&format!(" Path: {:?}\n", buffer.file_path)); - debug_info.push_str(&format!(" Modified: {}\n", buffer.modified)); - debug_info.push_str(&format!(" CSV Mode: {}\n", buffer.is_csv_mode())); - } - - // Add current buffer debug dump - if let Some(buffer) = self.buffer_manager.current() { - debug_info.push_str("\n========== CURRENT BUFFER DEBUG DUMP ==========\n"); - debug_info.push_str(&buffer.debug_dump()); - debug_info.push_str("================================================\n"); - } else { - debug_info.push_str("\nNo current buffer available!\n"); - } - debug_info.push_str("============================================\n"); - - // Add key press history - debug_info.push_str("\n========== KEY PRESS HISTORY ==========\n"); - debug_info.push_str("(Most recent at bottom, last 50 keys)\n"); - for key_event in &self.key_history { - debug_info.push_str(key_event); - debug_info.push('\n'); - } - debug_info.push_str("========================================\n"); - - // Add trace logs from ring buffer - debug_info.push_str("\n========== TRACE LOGS ==========\n"); - debug_info.push_str("(Most recent at bottom, last 100 entries)\n"); - if let Some(ref log_buffer) = self.log_buffer { - let recent_logs = log_buffer.get_recent(100); - for entry in recent_logs { - debug_info.push_str(&entry.format_for_display()); - debug_info.push('\n'); - } - debug_info.push_str(&format!("Total log entries: {}\n", log_buffer.len())); - } else { - debug_info.push_str("Log buffer not initialized\n"); - } - debug_info.push_str("================================\n"); - - // Store debug info and switch to debug mode - self.debug_text = debug_info.clone(); - self.debug_scroll = 0; - self.set_mode(AppMode::Debug); - - // Try to copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_info) { - Ok(_) => { - self.set_status_message("DEBUG INFO copied to clipboard!".to_string()); - } - Err(e) => { - self.set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - KeyCode::F(6) => { - // Pretty print query view - let query = self.get_input_text(); - if !query.trim().is_empty() { - self.debug_text = format!( - "Pretty SQL Query\n{}\n\n{}", - "=".repeat(50), - crate::recursive_parser::format_sql_pretty_compact(&query, 5).join("\n") - ); - self.debug_scroll = 0; - self.set_mode(AppMode::PrettyQuery); - self.set_status_message( - "Pretty query view (press Esc or q to return)".to_string(), - ); - } else { - self.set_status_message("No query to format".to_string()); - } - } - _ => { - // Use the new helper to handle input keys through buffer - self.handle_input_key(key); - - // Clear completion state when typing other characters - self.completion_state.suggestions.clear(); - self.completion_state.current_index = 0; - - // Handle completion based on mode - match self.edit_mode { - EditMode::SingleLine => self.handle_completion(), - EditMode::MultiLine => self.handle_completion_multiline(), - } - } - } - - // Update horizontal scroll if cursor moved - if self.get_input_cursor() != old_cursor { - self.update_horizontal_scroll(120); // Assume reasonable terminal width, will be adjusted in render - } - - Ok(false) - } - - fn handle_results_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('q') => return Ok(true), - KeyCode::F(5) => { - // Debug mode - show buffer state and parser information - // Build debug information similar to Command mode - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; // Get column position for single-line - let query = self.get_input_text(); - - // Create debug info showing buffer state - let mut debug_info = String::new(); - debug_info.push_str("=== Debug Information (Results Mode) ===\n\n"); - - // Add current query info - debug_info.push_str("Current Query:\n"); - debug_info.push_str(&format!(" Query: '{}'\n", query)); - debug_info.push_str(&format!(" Cursor Position: {}\n", cursor_pos)); - debug_info.push_str(&format!(" Visual Cursor: {}\n", visual_cursor)); - debug_info.push_str("\n"); - - // Add buffer manager info - debug_info.push_str("=== Buffer Manager ===\n"); - debug_info.push_str(&format!( - " Total Buffers: {}\n", - self.buffer_manager.all_buffers().len() - )); - debug_info.push_str(&format!( - " Current Buffer Index: {}\n", - self.buffer_manager.current_index() - )); - - // Add current buffer debug dump - if let Some(buffer) = self.buffer_manager.current() { - debug_info.push_str("\n=== Current Buffer State ===\n"); - debug_info.push_str(&buffer.debug_dump()); - } - - self.debug_text = debug_info; - self.debug_scroll = 0; - self.set_mode(AppMode::Debug); - self.set_status_message("Debug mode - Press 'q' or ESC to return".to_string()); - } - KeyCode::F(8) => { - // Toggle case-insensitive string comparisons - let current = self.get_case_insensitive(); - self.set_case_insensitive(!current); - - // Update CSV client if in CSV mode - self.set_csv_client_case_insensitive(!current); - - self.set_status_message(format!( - "Case-insensitive string comparisons: {}", - if !current { "ON" } else { "OFF" } - )); - } - KeyCode::Esc => { - if self.yank_mode.is_some() { - // Cancel yank mode - self.yank_mode = None; - self.set_status_message("Yank cancelled".to_string()); - } else { - // Save current position before switching to Command mode - if let Some(selected) = self.get_table_state().selected() { - self.set_last_results_row(Some(selected)); - self.set_last_scroll_offset(self.get_scroll_offset()); - } - self.set_mode(AppMode::Command); - self.get_table_state_mut().select(None); - } - } - KeyCode::Up => { - // Save current position before switching to Command mode - if let Some(selected) = self.get_table_state().selected() { - self.last_results_row = Some(selected); - self.last_scroll_offset = self.get_scroll_offset(); - } - self.set_mode(AppMode::Command); - self.get_table_state_mut().select(None); - } - // Vim-like navigation - KeyCode::Char('j') | KeyCode::Down => { - self.next_row(); - } - KeyCode::Char('k') => { - self.previous_row(); - } - KeyCode::Char('h') | KeyCode::Left => { - self.move_column_left(); - } - KeyCode::Char('l') | KeyCode::Right => { - self.move_column_right(); - } - KeyCode::Char('^') | KeyCode::Char('0') => { - // Jump to first column (vim-like) - self.goto_first_column(); - } - KeyCode::Char('$') => { - // Jump to last column (vim-like) - self.goto_last_column(); - } - KeyCode::Char('g') => { - self.goto_first_row(); - } - KeyCode::Char('G') => { - self.goto_last_row(); - } - KeyCode::Char('p') => { - // Toggle pin for current column - self.toggle_column_pin(); - } - KeyCode::Char('P') => { - // Clear all pinned columns - self.clear_all_pinned_columns(); - } - KeyCode::Char('C') => { - // Toggle compact mode with Shift+C - let current_mode = self.get_compact_mode(); - self.set_compact_mode(!current_mode); - self.set_status_message(if self.get_compact_mode() { - "Compact mode: ON (reduced padding, more columns visible)".to_string() - } else { - "Compact mode: OFF (standard padding)".to_string() - }); - // Recalculate column widths with new mode - self.calculate_optimal_column_widths(); - } - KeyCode::Char(':') => { - // Start jump to row command - self.set_mode(AppMode::JumpToRow); - self.jump_to_row_input.clear(); - self.set_status_message("Enter row number:".to_string()); - } - KeyCode::Char(' ') => { - // Toggle viewport lock with Space - let current_lock = self.is_viewport_lock(); - self.set_viewport_lock(!current_lock); - if self.is_viewport_lock() { - // Lock to current position in viewport (middle of screen) - let visible_rows = self.get_last_visible_rows(); - self.set_viewport_lock_row(Some(visible_rows / 2)); - self.set_status_message(format!( - "Viewport lock: ON (anchored at row {} of viewport)", - visible_rows / 2 + 1 - )); - } else { - self.set_viewport_lock_row(None); - self.set_status_message("Viewport lock: OFF (normal scrolling)".to_string()); - } - } - KeyCode::PageDown | KeyCode::Char('f') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.page_down(); - } - KeyCode::PageUp | KeyCode::Char('b') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.page_up(); - } - // Search functionality - KeyCode::Char('/') => { - self.set_mode(AppMode::Search); - self.clear_search_pattern(); - // Save SQL query and use temporary input for search display - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - self.input = tui_input::Input::default(); - } - // Column navigation/search functionality (backslash like vim reverse search) - KeyCode::Char('\\') => { - self.set_mode(AppMode::ColumnSearch); - self.column_search_state.pattern.clear(); - self.column_search_state.matching_columns.clear(); - self.column_search_state.current_match = 0; - // Save current SQL query before clearing input for column search - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - self.input = tui_input::Input::default(); - } - KeyCode::Char('n') => { - self.next_search_match(); - } - KeyCode::Char('N') if key.modifiers.contains(KeyModifiers::SHIFT) => { - // Only for search navigation when Shift is held - if !self.is_search_pattern_empty() { - self.previous_search_match(); - } else { - // Toggle row numbers display - let current = self.get_show_row_numbers(); - self.set_show_row_numbers(!current); - self.set_status_message(if self.get_show_row_numbers() { - "Row numbers: ON (showing line numbers)".to_string() - } else { - "Row numbers: OFF".to_string() - }); - // Recalculate column widths with new mode - self.calculate_optimal_column_widths(); - } - } - // Regex filter functionality (uppercase F) - KeyCode::Char('F') if key.modifiers.contains(KeyModifiers::SHIFT) => { - self.set_mode(AppMode::Filter); - self.get_filter_state_mut().pattern.clear(); - // Save SQL query and use temporary input for filter display - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - self.input = tui_input::Input::default(); - } - // Fuzzy filter functionality (lowercase f) - KeyCode::Char('f') - if !key.modifiers.contains(KeyModifiers::ALT) - && !key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.set_mode(AppMode::FuzzyFilter); - self.fuzzy_filter_state.pattern.clear(); - self.fuzzy_filter_state.filtered_indices.clear(); - self.fuzzy_filter_state.active = false; // Clear active state when entering mode - // Save SQL query and use temporary input for fuzzy filter display - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - self.input = tui_input::Input::default(); - } - // Sort functionality (lowercase s) - KeyCode::Char('s') - if !key.modifiers.contains(KeyModifiers::CONTROL) - && !key.modifiers.contains(KeyModifiers::SHIFT) => - { - self.sort_by_column(self.get_current_column()); - } - // Column statistics (uppercase S) - KeyCode::Char('S') | KeyCode::Char('s') - if key.modifiers.contains(KeyModifiers::SHIFT) => - { - self.calculate_column_statistics(); - } - // Toggle cell/row selection mode - KeyCode::Char('v') => { - self.selection_mode = match self.selection_mode { - SelectionMode::Row => { - self.set_status_message( - "Cell mode - Navigate to select individual cells".to_string(), - ); - SelectionMode::Cell - } - SelectionMode::Cell => { - self.set_status_message( - "Row mode - Navigate to select entire rows".to_string(), - ); - SelectionMode::Row - } - }; - } - // Clipboard operations (vim-like yank) - KeyCode::Char('y') => { - match self.selection_mode { - SelectionMode::Cell => { - // In cell mode, single 'y' yanks the cell - self.yank_cell(); - // Status message will be set by yank_cell - } - SelectionMode::Row => { - if self.yank_mode.is_some() { - // Second 'y' for yank row - self.yank_row(); - self.yank_mode = None; - } else { - // First 'y', enter yank mode - self.yank_mode = Some('y'); - self.set_status_message( - "Yank mode: y=row, c=column, a=all, ESC=cancel".to_string(), - ); - } - } - } - } - KeyCode::Char('c') if self.yank_mode.is_some() => { - // 'yc' - yank column - self.yank_column(); - self.yank_mode = None; - } - KeyCode::Char('a') if self.yank_mode.is_some() => { - // 'ya' - yank all (filtered or all data) - self.yank_all(); - self.yank_mode = None; - } - // Export to CSV - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_csv(); - } - // Export to JSON - KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_json(); - } - // Number keys for direct column sorting - KeyCode::Char(c) if c.is_ascii_digit() => { - if let Some(digit) = c.to_digit(10) { - let column_index = (digit as usize).saturating_sub(1); - self.sort_by_column(column_index); - } - } - KeyCode::F(1) | KeyCode::Char('?') => { - self.show_help = true; - self.set_mode(AppMode::Help); - } - _ => { - // Any other key cancels yank mode - if self.yank_mode.is_some() { - self.yank_mode = None; - self.set_status_message("Yank cancelled".to_string()); - } - } - } - Ok(false) - } - - fn handle_search_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - } - KeyCode::Enter => { - self.perform_search(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - } - KeyCode::Backspace => { - self.pop_search_pattern_char(); - // Update input for rendering - self.input = tui_input::Input::new(self.get_search_pattern()) - .with_cursor(self.get_search_pattern().len()); - } - KeyCode::Char(c) => { - self.push_search_pattern_char(c); - // Update input for rendering - self.input = tui_input::Input::new(self.get_search_pattern()) - .with_cursor(self.get_search_pattern().len()); - } - _ => {} - } - Ok(false) - } - - fn handle_filter_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - } - KeyCode::Enter => { - self.apply_filter(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - } - KeyCode::Backspace => { - self.get_filter_state_mut().pattern.pop(); - // Update input for rendering - let pattern = self.get_filter_state().pattern.clone(); - self.input = tui_input::Input::new(pattern.clone()).with_cursor(pattern.len()); - } - KeyCode::Char(c) => { - self.get_filter_state_mut().pattern.push(c); - // Update input for rendering - let pattern = self.get_filter_state().pattern.clone(); - self.input = tui_input::Input::new(pattern.clone()).with_cursor(pattern.len()); - } - _ => {} - } - Ok(false) - } - - fn handle_fuzzy_filter_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Clear fuzzy filter and return to results - self.fuzzy_filter_state.active = false; - self.fuzzy_filter_state.pattern.clear(); - self.fuzzy_filter_state.filtered_indices.clear(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - self.set_status_message("Fuzzy filter cleared".to_string()); - } - KeyCode::Enter => { - // Apply fuzzy filter and return to results - if !self.fuzzy_filter_state.pattern.is_empty() { - self.apply_fuzzy_filter(); - self.fuzzy_filter_state.active = true; - } - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - } - KeyCode::Backspace => { - self.fuzzy_filter_state.pattern.pop(); - // Update input for rendering - self.input = tui_input::Input::new(self.fuzzy_filter_state.pattern.clone()) - .with_cursor(self.fuzzy_filter_state.pattern.len()); - // Re-apply filter in real-time - if !self.fuzzy_filter_state.pattern.is_empty() { - self.apply_fuzzy_filter(); - } else { - self.fuzzy_filter_state.filtered_indices.clear(); - self.fuzzy_filter_state.active = false; - } - } - KeyCode::Char(c) => { - self.fuzzy_filter_state.pattern.push(c); - // Update input for rendering - self.input = tui_input::Input::new(self.fuzzy_filter_state.pattern.clone()) - .with_cursor(self.fuzzy_filter_state.pattern.len()); - // Apply filter in real-time as user types - self.apply_fuzzy_filter(); - } - _ => {} - } - Ok(false) - } - - fn handle_column_search_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Cancel column search and return to results - self.set_mode(AppMode::Results); - self.column_search_state.pattern.clear(); - self.column_search_state.matching_columns.clear(); - // Restore original SQL query from undo stack - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_status_message("Column search cancelled".to_string()); - } - KeyCode::Enter => { - // Jump to first matching column - if !self.column_search_state.matching_columns.is_empty() { - let (column_index, column_name) = self.column_search_state.matching_columns - [self.column_search_state.current_match] - .clone(); - self.set_current_column(column_index); - self.set_status_message(format!("Jumped to column: {}", column_name)); - } else { - self.set_status_message("No matching columns found".to_string()); - } - // Restore original SQL query from undo stack - if let Some((original_query, cursor_pos)) = self.pop_undo() { - self.input = tui_input::Input::new(original_query).with_cursor(cursor_pos); - } - self.set_mode(AppMode::Results); - } - KeyCode::Tab => { - // Next match (Tab only, not 'n' to allow typing 'n' in search) - if !self.column_search_state.matching_columns.is_empty() { - self.column_search_state.current_match = - (self.column_search_state.current_match + 1) - % self.column_search_state.matching_columns.len(); - let (column_index, column_name) = self.column_search_state.matching_columns - [self.column_search_state.current_match] - .clone(); - self.set_current_column(column_index); - self.set_status_message(format!( - "Column {} of {}: {}", - self.column_search_state.current_match + 1, - self.column_search_state.matching_columns.len(), - column_name - )); - } - } - KeyCode::BackTab => { - // Previous match (Shift+Tab only, not 'N' to allow typing 'N' in search) - if !self.column_search_state.matching_columns.is_empty() { - if self.column_search_state.current_match == 0 { - self.column_search_state.current_match = - self.column_search_state.matching_columns.len() - 1; - } else { - self.column_search_state.current_match -= 1; - } - let (column_index, column_name) = self.column_search_state.matching_columns - [self.column_search_state.current_match] - .clone(); - self.set_current_column(column_index); - self.set_status_message(format!( - "Column {} of {}: {}", - self.column_search_state.current_match + 1, - self.column_search_state.matching_columns.len(), - column_name - )); - } - } - KeyCode::Backspace => { - self.column_search_state.pattern.pop(); - // Also update input to keep it in sync for rendering - self.input = tui_input::Input::new(self.column_search_state.pattern.clone()) - .with_cursor(self.column_search_state.pattern.len()); - self.update_column_search(); - } - KeyCode::Char(c) => { - self.column_search_state.pattern.push(c); - // Also update input to keep it in sync for rendering - self.input = tui_input::Input::new(self.column_search_state.pattern.clone()) - .with_cursor(self.column_search_state.pattern.len()); - self.update_column_search(); - } - _ => {} - } - Ok(false) - } - - fn handle_help_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('q') | KeyCode::Esc | KeyCode::F(1) => { - self.show_help = false; - self.help_scroll = 0; // Reset scroll when closing - self.set_mode(if self.results.is_some() { - AppMode::Results - } else { - AppMode::Command - }); - } - // Scroll help with arrow keys or vim keys - KeyCode::Down | KeyCode::Char('j') => { - // Calculate max scroll based on help content - let max_lines: usize = 58; // Approximate number of lines in help - let visible_height: usize = 30; // Approximate visible height - let max_scroll = max_lines.saturating_sub(visible_height); - if (self.help_scroll as usize) < max_scroll { - self.help_scroll += 1; - } - } - KeyCode::Up | KeyCode::Char('k') => { - self.help_scroll = self.help_scroll.saturating_sub(1); - } - KeyCode::PageDown => { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - self.help_scroll = (self.help_scroll + 10).min(max_scroll as u16); - } - KeyCode::PageUp => { - self.help_scroll = self.help_scroll.saturating_sub(10); - } - KeyCode::Home => { - self.help_scroll = 0; - } - KeyCode::End => { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - self.help_scroll = max_scroll as u16; - } - _ => {} - } - Ok(false) - } - - fn handle_history_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc => { - self.set_mode(AppMode::Command); - } - KeyCode::Enter => { - if !self.history_state.matches.is_empty() - && self.history_state.selected_index < self.history_state.matches.len() - { - let selected_command = self.history_state.matches - [self.history_state.selected_index] - .entry - .command - .clone(); - // Use helper to set text through buffer - self.set_input_text(selected_command); - self.set_mode(AppMode::Command); - self.set_status_message("Command loaded from history".to_string()); - // Reset scroll to show end of command - self.input_scroll_offset = 0; - self.update_horizontal_scroll(120); // Will be properly updated on next render - } - } - KeyCode::Up | KeyCode::Char('k') => { - if !self.history_state.matches.is_empty() { - self.history_state.selected_index = - self.history_state.selected_index.saturating_sub(1); - } - } - KeyCode::Down | KeyCode::Char('j') => { - if !self.history_state.matches.is_empty() - && self.history_state.selected_index + 1 < self.history_state.matches.len() - { - self.history_state.selected_index += 1; - } - } - KeyCode::Backspace => { - self.history_state.search_query.pop(); - self.update_history_matches(); - } - KeyCode::Char(c) => { - self.history_state.search_query.push(c); - self.update_history_matches(); - } - _ => {} - } - Ok(false) - } - - fn update_history_matches(&mut self) { - self.history_state.matches = self - .command_history - .search(&self.history_state.search_query); - self.history_state.selected_index = 0; - } - - fn handle_debug_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => { - self.set_mode(AppMode::Command); - } - KeyCode::Up | KeyCode::Char('k') => { - if self.debug_scroll > 0 { - self.debug_scroll = self.debug_scroll.saturating_sub(1); - } - } - KeyCode::Down | KeyCode::Char('j') => { - self.debug_scroll = self.debug_scroll.saturating_add(1); - } - KeyCode::PageUp => { - self.debug_scroll = self.debug_scroll.saturating_sub(10); - } - KeyCode::PageDown => { - self.debug_scroll = self.debug_scroll.saturating_add(10); - } - _ => {} - } - Ok(false) - } - - fn handle_pretty_query_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => { - self.set_mode(AppMode::Command); - } - KeyCode::Up | KeyCode::Char('k') => { - if self.debug_scroll > 0 { - self.debug_scroll = self.debug_scroll.saturating_sub(1); - } - } - KeyCode::Down | KeyCode::Char('j') => { - self.debug_scroll = self.debug_scroll.saturating_add(1); - } - KeyCode::PageUp => { - self.debug_scroll = self.debug_scroll.saturating_sub(10); - } - KeyCode::PageDown => { - self.debug_scroll = self.debug_scroll.saturating_add(10); - } - _ => {} - } - Ok(false) - } - - fn execute_query(&mut self, query: &str) -> Result<()> { - info!(target: "query", "Executing query: {}", query); - self.set_status_message(format!("Executing query: '{}'...", query)); - let start_time = std::time::Instant::now(); - - let result = if self.is_cache_mode() { - // When in cache mode, use CSV client to query cached data - if let Some(cached_data) = self.get_cached_data() { - let mut csv_client = CsvApiClient::new(); - csv_client.set_case_insensitive(self.get_case_insensitive()); - csv_client.load_from_json(cached_data.clone(), "cached_data")?; - - csv_client.query_csv(query).map(|r| QueryResponse { - data: r.data, - count: r.count, - query: sql_cli::api_client::QueryInfo { - select: r.query.select, - where_clause: r.query.where_clause, - order_by: r.query.order_by, - }, - source: Some("cache".to_string()), - table: Some("cached_data".to_string()), - cached: Some(true), - }) - } else { - Err(anyhow::anyhow!("No cached data loaded")) - } - } else if self.is_csv_mode() { - if let Some(csv_client) = self.get_csv_client() { - // Convert CSV result to match the expected type - csv_client.query_csv(query).map(|r| QueryResponse { - data: r.data, - count: r.count, - query: sql_cli::api_client::QueryInfo { - select: r.query.select, - where_clause: r.query.where_clause, - order_by: r.query.order_by, - }, - source: Some("file".to_string()), - table: Some(self.get_csv_table_name()), - cached: Some(false), - }) - } else { - Err(anyhow::anyhow!("CSV client not initialized")) - } - } else { - self.api_client - .query_trades(query) - .map_err(|e| anyhow::anyhow!("{}", e)) - }; - - match result { - Ok(response) => { - let duration = start_time.elapsed(); - let _ = self.command_history.add_entry( - query.to_string(), - true, - Some(duration.as_millis() as u64), - ); - - // Add debug info about results - let row_count = response.data.len(); - - // Capture the source from the response - self.set_last_query_source(response.source.clone()); - - // Store results in the current buffer - if let Some(buffer) = self.current_buffer_mut() { - let buffer_id = buffer.get_id(); - buffer.set_results(Some(response.clone())); - info!(target: "buffer", "Stored {} results in buffer {}", row_count, buffer_id); - } - self.results = Some(response); // Keep for compatibility during migration - self.calculate_optimal_column_widths(); - self.reset_table_state(); - - if row_count == 0 { - self.set_status_message(format!( - "Query executed successfully but returned 0 rows ({}ms)", - duration.as_millis() - )); - } else { - self.set_status_message(format!("Query executed successfully - {} rows returned ({}ms) - Use ↓ or j/k to navigate", row_count, duration.as_millis())); - } - - self.set_mode(AppMode::Results); - self.get_table_state_mut().select(Some(0)); - } - Err(e) => { - let duration = start_time.elapsed(); - let _ = self.command_history.add_entry( - query.to_string(), - false, - Some(duration.as_millis() as u64), - ); - self.set_status_message(format!("Error: {}", e)); - } - } - Ok(()) - } - - fn parse_where_clause_ast(&self, query: &str) -> Result { - let query_lower = query.to_lowercase(); - if let Some(where_pos) = query_lower.find(" where ") { - let where_clause = &query[where_pos + 7..]; // Skip " where " - - // Get columns from CSV client if available - let columns = if self.is_csv_mode() { - if let Some(csv_client) = self.get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - schema - .iter() - .next() - .map(|(_, cols)| cols.clone()) - .unwrap_or_default() - } else { - vec![] - } - } else { - vec![] - } - } else { - vec![] - }; - - match WhereParser::parse_with_options( - where_clause, - columns, - self.get_case_insensitive(), - ) { - Ok(ast) => { - let tree = format_where_ast(&ast, 0); - Ok(format!( - "\n========== WHERE CLAUSE AST ==========\n\ - Query: {}\n\ - WHERE clause: {}\n\n\ - AST Tree:\n{}\n\n\ - Note: Parentheses in the query control operator precedence.\n\ - The parser respects: OR < AND < NOT < comparisons\n\ - Example: 'a = 1 OR b = 2 AND c = 3' parses as 'a = 1 OR (b = 2 AND c = 3)'\n\ - Use parentheses to override: '(a = 1 OR b = 2) AND c = 3'\n", - query, - where_clause, - tree - )) - } - Err(e) => Err(anyhow::anyhow!("Failed to parse WHERE clause: {}", e)), - } - } else { - Ok( - "\n========== WHERE CLAUSE AST ==========\nNo WHERE clause found in query\n" - .to_string(), - ) - } - } - - fn handle_completion(&mut self) { - let cursor_pos = self.get_input_cursor(); - let query_str = self.get_input_text(); - let query = query_str.as_str(); - - let hybrid_result = self.hybrid_parser.get_completions(query, cursor_pos); - if !hybrid_result.suggestions.is_empty() { - self.set_status_message(format!( - "Suggestions: {}", - hybrid_result.suggestions.join(", ") - )); - } - } - - fn apply_completion(&mut self) { - let cursor_pos = self.get_input_cursor(); - let query = self.get_input_text(); - - // Check if this is a continuation of the same completion session - let is_same_context = query == self.completion_state.last_query - && cursor_pos == self.completion_state.last_cursor_pos; - - if !is_same_context { - // New completion context - get fresh suggestions - let hybrid_result = self.hybrid_parser.get_completions(&query, cursor_pos); - if hybrid_result.suggestions.is_empty() { - self.set_status_message("No completions available".to_string()); - return; - } - - self.completion_state.suggestions = hybrid_result.suggestions; - self.completion_state.current_index = 0; - } else if !self.completion_state.suggestions.is_empty() { - // Cycle to next suggestion - self.completion_state.current_index = - (self.completion_state.current_index + 1) % self.completion_state.suggestions.len(); - } else { - self.set_status_message("No completions available".to_string()); - return; - } - - // Apply the current suggestion (clone to avoid borrow issues) - let suggestion = - self.completion_state.suggestions[self.completion_state.current_index].clone(); - let partial_word = self.extract_partial_word_at_cursor(&query, cursor_pos); - - if let Some(partial) = partial_word { - // Replace the partial word with the suggestion - let before_partial = &query[..cursor_pos - partial.len()]; - let after_cursor = &query[cursor_pos..]; - - // Handle quoted identifiers - if both partial and suggestion start with quotes, - // we need to avoid double quotes - let suggestion_to_use = if partial.starts_with('"') && suggestion.starts_with('"') { - // The partial already includes the opening quote, so use suggestion without its quote - if suggestion.len() > 1 { - suggestion[1..].to_string() - } else { - suggestion.clone() - } - } else { - suggestion.clone() - }; - - let new_query = format!("{}{}{}", before_partial, suggestion_to_use, after_cursor); - - // Update input and cursor position - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let cursor_pos = if suggestion_to_use.ends_with("('')") { - // Position cursor between the quotes - before_partial.len() + suggestion_to_use.len() - 2 - } else { - before_partial.len() + suggestion_to_use.len() - }; - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to correct position - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(cursor_pos); - // Sync for rendering - if self.get_edit_mode() == EditMode::SingleLine { - self.input = tui_input::Input::new(new_query.clone()).with_cursor(cursor_pos); - } - } - - // Update completion state for next tab press - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = cursor_pos; - - let suggestion_info = if self.completion_state.suggestions.len() > 1 { - format!( - "Completed: {} ({}/{} - Tab for next)", - suggestion, - self.completion_state.current_index + 1, - self.completion_state.suggestions.len() - ) - } else { - format!("Completed: {}", suggestion) - }; - self.set_status_message(suggestion_info); - } else { - // Just insert the suggestion at cursor position - let before_cursor = &query[..cursor_pos]; - let after_cursor = &query[cursor_pos..]; - let new_query = format!("{}{}{}", before_cursor, suggestion, after_cursor); - - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let cursor_pos_new = if suggestion.ends_with("('')") { - // Position cursor between the quotes - cursor_pos + suggestion.len() - 2 - } else { - cursor_pos + suggestion.len() - }; - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to correct position - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(cursor_pos_new); - // Sync for rendering - if self.get_edit_mode() == EditMode::SingleLine { - self.input = - tui_input::Input::new(new_query.clone()).with_cursor(cursor_pos_new); - } - } - - // Update completion state - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = cursor_pos_new; - - self.set_status_message(format!("Inserted: {}", suggestion)); - } - } - - fn apply_completion_multiline(&mut self) { - let (cursor_row, cursor_col) = self.textarea.cursor(); - let text = self.get_input_text(); - let lines: Vec = text.lines().map(|s| s.to_string()).collect(); - let query = lines.join("\n"); - - // Calculate cursor position in the full query string - let mut cursor_pos = 0; - for (i, line) in lines.iter().enumerate() { - if i < cursor_row { - cursor_pos += line.len() + 1; // +1 for newline - } else if i == cursor_row { - cursor_pos += cursor_col; - break; - } - } - - // Check if this is a continuation of the same completion session - let is_same_context = query == self.completion_state.last_query - && cursor_pos == self.completion_state.last_cursor_pos; - - if !is_same_context { - // New completion context - get fresh suggestions - let hybrid_result = self.hybrid_parser.get_completions(&query, cursor_pos); - if hybrid_result.suggestions.is_empty() { - self.set_status_message("No completions available".to_string()); - return; - } - - self.completion_state.suggestions = hybrid_result.suggestions; - self.completion_state.current_index = 0; - } else if !self.completion_state.suggestions.is_empty() { - // Cycle to next suggestion - self.completion_state.current_index = - (self.completion_state.current_index + 1) % self.completion_state.suggestions.len(); - } else { - self.set_status_message("No completions available".to_string()); - return; - } - - // Apply the current suggestion (clone to avoid borrow issues) - let suggestion = - self.completion_state.suggestions[self.completion_state.current_index].clone(); - let partial_word = self.extract_partial_word_at_cursor(&query, cursor_pos); - - if let Some(partial) = partial_word { - // Replace the partial word with the suggestion - let current_line = lines[cursor_row].clone(); - let line_before = ¤t_line[..cursor_col.saturating_sub(partial.len())]; - let line_after = ¤t_line[cursor_col..]; - - // Handle quoted identifiers - if both partial and suggestion start with quotes, - // we need to avoid double quotes - let suggestion_to_use = if partial.starts_with('"') && suggestion.starts_with('"') { - // The partial already includes the opening quote, so use suggestion without its quote - if suggestion.len() > 1 { - suggestion[1..].to_string() - } else { - suggestion.clone() - } - } else { - suggestion.clone() - }; - - let new_line = format!("{}{}{}", line_before, suggestion_to_use, line_after); - - // Update the line in textarea - self.textarea.delete_line_by_head(); - self.textarea.insert_str(&new_line); - - // Move cursor to after the completion - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let new_col = if suggestion_to_use.ends_with("('')") { - line_before.len() + suggestion_to_use.len() - 2 - } else { - line_before.len() + suggestion_to_use.len() - }; - for _ in 0..new_col { - self.textarea.move_cursor(CursorMove::Forward); - } - - // Update completion state - let new_query = self.get_input_text(); - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = - cursor_pos - partial.len() + suggestion_to_use.len(); - - let suggestion_info = if self.completion_state.suggestions.len() > 1 { - format!( - "Completed: {} ({}/{} - Tab for next)", - suggestion, - self.completion_state.current_index + 1, - self.completion_state.suggestions.len() - ) - } else { - format!("Completed: {}", suggestion) - }; - self.set_status_message(suggestion_info); - } else { - // Just insert the suggestion at cursor position - let ends_with_parens = suggestion.ends_with("('')"); - let suggestion_len = suggestion.len(); - let suggestion_str = suggestion.clone(); - self.textarea.insert_str(suggestion); - - // Special case: if we inserted a string method like Contains(''), move cursor back inside quotes - if ends_with_parens { - self.textarea.move_cursor(CursorMove::Back); - self.textarea.move_cursor(CursorMove::Back); - } - - // Update completion state - let new_query = self.get_input_text(); - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = if ends_with_parens { - cursor_pos + suggestion_len - 2 - } else { - cursor_pos + suggestion_len - }; - - self.set_status_message(format!("Inserted: {}", suggestion_str)); - } - } - - fn expand_asterisk(&mut self) { - // Expand SELECT * to all column names - let query = if self.edit_mode == EditMode::SingleLine { - self.get_input_text() - } else { - self.get_input_text().lines().collect::>().join(" ") - }; - - // Simple regex-like pattern to find SELECT * FROM table_name - let query_upper = query.to_uppercase(); - - // Find SELECT * pattern - if let Some(select_pos) = query_upper.find("SELECT") { - if let Some(star_pos) = query_upper[select_pos..].find("*") { - let star_abs_pos = select_pos + star_pos; - - // Find FROM clause after the * - if let Some(from_rel_pos) = query_upper[star_abs_pos..].find("FROM") { - let from_abs_pos = star_abs_pos + from_rel_pos; - - // Extract table name after FROM - let after_from = &query[from_abs_pos + 4..].trim_start(); - let table_name = after_from - .split_whitespace() - .next() - .unwrap_or("") - .trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_'); - - if !table_name.is_empty() { - // Get columns from the schema - let columns = self.get_table_columns(table_name); - - if !columns.is_empty() { - // Build the replacement with all columns - let columns_str = columns.join(", "); - - // Replace * with the column list - let before_star = &query[..star_abs_pos]; - let after_star = &query[star_abs_pos + 1..]; - let new_query = format!("{}{}{}", before_star, columns_str, after_star); - - // Update the input - if self.edit_mode == EditMode::SingleLine { - self.input = tui_input::Input::new(new_query.clone()) - .with_cursor(new_query.len()); - self.update_horizontal_scroll(120); - } else { - // For multiline, format nicely - let formatted_lines = - crate::recursive_parser::format_sql_pretty_compact( - &new_query, 5, - ); - self.textarea = TextArea::from(formatted_lines); - self.textarea.set_cursor_line_style( - Style::default().add_modifier(Modifier::UNDERLINED), - ); - } - - self.set_status_message(format!( - "Expanded * to {} columns", - columns.len() - )); - } else { - self.set_status_message(format!( - "No columns found for table '{}'", - table_name - )); - } - } else { - self.set_status_message("Could not determine table name".to_string()); - } - } else { - self.set_status_message("No FROM clause found after SELECT *".to_string()); - } - } else { - self.set_status_message("No * found in SELECT clause".to_string()); - } - } else { - self.set_status_message("No SELECT clause found".to_string()); - } - } - - fn get_table_columns(&self, table_name: &str) -> Vec { - // Try to get columns from the hybrid parser's schema - // This will include CSV/JSON loaded tables - self.hybrid_parser.get_table_columns(table_name) - } - - fn handle_completion_multiline(&mut self) { - // Similar to handle_completion but for multiline mode - let (cursor_row, cursor_col) = self.textarea.cursor(); - let text = self.get_input_text(); - let lines: Vec = text.lines().map(|s| s.to_string()).collect(); - let query = lines.join("\n"); - - // Calculate cursor position in the full query string - let mut cursor_pos = 0; - for (i, line) in lines.iter().enumerate() { - if i < cursor_row { - cursor_pos += line.len() + 1; // +1 for newline - } else if i == cursor_row { - cursor_pos += cursor_col; - break; - } - } - - // Update completions based on cursor position - let hybrid_result = self.hybrid_parser.get_completions(&query, cursor_pos); - self.completion_state.suggestions = hybrid_result.suggestions; - self.completion_state.current_index = 0; - self.completion_state.last_query = query; - self.completion_state.last_cursor_pos = cursor_pos; - } - - fn extract_partial_word_at_cursor(&self, query: &str, cursor_pos: usize) -> Option { - if cursor_pos == 0 || cursor_pos > query.len() { - return None; - } - - let chars: Vec = query.chars().collect(); - let mut start = cursor_pos; - let end = cursor_pos; - - // Check if we might be in a quoted identifier - let mut in_quote = false; - - // Find start of word (go backward) - while start > 0 { - let prev_char = chars[start - 1]; - if prev_char == '"' { - // Found a quote, include it and stop - start -= 1; - in_quote = true; - break; - } else if prev_char.is_alphanumeric() - || prev_char == '_' - || (prev_char == ' ' && in_quote) - { - start -= 1; - } else { - break; - } - } - - // If we found a quote but are in a quoted identifier, - // we need to continue backwards to include the identifier content - if in_quote && start > 0 { - // We've already moved past the quote, now get the content before it - // Actually, we want to include everything from the quote forward - // The logic above is correct - we stop at the quote - } - - // Convert back to byte positions - let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum(); - let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum(); - - if start_byte < end_byte { - Some(query[start_byte..end_byte].to_string()) - } else { - None - } - } - - // Helper to get estimated visible rows based on terminal size - fn get_visible_rows(&self) -> usize { - // Try to get terminal size, or use stored default - if let Ok((_, height)) = crossterm::terminal::size() { - let terminal_height = height as usize; - let available_height = terminal_height.saturating_sub(4); // Account for header, borders, etc. - let max_visible_rows = available_height.saturating_sub(1).max(10); // Reserve space for header - max_visible_rows - } else { - self.get_last_visible_rows() // Fallback to stored value - } - } - - // Navigation functions - fn next_row(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - // Update viewport size before navigation - self.update_viewport_size(); - - let current = self.get_table_state().selected().unwrap_or(0); - if current >= total_rows - 1 { - return; - } // Already at bottom - - let new_position = current + 1; - self.get_table_state_mut().select(Some(new_position)); - - // Update viewport based on lock mode - if self.is_viewport_lock() { - // In lock mode, keep cursor at fixed viewport position - if let Some(lock_row) = self.get_viewport_lock_row() { - // Adjust viewport so cursor stays at lock_row position - let mut offset = self.get_scroll_offset(); - offset.0 = new_position.saturating_sub(lock_row); - self.set_scroll_offset(offset); - } - } else { - // Normal scrolling behavior - let visible_rows = self.get_last_visible_rows(); - - // Check if cursor would be below the last visible row - let offset = self.get_scroll_offset(); - if new_position > offset.0 + visible_rows - 1 { - // Cursor moved below viewport - scroll down by one - self.set_scroll_offset((offset.0 + 1, offset.1)); - } - } - } - } - - fn previous_row(&mut self) { - let current = self.get_table_state().selected().unwrap_or(0); - if current == 0 { - return; - } // Already at top - - let new_position = current - 1; - self.get_table_state_mut().select(Some(new_position)); - - // Update viewport based on lock mode - if self.is_viewport_lock() { - // In lock mode, keep cursor at fixed viewport position - if let Some(lock_row) = self.get_viewport_lock_row() { - // Adjust viewport so cursor stays at lock_row position - let mut offset = self.get_scroll_offset(); - offset.0 = new_position.saturating_sub(lock_row); - self.set_scroll_offset(offset); - } - } else { - // Normal scrolling behavior - let mut offset = self.get_scroll_offset(); - if new_position < offset.0 { - // Cursor moved above viewport - scroll up - offset.0 = new_position; - self.set_scroll_offset(offset); - } - } - } - - fn move_column_left(&mut self) { - self.set_current_column(self.get_current_column().saturating_sub(1)); - let mut offset = self.get_scroll_offset(); - offset.1 = offset.1.saturating_sub(1); - self.set_scroll_offset(offset); - self.set_status_message(format!("Column {} selected", self.get_current_column() + 1)); - } - - fn move_column_right(&mut self) { - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let max_columns = obj.len(); - if self.get_current_column() + 1 < max_columns { - self.set_current_column(self.get_current_column() + 1); - let mut offset = self.get_scroll_offset(); - offset.1 += 1; - self.set_scroll_offset(offset); - self.set_status_message(format!( - "Column {} selected", - self.get_current_column() + 1 - )); - } - } - } - } - } - - fn goto_first_column(&mut self) { - self.set_current_column(0); - let mut offset = self.get_scroll_offset(); - offset.1 = 0; - self.set_scroll_offset(offset); - self.set_status_message("First column selected".to_string()); - } - - fn goto_last_column(&mut self) { - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let max_columns = obj.len(); - if max_columns > 0 { - self.set_current_column(max_columns - 1); - // Update horizontal scroll to show the last column - // This ensures the last column is visible in the viewport - let mut offset = self.get_scroll_offset(); - offset.1 = self.get_current_column().saturating_sub(5); // Keep some context - self.set_scroll_offset(offset); - self.set_status_message(format!( - "Last column selected ({})", - self.get_current_column() + 1 - )); - } - } - } - } - } - - fn goto_first_row(&mut self) { - self.get_table_state_mut().select(Some(0)); - let mut offset = self.get_scroll_offset(); - offset.0 = 0; // Reset viewport to top - self.set_scroll_offset(offset); - } - - fn toggle_column_pin(&mut self) { - // Pin or unpin the current column - let current_col = self.get_current_column(); - if self.contains_pinned_column(current_col) { - // Column is already pinned, unpin it - self.remove_pinned_column(current_col); - self.set_status_message(format!("Column {} unpinned", current_col + 1)); - } else { - // Pin the column (max 4 pinned columns) - if self.get_pinned_columns().len() < 4 { - self.add_pinned_column(current_col); - self.set_status_message(format!("Column {} pinned 📌", current_col + 1)); - } else { - self.set_status_message("Maximum 4 pinned columns allowed".to_string()); - } - } - } - - fn clear_all_pinned_columns(&mut self) { - self.clear_pinned_columns(); - self.set_status_message("All columns unpinned".to_string()); - } - - fn calculate_column_statistics(&mut self) { - // Get the current column name and data - if let Some(results) = &self.results { - if results.data.is_empty() { - return; - } - - // Get column names from first row - let headers: Vec = if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - obj.keys().map(|k| k.to_string()).collect() - } else { - return; - } - } else { - return; - }; - - if self.get_current_column() >= headers.len() { - return; - } - - let column_name = &headers[self.get_current_column()]; - - // Use filtered data if available, otherwise use original data - let data_to_analyze = if let Some(filtered) = self.get_filtered_data() { - // Convert filtered data back to JSON values for analysis - let mut json_data = Vec::new(); - for row in filtered { - if self.get_current_column() < row.len() { - json_data.push(row[self.get_current_column()].clone()); - } - } - json_data - } else { - // Extract column values from JSON data - results - .data - .iter() - .filter_map(|row| { - if let Some(obj) = row.as_object() { - obj.get(column_name).map(|v| match v { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => String::new(), - _ => v.to_string(), - }) - } else { - None - } - }) - .collect() - }; - - // Calculate statistics - let mut stats = ColumnStatistics { - column_name: column_name.clone(), - column_type: ColumnType::Mixed, - total_count: data_to_analyze.len(), - null_count: 0, - unique_count: 0, - frequency_map: None, - min: None, - max: None, - sum: None, - mean: None, - median: None, - }; - - // Analyze data type and calculate appropriate statistics - let mut numeric_values = Vec::new(); - let mut string_values = Vec::new(); - let mut frequency_map: BTreeMap = BTreeMap::new(); - - for value in &data_to_analyze { - if value.is_empty() { - stats.null_count += 1; - } else if let Ok(num) = value.parse::() { - numeric_values.push(num); - *frequency_map.entry(value.clone()).or_insert(0) += 1; - } else { - string_values.push(value.clone()); - *frequency_map.entry(value.clone()).or_insert(0) += 1; - } - } - - stats.unique_count = frequency_map.len(); - - // Determine column type - if numeric_values.len() > 0 && string_values.is_empty() { - stats.column_type = ColumnType::Numeric; - - // Calculate numeric statistics - if !numeric_values.is_empty() { - numeric_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - - stats.min = Some(numeric_values[0]); - stats.max = Some(numeric_values[numeric_values.len() - 1]); - stats.sum = Some(numeric_values.iter().sum()); - stats.mean = Some(stats.sum.unwrap() / numeric_values.len() as f64); - - // Calculate median - let mid = numeric_values.len() / 2; - stats.median = if numeric_values.len() % 2 == 0 { - Some((numeric_values[mid - 1] + numeric_values[mid]) / 2.0) - } else { - Some(numeric_values[mid]) - }; - } - - // Only keep frequency map for small number of unique values - if frequency_map.len() <= 20 { - stats.frequency_map = Some(frequency_map); - } - } else if string_values.len() > 0 && numeric_values.is_empty() { - stats.column_type = ColumnType::String; - stats.frequency_map = Some(frequency_map); - } else { - stats.column_type = ColumnType::Mixed; - stats.frequency_map = Some(frequency_map); - } - - self.column_stats = Some(stats); - self.set_mode(AppMode::ColumnStats); - } - } - - fn check_parser_error(&self, query: &str) -> Option { - // Quick check for common parser errors - let mut paren_depth = 0; - let mut in_string = false; - let mut escape_next = false; - - for ch in query.chars() { - if escape_next { - escape_next = false; - continue; - } - - match ch { - '\\' if in_string => escape_next = true, - '\'' => in_string = !in_string, - '(' if !in_string => paren_depth += 1, - ')' if !in_string => { - paren_depth -= 1; - if paren_depth < 0 { - return Some("Extra )".to_string()); - } - } - _ => {} - } - } - - if paren_depth > 0 { - return Some(format!("Missing {} )", paren_depth)); - } - - // Could add more checks here (unclosed strings, etc.) - if in_string { - return Some("Unclosed string".to_string()); - } - - None - } - - fn update_viewport_size(&mut self) { - // Update the stored viewport size based on current terminal size - if let Ok((_, height)) = crossterm::terminal::size() { - let terminal_height = height as usize; - // Match the actual layout calculation: - // - Input area: 3 rows (from input_height) - // - Status bar: 3 rows - // - Results area gets the rest - let input_height = 3; - let status_height = 3; - let results_area_height = terminal_height.saturating_sub(input_height + status_height); - - // Now match EXACTLY what the render function does: - // - 1 row for top border - // - 1 row for header - // - 1 row for bottom border - self.set_last_visible_rows(results_area_height.saturating_sub(3).max(10)); - } - } - - fn goto_last_row(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - let last_row = total_rows - 1; - self.get_table_state_mut().select(Some(last_row)); - // Position viewport to show the last row at the bottom - let visible_rows = self.get_last_visible_rows(); - let mut offset = self.get_scroll_offset(); - offset.0 = last_row.saturating_sub(visible_rows - 1); - self.set_scroll_offset(offset); - } - } - - fn page_down(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - let visible_rows = self.get_last_visible_rows(); - let current = self.get_table_state().selected().unwrap_or(0); - let new_position = (current + visible_rows).min(total_rows - 1); - - self.get_table_state_mut().select(Some(new_position)); - - // Scroll viewport down by a page - let mut offset = self.get_scroll_offset(); - offset.0 = (offset.0 + visible_rows).min(total_rows.saturating_sub(visible_rows)); - self.set_scroll_offset(offset); - } - } - - fn page_up(&mut self) { - let visible_rows = self.get_last_visible_rows(); - let current = self.get_table_state().selected().unwrap_or(0); - let new_position = current.saturating_sub(visible_rows); - - self.get_table_state_mut().select(Some(new_position)); - - // Scroll viewport up by a page - let mut offset = self.get_scroll_offset(); - offset.0 = offset.0.saturating_sub(visible_rows); - self.set_scroll_offset(offset); - } - - // Search and filter functions - fn perform_search(&mut self) { - if let Some(data) = self.get_current_data() { - self.clear_search_matches(); - - if let Ok(regex) = Regex::new(&self.get_search_pattern()) { - for (row_idx, row) in data.iter().enumerate() { - for (col_idx, cell) in row.iter().enumerate() { - if regex.is_match(cell) { - let mut matches = self.get_search_matches(); - matches.push((row_idx, col_idx)); - self.set_search_matches(matches); - } - } - } - - if !self.get_search_matches().is_empty() { - self.set_search_match_index(0); - let matches = self.get_search_matches(); - self.set_current_search_match(Some(matches[0])); - let (row, _) = matches[0]; - self.get_table_state_mut().select(Some(row)); - self.set_status_message(format!("Found {} matches", matches.len())); - } else { - self.set_status_message("No matches found".to_string()); - } - } else { - self.set_status_message("Invalid regex pattern".to_string()); - } - } - } - - fn next_search_match(&mut self) { - if !self.get_search_matches().is_empty() { - let matches = self.get_search_matches(); - let new_index = (self.get_search_match_index() + 1) % matches.len(); - self.set_search_match_index(new_index); - let (row, _) = matches[new_index]; - self.get_table_state_mut().select(Some(row)); - self.set_current_search_match(Some(matches[new_index])); - self.set_status_message(format!("Match {} of {}", new_index + 1, matches.len())); - } - } - - fn previous_search_match(&mut self) { - if !self.get_search_matches().is_empty() { - let matches = self.get_search_matches(); - let current_index = self.get_search_match_index(); - let new_index = if current_index == 0 { - matches.len() - 1 - } else { - current_index - 1 - }; - self.set_search_match_index(new_index); - let (row, _) = matches[new_index]; - self.get_table_state_mut().select(Some(row)); - self.set_current_search_match(Some(matches[new_index])); - self.set_status_message(format!("Match {} of {}", new_index + 1, matches.len())); - } - } - - fn apply_filter(&mut self) { - if self.get_filter_state().pattern.is_empty() { - self.set_filtered_data(None); - self.get_filter_state_mut().active = false; - self.set_status_message("Filter cleared".to_string()); - return; - } - - if let Some(results) = &self.results { - if let Ok(regex) = Regex::new(&self.get_filter_state().pattern) { - let mut filtered = Vec::new(); - - for item in &results.data { - let mut row = Vec::new(); - let mut matches = false; - - if let Some(obj) = item.as_object() { - for (_, value) in obj { - let cell_str = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "".to_string(), - _ => value.to_string(), - }; - - if regex.is_match(&cell_str) { - matches = true; - } - row.push(cell_str); - } - - if matches { - filtered.push(row); - } - } - } - - let filtered_count = filtered.len(); - self.set_filtered_data(Some(filtered)); - self.get_filter_state_mut().regex = Some(regex); - self.get_filter_state_mut().active = true; - - // Reset table state but preserve filtered data - *self.get_table_state_mut() = TableState::default(); - self.set_scroll_offset((0, 0)); - self.set_current_column(0); - - // Clear search state but keep filter state - self.search_state = SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }; - - self.set_status_message(format!("Filtered to {} rows", filtered_count)); - } else { - self.set_status_message("Invalid regex pattern".to_string()); - } - } - } - - fn apply_fuzzy_filter(&mut self) { - if self.fuzzy_filter_state.pattern.is_empty() { - self.fuzzy_filter_state.filtered_indices.clear(); - self.fuzzy_filter_state.active = false; - self.set_status_message("Fuzzy filter cleared".to_string()); - return; - } - - let pattern = self.fuzzy_filter_state.pattern.clone(); - let mut filtered_indices = Vec::new(); - - // Get the data to filter - either already filtered data or original results - let data_to_filter = if self.get_filter_state().active && self.has_filtered_data() { - // If regex filter is active, fuzzy filter on top of that - self.get_filtered_data() - } else if let Some(results) = &self.results { - // Otherwise filter original results - let mut rows = Vec::new(); - for item in &results.data { - let mut row = Vec::new(); - if let Some(obj) = item.as_object() { - for (_, value) in obj { - let cell_str = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "".to_string(), - _ => value.to_string(), - }; - row.push(cell_str); - } - rows.push(row); - } - } - self.set_filtered_data(Some(rows)); - self.get_filtered_data() - } else { - return; - }; - - if let Some(data) = data_to_filter { - for (index, row) in data.iter().enumerate() { - // Concatenate all columns into a single string for matching - let row_text = row.join(" "); - - // Check if pattern starts with ' for exact matching - let matches = if pattern.starts_with('\'') && pattern.len() > 1 { - // Exact substring matching (case-insensitive) - let exact_pattern = &pattern[1..]; - row_text - .to_lowercase() - .contains(&exact_pattern.to_lowercase()) - } else { - // Fuzzy matching - if let Some(score) = self - .fuzzy_filter_state - .matcher - .fuzzy_match(&row_text, &pattern) - { - score > 0 - } else { - false - } - }; - - if matches { - filtered_indices.push(index); - } - } - } - - let match_count = filtered_indices.len(); - self.fuzzy_filter_state.filtered_indices = filtered_indices; - self.fuzzy_filter_state.active = !self.fuzzy_filter_state.filtered_indices.is_empty(); - - if self.fuzzy_filter_state.active { - let filter_type = if pattern.starts_with('\'') { - "Exact" - } else { - "Fuzzy" - }; - self.set_status_message(format!( - "{} filter: {} matches for '{}' (highlighted in magenta)", - filter_type, match_count, pattern - )); - // Reset table state for new filtered view - *self.get_table_state_mut() = TableState::default(); - self.set_scroll_offset((0, 0)); - } else { - let filter_type = if pattern.starts_with('\'') { - "exact" - } else { - "fuzzy" - }; - self.set_status_message(format!("No {} matches for '{}'", filter_type, pattern)); - } - } - - fn update_column_search(&mut self) { - // Get column headers from the current results - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - // Find matching columns (case-insensitive) - let pattern = self.column_search_state.pattern.to_lowercase(); - let mut matching_columns = Vec::new(); - - for (index, header) in headers.iter().enumerate() { - if header.to_lowercase().contains(&pattern) { - matching_columns.push((index, header.to_string())); - } - } - - self.column_search_state.matching_columns = matching_columns; - self.column_search_state.current_match = 0; - - // Update status message - if self.column_search_state.pattern.is_empty() { - self.set_status_message("Enter column name to search".to_string()); - } else if self.column_search_state.matching_columns.is_empty() { - self.set_status_message(format!( - "No columns match '{}'", - self.column_search_state.pattern - )); - } else { - let (column_index, column_name) = - self.column_search_state.matching_columns[0].clone(); - self.set_current_column(column_index); - self.set_status_message(format!( - "Column 1 of {}: {} (Tab=next, Enter=select)", - self.column_search_state.matching_columns.len(), - column_name - )); - } - } else { - self.set_status_message("No column data available".to_string()); - } - } else { - self.set_status_message("No data available for column search".to_string()); - } - } else { - self.set_status_message("No results available for column search".to_string()); - } - } - - fn sort_by_column(&mut self, column_index: usize) { - let new_order = match &self.sort_state { - SortState { - column: Some(col), - order, - } if *col == column_index => match order { - SortOrder::Ascending => SortOrder::Descending, - SortOrder::Descending => SortOrder::None, - SortOrder::None => SortOrder::Ascending, - }, - _ => SortOrder::Ascending, - }; - - if new_order == SortOrder::None { - // Reset to original order - would need to store original data - self.sort_state = SortState { - column: None, - order: SortOrder::None, - }; - self.set_status_message("Sort cleared".to_string()); - return; - } - - // Sort using original JSON values for proper type-aware comparison - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - if column_index < headers.len() { - let column_name = headers[column_index]; - - // Create a vector of (original_json_row, row_index) pairs for sorting - let mut indexed_rows: Vec<(serde_json::Value, usize)> = results - .data - .iter() - .enumerate() - .map(|(i, row)| (row.clone(), i)) - .collect(); - - // Sort based on the original JSON values - indexed_rows.sort_by(|(row_a, _), (row_b, _)| { - let val_a = row_a.get(column_name); - let val_b = row_b.get(column_name); - - let cmp = match (val_a, val_b) { - ( - Some(serde_json::Value::Number(a)), - Some(serde_json::Value::Number(b)), - ) => { - // Numeric comparison - this handles integers and floats properly - let a_f64 = a.as_f64().unwrap_or(0.0); - let b_f64 = b.as_f64().unwrap_or(0.0); - a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal) - } - ( - Some(serde_json::Value::String(a)), - Some(serde_json::Value::String(b)), - ) => { - // String comparison - a.cmp(b) - } - ( - Some(serde_json::Value::Bool(a)), - Some(serde_json::Value::Bool(b)), - ) => { - // Boolean comparison (false < true) - a.cmp(b) - } - (Some(serde_json::Value::Null), Some(serde_json::Value::Null)) => { - Ordering::Equal - } - (Some(serde_json::Value::Null), Some(_)) => { - // NULL comes first - Ordering::Less - } - (Some(_), Some(serde_json::Value::Null)) => { - // NULL comes first - Ordering::Greater - } - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - // Mixed type comparison - fall back to string representation - (Some(a), Some(b)) => { - let a_str = match a { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - let b_str = match b { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - a_str.cmp(&b_str) - } - }; - - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - }); - - // Rebuild the QueryResponse with sorted data - let sorted_data: Vec = - indexed_rows.into_iter().map(|(row, _)| row).collect(); - - // Update both the results and clear filtered_data to force regeneration - let mut new_results = results.clone(); - new_results.data = sorted_data; - self.results = Some(new_results); - self.set_filtered_data(None); // Force regeneration of string data - } - } - } - } else if let Some(data) = self.get_current_data_mut() { - // Fallback to string-based sorting if no JSON data available - data.sort_by(|a, b| { - if column_index >= a.len() || column_index >= b.len() { - return Ordering::Equal; - } - - let cell_a = &a[column_index]; - let cell_b = &b[column_index]; - - // Try numeric comparison first - if let (Ok(num_a), Ok(num_b)) = (cell_a.parse::(), cell_b.parse::()) { - let cmp = num_a.partial_cmp(&num_b).unwrap_or(Ordering::Equal); - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - } else { - // String comparison - let cmp = cell_a.cmp(cell_b); - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - } - }); - } - - self.sort_state = SortState { - column: Some(column_index), - order: new_order, - }; - - // Reset table state but preserve current column position - let current_column = self.get_current_column(); - self.reset_table_state(); - self.set_current_column(current_column); - - self.set_status_message(format!( - "Sorted by column {} ({}) - type-aware", - column_index + 1, - match new_order { - SortOrder::Ascending => "ascending", - SortOrder::Descending => "descending", - SortOrder::None => "none", - } - )); - } - - fn get_current_data(&self) -> Option>> { - if let Some(filtered) = self.get_filtered_data() { - Some(filtered.clone()) - } else if let Some(results) = &self.results { - Some(self.convert_json_to_strings(results)) - } else { - None - } - } - - fn get_row_count(&self) -> usize { - // TODO: Fix row count when fuzzy filter is active - // Currently this returns the count from filtered_data (WHERE clause results) - // but doesn't account for fuzzy_filter_state.filtered_indices - // This causes incorrect row counts in the status line (e.g., showing 1/1513 instead of 1/257) - // This will be fixed when fuzzy_filter_state is migrated to the buffer system - // and we have a single source of truth for visible rows - if let Some(filtered) = self.get_filtered_data() { - filtered.len() - } else if let Some(results) = &self.results { - results.data.len() - } else { - 0 - } - } - - fn get_current_data_mut(&mut self) -> Option<&mut Vec>> { - if !self.has_filtered_data() && self.results.is_some() { - let results = self.results.as_ref().unwrap(); - self.set_filtered_data(Some(self.convert_json_to_strings(results))); - } - // TODO: Add get_filtered_data_mut() wrapper method to handle mutable access - self.filtered_data.as_mut() - } - - fn convert_json_to_strings(&self, results: &QueryResponse) -> Vec> { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - results - .data - .iter() - .map(|item| { - if let Some(obj) = item.as_object() { - headers - .iter() - .map(|&header| match obj.get(header) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "".to_string(), - Some(other) => other.to_string(), - None => "".to_string(), - }) - .collect() - } else { - vec![] - } - }) - .collect() - } else { - vec![] - } - } else { - vec![] - } - } - - fn reset_table_state(&mut self) { - *self.get_table_state_mut() = TableState::default(); - self.set_scroll_offset((0, 0)); - self.set_current_column(0); - self.set_last_results_row(None); // Reset saved position for new results - self.set_last_scroll_offset((0, 0)); // Reset saved scroll offset for new results - - // Clear filter state to prevent old filtered data from persisting - *self.get_filter_state_mut() = FilterState { - pattern: String::new(), - regex: None, - active: false, - }; - - // Clear search state - self.search_state = SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }; - - // Clear fuzzy filter state to prevent it from persisting across queries - self.fuzzy_filter_state = FuzzyFilterState { - pattern: String::new(), - active: false, - matcher: SkimMatcherV2::default(), - filtered_indices: Vec::new(), - }; - - // Clear filtered data - self.set_filtered_data(None); - } - - fn calculate_viewport_column_widths(&mut self, viewport_start: usize, viewport_end: usize) { - // Calculate column widths based only on visible rows in viewport - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let mut widths = Vec::with_capacity(headers.len()); - - // Use compact mode settings - let compact = self.get_compact_mode(); - let min_width = if compact { 4 } else { 6 }; - let max_width = if compact { 20 } else { 30 }; - let padding = if compact { 1 } else { 2 }; - - // Only check visible rows - let rows_to_check = - &results.data[viewport_start..viewport_end.min(results.data.len())]; - - for header in &headers { - // Start with header width - let mut max_col_width = header.len(); - - // Check only visible rows for this column - for row in rows_to_check { - if let Some(obj) = row.as_object() { - if let Some(value) = obj.get(*header) { - let display_value = if value.is_null() { - "NULL" - } else if let Some(s) = value.as_str() { - s - } else { - &value.to_string() - }; - max_col_width = max_col_width.max(display_value.len()); - } - } - } - - // Apply min/max constraints and padding - let width = (max_col_width + padding).clamp(min_width, max_width) as u16; - widths.push(width); - } - - self.set_column_widths(widths); - } - } - } - } - - fn calculate_optimal_column_widths(&mut self) { - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let mut widths = Vec::new(); - - // For large datasets, sample rows instead of checking all - const MAX_ROWS_TO_CHECK: usize = 100; - let total_rows = results.data.len(); - - // Determine which rows to sample - let rows_to_check: Vec = if total_rows <= MAX_ROWS_TO_CHECK { - // Check all rows for small datasets - (0..total_rows).collect() - } else { - // Sample evenly distributed rows for large datasets - let step = total_rows / MAX_ROWS_TO_CHECK; - (0..MAX_ROWS_TO_CHECK) - .map(|i| (i * step).min(total_rows - 1)) - .collect() - }; - - for header in &headers { - // Start with header width - let mut max_width = header.len(); - - // Check only sampled rows for this column - for &row_idx in &rows_to_check { - if let Some(row) = results.data.get(row_idx) { - if let Some(obj) = row.as_object() { - if let Some(value) = obj.get(*header) { - let display_len = match value { - serde_json::Value::String(s) => s.len(), - serde_json::Value::Number(n) => n.to_string().len(), - serde_json::Value::Bool(b) => b.to_string().len(), - serde_json::Value::Null => 4, // "null".len() - _ => value.to_string().len(), - }; - max_width = max_width.max(display_len); - } - } - } - } - - // Add some padding and set reasonable limits - let optimal_width = (max_width + 2).max(4).min(50); // 4-50 char range with 2 char padding - widths.push(optimal_width as u16); - } - - self.set_column_widths(widths); - } - } - } - } - - fn escape_csv_field(s: &str) -> String { - if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') { - format!("\"{}\"", s.replace('"', "\"\"")) - } else { - s.to_string() - } - } - - fn export_to_csv(&mut self) { - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - // Generate filename with timestamp - let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); - let filename = format!("query_results_{}.csv", timestamp); - - match File::create(&filename) { - Ok(mut file) => { - // Write headers - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let header_line = headers.join(","); - if let Err(e) = writeln!(file, "{}", header_line) { - self.set_status_message(format!("Failed to write headers: {}", e)); - return; - } - - // Write data rows - let mut row_count = 0; - for item in &results.data { - if let Some(obj) = item.as_object() { - let row: Vec = headers - .iter() - .map(|&header| match obj.get(header) { - Some(Value::String(s)) => Self::escape_csv_field(s), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => String::new(), - Some(other) => { - Self::escape_csv_field(&other.to_string()) - } - None => String::new(), - }) - .collect(); - - let row_line = row.join(","); - if let Err(e) = writeln!(file, "{}", row_line) { - self.set_status_message(format!( - "Failed to write row: {}", - e - )); - return; - } - row_count += 1; - } - } - - self.set_status_message(format!( - "Exported {} rows to {}", - row_count, filename - )); - } - Err(e) => { - self.set_status_message(format!("Failed to create file: {}", e)); - } - } - } else { - self.set_status_message("No data to export".to_string()); - } - } else { - self.set_status_message("No data to export".to_string()); - } - } else { - self.set_status_message("No results to export - run a query first".to_string()); - } - } - - fn yank_cell(&mut self) { - if let Some(results) = &self.results { - if let Some(selected_row) = self.get_table_state().selected() { - if let Some(row_data) = results.data.get(selected_row) { - if let Some(obj) = row_data.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - if self.get_current_column() < headers.len() { - let header = headers[self.get_current_column()]; - let value = match obj.get(header) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "NULL".to_string(), - Some(other) => other.to_string(), - None => String::new(), - }; - - // Copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&value) { - Ok(_) => { - // Store what was yanked - let col_name = header.to_string(); - let display_value = if value.len() > 20 { - format!("{}...", &value[..17]) - } else { - value.clone() - }; - self.last_yanked = Some((col_name, display_value)); - self.set_status_message(format!("Yanked cell: {}", value)); - } - Err(e) => { - self.set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!( - "Can't access clipboard: {}", - e - )); - } - } - } - } - } - } - } - } - - fn yank_row(&mut self) { - if let Some(results) = &self.results { - if let Some(selected_row) = self.get_table_state().selected() { - if let Some(row_data) = results.data.get(selected_row) { - // Convert row to tab-separated values - if let Some(obj) = row_data.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let values: Vec = headers - .iter() - .map(|&header| match obj.get(header) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "NULL".to_string(), - Some(other) => other.to_string(), - None => String::new(), - }) - .collect(); - - let row_text = values.join("\t"); - - // Copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&row_text) { - Ok(_) => { - self.last_yanked = Some(( - format!("Row {}", selected_row + 1), - format!("{} columns", values.len()), - )); - self.set_status_message(format!( - "Yanked row {} ({} columns)", - selected_row + 1, - values.len() - )); - } - Err(e) => { - self.set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - } - } - } - } - - fn yank_column(&mut self) { - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - if self.get_current_column() < headers.len() { - let header = headers[self.get_current_column()]; - - // Collect all values from this column - let column_values: Vec = results - .data - .iter() - .filter_map(|row| { - row.as_object().and_then(|obj| { - obj.get(header).map(|v| match v { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "NULL".to_string(), - other => other.to_string(), - }) - }) - }) - .collect(); - - let column_text = column_values.join("\n"); - - // Copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&column_text) { - Ok(_) => { - self.last_yanked = Some(( - format!("Column '{}'", header), - format!("{} rows", column_values.len()), - )); - self.set_status_message(format!( - "Yanked column '{}' ({} rows)", - header, - column_values.len() - )); - } - Err(e) => { - self.set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - } - } - } - } - - fn yank_all(&mut self) { - if let Some(results) = &self.results { - // Get the actual data to yank (filtered or all) - let data_to_export = if self.get_filter_state().active || self.fuzzy_filter_state.active - { - // Use filtered data - self.get_filtered_json_data() - } else { - // Use all data - results.data.clone() - }; - - if let Some(first_row) = data_to_export.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - // Create CSV format - let mut csv_text = headers.join(",") + "\n"; - - for row in &data_to_export { - if let Some(obj) = row.as_object() { - let values: Vec = headers - .iter() - .map(|&header| match obj.get(header) { - Some(Value::String(s)) => escape_csv_field(s), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => String::new(), - Some(other) => escape_csv_field(&other.to_string()), - None => String::new(), - }) - .collect(); - csv_text.push_str(&values.join(",")); - csv_text.push('\n'); - } - } - - // Copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&csv_text) { - Ok(_) => { - let filter_info = if self.get_filter_state().active - || self.fuzzy_filter_state.active - { - " (filtered)" - } else { - "" - }; - self.set_status_message(format!( - "Yanked all data{}: {} rows", - filter_info, - data_to_export.len() - )); - } - Err(e) => { - self.set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - } - } - } - - fn paste_from_clipboard(&mut self) { - // Paste from system clipboard into the current input field - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.get_text() { - Ok(text) => { - match self.get_mode() { - AppMode::Command => { - if self.edit_mode == EditMode::SingleLine { - // Get current cursor position - let cursor_pos = self.get_input_cursor(); - let current_value = self.get_input_text(); - - // Insert at cursor position - let mut new_value = String::new(); - new_value.push_str(¤t_value[..cursor_pos]); - new_value.push_str(&text); - new_value.push_str(¤t_value[cursor_pos..]); - - self.input = tui_input::Input::new(new_value) - .with_cursor(cursor_pos + text.len()); - - self.set_status_message(format!( - "Pasted {} characters", - text.len() - )); - } else { - // Multi-line mode - insert at cursor - self.textarea.insert_str(&text); - self.set_status_message(format!( - "Pasted {} characters", - text.len() - )); - } - } - AppMode::Filter - | AppMode::FuzzyFilter - | AppMode::Search - | AppMode::ColumnSearch => { - // For search/filter modes, append to current pattern - let cursor_pos = self.get_input_cursor(); - let current_value = self.get_input_text(); - - let mut new_value = String::new(); - new_value.push_str(¤t_value[..cursor_pos]); - new_value.push_str(&text); - new_value.push_str(¤t_value[cursor_pos..]); - - self.input = tui_input::Input::new(new_value) - .with_cursor(cursor_pos + text.len()); - - // Update the appropriate filter/search state - match self.get_mode() { - AppMode::Filter => { - self.get_filter_state_mut().pattern = - self.input.value().to_string(); - self.apply_filter(); - } - AppMode::FuzzyFilter => { - self.fuzzy_filter_state.pattern = - self.input.value().to_string(); - self.apply_fuzzy_filter(); - } - AppMode::Search => { - let search_text = self.get_input_text(); - self.set_search_pattern(search_text); - // TODO: self.search_in_results(); - } - AppMode::ColumnSearch => { - self.column_search_state.pattern = - self.input.value().to_string(); - // TODO: self.search_columns(); - } - _ => {} - } - } - _ => { - self.set_status_message("Paste not available in this mode".to_string()); - } - } - } - Err(e) => { - self.set_status_message(format!("Failed to paste: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - - fn export_to_json(&mut self) { - if let Some(results) = &self.results { - // Get the actual data to export (filtered or all) - let data_to_export = if self.get_filter_state().active || self.fuzzy_filter_state.active - { - self.get_filtered_json_data() - } else { - results.data.clone() - }; - - // Generate filename with timestamp - let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); - let filename = format!("query_results_{}.json", timestamp); - - match File::create(&filename) { - Ok(file) => match serde_json::to_writer_pretty(file, &data_to_export) { - Ok(_) => { - let filter_info = - if self.get_filter_state().active || self.fuzzy_filter_state.active { - " (filtered)" - } else { - "" - }; - self.set_status_message(format!( - "Exported{} {} rows to {}", - filter_info, - data_to_export.len(), - filename - )); - } - Err(e) => { - self.set_status_message(format!("Failed to write JSON: {}", e)); - } - }, - Err(e) => { - self.set_status_message(format!("Failed to create file: {}", e)); - } - } - } else { - self.set_status_message("No results to export - run a query first".to_string()); - } - } - - fn get_filtered_json_data(&self) -> Vec { - if let Some(results) = &self.results { - if self.fuzzy_filter_state.active - && !self.fuzzy_filter_state.filtered_indices.is_empty() - { - self.fuzzy_filter_state - .filtered_indices - .iter() - .filter_map(|&idx| results.data.get(idx).cloned()) - .collect() - } else if self.get_filter_state().active && self.has_filtered_data() { - // Convert filtered_data back to JSON values - // This is a bit inefficient but maintains consistency - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - self.get_filtered_data() - .unwrap() - .iter() - .map(|row| { - let mut json_obj = serde_json::Map::new(); - for (i, value) in row.iter().enumerate() { - if i < headers.len() { - json_obj.insert( - headers[i].to_string(), - Value::String(value.clone()), - ); - } - } - Value::Object(json_obj) - }) - .collect() - } else { - Vec::new() - } - } else { - Vec::new() - } - } else { - results.data.clone() - } - } else { - Vec::new() - } - } - - fn get_horizontal_scroll_offset(&self) -> u16 { - self.input_scroll_offset - } - - fn update_horizontal_scroll(&mut self, terminal_width: u16) { - let inner_width = terminal_width.saturating_sub(3) as usize; // Account for borders + 1 char padding - let cursor_pos = self.get_input_cursor(); - - // If cursor is before the scroll window, scroll left - if cursor_pos < self.input_scroll_offset as usize { - self.input_scroll_offset = cursor_pos as u16; - } - // If cursor is after the scroll window, scroll right - else if cursor_pos >= self.input_scroll_offset as usize + inner_width { - self.input_scroll_offset = (cursor_pos + 1).saturating_sub(inner_width) as u16; - } - } - - fn get_cursor_token_position(&self) -> (usize, usize) { - let query_str = self.get_input_text(); - let query = query_str.as_str(); - let cursor_pos = self.get_input_cursor(); - - if query.is_empty() { - return (0, 0); - } - - // Use our lexer to tokenize the query - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - if tokens.is_empty() { - return (0, 0); - } - - // Find which token the cursor is in - let mut current_token = 0; - for (i, (start, end, _)) in tokens.iter().enumerate() { - if cursor_pos >= *start && cursor_pos <= *end { - current_token = i + 1; - break; - } else if cursor_pos < *start { - // Cursor is between tokens - current_token = i; - break; - } - } - - // If cursor is after all tokens - if current_token == 0 && cursor_pos > 0 { - current_token = tokens.len(); - } - - (current_token, tokens.len()) - } - - fn get_token_at_cursor(&self) -> Option { - let query_str = self.get_input_text(); - let query = query_str.as_str(); - let cursor_pos = self.get_input_cursor(); - - if query.is_empty() { - return None; - } - - // Use our lexer to tokenize the query - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find the token at cursor position - for (start, end, token) in &tokens { - if cursor_pos >= *start && cursor_pos <= *end { - // Format token nicely - use crate::recursive_parser::Token; - let token_str = match token { - Token::Select => "SELECT", - Token::From => "FROM", - Token::Where => "WHERE", - Token::GroupBy => "GROUP BY", - Token::OrderBy => "ORDER BY", - Token::Having => "HAVING", - Token::Asc => "ASC", - Token::Desc => "DESC", - Token::And => "AND", - Token::Or => "OR", - Token::In => "IN", - Token::DateTime => "DateTime", - Token::Identifier(s) => s, - Token::QuotedIdentifier(s) => s, - Token::StringLiteral(s) => s, - Token::NumberLiteral(s) => s, - Token::Star => "*", - Token::Comma => ",", - Token::Dot => ".", - Token::LeftParen => "(", - Token::RightParen => ")", - Token::Equal => "=", - Token::GreaterThan => ">", - Token::LessThan => "<", - Token::GreaterThanOrEqual => ">=", - Token::LessThanOrEqual => "<=", - Token::NotEqual => "!=", - Token::Not => "NOT", - Token::Between => "BETWEEN", - Token::Like => "LIKE", - Token::Is => "IS", - Token::Null => "NULL", - Token::Limit => "LIMIT", - Token::Offset => "OFFSET", - Token::Eof => "EOF", - }; - return Some(token_str.to_string()); - } - } - - None - } - - fn move_cursor_word_backward(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if cursor_pos == 0 { - return; - } - - // Use our lexer to tokenize the query - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find the token boundary before the cursor - let mut target_pos = 0; - for (start, end, _) in tokens.iter().rev() { - if *end <= cursor_pos { - // If we're at the start of a token, go to the previous one - if *end == cursor_pos && start < &cursor_pos { - target_pos = *start; - } else { - // Otherwise go to the start of this token - for (s, e, _) in tokens.iter().rev() { - if *e <= cursor_pos && *s < cursor_pos { - target_pos = *s; - break; - } - } - } - break; - } - } - - // Move cursor to new position through buffer - let is_single_line = self.get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.input = tui_input::Input::new(text).with_cursor(target_pos); - } - } - - // Update status message - self.set_status_message(format!("Moved to position {} (word boundary)", target_pos)); - } - - fn delete_word_backward(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if cursor_pos == 0 { - return; - } - - // Save to undo stack before modifying - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - - // Find the start of the previous word - let chars: Vec = query.chars().collect(); - let mut word_start = cursor_pos; - - // Skip any whitespace before cursor - while word_start > 0 && chars[word_start - 1].is_whitespace() { - word_start -= 1; - } - - // Find the beginning of the word - while word_start > 0 - && !chars[word_start - 1].is_whitespace() - && !is_sql_delimiter(chars[word_start - 1]) - { - word_start -= 1; - } - - // If we only moved through whitespace, try to delete at least one word - if word_start == cursor_pos && word_start > 0 { - word_start -= 1; - while word_start > 0 - && !chars[word_start - 1].is_whitespace() - && !is_sql_delimiter(chars[word_start - 1]) - { - word_start -= 1; - } - } - - // Delete from word_start to cursor_pos - if word_start < cursor_pos { - let before = &query[..word_start]; - let after = &query[cursor_pos..]; - let new_query = format!("{}{}", before, after); - self.input = tui_input::Input::new(new_query).with_cursor(word_start); - } - } - - fn move_cursor_word_forward(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - let query_len = query.len(); - - if cursor_pos >= query_len { - return; - } - - // Use our lexer to tokenize the query - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find the next token boundary after the cursor - let mut target_pos = query_len; - for (start, end, _) in &tokens { - if *start > cursor_pos { - target_pos = *start; - break; - } else if *end > cursor_pos { - target_pos = *end; - break; - } - } - - // Move cursor to new position through buffer - let is_single_line = self.get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.input = tui_input::Input::new(text).with_cursor(target_pos); - } - } - - // Update status message - self.set_status_message(format!("Moved to position {} (word boundary)", target_pos)); - } - - fn delete_word_forward(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - let query_len = query.len(); - - if cursor_pos >= query_len { - return; - } - - // Save to undo stack before modifying - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - - // Find the end of the current/next word - let chars: Vec = query.chars().collect(); - let mut word_end = cursor_pos; - - // Skip any non-word characters first - while word_end < chars.len() && !chars[word_end].is_alphanumeric() && chars[word_end] != '_' - { - word_end += 1; - } - - // Then skip word characters - while word_end < chars.len() - && (chars[word_end].is_alphanumeric() || chars[word_end] == '_') - { - word_end += 1; - } - - // Delete from cursor to word end - if word_end > cursor_pos { - let before = query.chars().take(cursor_pos).collect::(); - let after = query.chars().skip(word_end).collect::(); - let new_query = format!("{}{}", before, after); - self.input = tui_input::Input::new(new_query).with_cursor(cursor_pos); - } - } - - fn kill_line(&mut self) { - match self.edit_mode { - EditMode::SingleLine => { - let query_str = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - let query_len = query_str.len(); - - // Debug info - self.set_status_message(format!( - "kill_line: cursor={}, len={}, text='{}'", - cursor_pos, query_len, query_str - )); - - if cursor_pos < query_len { - // Save to undo stack before modifying - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - - // Save to kill ring before deleting - self.set_kill_ring(query_str.chars().skip(cursor_pos).collect::()); - let new_query = query_str.chars().take(cursor_pos).collect::(); - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor back to original position - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(cursor_pos); - // Sync for rendering - if self.get_edit_mode() == EditMode::SingleLine { - self.input = tui_input::Input::new(new_query).with_cursor(cursor_pos); - } - } - - // Update status to show what was killed - self.set_status_message(format!( - "Killed '{}' (cursor was at {})", - self.get_kill_ring(), - cursor_pos - )); - } else { - self.set_status_message(format!( - "Nothing to kill - cursor at end (pos={}, len={})", - cursor_pos, query_len - )); - } - } - EditMode::MultiLine => { - // For multiline mode, kill from cursor to end of current line - let (row, col) = self.textarea.cursor(); - let text = self.get_input_text(); - let lines: Vec = text.lines().map(|s| s.to_string()).collect(); - if row < lines.len() { - let current_line = &lines[row]; - if col < current_line.len() { - // Collect text that will be killed - let killed_text = current_line.chars().skip(col).collect::(); - // Create new line with text up to cursor - let new_line = current_line.chars().take(col).collect::(); - - // Update the textarea - let mut new_lines: Vec = lines.iter().cloned().collect(); - new_lines[row] = new_line.clone(); - - // Save killed text to kill ring (after releasing the borrow) - self.set_kill_ring(killed_text); - self.textarea = TextArea::from(new_lines); - self.textarea.set_cursor_line_style( - Style::default().add_modifier(Modifier::UNDERLINED), - ); - self.textarea - .move_cursor(CursorMove::Jump(row as u16, col as u16)); - } - } - } - } - } - - fn kill_line_backward(&mut self) { - match self.edit_mode { - EditMode::SingleLine => { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if cursor_pos > 0 { - // Collect text that will be killed and the new query - let killed_text = query.chars().take(cursor_pos).collect::(); - let new_query = query.chars().skip(cursor_pos).collect::(); - - // Save to undo stack before modifying - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - - // Save to kill ring before deleting - self.set_kill_ring(killed_text); - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to beginning - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(0); - // Sync for rendering - if self.get_edit_mode() == EditMode::SingleLine { - self.input = tui_input::Input::new(new_query).with_cursor(0); - } - } - } - } - EditMode::MultiLine => { - // For multiline mode, kill from beginning of line to cursor - let (row, col) = self.textarea.cursor(); - let text = self.get_input_text(); - let lines: Vec = text.lines().map(|s| s.to_string()).collect(); - if row < lines.len() && col > 0 { - let current_line = &lines[row]; - // Collect text that will be killed - let killed_text = current_line.chars().take(col).collect::(); - // Create new line with text after cursor - let new_line = current_line.chars().skip(col).collect::(); - - // Update the textarea - let mut new_lines: Vec = lines.iter().cloned().collect(); - new_lines[row] = new_line; - - // Save killed text to kill ring (after releasing the borrow) - self.set_kill_ring(killed_text); - self.textarea = TextArea::from(new_lines); - self.textarea - .set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED)); - self.textarea.move_cursor(CursorMove::Jump(row as u16, 0)); - } - } - } - } - - fn undo(&mut self) { - // Use buffer's high-level undo operation - if let Some(buffer) = self.current_buffer_mut() { - if buffer.perform_undo() { - self.set_status_message("Undo performed".to_string()); - } else { - self.set_status_message("Nothing to undo".to_string()); - } - } - } - - fn redo(&mut self) { - // Use buffer's high-level redo operation - if let Some(buffer) = self.current_buffer_mut() { - if buffer.perform_redo() { - self.set_status_message("Redo performed".to_string()); - } else { - self.set_status_message("Nothing to redo".to_string()); - } - } - } - - // Buffer management methods - fn next_buffer(&mut self) { - let prev_index = self.buffer_manager.current_index(); - self.buffer_manager.next_buffer(); - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); - debug!(target: "buffer", "Switched from buffer {} to {} (total: {})", prev_index + 1, index + 1, total); - - // Sync results and state from the new buffer - if let Some(buffer) = self.buffer_manager.current() { - // Sync query text to input fields - let query_text = buffer.get_query(); - self.input = Input::new(query_text.clone()).with_cursor(query_text.len()); - self.textarea = { - let mut ta = TextArea::from( - query_text - .lines() - .map(|s| s.to_string()) - .collect::>(), - ); - ta.move_cursor(tui_textarea::CursorMove::End); - ta - }; - - // Sync edit mode - self.edit_mode = match buffer.get_edit_mode() { - sql_cli::buffer::EditMode::SingleLine => EditMode::SingleLine, - sql_cli::buffer::EditMode::MultiLine => EditMode::MultiLine, - }; - - // Sync results and data source state - self.results = buffer.get_results().cloned(); - self.csv_client = buffer.csv_client.clone(); - self.csv_mode = buffer.csv_mode; - self.csv_table_name = buffer.csv_table_name.clone(); - self.cache_mode = buffer.cache_mode; - self.cached_data = buffer.cached_data.clone(); - - let result_count = self.results.as_ref().map(|r| r.data.len()).unwrap_or(0); - info!(target: "buffer", "Loaded {} results from buffer {} ({}, csv_mode={}, cache_mode={}), query='{}'", - result_count, buffer.get_id(), buffer.get_name(), buffer.csv_mode, buffer.cache_mode, - buffer.get_query()); - - // Recalculate column widths and reset table state for new results - if self.results.is_some() { - self.calculate_optimal_column_widths(); - self.reset_table_state(); - } - } - - self.set_status_message(format!("Switched to buffer {}/{}", index + 1, total)); - } - - fn prev_buffer(&mut self) { - let prev_index = self.buffer_manager.current_index(); - self.buffer_manager.prev_buffer(); - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); - debug!(target: "buffer", "Switched from buffer {} to {} (total: {})", prev_index + 1, index + 1, total); - - // Sync results and state from the new buffer - if let Some(buffer) = self.buffer_manager.current() { - // Sync query text to input fields - let query_text = buffer.get_query(); - self.input = Input::new(query_text.clone()).with_cursor(query_text.len()); - self.textarea = { - let mut ta = TextArea::from( - query_text - .lines() - .map(|s| s.to_string()) - .collect::>(), - ); - ta.move_cursor(tui_textarea::CursorMove::End); - ta - }; - - // Sync edit mode - self.edit_mode = match buffer.get_edit_mode() { - sql_cli::buffer::EditMode::SingleLine => EditMode::SingleLine, - sql_cli::buffer::EditMode::MultiLine => EditMode::MultiLine, - }; - - // Sync results and data source state - self.results = buffer.get_results().cloned(); - self.csv_client = buffer.csv_client.clone(); - self.csv_mode = buffer.csv_mode; - self.csv_table_name = buffer.csv_table_name.clone(); - self.cache_mode = buffer.cache_mode; - self.cached_data = buffer.cached_data.clone(); - - let result_count = self.results.as_ref().map(|r| r.data.len()).unwrap_or(0); - info!(target: "buffer", "Loaded {} results from buffer {} ({}, csv_mode={}, cache_mode={}), query='{}'", - result_count, buffer.get_id(), buffer.get_name(), buffer.csv_mode, buffer.cache_mode, - buffer.get_query()); - - // Recalculate column widths and reset table state for new results - if self.results.is_some() { - self.calculate_optimal_column_widths(); - self.reset_table_state(); - } - } - - self.set_status_message(format!("Switched to buffer {}/{}", index + 1, total)); - } - - fn new_buffer(&mut self) { - let mut new_buffer = - sql_cli::buffer::Buffer::new(self.buffer_manager.all_buffers().len() + 1); - // Apply config settings to the new buffer - new_buffer.set_compact_mode(self.config.display.compact_mode); - new_buffer.set_case_insensitive(self.config.behavior.case_insensitive_default); - new_buffer.set_show_row_numbers(self.config.display.show_row_numbers); - - info!(target: "buffer", "Creating new buffer with config: compact_mode={}, case_insensitive={}, show_row_numbers={}", - self.config.display.compact_mode, - self.config.behavior.case_insensitive_default, - self.config.display.show_row_numbers); - - let index = self.buffer_manager.add_buffer(new_buffer); - self.set_status_message(format!("Created new buffer #{}", index + 1)); - } - - fn close_buffer(&mut self) -> bool { - if self.buffer_manager.close_current() { - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); - self.set_status_message(format!( - "Buffer closed. Now at buffer {}/{}", - index + 1, - total - )); - true - } else { - self.set_status_message("Cannot close the last buffer".to_string()); - false - } - } - - fn list_buffers(&self) -> Vec { - let current_index = self.buffer_manager.current_index(); - self.buffer_manager - .all_buffers() - .iter() - .enumerate() - .map(|(i, buffer)| { - let marker = if i == current_index { "*" } else { " " }; - let modified = if buffer.is_modified() { "+" } else { "" }; - - format!("{} [{}] {}{}", marker, i + 1, buffer.get_name(), modified) - }) - .collect() - } - - fn yank(&mut self) { - if !self.is_kill_ring_empty() { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - // Get kill ring content and calculate new query - let kill_ring_content = self.get_kill_ring(); - let before = query.chars().take(cursor_pos).collect::(); - let after = query.chars().skip(cursor_pos).collect::(); - let new_query = format!("{}{}{}", before, kill_ring_content, after); - let new_cursor = cursor_pos + kill_ring_content.len(); - - // Save to undo stack before modifying - if let Some(buffer) = self.current_buffer_mut() { - buffer.save_state_for_undo(); - } - - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to new position - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(new_cursor); - // Sync for rendering - if self.get_edit_mode() == EditMode::SingleLine { - self.input = tui_input::Input::new(new_query).with_cursor(new_cursor); - } - } - } - } - - fn jump_to_prev_token(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if cursor_pos == 0 { - return; - } - - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find current token position - let mut in_token = false; - let mut current_token_start = 0; - for (start, end, _) in &tokens { - if cursor_pos > *start && cursor_pos <= *end { - in_token = true; - current_token_start = *start; - break; - } - } - - // Find the previous token start - let mut target_pos = 0; - - if in_token && cursor_pos > current_token_start { - // If we're in the middle of a token, go to its start - target_pos = current_token_start; - } else { - // Otherwise, find the previous token - for (start, _, _) in tokens.iter().rev() { - if *start < cursor_pos { - target_pos = *start; - break; - } - } - } - - // Move cursor through buffer - if target_pos < cursor_pos { - let is_single_line = self.get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.input = tui_input::Input::new(text).with_cursor(target_pos); - } - } - } - } - - fn jump_to_next_token(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - let query_len = query.len(); - - if cursor_pos >= query_len { - return; - } - - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find the next token start after cursor - let mut target_pos = query_len; - let mut in_current_token = false; - - for (start, end, _) in &tokens { - if cursor_pos >= *start && cursor_pos < *end { - in_current_token = true; - } else if in_current_token && *start >= *end { - // Move to the start of the next token after the current one - target_pos = *start; - break; - } else if *start > cursor_pos { - target_pos = *start; - break; - } - } - - // Move cursor through buffer - let is_single_line = self.get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.current_buffer_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.input = tui_input::Input::new(text).with_cursor(target_pos); - } - } - } - - /* - fn handle_vim_mode(&mut self, key: KeyEvent) -> bool { - // Returns true if the key was handled by vim mode - match self.vim_state.mode { - VimMode::Normal => { - match key.code { - // Mode switching - KeyCode::Char('i') => { - self.vim_state.mode = VimMode::Insert; - self.update_vim_status(); - true - } - KeyCode::Char('I') => { - self.vim_state.mode = VimMode::Insert; - self.textarea.move_cursor(CursorMove::Head); - self.update_vim_status(); - true - } - KeyCode::Char('a') => { - self.vim_state.mode = VimMode::Insert; - self.textarea.move_cursor(CursorMove::Forward); - self.update_vim_status(); - true - } - KeyCode::Char('A') => { - self.vim_state.mode = VimMode::Insert; - self.textarea.move_cursor(CursorMove::End); - self.update_vim_status(); - true - } - KeyCode::Char('o') => { - self.vim_state.mode = VimMode::Insert; - self.textarea.move_cursor(CursorMove::End); - self.textarea.insert_newline(); - self.update_vim_status(); - true - } - KeyCode::Char('O') => { - self.vim_state.mode = VimMode::Insert; - self.textarea.move_cursor(CursorMove::Head); - self.textarea.insert_newline(); - self.textarea.move_cursor(CursorMove::Up); - self.update_vim_status(); - true - } - KeyCode::Char('v') => { - self.vim_state.mode = VimMode::Visual; - let cursor = self.textarea.cursor(); - self.vim_state.visual_start = Some(cursor); - self.update_vim_status(); - true - } - - // Movement - KeyCode::Char('h') | KeyCode::Left => { - self.textarea.move_cursor(CursorMove::Back); - true - } - KeyCode::Char('j') | KeyCode::Down => { - self.textarea.move_cursor(CursorMove::Down); - true - } - KeyCode::Char('k') | KeyCode::Up => { - self.textarea.move_cursor(CursorMove::Up); - true - } - KeyCode::Char('l') | KeyCode::Right => { - self.textarea.move_cursor(CursorMove::Forward); - true - } - KeyCode::Char('0') => { - self.textarea.move_cursor(CursorMove::Head); - true - } - KeyCode::Char('$') => { - self.textarea.move_cursor(CursorMove::End); - true - } - KeyCode::Char('w') => { - self.textarea.move_cursor(CursorMove::WordForward); - true - } - KeyCode::Char('b') => { - self.textarea.move_cursor(CursorMove::WordBack); - true - } - KeyCode::Char('e') => { - // Move to end of word - self.textarea.move_cursor(CursorMove::WordForward); - self.textarea.move_cursor(CursorMove::Back); - true - } - KeyCode::Char('g') => { - // gg - go to first line (need to handle double-g) - self.textarea.move_cursor(CursorMove::Top); - true - } - KeyCode::Char('G') => { - self.textarea.move_cursor(CursorMove::Bottom); - true - } - - // Editing - KeyCode::Char('x') => { - self.textarea.delete_char(); - true - } - KeyCode::Char('d') => { - if key.modifiers.contains(KeyModifiers::CONTROL) { - // Ctrl-d - half page down - for _ in 0..10 { - self.textarea.move_cursor(CursorMove::Down); - } - } else { - // dd - delete line - self.textarea.move_cursor(CursorMove::Head); - self.textarea.delete_line_by_end(); - self.textarea.delete_newline(); - } - true - } - KeyCode::Char('y') => { - // yy - yank line - let text = self.get_input_text(); - let lines: Vec<&str> = text.lines().collect(); - let cursor_line = self.textarea.cursor().0; - if cursor_line < lines.len() { - self.vim_state.yank_buffer = lines[cursor_line].to_string(); - self.set_status_message("Line yanked".to_string()); - } - true - } - KeyCode::Char('p') => { - // Paste after cursor - if !self.vim_state.yank_buffer.is_empty() { - self.textarea.move_cursor(CursorMove::End); - self.textarea.insert_newline(); - self.textarea.insert_str(&self.vim_state.yank_buffer); - } - true - } - KeyCode::Char('P') => { - // Paste before cursor - if !self.vim_state.yank_buffer.is_empty() { - self.textarea.move_cursor(CursorMove::Head); - self.textarea.insert_str(&self.vim_state.yank_buffer); - self.textarea.insert_newline(); - self.textarea.move_cursor(CursorMove::Up); - } - true - } - KeyCode::Char('u') => { - self.textarea.undo(); - true - } - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.textarea.redo(); - true - } - - _ => false - } - } - VimMode::Insert => { - match key.code { - KeyCode::Esc => { - self.vim_state.mode = VimMode::Normal; - self.update_vim_status(); - true - } - _ => false // Let textarea handle the input - } - } - VimMode::Visual => { - match key.code { - KeyCode::Esc => { - self.vim_state.mode = VimMode::Normal; - self.vim_state.visual_start = None; - self.update_vim_status(); - true - } - KeyCode::Char('y') => { - // Yank selected text - if let Some(start) = self.vim_state.visual_start { - let end = self.textarea.cursor(); - // Simple line-based yanking for now - let text = self.get_input_text(); - let lines: Vec = text.lines().map(|s| s.to_string()).collect(); - let start_row = start.0.min(end.0); - let end_row = start.0.max(end.0); - let yanked: Vec = lines[start_row..=end_row] - .iter() - .map(|s| s.to_string()) - .collect(); - self.vim_state.yank_buffer = yanked.join("\n"); - self.set_status_message(format!("{} lines yanked", yanked.len())); - } - self.vim_state.mode = VimMode::Normal; - self.vim_state.visual_start = None; - self.update_vim_status(); - true - } - // Movement in visual mode - KeyCode::Char('h') | KeyCode::Left => { - self.textarea.move_cursor(CursorMove::Back); - true - } - KeyCode::Char('j') | KeyCode::Down => { - self.textarea.move_cursor(CursorMove::Down); - true - } - KeyCode::Char('k') | KeyCode::Up => { - self.textarea.move_cursor(CursorMove::Up); - true - } - KeyCode::Char('l') | KeyCode::Right => { - self.textarea.move_cursor(CursorMove::Forward); - true - } - _ => false - } - } - } - } - - */ - - /* - fn update_vim_status(&mut self) { - let mode_str = match self.vim_state.mode { - VimMode::Normal => "NORMAL", - VimMode::Insert => "INSERT", - VimMode::Visual => "VISUAL", - }; - - // Update cursor style based on mode - match self.vim_state.mode { - VimMode::Normal => { - self.textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); - }, - VimMode::Insert => { - self.textarea.set_cursor_style(Style::default().add_modifier(Modifier::UNDERLINED)); - }, - VimMode::Visual => { - self.textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED).fg(Color::Yellow)); - }, - } - - // Get cursor position - let (row, col) = self.textarea.cursor(); - self.set_status_message(format!("-- {} -- L{}:C{} (F3 single-line)", mode_str, row + 1, col + 1)); - } - */ - - fn ui(&mut self, f: &mut Frame) { - // Dynamically adjust layout based on edit mode - let input_height = match self.edit_mode { - EditMode::SingleLine => 3, - EditMode::MultiLine => { - // Use 1/3 of terminal height or 10 lines, whichever is larger (max 20) - let dynamic_height = f.area().height / 3; - std::cmp::min(20, std::cmp::max(10, dynamic_height)) - } - }; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(input_height), // Command input area - Constraint::Min(0), // Results - Constraint::Length(3), // Status bar - ] - .as_ref(), - ) - .split(f.area()); - - // Update horizontal scroll based on actual terminal width - self.update_horizontal_scroll(chunks[0].width); - - // Command input area - let input_title = match self.get_mode() { - AppMode::Command => "SQL Query".to_string(), - AppMode::Results => "SQL Query (Results Mode - Press ↑ to edit)".to_string(), - AppMode::Search => "Search Pattern".to_string(), - AppMode::Filter => "Filter Pattern".to_string(), - AppMode::FuzzyFilter => "Fuzzy Filter".to_string(), - AppMode::ColumnSearch => "Column Search".to_string(), - AppMode::Help => "Help".to_string(), - AppMode::History => format!( - "History Search: '{}' (Esc to cancel)", - self.history_state.search_query - ), - AppMode::Debug => "Parser Debug (F5)".to_string(), - AppMode::PrettyQuery => "Pretty Query View (F6)".to_string(), - AppMode::CacheList => "Cache Management (F7)".to_string(), - AppMode::JumpToRow => format!("Jump to row: {}", self.jump_to_row_input), - AppMode::ColumnStats => "Column Statistics (S to close)".to_string(), - }; - - let input_block = Block::default().borders(Borders::ALL).title(input_title); - - // Always get input text through the buffer API for consistency - let input_text_string = self.get_input_text(); - let input_text = match self.get_mode() { - AppMode::History => &self.history_state.search_query, - _ => &input_text_string, - }; - - let input_paragraph = match self.get_mode() { - AppMode::Command => { - match self.edit_mode { - EditMode::SingleLine => { - // Use syntax highlighting for SQL command input with horizontal scrolling - let highlighted_line = - self.sql_highlighter.simple_sql_highlight(input_text); - Paragraph::new(Text::from(vec![highlighted_line])) - .block(input_block) - .scroll((0, self.get_horizontal_scroll_offset())) - } - EditMode::MultiLine => { - // For multiline mode, we'll render the textarea widget instead - // This is a placeholder - actual textarea rendering happens below - Paragraph::new("").block(input_block) - } - } - } - _ => { - // Plain text for other modes - Paragraph::new(input_text.as_str()) - .block(input_block) - .style(match self.get_mode() { - AppMode::Results => Style::default().fg(Color::DarkGray), - AppMode::Search => Style::default().fg(Color::Yellow), - AppMode::Filter => Style::default().fg(Color::Cyan), - AppMode::FuzzyFilter => Style::default().fg(Color::Magenta), - AppMode::ColumnSearch => Style::default().fg(Color::Green), - AppMode::Help => Style::default().fg(Color::DarkGray), - AppMode::History => Style::default().fg(Color::Magenta), - AppMode::Debug => Style::default().fg(Color::Yellow), - AppMode::PrettyQuery => Style::default().fg(Color::Green), - AppMode::CacheList => Style::default().fg(Color::Cyan), - AppMode::JumpToRow => Style::default().fg(Color::Magenta), - AppMode::ColumnStats => Style::default().fg(Color::Cyan), - _ => Style::default(), - }) - .scroll((0, self.get_horizontal_scroll_offset())) - } - }; - - // Determine the actual results area based on edit mode - let results_area = - if self.get_mode() == AppMode::Command && self.edit_mode == EditMode::MultiLine { - // In multi-line mode, render textarea in the input area - f.render_widget(&self.textarea, chunks[0]); - - // Use the full results area - no preview in multi-line mode anymore - chunks[1] - } else { - // Single-line mode - render the input - f.render_widget(input_paragraph, chunks[0]); - // Use the full results area - chunks[1] - }; - - // Set cursor position for input modes - match self.get_mode() { - AppMode::Command => { - match self.edit_mode { - EditMode::SingleLine => { - // Calculate cursor position with horizontal scrolling - let inner_width = chunks[0].width.saturating_sub(2) as usize; - let cursor_pos = self.get_visual_cursor().1; // Get column position for single-line - let scroll_offset = self.get_horizontal_scroll_offset() as usize; - - // Calculate visible cursor position - if cursor_pos >= scroll_offset && cursor_pos < scroll_offset + inner_width { - let visible_pos = cursor_pos - scroll_offset; - f.set_cursor_position(( - chunks[0].x + visible_pos as u16 + 1, - chunks[0].y + 1, - )); - } - } - EditMode::MultiLine => { - // Cursor is handled by the textarea widget - } - } - } - AppMode::Search => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::Filter => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::FuzzyFilter => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::ColumnSearch => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::JumpToRow => { - f.set_cursor_position(( - chunks[0].x + self.jump_to_row_input.len() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::History => { - f.set_cursor_position(( - chunks[0].x + self.history_state.search_query.len() as u16 + 1, - chunks[0].y + 1, - )); - } - _ => {} - } - - // Results area - render based on mode to reduce complexity - match (&self.get_mode(), self.show_help) { - (_, true) => self.render_help(f, results_area), - (AppMode::History, false) => self.render_history(f, results_area), - (AppMode::Debug, false) => self.render_debug(f, results_area), - (AppMode::PrettyQuery, false) => self.render_pretty_query(f, results_area), - (AppMode::CacheList, false) => self.render_cache_list(f, results_area), - (AppMode::ColumnStats, false) => self.render_column_stats(f, results_area), - (_, false) if self.results.is_some() => { - // We need to work around the borrow checker here - // Calculate widths needs mutable self, but we also need to pass results - if let Some(results) = &self.results { - // Extract viewport info first - let terminal_height = results_area.height as usize; - let max_visible_rows = terminal_height.saturating_sub(3).max(10); - let total_rows = if let Some(filtered) = self.get_filtered_data() { - filtered.len() - } else { - results.data.len() - }; - let row_viewport_start = - self.get_scroll_offset().0.min(total_rows.saturating_sub(1)); - let row_viewport_end = (row_viewport_start + max_visible_rows).min(total_rows); - - // Calculate column widths based on viewport - self.calculate_viewport_column_widths(row_viewport_start, row_viewport_end); - } - - // Now render the table - if let Some(results) = &self.results { - self.render_table_immutable(f, results_area, results); - } - } - _ => { - // Simple placeholder - reduced text to improve rendering speed - let placeholder = Paragraph::new("Enter SQL query and press Enter\n\nTip: Use Tab for completion, Ctrl+R for history") - .block(Block::default().borders(Borders::ALL).title("Results")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(placeholder, results_area); - } - } - - // Render mode-specific status line - self.render_status_line(f, chunks[2]); - } - - fn render_status_line(&self, f: &mut Frame, area: Rect) { - // Determine the mode color - let (status_style, mode_color) = match self.get_mode() { - AppMode::Command => (Style::default().fg(Color::Green), Color::Green), - AppMode::Results => (Style::default().fg(Color::Blue), Color::Blue), - AppMode::Search => (Style::default().fg(Color::Yellow), Color::Yellow), - AppMode::Filter => (Style::default().fg(Color::Cyan), Color::Cyan), - AppMode::FuzzyFilter => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::ColumnSearch => (Style::default().fg(Color::Green), Color::Green), - AppMode::Help => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::History => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::Debug => (Style::default().fg(Color::Yellow), Color::Yellow), - AppMode::PrettyQuery => (Style::default().fg(Color::Green), Color::Green), - AppMode::CacheList => (Style::default().fg(Color::Cyan), Color::Cyan), - AppMode::JumpToRow => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::ColumnStats => (Style::default().fg(Color::Cyan), Color::Cyan), - }; - - let mode_indicator = match self.get_mode() { - AppMode::Command => "CMD", - AppMode::Results => "NAV", - AppMode::Search => "SEARCH", - AppMode::Filter => "FILTER", - AppMode::FuzzyFilter => "FUZZY", - AppMode::ColumnSearch => "COL", - AppMode::Help => "HELP", - AppMode::History => "HISTORY", - AppMode::Debug => "DEBUG", - AppMode::PrettyQuery => "PRETTY", - AppMode::CacheList => "CACHE", - AppMode::JumpToRow => "JUMP", - AppMode::ColumnStats => "STATS", - }; - - let mut spans = Vec::new(); - - // Mode indicator with color - spans.push(Span::styled( - format!("[{}]", mode_indicator), - Style::default().fg(mode_color).add_modifier(Modifier::BOLD), - )); - - // Show buffer information - { - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); - - // Show buffer indicator if multiple buffers - if total > 1 { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}/{}]", index + 1, total), - Style::default().fg(Color::Yellow), - )); - } - - // Show current buffer name - if let Some(buffer) = self.buffer_manager.current() { - spans.push(Span::raw(" ")); - let name = buffer.get_name(); - let modified = if buffer.is_modified() { "*" } else { "" }; - spans.push(Span::styled( - format!("{}{}", name, modified), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } - } - - // Fallback to old buffer name if no buffer manager - if let Some(buffer_name) = &self.current_buffer_name { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - buffer_name.clone(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } else if self.is_csv_mode() && !self.get_csv_table_name().is_empty() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - self.get_csv_table_name(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } - - // Mode-specific information - match self.get_mode() { - AppMode::Command => { - // In command mode, show editing-related info - if !self.input.value().trim().is_empty() { - let (token_pos, total_tokens) = self.get_cursor_token_position(); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Token {}/{}", token_pos, total_tokens), - Style::default().fg(Color::DarkGray), - )); - - // Show current token if available - if let Some(token) = self.get_token_at_cursor() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}]", token), - Style::default().fg(Color::Cyan), - )); - } - - // Check for parser errors - if let Some(error_msg) = self.check_parser_error(self.input.value()) { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("{} {}", self.config.display.icons.warning, error_msg), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )); - } - } - } - AppMode::Results => { - // In results mode, show navigation and data info - let total_rows = self.get_row_count(); - if total_rows > 0 { - let selected = self.get_table_state().selected().unwrap_or(0) + 1; - spans.push(Span::raw(" | ")); - - // Show selection mode - let mode_text = match self.selection_mode { - SelectionMode::Cell => "CELL", - SelectionMode::Row => "ROW", - }; - spans.push(Span::styled( - format!("[{}]", mode_text), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("Row {}/{}", selected, total_rows), - Style::default().fg(Color::White), - )); - - // Column information - if let Some(results) = &self.results { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - if self.get_current_column() < headers.len() { - spans.push(Span::raw(" | Col: ")); - spans.push(Span::styled( - headers[self.get_current_column()], - Style::default().fg(Color::Cyan), - )); - - // Show pinned columns count if any - if !self.get_pinned_columns().is_empty() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("📌{}", self.get_pinned_columns().len()), - Style::default().fg(Color::Magenta), - )); - } - - // In cell mode, show the current cell value - if self.selection_mode == SelectionMode::Cell { - if let Some(selected_row) = - self.get_table_state().selected() - { - if let Some(row_data) = results.data.get(selected_row) { - if let Some(row_obj) = row_data.as_object() { - if let Some(value) = row_obj - .get(headers[self.get_current_column()]) - { - let cell_value = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "NULL".to_string(), - other => other.to_string(), - }; - - // Truncate if too long - let display_value = if cell_value.len() > 30 - { - format!("{}...", &cell_value[..27]) - } else { - cell_value - }; - - spans.push(Span::raw(" = ")); - spans.push(Span::styled( - display_value, - Style::default().fg(Color::Yellow), - )); - } - } - } - } - } - } - } - } - } - - // Filter indicators - if self.fuzzy_filter_state.active { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Fuzzy: {}", self.fuzzy_filter_state.pattern), - Style::default().fg(Color::Magenta), - )); - } else if self.get_filter_state().active { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Filter: {}", self.get_filter_state().pattern), - Style::default().fg(Color::Cyan), - )); - } - - // Show last yanked value - if let Some((col, val)) = &self.last_yanked { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - "Yanked: ", - Style::default().fg(Color::DarkGray), - )); - spans.push(Span::styled( - format!("{}={}", col, val), - Style::default().fg(Color::Green), - )); - } - } - } - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // Show the pattern being typed - always use input for consistency - let pattern = self.input.value(); - if !pattern.is_empty() { - spans.push(Span::raw(" | Pattern: ")); - spans.push(Span::styled(pattern, Style::default().fg(mode_color))); - } - } - _ => {} - } - - // Data source indicator (shown in all modes) - if let Some(source) = self.get_last_query_source() { - spans.push(Span::raw(" | ")); - let (icon, label, color) = match source.as_str() { - "cache" => ( - &self.config.display.icons.cache, - "CACHE".to_string(), - Color::Cyan, - ), - "file" | "FileDataSource" => ( - &self.config.display.icons.file, - "FILE".to_string(), - Color::Green, - ), - "SqlServerDataSource" => ( - &self.config.display.icons.database, - "SQL".to_string(), - Color::Blue, - ), - "PublicApiDataSource" => ( - &self.config.display.icons.api, - "API".to_string(), - Color::Yellow, - ), - _ => ( - &self.config.display.icons.api, - source.clone(), - Color::Magenta, - ), - }; - spans.push(Span::raw(format!("{} ", icon))); - spans.push(Span::styled(label, Style::default().fg(color))); - } else if self.is_csv_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::raw(&self.config.display.icons.file)); - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("CSV: {}", self.get_csv_table_name()), - Style::default().fg(Color::Green), - )); - } else if self.is_cache_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::raw(&self.config.display.icons.cache)); - spans.push(Span::raw(" ")); - spans.push(Span::styled("CACHE", Style::default().fg(Color::Cyan))); - } - - // Global indicators (shown when active) - let case_insensitive = self.get_case_insensitive(); - if case_insensitive { - spans.push(Span::raw(" | ")); - // Use to_string() to ensure we get the actual string value - let icon = self.config.display.icons.case_insensitive.clone(); - spans.push(Span::styled( - format!("{} CASE", icon), - Style::default().fg(Color::Cyan), - )); - } - - if self.get_compact_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled("COMPACT", Style::default().fg(Color::Green))); - } - - if self.is_viewport_lock() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - &self.config.display.icons.lock, - Style::default().fg(Color::Magenta), - )); - } - - // Help shortcuts (right side) - let help_text = match self.get_mode() { - AppMode::Command => "Enter:Run | Tab:Complete | ↓:Results | F1:Help", - AppMode::Results => match self.selection_mode { - SelectionMode::Cell => "v:Row mode | y:Yank cell | ↑:Edit | F1:Help", - SelectionMode::Row => "v:Cell mode | y:Yank | f:Filter | ↑:Edit | F1:Help", - }, - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - "Enter:Apply | Esc:Cancel" - } - AppMode::Help - | AppMode::Debug - | AppMode::PrettyQuery - | AppMode::CacheList - | AppMode::ColumnStats => "Esc:Close", - AppMode::History => "Enter:Select | Esc:Cancel", - AppMode::JumpToRow => "Enter:Jump | Esc:Cancel", - }; - - // Calculate available space for help text - let current_length: usize = spans.iter().map(|s| s.content.len()).sum(); - let available_width = area.width.saturating_sub(4) as usize; // Account for borders - let help_length = help_text.len(); - - if current_length + help_length + 3 < available_width { - // Add spacing to right-align help text - let padding = available_width - current_length - help_length - 3; - spans.push(Span::raw(" ".repeat(padding))); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - help_text, - Style::default().fg(Color::DarkGray), - )); - } - - let status_line = Line::from(spans); - let status = Paragraph::new(status_line) - .block(Block::default().borders(Borders::ALL)) - .style(status_style); - f.render_widget(status, area); - } - - fn render_table_immutable(&self, f: &mut Frame, area: Rect, results: &QueryResponse) { - if results.data.is_empty() { - let empty = Paragraph::new("No results found") - .block(Block::default().borders(Borders::ALL).title("Results")) - .style(Style::default().fg(Color::Yellow)); - f.render_widget(empty, area); - return; - } - - // Get headers from first row - let headers: Vec<&str> = if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - obj.keys().map(|k| k.as_str()).collect() - } else { - vec![] - } - } else { - vec![] - }; - - // Calculate visible columns for virtual scrolling based on actual widths - let terminal_width = area.width as usize; - let available_width = terminal_width.saturating_sub(4); // Account for borders and padding - - // Split columns into pinned and scrollable - let mut pinned_headers: Vec<(usize, &str)> = Vec::new(); - let mut scrollable_indices: Vec = Vec::new(); - - for (i, header) in headers.iter().enumerate() { - if self.contains_pinned_column(i) { - pinned_headers.push((i, header)); - } else { - scrollable_indices.push(i); - } - } - - // Calculate space used by pinned columns - let mut pinned_width = 0; - for &(idx, _) in &pinned_headers { - let column_widths = self.get_column_widths(); - if idx < column_widths.len() { - pinned_width += column_widths[idx] as usize; - } else { - pinned_width += 15; // Default width - } - } - - // Calculate how many scrollable columns can fit in remaining space - let remaining_width = available_width.saturating_sub(pinned_width); - let column_widths = self.get_column_widths(); - let max_visible_scrollable_cols = if !column_widths.is_empty() { - let mut width_used = 0; - let mut cols_that_fit = 0; - - for &idx in &scrollable_indices { - if idx >= headers.len() { - break; - } - let col_width = if idx < column_widths.len() { - column_widths[idx] as usize - } else { - 15 - }; - if width_used + col_width <= remaining_width { - width_used += col_width; - cols_that_fit += 1; - } else { - break; - } - } - cols_that_fit.max(1) - } else { - // Fallback to old method if no calculated widths - let avg_col_width = 15; - (remaining_width / avg_col_width).max(1) - }; - - // Calculate viewport for scrollable columns based on current_column - let current_in_scrollable = scrollable_indices - .iter() - .position(|&x| x == self.get_current_column()); - let viewport_start = if let Some(pos) = current_in_scrollable { - if pos < max_visible_scrollable_cols / 2 { - 0 - } else if pos + max_visible_scrollable_cols / 2 >= scrollable_indices.len() { - scrollable_indices - .len() - .saturating_sub(max_visible_scrollable_cols) - } else { - pos.saturating_sub(max_visible_scrollable_cols / 2) - } - } else { - // Current column is pinned, use scroll offset - self.get_scroll_offset().1.min( - scrollable_indices - .len() - .saturating_sub(max_visible_scrollable_cols), - ) - }; - let viewport_end = - (viewport_start + max_visible_scrollable_cols).min(scrollable_indices.len()); - - // Build final list of visible columns (pinned + scrollable viewport) - let mut visible_columns: Vec<(usize, &str)> = Vec::new(); - visible_columns.extend(pinned_headers.iter().copied()); - for i in viewport_start..viewport_end { - let idx = scrollable_indices[i]; - visible_columns.push((idx, headers[idx])); - } - - // Only work with visible headers - let visible_headers: Vec<&str> = visible_columns.iter().map(|(_, h)| *h).collect(); - - // Calculate viewport dimensions FIRST before processing any data - let terminal_height = area.height as usize; - let max_visible_rows = terminal_height.saturating_sub(3).max(10); - - let total_rows = if let Some(filtered) = self.get_filtered_data() { - if self.fuzzy_filter_state.active - && !self.fuzzy_filter_state.filtered_indices.is_empty() - { - self.fuzzy_filter_state.filtered_indices.len() - } else { - filtered.len() - } - } else { - results.data.len() - }; - - // Calculate row viewport - let row_viewport_start = self.get_scroll_offset().0.min(total_rows.saturating_sub(1)); - let row_viewport_end = (row_viewport_start + max_visible_rows).min(total_rows); - - // Prepare table data (only visible rows AND columns) - let data_to_display = if let Some(filtered) = self.get_filtered_data() { - // Check if fuzzy filter is active - if self.fuzzy_filter_state.active - && !self.fuzzy_filter_state.filtered_indices.is_empty() - { - // Apply fuzzy filter on top of existing filter - let mut fuzzy_filtered = Vec::new(); - for &idx in &self.fuzzy_filter_state.filtered_indices { - if idx < filtered.len() { - fuzzy_filtered.push(filtered[idx].clone()); - } - } - - // Recalculate viewport for fuzzy filtered data - let fuzzy_total = fuzzy_filtered.len(); - let fuzzy_start = self - .get_scroll_offset() - .0 - .min(fuzzy_total.saturating_sub(1)); - let fuzzy_end = (fuzzy_start + max_visible_rows).min(fuzzy_total); - - fuzzy_filtered[fuzzy_start..fuzzy_end] - .iter() - .map(|row| { - visible_columns - .iter() - .map(|(idx, _)| row[*idx].clone()) - .collect() - }) - .collect() - } else { - // Apply both row and column viewport to filtered data - filtered[row_viewport_start..row_viewport_end] - .iter() - .map(|row| { - visible_columns - .iter() - .map(|(idx, _)| row[*idx].clone()) - .collect() - }) - .collect() - } - } else { - // Convert JSON data to string matrix (only visible rows AND columns) - results.data[row_viewport_start..row_viewport_end] - .iter() - .map(|item| { - if let Some(obj) = item.as_object() { - visible_columns - .iter() - .map(|(_, header)| match obj.get(*header) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "".to_string(), - Some(other) => other.to_string(), - None => "".to_string(), - }) - .collect() - } else { - vec![] - } - }) - .collect::>>() - }; - - // Create header row with sort indicators and column selection - let mut header_cells: Vec = Vec::new(); - - // Add row number header if enabled - if self.get_show_row_numbers() { - header_cells.push( - Cell::from("#").style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - ); - } - - // Add data headers - header_cells.extend(visible_columns.iter().map(|(actual_col_index, header)| { - let sort_indicator = if let Some(col) = self.sort_state.column { - if col == *actual_col_index { - match self.sort_state.order { - SortOrder::Ascending => " ↑", - SortOrder::Descending => " ↓", - SortOrder::None => "", - } - } else { - "" - } - } else { - "" - }; - - let column_indicator = if *actual_col_index == self.get_current_column() { - " [*]" - } else { - "" - }; - - // Add pin indicator for pinned columns - let pin_indicator = if self.contains_pinned_column(*actual_col_index) { - "📌 " - } else { - "" - }; - - let header_text = format!( - "{}{}{}{}", - pin_indicator, header, sort_indicator, column_indicator - ); - let mut style = Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD); - - // Highlight the current column - if *actual_col_index == self.get_current_column() { - style = style.bg(Color::DarkGray); - } - - Cell::from(header_text).style(style) - })); - - let selected_row = self.get_table_state().selected().unwrap_or(0); - - // Create data rows (already filtered to visible rows and columns) - let rows: Vec = data_to_display - .iter() - .enumerate() - .map(|(visible_row_idx, row)| { - let actual_row_idx = row_viewport_start + visible_row_idx; - let mut cells: Vec = Vec::new(); - - // Add row number if enabled - if self.get_show_row_numbers() { - let row_num = actual_row_idx + 1; // 1-based numbering - cells.push( - Cell::from(row_num.to_string()).style(Style::default().fg(Color::Magenta)), - ); - } - - // Add data cells - cells.extend(row.iter().enumerate().map(|(visible_col_idx, cell)| { - let actual_col_idx = visible_columns[visible_col_idx].0; - let mut style = Style::default(); - - // Cell mode highlighting - highlight only the selected cell - let is_selected_row = actual_row_idx == selected_row; - let is_selected_cell = - is_selected_row && actual_col_idx == self.get_current_column(); - - if self.selection_mode == SelectionMode::Cell { - // In cell mode, only highlight the specific cell - if is_selected_cell { - // Use a highlighted foreground instead of changing background - // This works better with various terminal color schemes - style = style - .fg(Color::Yellow) // Bright, readable color - .add_modifier(Modifier::BOLD | Modifier::UNDERLINED); - } - } else { - // In row mode, highlight the current column for all rows - if actual_col_idx == self.get_current_column() { - style = style.bg(Color::DarkGray); - } - } - - // Highlight search matches (override column highlight) - if let Some((match_row, match_col)) = self.get_current_search_match() { - if actual_row_idx == match_row && actual_col_idx == match_col { - style = style.bg(Color::Yellow).fg(Color::Black); - } - } - - // Highlight filter matches - if self.get_filter_state().active { - if let Some(ref regex) = self.get_filter_state().regex { - if regex.is_match(cell) { - style = style.fg(Color::Cyan); - } - } - } - - // Highlight fuzzy/exact filter matches - if self.fuzzy_filter_state.active && !self.fuzzy_filter_state.pattern.is_empty() - { - let pattern = &self.fuzzy_filter_state.pattern; - let cell_matches = if pattern.starts_with('\'') && pattern.len() > 1 { - // Exact match highlighting - let exact_pattern = &pattern[1..]; - cell.to_lowercase().contains(&exact_pattern.to_lowercase()) - } else { - // Fuzzy match highlighting - check if this cell contributes to the fuzzy match - if let Some(score) = - self.fuzzy_filter_state.matcher.fuzzy_match(cell, &pattern) - { - score > 0 - } else { - false - } - }; - - if cell_matches { - style = style.fg(Color::Magenta).add_modifier(Modifier::BOLD); - } - } - - Cell::from(cell.as_str()).style(style) - })); - - Row::new(cells) - }) - .collect(); - - // Calculate column constraints using optimal widths (only for visible columns) - let mut constraints: Vec = Vec::new(); - - // Add constraint for row number column if enabled - if self.get_show_row_numbers() { - // Calculate width needed for row numbers (max row count digits + padding) - let max_row_num = total_rows; - let row_num_width = max_row_num.to_string().len() as u16 + 2; - constraints.push(Constraint::Length(row_num_width.min(8))); // Cap at 8 chars - } - - // Add data column constraints - let column_widths = self.get_column_widths(); - if !column_widths.is_empty() { - // Use calculated optimal widths for visible columns - constraints.extend(visible_columns.iter().map(|(col_idx, _)| { - if *col_idx < column_widths.len() { - Constraint::Length(column_widths[*col_idx]) - } else { - Constraint::Min(10) // Fallback - } - })); - } else { - // Fallback to minimum width if no calculated widths available - constraints.extend((0..visible_headers.len()).map(|_| Constraint::Min(10))); - } - - // Build the table with conditional row highlighting - let mut table = Table::new(rows, constraints) - .header(Row::new(header_cells).height(1)) - .block(Block::default() - .borders(Borders::ALL) - .title(format!("Results ({} rows) - {} pinned, {} visible of {} | Viewport rows {}-{} (selected: {}) | Use h/l to scroll", - total_rows, - self.get_pinned_columns().len(), - visible_columns.len(), - headers.len(), - row_viewport_start + 1, - row_viewport_end, - selected_row + 1))); - - // Only apply row highlighting in row mode - if self.selection_mode == SelectionMode::Row { - table = table - .row_highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol("► "); - } else { - // In cell mode, no row highlighting - cell highlighting is handled above - table = table.highlight_symbol(" "); - } - - let mut table_state = self.get_table_state().clone(); - // Adjust table state to use relative position within the viewport - if let Some(selected) = table_state.selected() { - let relative_position = selected.saturating_sub(row_viewport_start); - table_state.select(Some(relative_position)); - } - f.render_stateful_widget(table, area, &mut table_state); - } - - fn render_help(&self, f: &mut Frame, area: Rect) { - // Create two-column layout - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - // Left column content - let left_content = vec![ - Line::from("SQL CLI Help - Enhanced Features 🚀").style( - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Line::from(""), - Line::from("COMMAND MODE").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" Enter - Execute query"), - Line::from(" Tab - Auto-complete"), - Line::from(" Ctrl+R - Search history"), - Line::from(" Ctrl+X - Expand SELECT * to columns"), - Line::from(" F3 - Toggle multi-line"), - Line::from(""), - Line::from("NAVIGATION").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" Ctrl+A - Beginning of line"), - Line::from(" Ctrl+E - End of line"), - Line::from(" Ctrl+← - Move backward word"), - Line::from(" Ctrl+→ - Move forward word"), - Line::from(""), - Line::from("EDITING").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" Ctrl+W - Delete word backward"), - Line::from(" Alt+D - Delete word forward"), - Line::from(" F9 - Kill to end of line (Ctrl+K alternative)"), - Line::from(" F10 - Kill to beginning (Ctrl+U alternative)"), - Line::from(" Ctrl+Y - Yank (paste)"), - Line::from(" Ctrl+Z - Undo"), - Line::from(""), - Line::from("BUFFER MANAGEMENT").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" F11/Ctrl+PgUp - Previous buffer"), - Line::from(" F12/Ctrl+PgDn - Next buffer"), - Line::from(" Ctrl+6 - Quick switch"), - Line::from(" Alt+N - New buffer"), - Line::from(" Alt+W - Close buffer"), - Line::from(" Alt+B - List buffers"), - Line::from(""), - Line::from("VIEW MODES").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" F1/? - Toggle this help"), - Line::from(" F5 - Debug info"), - Line::from(" F6 - Pretty query view"), - Line::from(" F7 - Cache management"), - Line::from(" F8 - Case-insensitive"), - Line::from(" ↓ - Enter results mode"), - Line::from(" Ctrl+C/q - Exit"), - Line::from(""), - Line::from("CACHE COMMANDS").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" :cache save [id] - Save with ID"), - Line::from(" :cache load ID - Load by ID"), - Line::from(" :cache list - Show cached"), - Line::from(" :cache clear - Disable cache"), - Line::from(""), - Line::from("🌟 FEATURES").style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - Line::from(" • Column statistics (S key)"), - Line::from(" • Column pinning (p/P keys)"), - Line::from(" • Dynamic column sizing"), - Line::from(" • Compact mode (C key)"), - Line::from(" • Rainbow parentheses"), - Line::from(" • Auto-execute CSV/JSON"), - Line::from(" • Multi-source indicators"), - Line::from(" • LINQ-style null checking"), - Line::from(" • Named cache IDs"), - Line::from(" • Row numbers (N key)"), - Line::from(" • Jump to row (: key)"), - ]; - - // Right column content - let right_content = vec![ - Line::from("Use ↓/↑ or j/k to scroll help").style(Style::default().fg(Color::DarkGray)), - Line::from(""), - Line::from("RESULTS NAVIGATION").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" j/↓ - Next row"), - Line::from(" k/↑ - Previous row"), - Line::from(" h/← - Previous column"), - Line::from(" l/→ - Next column"), - Line::from(" g - First row"), - Line::from(" G - Last row"), - Line::from(" 0/^ - First column"), - Line::from(" $ - Last column"), - Line::from(" PgDn - Page down"), - Line::from(" PgUp - Page up"), - Line::from(""), - Line::from("RESULTS FEATURES").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" C - 🎯 Toggle compact"), - Line::from(" N - 🔢 Toggle row nums"), - Line::from(" : - 📍 Jump to row"), - Line::from(" Space - 🔒 Viewport lock"), - Line::from(" p - 📌 Pin/unpin column"), - Line::from(" P - Clear all pins"), - Line::from(" / - Search in results"), - Line::from(" \\ - Search column names"), - Line::from(" n/N - Next/prev match"), - Line::from(" Shift+F - Filter rows (regex)"), - Line::from(" f - Fuzzy filter rows"), - Line::from(" 'text - Exact match filter"), - Line::from(" (matches highlighted)"), - Line::from(" v - Toggle cell/row mode"), - Line::from(" s - Sort by column"), - Line::from(" S - 📊 Column statistics"), - Line::from(" 1-9 - Sort by column #"), - Line::from(" y - Yank (cell mode: yank cell)"), - Line::from(" yy - Yank current row (row mode)"), - Line::from(" yc - Yank current column"), - Line::from(" ya - Yank all data"), - Line::from(" Ctrl+E - Export to CSV"), - Line::from(" Ctrl+J - Export to JSON"), - Line::from(" ↑/Esc - Back to command"), - Line::from(" q - Quit"), - Line::from(""), - Line::from("SEARCH/FILTER").style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ), - Line::from(" Enter - Apply"), - Line::from(" Esc - Cancel"), - Line::from(""), - Line::from("💡 TIPS").style( - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - Line::from(" • Load CSV: sql-cli data.csv"), - Line::from(" • Press C for compact view"), - Line::from(" • Press N for row numbers"), - Line::from(" • Press : then 200 → row 200"), - Line::from(" • Space locks viewport"), - Line::from(" • Columns auto-adjust width"), - Line::from(" • Named: :cache save q1"), - Line::from(" • f + 'ubs = exact 'ubs' match"), - Line::from(" • \\ + name = find column by name"), - Line::from(""), - Line::from("📦 Cache 📁 File 🌐 API 🗄️ SQL"), - ]; - - // Calculate visible area for scrolling - let visible_height = area.height.saturating_sub(2) as usize; // Account for borders - let left_total_lines = left_content.len(); - let right_total_lines = right_content.len(); - let max_lines = left_total_lines.max(right_total_lines); - - // Apply scroll offset - let scroll_offset = self.help_scroll as usize; - - // Get visible portions with scrolling - let left_visible: Vec = left_content - .into_iter() - .skip(scroll_offset) - .take(visible_height) - .collect(); - - let right_visible: Vec = right_content - .into_iter() - .skip(scroll_offset) - .take(visible_height) - .collect(); - - // Create scroll indicator in title - let scroll_indicator = if max_lines > visible_height { - format!( - " (↓/↑ to scroll, {}/{})", - scroll_offset + 1, - max_lines.saturating_sub(visible_height) + 1 - ) - } else { - String::new() - }; - - // Render left column - let left_paragraph = Paragraph::new(Text::from(left_visible)) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("Help - Commands{}", scroll_indicator)), - ) - .style(Style::default()); - - // Render right column - let right_paragraph = Paragraph::new(Text::from(right_visible)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Help - Navigation & Features"), - ) - .style(Style::default()); - - f.render_widget(left_paragraph, chunks[0]); - f.render_widget(right_paragraph, chunks[1]); - } - - fn render_debug(&self, f: &mut Frame, area: Rect) { - let debug_lines: Vec = self - .debug_text - .lines() - .map(|line| Line::from(line.to_string())) - .collect(); - - let total_lines = debug_lines.len(); - let visible_height = area.height.saturating_sub(2) as usize; // Account for borders - - // Calculate visible range based on scroll - let start = self.debug_scroll as usize; - let end = (start + visible_height).min(total_lines); - - let visible_lines: Vec = if start < total_lines { - debug_lines[start..end].to_vec() - } else { - vec![] - }; - - let debug_text = Text::from(visible_lines); - - // Check if there's a parse error - let has_parse_error = self.debug_text.contains("❌ PARSE ERROR ❌"); - let (border_color, title_prefix) = if has_parse_error { - (Color::Red, "⚠️ Parser Debug Info [PARSE ERROR] ") - } else { - (Color::Yellow, "Parser Debug Info ") - }; - - let debug_paragraph = Paragraph::new(debug_text) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!( - "{}- Lines {}-{} of {} (↑↓ to scroll, Enter/Esc to close)", - title_prefix, - start + 1, - end, - total_lines - )) - .border_style(Style::default().fg(border_color)), - ) - .style(Style::default().fg(Color::White)) - .wrap(Wrap { trim: false }); - - f.render_widget(debug_paragraph, area); - } - - fn render_pretty_query(&self, f: &mut Frame, area: Rect) { - let pretty_lines: Vec = self - .debug_text - .lines() - .map(|line| Line::from(line.to_string())) - .collect(); - - let total_lines = pretty_lines.len(); - let visible_height = area.height.saturating_sub(2) as usize; // Account for borders - - // Calculate visible range based on scroll - let start = self.debug_scroll as usize; - let end = (start + visible_height).min(total_lines); - - let visible_lines: Vec = if start < total_lines { - pretty_lines[start..end].to_vec() - } else { - vec![] - }; - - let pretty_text = Text::from(visible_lines); - - let pretty_paragraph = Paragraph::new(pretty_text) - .block( - Block::default() - .borders(Borders::ALL) - .title("Pretty SQL Query (F6) - ↑↓ to scroll, Esc/q to close") - .border_style(Style::default().fg(Color::Green)), - ) - .style(Style::default().fg(Color::White)) - .wrap(Wrap { trim: false }); - - f.render_widget(pretty_paragraph, area); - } - - fn render_history(&self, f: &mut Frame, area: Rect) { - if self.history_state.matches.is_empty() { - let no_history = if self.history_state.search_query.is_empty() { - "No command history found.\nExecute some queries to build history." - } else { - "No matches found for your search.\nTry a different search term." - }; - - let placeholder = Paragraph::new(no_history) - .block( - Block::default() - .borders(Borders::ALL) - .title("Command History"), - ) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(placeholder, area); - return; - } - - // Split the area to show selected command details - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(50), // History list - 50% of space - Constraint::Percentage(50), // Selected command preview - 50% of space - ]) - .split(area); - - self.render_history_list(f, chunks[0]); - self.render_selected_command_preview(f, chunks[1]); - } - - fn render_history_list(&self, f: &mut Frame, area: Rect) { - // Create more compact history list - just show essential info - let history_items: Vec = self - .history_state - .matches - .iter() - .enumerate() - .map(|(i, history_match)| { - let entry = &history_match.entry; - let is_selected = i == self.history_state.selected_index; - - let success_indicator = if entry.success { "✓" } else { "✗" }; - let time_ago = { - let elapsed = chrono::Utc::now() - entry.timestamp; - if elapsed.num_days() > 0 { - format!("{}d", elapsed.num_days()) - } else if elapsed.num_hours() > 0 { - format!("{}h", elapsed.num_hours()) - } else if elapsed.num_minutes() > 0 { - format!("{}m", elapsed.num_minutes()) - } else { - "now".to_string() - } - }; - - // Use more space for the command, less for metadata - let terminal_width = area.width as usize; - let metadata_space = 15; // Reduced metadata: " ✓ 2x 1h" - let available_for_command = terminal_width.saturating_sub(metadata_space).max(50); - - let command_text = if entry.command.len() > available_for_command { - format!( - "{}…", - &entry.command[..available_for_command.saturating_sub(1)] - ) - } else { - entry.command.clone() - }; - - let line_text = format!( - "{} {} {} {}x {}", - if is_selected { "►" } else { " " }, - command_text, - success_indicator, - entry.execution_count, - time_ago - ); - - let mut style = Style::default(); - if is_selected { - style = style.bg(Color::DarkGray).add_modifier(Modifier::BOLD); - } - if !entry.success { - style = style.fg(Color::Red); - } - - // Highlight matching characters for fuzzy search - if !history_match.indices.is_empty() && is_selected { - style = style.fg(Color::Yellow); - } - - Line::from(line_text).style(style) - }) - .collect(); - - let history_paragraph = Paragraph::new(history_items) - .block(Block::default().borders(Borders::ALL).title(format!( - "History ({} matches) - j/k to navigate, Enter to select", - self.history_state.matches.len() - ))) - .wrap(ratatui::widgets::Wrap { trim: false }); - - f.render_widget(history_paragraph, area); - } - - fn render_selected_command_preview(&self, f: &mut Frame, area: Rect) { - if let Some(selected_match) = self - .history_state - .matches - .get(self.history_state.selected_index) - { - let entry = &selected_match.entry; - - // Pretty format the SQL command - adjust compactness based on available space - use crate::recursive_parser::format_sql_pretty_compact; - - // Calculate how many columns we can fit per line - let available_width = area.width.saturating_sub(6) as usize; // Account for indentation and borders - let avg_col_width = 15; // Assume average column name is ~15 chars - let cols_per_line = (available_width / avg_col_width).max(3).min(12); // Between 3-12 columns per line - - let mut pretty_lines = format_sql_pretty_compact(&entry.command, cols_per_line); - - // If too many lines for the area, use a more compact format - let max_lines = area.height.saturating_sub(2) as usize; // Account for borders - if pretty_lines.len() > max_lines && cols_per_line < 12 { - // Try with more columns per line - pretty_lines = format_sql_pretty_compact(&entry.command, 15); - } - - // Convert to Text with syntax highlighting - let mut highlighted_lines = Vec::new(); - for line in pretty_lines { - highlighted_lines.push(self.sql_highlighter.simple_sql_highlight(&line)); - } - - let preview_text = Text::from(highlighted_lines); - - let duration_text = entry - .duration_ms - .map(|d| format!("{}ms", d)) - .unwrap_or_else(|| "?ms".to_string()); - - let success_text = if entry.success { - "✓ Success" - } else { - "✗ Failed" - }; - - let preview = Paragraph::new(preview_text) - .block(Block::default().borders(Borders::ALL).title(format!( - "Pretty SQL Preview: {} | {} | Used {}x", - success_text, duration_text, entry.execution_count - ))) - .scroll((0, 0)); // Allow scrolling if needed - - f.render_widget(preview, area); - } else { - let empty_preview = Paragraph::new("No command selected") - .block(Block::default().borders(Borders::ALL).title("Preview")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(empty_preview, area); - } - } - - fn handle_cache_command(&mut self, command: &str) -> Result<()> { - let parts: Vec<&str> = command.split_whitespace().collect(); - if parts.len() < 2 { - self.set_status_message( - "Invalid cache command. Use :cache save or :cache load ".to_string(), - ); - return Ok(()); - } - - match parts[1] { - "save" => { - // Save last query results to cache with optional custom ID - if let Some(ref results) = self.results { - if let Some(ref mut cache) = self.query_cache { - // Check if a custom ID is provided - let (custom_id, query) = if parts.len() > 2 { - // Check if the first word after "save" could be an ID (alphanumeric) - let potential_id = parts[2]; - if potential_id - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-') - && !potential_id.starts_with("SELECT") - && !potential_id.starts_with("select") - { - // First word is likely an ID - let id = Some(potential_id.to_string()); - let query = if parts.len() > 3 { - parts[3..].join(" ") - } else if let Some(last_entry) = - self.command_history.get_last_entry() - { - last_entry.command.clone() - } else { - self.set_status_message("No query to cache".to_string()); - return Ok(()); - }; - (id, query) - } else { - // No ID provided, treat everything as the query - (None, parts[2..].join(" ")) - } - } else if let Some(last_entry) = self.command_history.get_last_entry() { - (None, last_entry.command.clone()) - } else { - self.set_status_message("No query to cache".to_string()); - return Ok(()); - }; - - match cache.save_query(&query, &results.data, custom_id) { - Ok(id) => { - self.set_status_message(format!( - "Query cached with ID: {} ({} rows)", - id, - results.data.len() - )); - } - Err(e) => { - self.set_status_message(format!("Failed to cache query: {}", e)); - } - } - } - } else { - self.set_status_message( - "No results to cache. Execute a query first.".to_string(), - ); - } - } - "load" => { - if parts.len() < 3 { - self.set_status_message("Usage: :cache load ".to_string()); - return Ok(()); - } - - if let Ok(id) = parts[2].parse::() { - if let Some(ref cache) = self.query_cache { - match cache.load_query(id) { - Ok((_query, data)) => { - self.set_cached_data(Some(data.clone())); - self.set_cache_mode(true); - self.set_status_message(format!( - "Loaded cache ID {} with {} rows. Cache mode enabled.", - id, - data.len() - )); - - // Update parser with cached data schema if available - if let Some(first_row) = data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = - obj.keys().map(|k| k.to_string()).collect(); - self.hybrid_parser.update_single_table( - "cached_data".to_string(), - columns, - ); - } - } - } - Err(e) => { - self.set_status_message(format!("Failed to load cache: {}", e)); - } - } - } - } else { - self.set_status_message("Invalid cache ID".to_string()); - } - } - "list" => { - self.set_mode(AppMode::CacheList); - } - "clear" => { - self.set_cache_mode(false); - self.set_cached_data(None); - self.set_status_message("Cache mode disabled".to_string()); - } - _ => { - self.set_status_message( - "Unknown cache command. Use save, load, list, or clear.".to_string(), - ); - } - } - - Ok(()) - } - - fn handle_cache_list_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => { - self.set_mode(AppMode::Command); - } - _ => {} - } - Ok(false) - } - - fn handle_column_stats_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('q') | KeyCode::Esc | KeyCode::Char('S') => { - self.column_stats = None; - self.set_mode(AppMode::Results); - } - _ => {} - } - Ok(false) - } - - fn handle_jump_to_row_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - self.set_mode(AppMode::Results); - self.jump_to_row_input.clear(); - self.set_status_message("Jump cancelled".to_string()); - } - KeyCode::Enter => { - if let Ok(row_num) = self.jump_to_row_input.parse::() { - if row_num > 0 { - let target_row = row_num - 1; // Convert to 0-based index - let max_row = self.get_current_data().map(|d| d.len()).unwrap_or(0); - - if target_row < max_row { - self.get_table_state_mut().select(Some(target_row)); - - // Adjust viewport to center the target row - let visible_rows = self.get_last_visible_rows(); - if visible_rows > 0 { - let mut offset = self.get_scroll_offset(); - offset.0 = target_row.saturating_sub(visible_rows / 2); - self.set_scroll_offset(offset); - } - - self.set_status_message(format!("Jumped to row {}", row_num)); - } else { - self.set_status_message(format!( - "Row {} out of range (max: {})", - row_num, max_row - )); - } - } - } - self.set_mode(AppMode::Results); - self.jump_to_row_input.clear(); - } - KeyCode::Backspace => { - self.jump_to_row_input.pop(); - } - KeyCode::Char(c) if c.is_ascii_digit() => { - self.jump_to_row_input.push(c); - } - _ => {} - } - Ok(false) - } - - fn render_cache_list(&self, f: &mut Frame, area: Rect) { - if let Some(ref cache) = self.query_cache { - let cached_queries = cache.list_cached_queries(); - - if cached_queries.is_empty() { - let empty = Paragraph::new("No cached queries found.\n\nUse :cache save after running a query to cache results.") - .block(Block::default().borders(Borders::ALL).title("Cached Queries (F7)")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(empty, area); - return; - } - - // Create table of cached queries - let header_cells = vec!["ID", "Query", "Rows", "Cached At"] - .into_iter() - .map(|h| { - Cell::from(h).style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - }) - .collect::>(); - - let rows: Vec = cached_queries - .iter() - .map(|query| { - let cells = vec![ - Cell::from(query.id.to_string()), - Cell::from(if query.query_text.len() > 50 { - format!("{}...", &query.query_text[..47]) - } else { - query.query_text.clone() - }), - Cell::from(query.row_count.to_string()), - Cell::from(query.timestamp.format("%Y-%m-%d %H:%M:%S").to_string()), - ]; - Row::new(cells) - }) - .collect(); - - let table = Table::new( - rows, - vec![ - Constraint::Length(6), - Constraint::Percentage(50), - Constraint::Length(8), - Constraint::Length(20), - ], - ) - .header(Row::new(header_cells)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Cached Queries (F7) - Use :cache load to load"), - ) - .row_highlight_style(Style::default().bg(Color::DarkGray)); - - f.render_widget(table, area); - } else { - let error = Paragraph::new("Cache not available") - .block(Block::default().borders(Borders::ALL).title("Cache Error")) - .style(Style::default().fg(Color::Red)); - f.render_widget(error, area); - } - } - - fn render_column_stats(&self, f: &mut Frame, area: Rect) { - if let Some(ref stats) = self.column_stats { - let mut lines = vec![ - Line::from(format!("Column Statistics: {}", stats.column_name)).style( - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Line::from(""), - Line::from(format!("Type: {:?}", stats.column_type)) - .style(Style::default().fg(Color::Yellow)), - Line::from(format!("Total Rows: {}", stats.total_count)), - Line::from(format!("Unique Values: {}", stats.unique_count)), - Line::from(format!("Null/Empty Count: {}", stats.null_count)), - Line::from(""), - ]; - - // Add numeric statistics if available - if matches!(stats.column_type, ColumnType::Numeric | ColumnType::Mixed) { - lines.push( - Line::from("Numeric Statistics:").style( - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - ); - if let Some(min) = stats.min { - lines.push(Line::from(format!(" Min: {:.2}", min))); - } - if let Some(max) = stats.max { - lines.push(Line::from(format!(" Max: {:.2}", max))); - } - if let Some(mean) = stats.mean { - lines.push(Line::from(format!(" Mean: {:.2}", mean))); - } - if let Some(median) = stats.median { - lines.push(Line::from(format!(" Median: {:.2}", median))); - } - if let Some(sum) = stats.sum { - lines.push(Line::from(format!(" Sum: {:.2}", sum))); - } - lines.push(Line::from("")); - } - - // Add frequency distribution if available - if let Some(ref freq_map) = stats.frequency_map { - lines.push( - Line::from("Frequency Distribution:").style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - ); - - // Sort by frequency (descending) and take top 20 - let mut freq_vec: Vec<_> = freq_map.iter().collect(); - freq_vec.sort_by(|a, b| b.1.cmp(a.1)); - - let max_count = freq_vec.first().map(|(_, c)| **c).unwrap_or(1); - - for (value, count) in freq_vec.iter().take(20) { - let bar_width = ((**count as f64 / max_count as f64) * 30.0) as usize; - let bar = "█".repeat(bar_width); - let display_value = if value.len() > 30 { - format!("{}...", &value[..27]) - } else { - value.to_string() - }; - lines.push(Line::from(format!( - " {:30} {} ({})", - display_value, bar, count - ))); - } - - if freq_vec.len() > 20 { - lines.push( - Line::from(format!( - " ... and {} more unique values", - freq_vec.len() - 20 - )) - .style(Style::default().fg(Color::DarkGray)), - ); - } - } - - lines.push(Line::from("")); - lines.push( - Line::from("Press S or Esc to return to results") - .style(Style::default().fg(Color::DarkGray)), - ); - - let stats_paragraph = Paragraph::new(Text::from(lines)) - .block(Block::default().borders(Borders::ALL).title(format!( - "Column Statistics - {} (S to close)", - stats.column_name - ))) - .wrap(Wrap { trim: false }); - - f.render_widget(stats_paragraph, area); - } else { - let error = Paragraph::new("No statistics available") - .block( - Block::default() - .borders(Borders::ALL) - .title("Column Statistics"), - ) - .style(Style::default().fg(Color::Red)); - f.render_widget(error, area); - } - } -} - -pub fn run_enhanced_tui_multi(api_url: &str, data_files: Vec<&str>) -> Result<()> { - let mut app = if !data_files.is_empty() { - // Load the first file using existing logic - let first_file = data_files[0]; - let extension = std::path::Path::new(first_file) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - - let mut app = match extension.to_lowercase().as_str() { - "csv" => EnhancedTuiApp::new_with_csv(first_file)?, - "json" => EnhancedTuiApp::new_with_json(first_file)?, - _ => { - return Err(anyhow::anyhow!( - "Unsupported file type: {}. Use .csv or .json files.", - first_file - )) - } - }; - - // Set the file path for the first buffer if we have multiple files - if data_files.len() > 1 { - if let Some(buffer) = app.current_buffer_mut() { - buffer.set_file_path(Some(first_file.to_string())); - let filename = std::path::Path::new(first_file) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - buffer.set_name(filename.to_string()); - } - } - - // Load additional files into separate buffers - if data_files.len() > 1 { - for (index, file_path) in data_files.iter().skip(1).enumerate() { - let extension = std::path::Path::new(file_path) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - - match extension.to_lowercase().as_str() { - "csv" | "json" => { - // Get config value before mutable borrow - let case_insensitive = app.config.behavior.case_insensitive_default; - - // Create a new buffer for each additional file - app.new_buffer(); - - // Get the current buffer and set it up - if let Some(buffer) = app.current_buffer_mut() { - // Create and configure CSV client for this buffer - let mut csv_client = CsvApiClient::new(); - csv_client.set_case_insensitive(case_insensitive); - - // Get table name from file - let raw_name = std::path::Path::new(file_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - let table_name = EnhancedTuiApp::sanitize_table_name(&raw_name); - - // Load the data - if extension.to_lowercase() == "csv" { - if let Err(e) = csv_client.load_csv(file_path, &table_name) { - app.set_status_message(format!( - "Error loading {}: {}", - file_path, e - )); - continue; - } - } else { - if let Err(e) = csv_client.load_json(file_path, &table_name) { - app.set_status_message(format!( - "Error loading {}: {}", - file_path, e - )); - continue; - } - } - - // Set the CSV client and metadata in the buffer - buffer.set_csv_client(Some(csv_client)); - buffer.set_csv_mode(true); - buffer.set_table_name(table_name.clone()); - - info!(target: "buffer", "Loaded {} file '{}' into buffer {}: table='{}', case_insensitive={}", - extension.to_uppercase(), file_path, buffer.get_id(), table_name, case_insensitive); - - // Set query - let query = format!("SELECT * FROM {}", table_name); - buffer.set_input_text(query); - - // Store the file path and name - buffer.set_file_path(Some(file_path.to_string())); - let filename = std::path::Path::new(file_path) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - buffer.set_name(filename.to_string()); - } - } - _ => { - app.set_status_message(format!("Skipping unsupported file: {}", file_path)); - continue; - } - } - } - - // Switch back to the first buffer - app.buffer_manager.switch_to(0); - - app.set_status_message(format!( - "Loaded {} files into separate buffers. Use Alt+Tab to switch.", - data_files.len() - )); - } - - app - } else { - EnhancedTuiApp::new(api_url) - }; - - app.run() -} - -pub fn run_enhanced_tui(api_url: &str, data_file: Option<&str>) -> Result<()> { - // For backward compatibility, convert single file to vec and call multi version - let files = if let Some(file) = data_file { - vec![file] - } else { - vec![] - }; - run_enhanced_tui_multi(api_url, files) -} diff --git a/sql-cli/src/enhanced_tui.rs.backup_v10 b/sql-cli/src/enhanced_tui.rs.backup_v10 deleted file mode 100644 index 25ce92b0..00000000 --- a/sql-cli/src/enhanced_tui.rs.backup_v10 +++ /dev/null @@ -1,6059 +0,0 @@ -use crate::parser::SqlParser; -use crate::sql_highlighter::SqlHighlighter; -use anyhow::Result; -use chrono::Local; -use crossterm::{ - event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, - }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Wrap}, - Frame, Terminal, -}; -use regex::Regex; -use serde_json::Value; -use sql_cli::api_client::{ApiClient, QueryResponse}; -use sql_cli::buffer::{ - AppMode, BufferAPI, BufferManager, ColumnStatistics, ColumnType, EditMode, SortOrder, SortState, -}; -use sql_cli::buffer_handler::BufferHandler; -use sql_cli::cache::QueryCache; -use sql_cli::config::Config; -use sql_cli::csv_datasource::CsvApiClient; -use sql_cli::cursor_manager::CursorManager; -use sql_cli::data_analyzer::DataAnalyzer; -use sql_cli::data_exporter::DataExporter; -use sql_cli::debug_info::{DebugInfo, DebugView}; -use sql_cli::debug_widget::DebugWidget; -use sql_cli::editor_widget::{BufferAction, EditorAction, EditorWidget}; -use sql_cli::help_text::HelpText; -use sql_cli::history::{CommandHistory, HistoryMatch}; -use sql_cli::hybrid_parser::HybridParser; -use sql_cli::key_chord_handler::{ChordResult, KeyChordHandler}; -use sql_cli::key_dispatcher::KeyDispatcher; -use sql_cli::logging::{get_log_buffer, LogRingBuffer}; -use sql_cli::stats_widget::{StatsAction, StatsWidget}; -use sql_cli::text_navigation::{TextEditor, TextNavigator}; -use sql_cli::where_ast::format_where_ast; -use sql_cli::where_parser::WhereParser; -use sql_cli::yank_manager::YankManager; -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::io; -use tracing::{debug, info, trace, warn}; -use tui_input::{backend::crossterm::EventHandler, Input}; - -// Using AppMode and EditMode from sql_cli::buffer module - -#[derive(Clone, PartialEq, Debug)] -enum SelectionMode { - Row, - Cell, -} - -// Using SortOrder and SortState from sql_cli::buffer module - -struct FuzzyFilterState { - pattern: String, - active: bool, - matcher: SkimMatcherV2, - filtered_indices: Vec, // Indices of rows that match -} - -impl Clone for FuzzyFilterState { - fn clone(&self) -> Self { - Self { - pattern: self.pattern.clone(), - active: self.active, - matcher: SkimMatcherV2::default(), // Create new matcher - filtered_indices: self.filtered_indices.clone(), - } - } -} - -#[derive(Clone)] -struct FilterState { - pattern: String, - regex: Option, - active: bool, -} - -#[derive(Clone)] -struct ColumnSearchState { - pattern: String, - matching_columns: Vec<(usize, String)>, // (index, column_name) - current_match: usize, // Index into matching_columns -} - -#[derive(Clone)] -struct SearchState { - pattern: String, - current_match: Option<(usize, usize)>, // (row, col) - matches: Vec<(usize, usize)>, - match_index: usize, -} - -#[derive(Clone)] -struct CompletionState { - suggestions: Vec, - current_index: usize, - last_query: String, - last_cursor_pos: usize, -} - -#[derive(Clone)] -struct HistoryState { - search_query: String, - matches: Vec, - selected_index: usize, -} - -pub struct EnhancedTuiApp { - api_client: ApiClient, - input: Input, - cursor_manager: CursorManager, // New: manages cursor/navigation logic - data_analyzer: DataAnalyzer, // New: manages data analysis/statistics - // results: Option, // MIGRATED to buffer system - table_state: TableState, - show_help: bool, - sql_parser: SqlParser, - hybrid_parser: HybridParser, - - // Configuration - config: Config, - - // Enhanced features - sort_state: SortState, - filter_state: FilterState, - search_state: SearchState, - completion_state: CompletionState, - history_state: HistoryState, - command_history: CommandHistory, - scroll_offset: (usize, usize), // (row, col) - current_column: usize, // For column-based operations - sql_highlighter: SqlHighlighter, - debug_widget: DebugWidget, - editor_widget: EditorWidget, - stats_widget: StatsWidget, - key_chord_handler: KeyChordHandler, // Manages key sequences and history - key_dispatcher: KeyDispatcher, // Maps keys to actions - help_scroll: u16, // Scroll offset for help page - input_scroll_offset: u16, // Horizontal scroll offset for input - - // Selection and clipboard - selection_mode: SelectionMode, // Row or Cell mode - last_yanked: Option<(String, String)>, // (description, value) of last yanked item - - // Buffer management (new - for supporting multiple files) - buffer_manager: BufferManager, - buffer_handler: BufferHandler, // Handles buffer operations like switching - // Cache - query_cache: Option, - // Data source tracking - - // Undo/redo and kill ring - undo_stack: Vec<(String, usize)>, // (text, cursor_pos) - redo_stack: Vec<(String, usize)>, - - // Viewport tracking - last_visible_rows: usize, // Track the last calculated viewport height - - // Display options - jump_to_row_input: String, // Input buffer for jump to row command - log_buffer: Option, // Ring buffer for debug logs -} - -impl EnhancedTuiApp { - // --- Buffer Compatibility Layer --- - // These methods provide a gradual migration path from direct field access to BufferAPI - - /// Get current buffer if available (for reading) - fn current_buffer(&self) -> Option<&dyn sql_cli::buffer::BufferAPI> { - self.buffer_manager - .current() - .map(|b| b as &dyn sql_cli::buffer::BufferAPI) - } - - /// Get current buffer (panics if none exists) - /// Use this when we know a buffer should always exist - fn buffer(&self) -> &dyn sql_cli::buffer::BufferAPI { - self.current_buffer() - .expect("No buffer available - this should not happen") - } - - // Note: current_buffer_mut removed - use buffer_manager.current_mut() directly - - /// Get current mutable buffer (panics if none exists) - /// Use this when we know a buffer should always exist - fn buffer_mut(&mut self) -> &mut sql_cli::buffer::Buffer { - self.buffer_manager - .current_mut() - .expect("No buffer available - this should not happen") - } - - // Note: edit_mode methods removed - use buffer directly - - // Helper to get input text from buffer or fallback to direct input - fn get_input_text(&self) -> String { - // For special modes that use the input field for their own purposes - match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes temporarily use the input field for their patterns - self.input.value().to_string() // TODO: Migrate to buffer-based input - } - _ => { - // All other modes use the buffer - self.buffer().get_input_text() - } - } - } - - // Helper to get cursor position from buffer or fallback to direct input - fn get_input_cursor(&self) -> usize { - // For special modes that use the input field directly - match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes use the input field for their patterns - self.input.cursor() - } - _ => { - // All other modes use the buffer - self.buffer().get_input_cursor_position() - } - } - } - - // Helper to set input text through buffer and sync input field - fn set_input_text(&mut self, text: String) { - self.buffer_mut().set_input_text(text.clone()); - // Also sync cursor position to end of text - self.buffer_mut().set_input_cursor_position(text.len()); - - // Always update the input field for all modes - // TODO: Eventually migrate special modes to use buffer input - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - - // Helper to set input text with specific cursor position - fn set_input_text_with_cursor(&mut self, text: String, cursor_pos: usize) { - self.buffer_mut().set_input_text(text.clone()); - self.buffer_mut().set_input_cursor_position(cursor_pos); - - // Always update the input field for consistency - // TODO: Eventually migrate special modes to use buffer input - self.input = tui_input::Input::new(text).with_cursor(cursor_pos); - } - - // Helper to clear input - fn clear_input(&mut self) { - self.set_input_text(String::new()); - } - - // Helper to handle key events in the input - fn handle_input_key(&mut self, key: KeyEvent) -> bool { - // For special modes that handle input directly - match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - self.input.handle_event(&Event::Key(key)); - false - } - _ => { - // Route to buffer's input handling - self.buffer_mut().handle_input_key(key) - } - } - } - - // Helper to get visual cursor position (for rendering) - fn get_visual_cursor(&self) -> (usize, usize) { - // Get text and cursor from appropriate source based on mode - let (text, cursor) = match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // Special modes use self.input directly - (self.input.value().to_string(), self.input.cursor()) - } - _ => { - // Other modes use buffer - ( - self.buffer().get_input_text(), - self.buffer().get_input_cursor_position(), - ) - } - }; - - let lines: Vec<&str> = text.split('\n').collect(); - let mut current_pos = 0; - for (row, line) in lines.iter().enumerate() { - if current_pos + line.len() >= cursor { - return (row, cursor - current_pos); - } - current_pos += line.len() + 1; // +1 for newline - } - (0, cursor) - } - - // Note: mode methods removed - use buffer directly - - fn get_filter_state(&self) -> &FilterState { - &self.filter_state - } - - fn get_filter_state_mut(&mut self) -> &mut FilterState { - &mut self.filter_state - } - - fn sanitize_table_name(name: &str) -> String { - // Replace spaces and other problematic characters with underscores - // to create SQL-friendly table names - // Examples: "Business Crime Borough Level" -> "Business_Crime_Borough_Level" - name.trim() - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect() - } - - pub fn new(api_url: &str) -> Self { - // Load configuration - let config = Config::load().unwrap_or_else(|_e| { - // Config loading error - using defaults - Config::default() - }); - - Self { - api_client: ApiClient::new(api_url), - input: Input::default(), - cursor_manager: CursorManager::new(), - data_analyzer: DataAnalyzer::new(), - // results: None, // MIGRATED to buffer system - table_state: TableState::default(), - show_help: false, - sql_parser: SqlParser::new(), - hybrid_parser: HybridParser::new(), - config: config.clone(), - - sort_state: SortState { - column: None, - order: SortOrder::None, - }, - filter_state: FilterState { - pattern: String::new(), - regex: None, - active: false, - }, - // fuzzy_filter_state: FuzzyFilterState { ... }, // MIGRATED to buffer system - search_state: SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }, - completion_state: CompletionState { - suggestions: Vec::new(), - current_index: 0, - last_query: String::new(), - last_cursor_pos: 0, - }, - history_state: HistoryState { - search_query: String::new(), - matches: Vec::new(), - selected_index: 0, - }, - command_history: CommandHistory::new().unwrap_or_default(), - scroll_offset: (0, 0), - current_column: 0, - sql_highlighter: SqlHighlighter::new(), - debug_widget: DebugWidget::new(), - editor_widget: EditorWidget::new(), - stats_widget: StatsWidget::new(), - key_chord_handler: KeyChordHandler::new(), - key_dispatcher: KeyDispatcher::new(), - help_scroll: 0, - input_scroll_offset: 0, - selection_mode: SelectionMode::Row, // Default to row mode - last_yanked: None, - // CSV fields now in Buffer - buffer_manager: { - // Initialize buffer manager with a default buffer - let mut manager = BufferManager::new(); - let mut buffer = sql_cli::buffer::Buffer::new(1); - // Sync initial settings from config - buffer.set_case_insensitive(config.behavior.case_insensitive_default); - buffer.set_compact_mode(config.display.compact_mode); - buffer.set_show_row_numbers(config.display.show_row_numbers); - manager.add_buffer(buffer); - manager - }, - buffer_handler: BufferHandler::new(), - query_cache: QueryCache::new().ok(), - // Cache fields now in Buffer - undo_stack: Vec::new(), - redo_stack: Vec::new(), - last_visible_rows: 30, // Default estimate - jump_to_row_input: String::new(), - log_buffer: get_log_buffer(), - } - } - - pub fn new_with_csv(csv_path: &str) -> Result { - let mut csv_client = CsvApiClient::new(); - - // First create the app to get its config - let mut app = Self::new(""); // Empty API URL for CSV mode - - // Use the app's config for consistency - csv_client.set_case_insensitive(app.config.behavior.case_insensitive_default); - - let raw_name = std::path::Path::new(csv_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - // Sanitize the table name to be SQL-friendly - let table_name = Self::sanitize_table_name(&raw_name); - - csv_client.load_csv(csv_path, &table_name)?; - - // Get schema from CSV - let schema = csv_client - .get_schema() - .ok_or_else(|| anyhow::anyhow!("Failed to get CSV schema"))?; - - // Replace the default buffer with a CSV buffer - { - // Clear all buffers and add a CSV buffer - app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_csv( - 1, - std::path::PathBuf::from(csv_path), - csv_client, - table_name.clone(), - ); - // Apply config settings to the buffer - use app's config - buffer.set_case_insensitive(app.config.behavior.case_insensitive_default); - buffer.set_compact_mode(app.config.display.compact_mode); - buffer.set_show_row_numbers(app.config.display.show_row_numbers); - - info!(target: "buffer", "Configured CSV buffer with: compact_mode={}, case_insensitive={}, show_row_numbers={}", - app.config.display.compact_mode, - app.config.behavior.case_insensitive_default, - app.config.display.show_row_numbers); - app.buffer_manager.add_buffer(buffer); - } - - // Update parser with CSV columns - if let Some(columns) = schema.get(&table_name) { - // Update the parser with CSV columns - app.hybrid_parser - .update_single_table(table_name.clone(), columns.clone()); - let display_msg = if raw_name != table_name { - format!( - "CSV loaded: '{}' as table '{}' with {} columns", - raw_name, - table_name, - columns.len() - ) - } else { - format!( - "CSV loaded: table '{}' with {} columns", - table_name, - columns.len() - ) - }; - app.buffer_mut().set_status_message(display_msg); - } - - // Auto-execute SELECT * FROM table_name to show data immediately (if configured) - let auto_query = format!("SELECT * FROM {}", table_name); - - // Populate the input field with the query for easy editing - app.set_input_text(auto_query.clone()); - - if app.config.behavior.auto_execute_on_load { - if let Err(e) = app.execute_query(&auto_query) { - // If auto-query fails, just log it in status but don't fail the load - app.buffer_mut().set_status_message(format!( - "CSV loaded: table '{}' ({} columns) - Note: {}", - table_name, - schema.get(&table_name).map(|c| c.len()).unwrap_or(0), - e - )); - } - } - - Ok(app) - } - - pub fn new_with_json(json_path: &str) -> Result { - let mut csv_client = CsvApiClient::new(); - - // First create the app to get its config - let mut app = Self::new(""); // Empty API URL for JSON mode - - // Use the app's config for consistency - csv_client.set_case_insensitive(app.config.behavior.case_insensitive_default); - - let raw_name = std::path::Path::new(json_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - // Sanitize the table name to be SQL-friendly - let table_name = Self::sanitize_table_name(&raw_name); - - csv_client.load_json(json_path, &table_name)?; - - // Get schema from JSON data - let schema = csv_client - .get_schema() - .ok_or_else(|| anyhow::anyhow!("Failed to get JSON schema"))?; - - // Replace the default buffer with a JSON buffer - { - // Clear all buffers and add a JSON buffer - app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_json( - 1, - std::path::PathBuf::from(json_path), - csv_client, - table_name.clone(), - ); - // Apply config settings to the buffer - use app's config - buffer.set_case_insensitive(app.config.behavior.case_insensitive_default); - buffer.set_compact_mode(app.config.display.compact_mode); - buffer.set_show_row_numbers(app.config.display.show_row_numbers); - - info!(target: "buffer", "Configured CSV buffer with: compact_mode={}, case_insensitive={}, show_row_numbers={}", - app.config.display.compact_mode, - app.config.behavior.case_insensitive_default, - app.config.display.show_row_numbers); - app.buffer_manager.add_buffer(buffer); - } - - // Buffer state is now initialized - - // Update parser with JSON columns - if let Some(columns) = schema.get(&table_name) { - app.hybrid_parser - .update_single_table(table_name.clone(), columns.clone()); - let display_msg = if raw_name != table_name { - format!( - "JSON loaded: '{}' as table '{}' with {} columns", - raw_name, - table_name, - columns.len() - ) - } else { - format!( - "JSON loaded: table '{}' with {} columns", - table_name, - columns.len() - ) - }; - app.buffer_mut().set_status_message(display_msg); - } - - // Auto-execute SELECT * FROM table_name to show data immediately (if configured) - let auto_query = format!("SELECT * FROM {}", table_name); - - // Populate the input field with the query for easy editing - app.set_input_text(auto_query.clone()); - - if app.config.behavior.auto_execute_on_load { - if let Err(e) = app.execute_query(&auto_query) { - // If auto-query fails, just log it in status but don't fail the load - app.buffer_mut().set_status_message(format!( - "JSON loaded: table '{}' ({} columns) - Note: {}", - table_name, - schema.get(&table_name).map(|c| c.len()).unwrap_or(0), - e - )); - } - } - - Ok(app) - } - - pub fn run(mut self) -> Result<()> { - // Setup terminal with error handling - if let Err(e) = enable_raw_mode() { - return Err(anyhow::anyhow!( - "Failed to enable raw mode: {}. Try running with --classic flag.", - e - )); - } - - let mut stdout = io::stdout(); - if let Err(e) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) { - let _ = disable_raw_mode(); - return Err(anyhow::anyhow!( - "Failed to setup terminal: {}. Try running with --classic flag.", - e - )); - } - - let backend = CrosstermBackend::new(stdout); - let mut terminal = match Terminal::new(backend) { - Ok(t) => t, - Err(e) => { - let _ = disable_raw_mode(); - return Err(anyhow::anyhow!( - "Failed to create terminal: {}. Try running with --classic flag.", - e - )); - } - }; - - let res = self.run_app(&mut terminal); - - // Always restore terminal, even on error - let _ = disable_raw_mode(); - let _ = execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ); - let _ = terminal.show_cursor(); - - match res { - Ok(_) => Ok(()), - Err(e) => Err(anyhow::anyhow!("TUI error: {}", e)), - } - } - - fn run_app(&mut self, terminal: &mut Terminal) -> Result<()> { - // Initial draw - terminal.draw(|f| self.ui(f))?; - - loop { - // Use blocking read for better performance - only process when there's an actual event - match event::read()? { - Event::Key(key) => { - // On Windows, filter out key release events - only handle key press - // This prevents double-triggering of toggles - if key.kind != crossterm::event::KeyEventKind::Press { - continue; - } - - let should_exit = match self.buffer().get_mode() { - AppMode::Command => self.handle_command_input(key)?, - AppMode::Results => self.handle_results_input(key)?, - AppMode::Search => self.handle_search_input(key)?, - AppMode::Filter => self.handle_filter_input(key)?, - AppMode::FuzzyFilter => self.handle_fuzzy_filter_input(key)?, - AppMode::ColumnSearch => self.handle_column_search_input(key)?, - AppMode::Help => self.handle_help_input(key)?, - AppMode::History => self.handle_history_input(key)?, - AppMode::Debug => self.handle_debug_input(key)?, - AppMode::PrettyQuery => self.handle_pretty_query_input(key)?, - AppMode::CacheList => self.handle_cache_list_input(key)?, - AppMode::JumpToRow => self.handle_jump_to_row_input(key)?, - AppMode::ColumnStats => self.handle_column_stats_input(key)?, - }; - - if should_exit { - break; - } - - // Only redraw after handling a key event - terminal.draw(|f| self.ui(f))?; - } - _ => { - // Ignore other events (mouse, resize, etc.) to reduce CPU - } - } - } - Ok(()) - } - - fn handle_command_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // NEW: Try editor widget first for high-level actions - let key_dispatcher = self.key_dispatcher.clone(); - // Handle editor widget actions by splitting the borrow - let editor_result = if let Some(buffer) = self.buffer_manager.current_mut() { - self.editor_widget - .handle_key(key.clone(), &key_dispatcher, buffer)? - } else { - EditorAction::PassToMainApp(key.clone()) - }; - - match editor_result { - EditorAction::Quit => return Ok(true), - EditorAction::ExecuteQuery => { - // Execute the current query - delegate to existing logic for now - return self.handle_execute_query(); - } - EditorAction::BufferAction(buffer_action) => { - return self.handle_buffer_action(buffer_action); - } - EditorAction::ExpandAsterisk => { - return self.handle_expand_asterisk(); - } - EditorAction::ShowHelp => { - self.show_help = true; - return Ok(false); - } - EditorAction::ShowDebug => { - // This is now handled by passing through to original F5 handler - return Ok(false); - } - EditorAction::ShowPrettyQuery => { - self.show_pretty_query(); - return Ok(false); - } - EditorAction::SwitchMode(mode) => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_mode(mode.clone()); - } - // Special handling for History mode - initialize history search - if mode == AppMode::History { - self.history_state.search_query.clear(); - self.update_history_matches(); - // Debug: log how many history entries we have - let total_entries = self.command_history.get_all().len(); - self.buffer_mut().set_status_message(format!( - "History search: {} total entries", - total_entries - )); - } - return Ok(false); - } - EditorAction::PassToMainApp(_) => { - // Fall through to original logic below - } - EditorAction::Continue => return Ok(false), - } - - // ORIGINAL LOGIC: Keep all existing logic as fallback - // Store old cursor position - let old_cursor = self.get_input_cursor(); - - // Also log to tracing - trace!(target: "input", "Key: {:?} Modifiers: {:?}", key.code, key.modifiers); - - // DON'T process chord handler in Command mode - yanking makes no sense when editing queries! - // The 'y' key should just type 'y' in the query editor. - - // Try dispatcher first for buffer operations and other actions - if let Some(action) = self.key_dispatcher.get_command_action(&key) { - match action { - "quit" => return Ok(true), - "next_buffer" => { - let message = self.buffer_handler.next_buffer(&mut self.buffer_manager); - debug!("{}", message); - return Ok(false); - } - "previous_buffer" => { - let message = self - .buffer_handler - .previous_buffer(&mut self.buffer_manager); - debug!("{}", message); - return Ok(false); - } - "quick_switch_buffer" => { - let message = self.buffer_handler.quick_switch(&mut self.buffer_manager); - debug!("{}", message); - return Ok(false); - } - "new_buffer" => { - let message = self - .buffer_handler - .new_buffer(&mut self.buffer_manager, &self.config); - debug!("{}", message); - return Ok(false); - } - "close_buffer" => { - let (success, message) = - self.buffer_handler.close_buffer(&mut self.buffer_manager); - debug!("{}", message); - return Ok(!success); // Exit if we couldn't close (only one left) - } - "list_buffers" => { - let buffer_list = self.buffer_handler.list_buffers(&self.buffer_manager); - // For now, just log the list - later we can show a popup - for line in &buffer_list { - debug!("{}", line); - } - return Ok(false); - } - action if action.starts_with("switch_to_buffer_") => { - if let Some(buffer_num_str) = action.strip_prefix("switch_to_buffer_") { - if let Ok(buffer_num) = buffer_num_str.parse::() { - let message = self - .buffer_handler - .switch_to_buffer(&mut self.buffer_manager, buffer_num - 1); // Convert to 0-based - debug!("{}", message); - } - } - return Ok(false); - } - "expand_asterisk" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.expand_asterisk(&self.hybrid_parser) { - // Sync for rendering if needed - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - } - } - } - return Ok(false); - } - // "move_to_line_start" and "move_to_line_end" now handled by editor_widget - "delete_word_backward" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.delete_word_backward(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "delete_word_forward" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.delete_word_forward(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "kill_line" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.kill_line(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "kill_line_backward" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.kill_line_backward(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "move_word_backward" => { - self.move_cursor_word_backward(); - return Ok(false); - } - "move_word_forward" => { - self.move_cursor_word_forward(); - return Ok(false); - } - "jump_to_prev_token" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.jump_to_prev_token(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "jump_to_next_token" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.jump_to_next_token(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - _ => {} // Fall through to hardcoded handling - } - } - - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - // KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::ALT) && key.modifiers.contains(KeyModifiers::SHIFT) => { - // // Alt+Shift+D - new DataTable buffer (for testing) - disabled during revert - // self.new_datatable_buffer(); - // } - KeyCode::F(1) | KeyCode::Char('?') => { - self.show_help = !self.show_help; - let mode = if self.show_help { - AppMode::Help - } else { - AppMode::Command - }; - self.buffer_mut().set_mode(mode); - } - KeyCode::F(3) => { - // F3 no longer toggles modes - always stay in single-line mode - self.buffer_mut().set_status_message( - "Multi-line mode has been removed. Use F6 for pretty print.".to_string(), - ); - } - KeyCode::F(7) => { - // F7 - Toggle cache mode or show cache list - if self.buffer().is_cache_mode() { - self.buffer_mut().set_mode(AppMode::CacheList); - } else { - self.buffer_mut().set_mode(AppMode::CacheList); - } - } - KeyCode::Enter => { - // Always use single-line mode handling - let query = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query: {}", query); - - if !query.is_empty() { - // Check for special commands - if query == ":help" { - self.show_help = true; - self.buffer_mut().set_mode(AppMode::Help); - self.buffer_mut() - .set_status_message("Help Mode - Press ESC to return".to_string()); - } else if query == ":exit" || query == ":quit" { - return Ok(true); - } else if query == ":tui" { - // Already in TUI mode - self.buffer_mut() - .set_status_message("Already in TUI mode".to_string()); - } else if query.starts_with(":cache ") { - self.handle_cache_command(&query)?; - } else { - self.buffer_mut() - .set_status_message(format!("Processing query: '{}'", query)); - self.execute_query(&query)?; - } - } else { - self.buffer_mut() - .set_status_message("Empty query - please enter a SQL command".to_string()); - } - } - KeyCode::Tab => { - // Tab completion works in both modes - // Always use single-line completion - self.apply_completion() - } - // Ctrl+R is now handled by the editor widget above - // History navigation - Ctrl+P or Alt+Up - KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to previous command in history - // Get history entries first, before mutable borrow - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_up(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Debug: show what we got from history - let debug_msg = if text.is_empty() { - "History navigation returned empty text!".to_string() - } else { - format!( - "History: {}", - if text.len() > 50 { - format!("{}...", &text[..50]) - } else { - text.clone() - } - ) - }; - - // Update the appropriate input field based on edit mode - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut().set_status_message(debug_msg); - } - } - } - // History navigation - Ctrl+N or Alt+Down - KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to next command in history - // Get history entries first, before mutable borrow - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_down(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Update the appropriate input field based on edit mode - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut() - .set_status_message("Next command from history".to_string()); - } - } - } - // Alternative: Alt+Up for history previous (in case Ctrl+P is intercepted) - KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_up(&history_commands) { - let text = buffer.get_input_text(); - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut() - .set_status_message("Previous command (Alt+Up)".to_string()); - } - } - } - // Alternative: Alt+Down for history next - KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_down(&history_commands) { - let text = buffer.get_input_text(); - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut() - .set_status_message("Next command (Alt+Down)".to_string()); - } - } - } - KeyCode::F(8) => { - // Toggle case-insensitive string comparisons - let current = self.buffer().is_case_insensitive(); - self.buffer_mut().set_case_insensitive(!current); - - // Update CSV client if in CSV mode - // Update CSV client if in CSV mode - if let Some(csv_client) = self.buffer_mut().get_csv_client_mut() { - csv_client.set_case_insensitive(!current); - } - - self.buffer_mut().set_status_message(format!( - "Case-insensitive string comparisons: {}", - if !current { "ON" } else { "OFF" } - )); - } - KeyCode::F(9) => { - // F9 as alternative for kill line (for terminals that intercept Ctrl+K) - self.kill_line(); - let message = if !self.buffer().is_kill_ring_empty() { - format!( - "Killed to end of line ('{}' saved to kill ring)", - self.buffer().get_kill_ring() - ) - } else { - "Killed to end of line".to_string() - }; - self.buffer_mut().set_status_message(message); - } - KeyCode::F(10) => { - // F10 as alternative for kill line backward (for consistency with F9) - self.kill_line_backward(); - let message = if !self.buffer().is_kill_ring_empty() { - format!( - "Killed to beginning of line ('{}' saved to kill ring)", - self.buffer().get_kill_ring() - ) - } else { - "Killed to beginning of line".to_string() - }; - self.buffer_mut().set_status_message(message); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line - delete from cursor to end of line - self.buffer_mut() - .set_status_message("Ctrl+K pressed - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alternative: Alt+K for kill line (for terminals that intercept Ctrl+K) - self.buffer_mut() - .set_status_message("Alt+K - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line backward - delete from cursor to beginning of line - self.kill_line_backward(); - } - // Ctrl+Z (undo) now handled by editor_widget - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Yank - paste from kill ring - self.yank(); - } - KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Paste from system clipboard - self.paste_from_clipboard(); - } - KeyCode::Char('[') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to previous SQL token - self.jump_to_prev_token(); - } - KeyCode::Char(']') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to next SQL token - self.jump_to_next_token(); - } - KeyCode::Left if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move backward one word - self.move_cursor_word_backward(); - } - KeyCode::Right if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move forward one word - self.move_cursor_word_forward(); - } - KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move backward one word (alt+b like in bash) - self.move_cursor_word_backward(); - } - KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move forward one word (alt+f like in bash) - self.move_cursor_word_forward(); - } - KeyCode::Down - if self.buffer().get_results().is_some() - && self.buffer().get_edit_mode() == EditMode::SingleLine => - { - self.buffer_mut().set_mode(AppMode::Results); - // Restore previous position or default to 0 - let row = self.buffer().get_last_results_row().unwrap_or(0); - &mut self.table_state.select(Some(row)); - - // Restore the exact scroll offset from when we left - let last_offset = self.buffer().get_last_scroll_offset(); - self.buffer_mut().set_scroll_offset(last_offset); - } - KeyCode::F(5) => { - // Generate full debug information - self.debug_current_buffer(); - - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; - let query = self.get_input_text(); - - // Collect all needed data before mutable borrow - let buffer_names: Vec = self - .buffer_manager - .all_buffers() - .iter() - .map(|b| b.get_name()) - .collect(); - let buffer_count = self.buffer_manager.all_buffers().len(); - let buffer_index = self.buffer_manager.current_index(); - let api_url = self.api_client.base_url.clone(); - - // Generate debug info directly without buffer reference - let mut debug_info = self - .hybrid_parser - .get_detailed_debug_info(&query, cursor_pos); - - // Add input state - debug_info.push_str(&format!( - "\n========== INPUT STATE ==========\n\ - Input Value Length: {}\n\ - Cursor Position: {}\n\ - Visual Cursor: {}\n\ - Input Mode: Command\n", - query.len(), - cursor_pos, - visual_cursor - )); - - // Add buffer state info - debug_info.push_str(&format!( - "\n========== BUFFER MANAGER STATE ==========\n\ - Number of Buffers: {}\n\ - Current Buffer Index: {}\n\ - Buffer Names: {}\n", - buffer_count, - buffer_index, - buffer_names.join(", ") - )); - - // Add WHERE clause AST if needed - if query.to_lowercase().contains(" where ") { - let where_ast_info = match self.parse_where_clause_ast(&query) { - Ok(ast_str) => ast_str, - Err(e) => format!("\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {}\n", e) - }; - debug_info.push_str(&where_ast_info); - } - - // Add key chord handler debug info - debug_info.push_str("\n"); - debug_info.push_str(&self.key_chord_handler.format_debug_info()); - debug_info.push_str("========================================\n"); - - // Add trace logs from ring buffer - debug_info.push_str("\n========== TRACE LOGS ==========\n"); - debug_info.push_str("(Most recent at bottom, last 100 entries)\n"); - if let Some(ref log_buffer) = self.log_buffer { - let recent_logs = log_buffer.get_recent(100); - for entry in recent_logs { - debug_info.push_str(&entry.format_for_display()); - debug_info.push('\n'); - } - debug_info.push_str(&format!("Total log entries: {}\n", log_buffer.len())); - } else { - debug_info.push_str("Log buffer not initialized\n"); - } - debug_info.push_str("================================\n"); - - // Set the final content in debug widget - self.debug_widget.set_content(debug_info.clone()); - - // Try to copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_info) { - Ok(_) => { - // Verify clipboard write by reading it back - match clipboard.get_text() { - Ok(clipboard_content) => { - let clipboard_len = clipboard_content.len(); - if clipboard_content == debug_info { - self.buffer_mut().set_status_message(format!( - "DEBUG INFO copied to clipboard ({} chars)!", - clipboard_len - )); - } else { - self.buffer_mut().set_status_message(format!( - "Clipboard verification failed! Expected {} chars, got {} chars", - debug_info.len(), clipboard_len - )); - } - } - Err(e) => { - self.buffer_mut().set_status_message(format!( - "Debug info copied but verification failed: {}", - e - )); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Can't access clipboard: {}", e)); - } - } - - self.buffer_mut().set_mode(AppMode::Debug); - } - KeyCode::F(6) => { - // Pretty print query view - let query = self.get_input_text(); - if !query.trim().is_empty() { - self.debug_widget.generate_pretty_sql(&query); - self.buffer_mut().set_mode(AppMode::PrettyQuery); - self.buffer_mut().set_status_message( - "Pretty query view (press Esc or q to return)".to_string(), - ); - } else { - self.buffer_mut() - .set_status_message("No query to format".to_string()); - } - } - _ => { - // Use the new helper to handle input keys through buffer - self.handle_input_key(key); - - // Clear completion state when typing other characters - self.completion_state.suggestions.clear(); - self.completion_state.current_index = 0; - - // Always use single-line completion - self.handle_completion() - } - } - - // Update horizontal scroll if cursor moved - if self.get_input_cursor() != old_cursor { - self.update_horizontal_scroll(120); // Assume reasonable terminal width, will be adjusted in render - } - - Ok(false) - } - - fn handle_results_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - debug!( - "handle_results_input: key={:?}, selection_mode={:?}", - key, self.selection_mode - ); - - // Debug uppercase G specifically - if matches!(key.code, KeyCode::Char('G')) { - debug!("Detected uppercase G key press!"); - } - - // In cell mode, skip chord handler for 'y' key - handle it directly - // Also skip 'G' as it's a single key action, not a chord - let should_skip_chord = (matches!(self.selection_mode, SelectionMode::Cell) - && matches!(key.code, KeyCode::Char('y'))) - || matches!(key.code, KeyCode::Char('G')); - - let chord_result = if should_skip_chord { - debug!("Skipping chord handler for key {:?}", key.code); - // Still log the key press even when skipping chord handler - self.key_chord_handler.log_key_press(&key); - ChordResult::SingleKey(key.clone()) - } else { - // Process key through chord handler - self.key_chord_handler.process_key(key.clone()) - }; - - // Handle chord results - match chord_result { - ChordResult::CompleteChord(action) => { - // Handle completed chord actions - match action.as_str() { - "yank_row" => { - self.yank_row(); - return Ok(false); - } - "yank_column" => { - self.yank_column(); - return Ok(false); - } - "yank_all" => { - self.yank_all(); - return Ok(false); - } - "yank_cell" => { - self.yank_cell(); - return Ok(false); - } - _ => { - // Unknown action, continue with normal key handling - } - } - } - ChordResult::PartialChord(description) => { - // Update status to show chord mode - self.buffer_mut().set_status_message(description); - return Ok(false); - } - ChordResult::Cancelled => { - self.buffer_mut() - .set_status_message("Chord cancelled".to_string()); - return Ok(false); - } - ChordResult::SingleKey(_) => { - // Continue with normal key handling - } - } - - // Use dispatcher to get action first - if let Some(action) = self.key_dispatcher.get_results_action(&key) { - debug!("Dispatcher returned action '{}' for key {:?}", action, key); - match action { - "quit" => return Ok(true), - "exit_results_mode" => { - // Save current position before switching to Command mode - if let Some(selected) = self.table_state.selected() { - self.buffer_mut().set_last_results_row(Some(selected)); - let scroll_offset = self.buffer().get_scroll_offset(); - self.buffer_mut().set_last_scroll_offset(scroll_offset); - } - self.buffer_mut().set_mode(AppMode::Command); - &mut self.table_state.select(None); - } - "next_row" => self.next_row(), - "previous_row" => self.previous_row(), - "move_column_left" => self.move_column_left(), - "move_column_right" => self.move_column_right(), - "goto_first_row" => self.goto_first_row(), - "goto_last_row" => { - debug!("Executing goto_last_row action"); - self.goto_last_row(); - } - "goto_first_column" => self.goto_first_column(), - "goto_last_column" => self.goto_last_column(), - "page_up" => self.page_up(), - "page_down" => self.page_down(), - "start_search" => { - // Save SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::Search); - self.buffer_mut().set_search_pattern(String::new()); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - "start_column_search" => { - // Save current SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::ColumnSearch); - self.buffer_mut().set_column_search_pattern(String::new()); - self.buffer_mut().set_column_search_matches(Vec::new()); - self.buffer_mut().set_column_search_current_match(0); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - "start_filter" => { - self.buffer_mut().set_mode(AppMode::Filter); - self.get_filter_state_mut().pattern.clear(); - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - "start_fuzzy_filter" => { - self.buffer_mut().set_mode(AppMode::FuzzyFilter); - self.buffer_mut().set_fuzzy_filter_pattern(String::new()); - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - "sort_by_column" => self.sort_by_column(self.buffer().get_current_column()), - "show_column_stats" => self.calculate_column_statistics(), - "next_search_match" => self.next_search_match(), - "previous_search_match" => self.previous_search_match(), - "toggle_compact_mode" => { - let current_mode = self.buffer().is_compact_mode(); - self.buffer_mut().set_compact_mode(!current_mode); - let message = if !current_mode { - "Compact mode: ON (reduced padding, more columns visible)".to_string() - } else { - "Compact mode: OFF (normal padding)".to_string() - }; - self.buffer_mut().set_status_message(message); - } - "toggle_row_numbers" => { - let current_mode = self.buffer().is_show_row_numbers(); - self.buffer_mut().set_show_row_numbers(!current_mode); - let message = if !current_mode { - "Row numbers: ON".to_string() - } else { - "Row numbers: OFF".to_string() - }; - self.buffer_mut().set_status_message(message); - } - "jump_to_row" => { - self.buffer_mut().set_mode(AppMode::JumpToRow); - self.jump_to_row_input.clear(); - self.buffer_mut() - .set_status_message("Enter row number:".to_string()); - } - "pin_column" => self.toggle_column_pin(), - "clear_pins" => self.clear_all_pinned_columns(), - "toggle_selection_mode" => { - self.selection_mode = match self.selection_mode { - SelectionMode::Row => { - self.buffer_mut().set_status_message( - "Cell mode - Navigate to select individual cells".to_string(), - ); - SelectionMode::Cell - } - SelectionMode::Cell => { - self.buffer_mut().set_status_message( - "Row mode - Navigate to select rows".to_string(), - ); - SelectionMode::Row - } - }; - return Ok(false); // Return to prevent duplicate handling - } - "export_to_csv" => self.export_to_csv(), - "export_to_json" => self.export_to_json(), - "toggle_help" => { - if self.buffer().get_mode() == AppMode::Help { - self.buffer_mut().set_mode(AppMode::Results); - } else { - self.buffer_mut().set_mode(AppMode::Help); - } - } - "toggle_debug" => { - // Debug mode - show buffer state and parser information - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; // Get column position for single-line - let query = self.get_input_text(); - - // Create debug info showing buffer state - let mut debug_info = String::new(); - debug_info.push_str("=== Debug Information (Results Mode) ===\n\n"); - - // Add current query info - debug_info.push_str("Current Query:\n"); - debug_info.push_str(&format!(" Query: '{}'\n", query)); - debug_info.push_str(&format!(" Cursor Position: {}\n", cursor_pos)); - debug_info.push_str(&format!(" Visual Cursor: {}\n", visual_cursor)); - debug_info.push_str("\n"); - - // Add buffer manager info - debug_info.push_str("=== Buffer Manager ===\n"); - debug_info.push_str(&format!( - " Total Buffers: {}\n", - self.buffer_manager.all_buffers().len() - )); - debug_info.push_str(&format!( - " Current Buffer Index: {}\n", - self.buffer_manager.current_index() - )); - - // Add current buffer debug dump - if let Some(buffer) = self.buffer_manager.current() { - debug_info.push_str("\n=== Current Buffer State ===\n"); - debug_info.push_str(&buffer.debug_dump()); - } - - self.debug_widget.set_content(debug_info); - self.buffer_mut().set_mode(AppMode::Debug); - self.buffer_mut() - .set_status_message("Debug mode - Press 'q' or ESC to return".to_string()); - } - "toggle_case_insensitive" => { - // Toggle case-insensitive string comparisons - let current = self.buffer().is_case_insensitive(); - self.buffer_mut().set_case_insensitive(!current); - - // Update CSV client if in CSV mode - if let Some(csv_client) = self.buffer_mut().get_csv_client_mut() { - csv_client.set_case_insensitive(!current); - } - - self.buffer_mut().set_status_message(format!( - "Case-insensitive string comparisons: {}", - if !current { "ON" } else { "OFF" } - )); - } - _ => { - // Action not recognized, continue to handle key directly - } - } - } - - // Fall back to direct key handling for special cases not in dispatcher - match key.code { - KeyCode::Char(' ') => { - // Toggle viewport lock with Space - let current_lock = self.buffer().is_viewport_lock(); - self.buffer_mut().set_viewport_lock(!current_lock); - if self.buffer().is_viewport_lock() { - // Lock to current position in viewport (middle of screen) - let visible_rows = self.buffer().get_last_visible_rows(); - self.buffer_mut() - .set_viewport_lock_row(Some(visible_rows / 2)); - self.buffer_mut().set_status_message(format!( - "Viewport lock: ON (anchored at row {} of viewport)", - visible_rows / 2 + 1 - )); - } else { - self.buffer_mut().set_viewport_lock_row(None); - self.buffer_mut() - .set_status_message("Viewport lock: OFF (normal scrolling)".to_string()); - } - } - KeyCode::PageDown | KeyCode::Char('f') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.page_down(); - } - KeyCode::PageUp | KeyCode::Char('b') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.page_up(); - } - // Search functionality - KeyCode::Char('/') => { - // Save SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::Search); - self.buffer_mut().set_search_pattern(String::new()); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - // Column navigation/search functionality (backslash like vim reverse search) - KeyCode::Char('\\') => { - // Save current SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::ColumnSearch); - self.buffer_mut().set_column_search_pattern(String::new()); - self.buffer_mut().set_column_search_matches(Vec::new()); - self.buffer_mut().set_column_search_current_match(0); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - KeyCode::Char('n') => { - self.next_search_match(); - } - KeyCode::Char('N') if key.modifiers.contains(KeyModifiers::SHIFT) => { - // Only for search navigation when Shift is held - if !self.buffer().get_search_pattern().is_empty() { - self.previous_search_match(); - } else { - // Toggle row numbers display - let current = self.buffer().is_show_row_numbers(); - self.buffer_mut().set_show_row_numbers(!current); - let message = if !current { - "Row numbers: ON (showing line numbers)".to_string() - } else { - "Row numbers: OFF".to_string() - }; - self.buffer_mut().set_status_message(message); - // Recalculate column widths with new mode - self.calculate_optimal_column_widths(); - } - } - // Regex filter functionality (uppercase F) - KeyCode::Char('F') if key.modifiers.contains(KeyModifiers::SHIFT) => { - self.buffer_mut().set_mode(AppMode::Filter); - self.get_filter_state_mut().pattern.clear(); - // Save SQL query and use temporary input for filter display - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - // Fuzzy filter functionality (lowercase f) - KeyCode::Char('f') - if !key.modifiers.contains(KeyModifiers::ALT) - && !key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.buffer_mut().set_mode(AppMode::FuzzyFilter); - self.buffer_mut().set_fuzzy_filter_pattern(String::new()); - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); // Clear active state when entering mode - // Save SQL query and use temporary input for fuzzy filter display - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - // Sort functionality (lowercase s) - KeyCode::Char('s') - if !key.modifiers.contains(KeyModifiers::CONTROL) - && !key.modifiers.contains(KeyModifiers::SHIFT) => - { - self.sort_by_column(self.buffer().get_current_column()); - } - // Column statistics (uppercase S) - KeyCode::Char('S') | KeyCode::Char('s') - if key.modifiers.contains(KeyModifiers::SHIFT) => - { - self.calculate_column_statistics(); - } - // Clipboard operations (vim-like yank) - KeyCode::Char('y') => { - debug!("'y' key pressed - selection_mode={:?}", self.selection_mode); - match self.selection_mode { - SelectionMode::Cell => { - // In cell mode, single 'y' yanks the cell directly - debug!("Yanking cell in cell selection mode"); - self.buffer_mut() - .set_status_message("Yanking cell...".to_string()); - self.yank_cell(); - // Status message will be set by yank_cell - } - SelectionMode::Row => { - // In row mode, 'y' is handled by chord handler (yy, yc, ya) - // The chord handler will process the key sequence - debug!("'y' pressed in row mode - waiting for chord completion"); - self.buffer_mut().set_status_message( - "Press second key for chord: yy=row, yc=column, ya=all, yv=cell" - .to_string(), - ); - } - } - } - // Export to CSV - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_csv(); - } - // Export to JSON - KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_json(); - } - // Number keys for direct column sorting - KeyCode::Char(c) if c.is_ascii_digit() => { - if let Some(digit) = c.to_digit(10) { - let column_index = (digit as usize).saturating_sub(1); - self.sort_by_column(column_index); - } - } - KeyCode::F(1) | KeyCode::Char('?') => { - self.show_help = true; - self.buffer_mut().set_mode(AppMode::Help); - } - _ => { - // Other keys handled normally - } - } - Ok(false) - } - - fn handle_search_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Enter => { - self.perform_search(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Backspace => { - { - let mut pattern = self.buffer().get_search_pattern(); - pattern.pop(); - self.buffer_mut().set_search_pattern(pattern); - }; - // Update input for rendering - let pattern = self.buffer().get_search_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - KeyCode::Char(c) => { - { - let mut pattern = self.buffer().get_search_pattern(); - pattern.push(c); - self.buffer_mut().set_search_pattern(pattern); - } - // Update input for rendering - let pattern = self.buffer().get_search_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - _ => {} - } - Ok(false) - } - - fn handle_filter_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Enter => { - self.apply_filter(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Backspace => { - self.get_filter_state_mut().pattern.pop(); - // Update input for rendering - let pattern = self.get_filter_state().pattern.clone(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - KeyCode::Char(c) => { - self.get_filter_state_mut().pattern.push(c); - // Update input for rendering - let pattern = self.get_filter_state().pattern.clone(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - _ => {} - } - Ok(false) - } - - fn handle_fuzzy_filter_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Clear fuzzy filter and return to results - self.buffer_mut().set_fuzzy_filter_active(false); - self.buffer_mut().set_fuzzy_filter_pattern(String::new()); - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - self.buffer_mut() - .set_status_message("Fuzzy filter cleared".to_string()); - } - KeyCode::Enter => { - // Apply fuzzy filter and return to results - if !self.buffer().get_fuzzy_filter_pattern().is_empty() { - self.apply_fuzzy_filter(); - self.buffer_mut().set_fuzzy_filter_active(true); - } - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Backspace => { - { - let mut pattern = self.buffer().get_fuzzy_filter_pattern(); - pattern.pop(); - self.buffer_mut().set_fuzzy_filter_pattern(pattern); - }; - // Update input for rendering - let pattern = self.buffer().get_fuzzy_filter_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - // Re-apply filter in real-time - if !self.buffer().get_fuzzy_filter_pattern().is_empty() { - self.apply_fuzzy_filter(); - } else { - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); - } - } - KeyCode::Char(c) => { - { - let mut pattern = self.buffer().get_fuzzy_filter_pattern(); - pattern.push(c); - self.buffer_mut().set_fuzzy_filter_pattern(pattern); - }; - // Update input for rendering - let pattern = self.buffer().get_fuzzy_filter_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - // Apply filter in real-time as user types - self.apply_fuzzy_filter(); - } - _ => {} - } - Ok(false) - } - - fn handle_column_search_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query from undo stack FIRST - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } else { - // Fallback: restore from buffer's stored text if undo fails - let text = self.buffer().get_input_text(); - let cursor = self.buffer().get_input_cursor_position(); - self.input = tui_input::Input::new(text.clone()).with_cursor(cursor); - } - - // Cancel column search and return to results - self.buffer_mut().set_mode(AppMode::Results); - self.buffer_mut().set_column_search_pattern(String::new()); - self.buffer_mut().set_column_search_matches(Vec::new()); - self.buffer_mut() - .set_status_message("Column search cancelled".to_string()); - } - KeyCode::Enter => { - // Jump to first matching column - if !self.buffer().get_column_search_matches().clone().is_empty() { - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone() - [self.buffer().get_column_search_current_match()] - .clone(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut() - .set_status_message(format!("Jumped to column: {}", column_name)); - } else { - self.buffer_mut() - .set_status_message("No matching columns found".to_string()); - } - - // Restore original SQL query from undo stack - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } else { - // Fallback: restore from buffer's stored text if undo fails - let text = self.buffer().get_input_text(); - let cursor = self.buffer().get_input_cursor_position(); - self.input = tui_input::Input::new(text.clone()).with_cursor(cursor); - } - - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Tab => { - // Next match (Tab only, not 'n' to allow typing 'n' in search) - if !self.buffer().get_column_search_matches().clone().is_empty() { - let matches_len = self.buffer().get_column_search_matches().clone().len(); - let current = self.buffer().get_column_search_current_match(); - self.buffer_mut() - .set_column_search_current_match((current + 1) % matches_len); - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone() - [self.buffer().get_column_search_current_match()] - .clone(); - let current_match = self.buffer().get_column_search_current_match() + 1; - let total_matches = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut().set_status_message(format!( - "Column {} of {}: {}", - current_match, total_matches, column_name - )); - } - } - KeyCode::BackTab => { - // Previous match (Shift+Tab only, not 'N' to allow typing 'N' in search) - if !self.buffer().get_column_search_matches().clone().is_empty() { - let current = self.buffer().get_column_search_current_match(); - if current == 0 { - let matches_len = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut() - .set_column_search_current_match(matches_len - 1); - } else { - self.buffer_mut() - .set_column_search_current_match(current - 1); - } - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone() - [self.buffer().get_column_search_current_match()] - .clone(); - let current_match = self.buffer().get_column_search_current_match() + 1; - let total_matches = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut().set_status_message(format!( - "Column {} of {}: {}", - current_match, total_matches, column_name - )); - } - } - KeyCode::Backspace => { - let mut pattern = self.buffer().get_column_search_pattern(); - pattern.pop(); - self.buffer_mut().set_column_search_pattern(pattern.clone()); - // Also update input to keep it in sync for rendering - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - self.update_column_search(); - } - KeyCode::Char(c) => { - let mut pattern = self.buffer().get_column_search_pattern(); - pattern.push(c); - self.buffer_mut().set_column_search_pattern(pattern.clone()); - // Also update input to keep it in sync for rendering - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - self.update_column_search(); - } - _ => {} - } - Ok(false) - } - - fn handle_help_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // Use dispatcher to get action - if let Some(action) = self.key_dispatcher.get_help_action(&key) { - match action { - "quit" => return Ok(true), - "exit_help" => self.exit_help(), - "scroll_help_down" => self.scroll_help_down(), - "scroll_help_up" => self.scroll_help_up(), - "help_page_down" => self.help_page_down(), - "help_page_up" => self.help_page_up(), - _ => {} - } - } else { - // Handle any keys not in the dispatcher (like 'j' and 'k' for vim-style) - match key.code { - KeyCode::Char('j') => self.scroll_help_down(), - KeyCode::Char('k') => self.scroll_help_up(), - KeyCode::F(1) => self.exit_help(), - KeyCode::Home => self.help_scroll = 0, - KeyCode::End => { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - self.help_scroll = max_scroll as u16; - } - _ => {} - } - } - Ok(false) - } - - // Helper methods for help mode actions - fn exit_help(&mut self) { - self.show_help = false; - self.help_scroll = 0; - let mode = if self.buffer().get_results().is_some() { - AppMode::Results - } else { - AppMode::Command - }; - self.buffer_mut().set_mode(mode); - } - - fn scroll_help_down(&mut self) { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - if (self.help_scroll as usize) < max_scroll { - self.help_scroll += 1; - } - } - - fn scroll_help_up(&mut self) { - self.help_scroll = self.help_scroll.saturating_sub(1); - } - - fn help_page_down(&mut self) { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - self.help_scroll = (self.help_scroll + 10).min(max_scroll as u16); - } - - fn help_page_up(&mut self) { - self.help_scroll = self.help_scroll.saturating_sub(10); - } - - fn handle_history_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc => { - self.buffer_mut().set_mode(AppMode::Command); - } - KeyCode::Enter => { - if !self.history_state.matches.is_empty() - && self.history_state.selected_index < self.history_state.matches.len() - { - let selected_command = self.history_state.matches - [self.history_state.selected_index] - .entry - .command - .clone(); - // Use helper to set text through buffer - self.set_input_text(selected_command); - self.buffer_mut().set_mode(AppMode::Command); - self.buffer_mut() - .set_status_message("Command loaded from history".to_string()); - // Reset scroll to show end of command - self.input_scroll_offset = 0; - self.update_horizontal_scroll(120); // Will be properly updated on next render - } - } - KeyCode::Up | KeyCode::Char('k') => { - if !self.history_state.matches.is_empty() { - self.history_state.selected_index = - self.history_state.selected_index.saturating_sub(1); - } - } - KeyCode::Down | KeyCode::Char('j') => { - if !self.history_state.matches.is_empty() - && self.history_state.selected_index + 1 < self.history_state.matches.len() - { - self.history_state.selected_index += 1; - } - } - KeyCode::Backspace => { - self.history_state.search_query.pop(); - self.update_history_matches(); - } - KeyCode::Char(c) => { - self.history_state.search_query.push(c); - self.update_history_matches(); - } - _ => {} - } - Ok(false) - } - - fn update_history_matches(&mut self) { - // Get current schema columns and data source for better matching - let (current_columns, current_source_str) = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the first (and usually only) table's columns and name - let (cols, table_name) = schema - .iter() - .next() - .map(|(table_name, cols)| (cols.clone(), Some(table_name.clone()))) - .unwrap_or((vec![], None)); - (cols, table_name) - } else { - (vec![], None) - } - } else { - (vec![], None) - } - } else if self.buffer().is_cache_mode() { - (vec![], Some("cache".to_string())) - } else { - (vec![], Some("api".to_string())) - }; - - let current_source = current_source_str.as_deref(); - - self.history_state.matches = self.command_history.search_with_schema( - &self.history_state.search_query, - ¤t_columns, - current_source, - ); - self.history_state.selected_index = 0; - } - - fn handle_debug_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // Handle special keys for test case generation - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+C to quit - return Ok(true); - } - KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+T: "Yank as Test" - capture current session as test case - self.yank_as_test_case(); - return Ok(false); - } - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::SHIFT) => { - // Shift+Y: Yank debug dump with test context - self.yank_debug_with_context(); - return Ok(false); - } - _ => {} - } - - // Let the widget handle navigation and exit - if self.debug_widget.handle_key(key) { - // Widget returned true - exit debug mode - self.buffer_mut().set_mode(AppMode::Command); - } - Ok(false) - } - - fn handle_pretty_query_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { - return Ok(true); - } - - // Let debug widget handle the key (includes scrolling and exit) - if self.debug_widget.handle_key(key) { - // Widget returned true - exit pretty query mode - self.buffer_mut().set_mode(AppMode::Command); - } - Ok(false) - } - - fn execute_query(&mut self, query: &str) -> Result<()> { - info!(target: "query", "Executing query: {}", query); - self.buffer_mut() - .set_status_message(format!("Executing query: '{}'...", query)); - let start_time = std::time::Instant::now(); - - let result = if self.buffer().is_cache_mode() { - // When in cache mode, use CSV client to query cached data - if let Some(cached_data) = self.buffer().get_cached_data() { - let mut csv_client = CsvApiClient::new(); - csv_client.set_case_insensitive(self.buffer().is_case_insensitive()); - csv_client.load_from_json(cached_data.clone(), "cached_data")?; - - csv_client.query_csv(query).map(|r| QueryResponse { - data: r.data, - count: r.count, - query: sql_cli::api_client::QueryInfo { - select: r.query.select, - where_clause: r.query.where_clause, - order_by: r.query.order_by, - }, - source: Some("cache".to_string()), - table: Some("cached_data".to_string()), - cached: Some(true), - }) - } else { - Err(anyhow::anyhow!("No cached data loaded")) - } - } else if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - // Convert CSV result to match the expected type - csv_client.query_csv(query).map(|r| QueryResponse { - data: r.data, - count: r.count, - query: sql_cli::api_client::QueryInfo { - select: r.query.select, - where_clause: r.query.where_clause, - order_by: r.query.order_by, - }, - source: Some("file".to_string()), - table: Some(self.buffer().get_table_name()), - cached: Some(false), - }) - } else { - Err(anyhow::anyhow!("CSV client not initialized")) - } - } else { - self.api_client - .query_trades(query) - .map_err(|e| anyhow::anyhow!("{}", e)) - }; - - match result { - Ok(response) => { - let duration = start_time.elapsed(); - - // Get schema columns and data source for history - let (schema_columns, data_source) = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the first (and usually only) table's columns - let cols = schema - .iter() - .next() - .map(|(table_name, cols)| (cols.clone(), Some(table_name.clone()))) - .unwrap_or((vec![], None)); - cols - } else { - (vec![], None) - } - } else { - (vec![], None) - } - } else if self.buffer().is_cache_mode() { - (vec![], Some("cache".to_string())) - } else { - (vec![], Some("api".to_string())) - }; - - let _ = self.command_history.add_entry_with_schema( - query.to_string(), - true, - Some(duration.as_millis() as u64), - schema_columns, - data_source, - ); - - // Add debug info about results - let row_count = response.data.len(); - - // Capture the source from the response - self.buffer_mut() - .set_last_query_source(response.source.clone()); - - // Store results in the current buffer - if let Some(buffer) = self.buffer_manager.current_mut() { - let buffer_id = buffer.get_id(); - buffer.set_results(Some(response.clone())); - info!(target: "buffer", "Stored {} results in buffer {}", row_count, buffer_id); - } - self.buffer_mut().set_results(Some(response.clone())); // Keep for compatibility during migration - - // Update parser with the FULL schema if we're in CSV/cache mode - // For CSV mode, get the complete schema from the CSV client, not from query results - if self.buffer().is_csv_mode() { - let table_name = self.buffer().get_table_name(); - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the full column list from the schema - if let Some(columns) = schema.get(&table_name) { - info!(target: "buffer", "Query executed, updating parser with FULL schema ({} columns) for table '{}'", columns.len(), table_name); - self.hybrid_parser - .update_single_table(table_name, columns.clone()); - } - } - } - } else if self.buffer().is_cache_mode() { - // For cache mode, we still use the results columns since cached data might be filtered - if let Some(first_row) = response.data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = obj.keys().map(|k| k.to_string()).collect(); - info!(target: "buffer", "Query executed, updating parser with {} columns for cached table", columns.len()); - self.hybrid_parser - .update_single_table("cached_data".to_string(), columns); - } - } - } - - self.calculate_optimal_column_widths(); - self.reset_table_state(); - - if row_count == 0 { - self.buffer_mut().set_status_message(format!( - "Query executed successfully but returned 0 rows ({}ms)", - duration.as_millis() - )); - } else { - self.buffer_mut().set_status_message(format!("Query executed successfully - {} rows returned ({}ms) - Use ↓ or j/k to navigate", row_count, duration.as_millis())); - } - - self.buffer_mut().set_mode(AppMode::Results); - &mut self.table_state.select(Some(0)); - } - Err(e) => { - let duration = start_time.elapsed(); - - // Get schema columns and data source for history (even for failed queries) - let (schema_columns, data_source) = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the first (and usually only) table's columns - let cols = schema - .iter() - .next() - .map(|(table_name, cols)| (cols.clone(), Some(table_name.clone()))) - .unwrap_or((vec![], None)); - cols - } else { - (vec![], None) - } - } else { - (vec![], None) - } - } else if self.buffer().is_cache_mode() { - (vec![], Some("cache".to_string())) - } else { - (vec![], Some("api".to_string())) - }; - - let _ = self.command_history.add_entry_with_schema( - query.to_string(), - false, - Some(duration.as_millis() as u64), - schema_columns, - data_source, - ); - self.buffer_mut() - .set_status_message(format!("Error: {}", e)); - } - } - Ok(()) - } - - fn parse_where_clause_ast(&self, query: &str) -> Result { - let query_lower = query.to_lowercase(); - if let Some(where_pos) = query_lower.find(" where ") { - let where_clause = &query[where_pos + 7..]; // Skip " where " - - // Get columns from CSV client if available - let columns = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - schema - .iter() - .next() - .map(|(_, cols)| cols.clone()) - .unwrap_or_default() - } else { - vec![] - } - } else { - vec![] - } - } else { - vec![] - }; - - match WhereParser::parse_with_options( - where_clause, - columns, - self.buffer().is_case_insensitive(), - ) { - Ok(ast) => { - let tree = format_where_ast(&ast, 0); - Ok(format!( - "\n========== WHERE CLAUSE AST ==========\n\ - Query: {}\n\ - WHERE clause: {}\n\n\ - AST Tree:\n{}\n\n\ - Note: Parentheses in the query control operator precedence.\n\ - The parser respects: OR < AND < NOT < comparisons\n\ - Example: 'a = 1 OR b = 2 AND c = 3' parses as 'a = 1 OR (b = 2 AND c = 3)'\n\ - Use parentheses to override: '(a = 1 OR b = 2) AND c = 3'\n", - query, - where_clause, - tree - )) - } - Err(e) => Err(anyhow::anyhow!("Failed to parse WHERE clause: {}", e)), - } - } else { - Ok( - "\n========== WHERE CLAUSE AST ==========\nNo WHERE clause found in query\n" - .to_string(), - ) - } - } - - fn handle_completion(&mut self) { - let cursor_pos = self.get_input_cursor(); - let query_str = self.get_input_text(); - let query = query_str.as_str(); - - let hybrid_result = self.hybrid_parser.get_completions(query, cursor_pos); - if !hybrid_result.suggestions.is_empty() { - self.buffer_mut().set_status_message(format!( - "Suggestions: {}", - hybrid_result.suggestions.join(", ") - )); - } - } - - fn apply_completion(&mut self) { - let cursor_pos = self.get_input_cursor(); - let query = self.get_input_text(); - - // Check if this is a continuation of the same completion session - let is_same_context = query == self.completion_state.last_query - && cursor_pos == self.completion_state.last_cursor_pos; - - if !is_same_context { - // New completion context - get fresh suggestions - let hybrid_result = self.hybrid_parser.get_completions(&query, cursor_pos); - if hybrid_result.suggestions.is_empty() { - self.buffer_mut() - .set_status_message("No completions available".to_string()); - return; - } - - self.completion_state.suggestions = hybrid_result.suggestions; - self.completion_state.current_index = 0; - } else if !self.completion_state.suggestions.is_empty() { - // Cycle to next suggestion - self.completion_state.current_index = - (self.completion_state.current_index + 1) % self.completion_state.suggestions.len(); - } else { - self.buffer_mut() - .set_status_message("No completions available".to_string()); - return; - } - - // Apply the current suggestion (clone to avoid borrow issues) - let suggestion = - self.completion_state.suggestions[self.completion_state.current_index].clone(); - let partial_word = self.extract_partial_word_at_cursor(&query, cursor_pos); - - if let Some(partial) = partial_word { - // Replace the partial word with the suggestion - let before_partial = &query[..cursor_pos - partial.len()]; - let after_cursor = &query[cursor_pos..]; - - // Handle quoted identifiers - if both partial and suggestion start with quotes, - // we need to avoid double quotes - let suggestion_to_use = if partial.starts_with('"') && suggestion.starts_with('"') { - // The partial already includes the opening quote, so use suggestion without its quote - if suggestion.len() > 1 { - suggestion[1..].to_string() - } else { - suggestion.clone() - } - } else { - suggestion.clone() - }; - - let new_query = format!("{}{}{}", before_partial, suggestion_to_use, after_cursor); - - // Update input and cursor position - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let cursor_pos = if suggestion_to_use.ends_with("('')") { - // Position cursor between the quotes - before_partial.len() + suggestion_to_use.len() - 2 - } else { - before_partial.len() + suggestion_to_use.len() - }; - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to correct position - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(cursor_pos); - // Sync for rendering - if self.buffer().get_edit_mode() == EditMode::SingleLine { - self.set_input_text_with_cursor(new_query.clone(), cursor_pos); - } - } - - // Update completion state for next tab press - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = cursor_pos; - - let suggestion_info = if self.completion_state.suggestions.len() > 1 { - format!( - "Completed: {} ({}/{} - Tab for next)", - suggestion, - self.completion_state.current_index + 1, - self.completion_state.suggestions.len() - ) - } else { - format!("Completed: {}", suggestion) - }; - self.buffer_mut().set_status_message(suggestion_info); - } else { - // Just insert the suggestion at cursor position - let before_cursor = &query[..cursor_pos]; - let after_cursor = &query[cursor_pos..]; - let new_query = format!("{}{}{}", before_cursor, suggestion, after_cursor); - - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let cursor_pos_new = if suggestion.ends_with("('')") { - // Position cursor between the quotes - cursor_pos + suggestion.len() - 2 - } else { - cursor_pos + suggestion.len() - }; - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to correct position - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(cursor_pos_new); - // Sync for rendering - if self.buffer().get_edit_mode() == EditMode::SingleLine { - self.input = - tui_input::Input::new(new_query.clone()).with_cursor(cursor_pos_new); - } - } - - // Update completion state - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = cursor_pos_new; - - self.buffer_mut() - .set_status_message(format!("Inserted: {}", suggestion)); - } - } - - // Note: expand_asterisk and get_table_columns removed - moved to Buffer and use hybrid_parser directly - - fn extract_partial_word_at_cursor(&self, query: &str, cursor_pos: usize) -> Option { - if cursor_pos == 0 || cursor_pos > query.len() { - return None; - } - - let chars: Vec = query.chars().collect(); - let mut start = cursor_pos; - let end = cursor_pos; - - // Check if we might be in a quoted identifier - let mut in_quote = false; - - // Find start of word (go backward) - while start > 0 { - let prev_char = chars[start - 1]; - if prev_char == '"' { - // Found a quote, include it and stop - start -= 1; - in_quote = true; - break; - } else if prev_char.is_alphanumeric() - || prev_char == '_' - || (prev_char == ' ' && in_quote) - { - start -= 1; - } else { - break; - } - } - - // If we found a quote but are in a quoted identifier, - // we need to continue backwards to include the identifier content - if in_quote && start > 0 { - // We've already moved past the quote, now get the content before it - // Actually, we want to include everything from the quote forward - // The logic above is correct - we stop at the quote - } - - // Convert back to byte positions - let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum(); - let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum(); - - if start_byte < end_byte { - Some(query[start_byte..end_byte].to_string()) - } else { - None - } - } - - // Helper to get estimated visible rows based on terminal size - - // Navigation functions - fn next_row(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - // Update viewport size before navigation - self.update_viewport_size(); - - let current = self.table_state.selected().unwrap_or(0); - if current >= total_rows - 1 { - return; - } // Already at bottom - - let new_position = current + 1; - &mut self.table_state.select(Some(new_position)); - - // Update viewport based on lock mode - if self.buffer().is_viewport_lock() { - // In lock mode, keep cursor at fixed viewport position - if let Some(lock_row) = self.buffer().get_viewport_lock_row() { - // Adjust viewport so cursor stays at lock_row position - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = new_position.saturating_sub(lock_row); - self.buffer_mut().set_scroll_offset(offset); - } - } else { - // Normal scrolling behavior - let visible_rows = self.buffer().get_last_visible_rows(); - - // Check if cursor would be below the last visible row - let offset = self.buffer().get_scroll_offset(); - if new_position > offset.0 + visible_rows - 1 { - // Cursor moved below viewport - scroll down by one - self.buffer_mut() - .set_scroll_offset((offset.0 + 1, offset.1)); - } - } - } - } - - fn previous_row(&mut self) { - let current = self.table_state.selected().unwrap_or(0); - if current == 0 { - return; - } // Already at top - - let new_position = current - 1; - &mut self.table_state.select(Some(new_position)); - - // Update viewport based on lock mode - if self.buffer().is_viewport_lock() { - // In lock mode, keep cursor at fixed viewport position - if let Some(lock_row) = self.buffer().get_viewport_lock_row() { - // Adjust viewport so cursor stays at lock_row position - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = new_position.saturating_sub(lock_row); - self.buffer_mut().set_scroll_offset(offset); - } - } else { - // Normal scrolling behavior - let mut offset = self.buffer().get_scroll_offset(); - if new_position < offset.0 { - // Cursor moved above viewport - scroll up - offset.0 = new_position; - self.buffer_mut().set_scroll_offset(offset); - } - } - } - - fn move_column_left(&mut self) { - // Update cursor_manager for table navigation (incremental step) - let (_row, _col) = self.cursor_manager.table_position(); - self.cursor_manager.move_table_left(); - - // Keep existing logic for now - let new_column = self.buffer().get_current_column().saturating_sub(1); - self.buffer_mut().set_current_column(new_column); - let mut offset = self.buffer().get_scroll_offset(); - offset.1 = offset.1.saturating_sub(1); - let column_num = self.buffer().get_current_column() + 1; - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message(format!("Column {} selected", column_num)); - } - - fn move_column_right(&mut self) { - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let max_columns = obj.len(); - - // Update cursor_manager for table navigation (incremental step) - self.cursor_manager.move_table_right(max_columns); - - // Keep existing logic for now - let current_column = self.buffer().get_current_column(); - if current_column + 1 < max_columns { - self.buffer_mut().set_current_column(current_column + 1); - let mut offset = self.buffer().get_scroll_offset(); - offset.1 += 1; - let column_num = self.buffer().get_current_column() + 1; - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message(format!("Column {} selected", column_num)); - } - } - } - } - } - - fn goto_first_column(&mut self) { - self.buffer_mut().set_current_column(0); - let mut offset = self.buffer().get_scroll_offset(); - offset.1 = 0; - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message("First column selected".to_string()); - } - - fn goto_last_column(&mut self) { - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let max_columns = obj.len(); - if max_columns > 0 { - self.buffer_mut().set_current_column(max_columns - 1); - // Update horizontal scroll to show the last column - // This ensures the last column is visible in the viewport - let mut offset = self.buffer().get_scroll_offset(); - let column = self.buffer().get_current_column(); - offset.1 = column.saturating_sub(5); // Keep some context - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message(format!("Last column selected ({})", column + 1)); - } - } - } - } - } - - fn goto_first_row(&mut self) { - self.table_state.select(Some(0)); - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = 0; // Reset viewport to top - self.buffer_mut().set_scroll_offset(offset); - - let total_rows = self.get_row_count(); - if total_rows > 0 { - self.buffer_mut() - .set_status_message(format!("Jumped to first row (1/{})", total_rows)); - } - } - - fn toggle_column_pin(&mut self) { - // Pin or unpin the current column - let current_col = self.buffer().get_current_column(); - if self.buffer().get_pinned_columns().contains(¤t_col) { - // Column is already pinned, unpin it - self.buffer_mut().remove_pinned_column(current_col); - self.buffer_mut() - .set_status_message(format!("Column {} unpinned", current_col + 1)); - } else { - // Pin the column (max 4 pinned columns) - if self.buffer().get_pinned_columns().clone().len() < 4 { - self.buffer_mut().add_pinned_column(current_col); - self.buffer_mut() - .set_status_message(format!("Column {} pinned 📌", current_col + 1)); - } else { - self.buffer_mut() - .set_status_message("Maximum 4 pinned columns allowed".to_string()); - } - } - } - - fn clear_all_pinned_columns(&mut self) { - self.buffer_mut().clear_pinned_columns(); - self.buffer_mut() - .set_status_message("All columns unpinned".to_string()); - } - - fn calculate_column_statistics(&mut self) { - use std::time::Instant; - - let start_total = Instant::now(); - - // Collect all data first, then drop the buffer reference before calling analyzer - let (column_name, data_to_analyze) = { - // Get the current column name and data - let results = match self.buffer().get_results() { - Some(r) if !r.data.is_empty() => r, - _ => return, - }; - - // Get column names from first row - let headers: Vec = if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - obj.keys().map(|k| k.to_string()).collect() - } else { - return; - } - } else { - return; - }; - - let current_column = self.buffer().get_current_column(); - if current_column >= headers.len() { - return; - } - - let column_name = headers[current_column].clone(); - - // Extract column data more efficiently - avoid cloning strings when possible - let data_to_analyze: Vec = - if let Some(filtered) = self.buffer().get_filtered_data() { - // For filtered data, we already have strings - let mut string_data = Vec::new(); - for row in filtered { - if current_column < row.len() { - string_data.push(row[current_column].clone()); - } - } - string_data - } else { - // For JSON data, we need to convert to owned strings - results - .data - .iter() - .filter_map(|row| { - if let Some(obj) = row.as_object() { - obj.get(&column_name).map(|v| match v { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => String::new(), - _ => v.to_string(), - }) - } else { - None - } - }) - .collect() - }; - - (column_name, data_to_analyze) - }; - - // Convert to references for the analyzer - let data_refs: Vec<&str> = data_to_analyze.iter().map(|s| s.as_str()).collect(); - - // Use DataAnalyzer to calculate statistics - let analyzer_stats = self - .data_analyzer - .calculate_column_statistics(&column_name, &data_refs); - - // Convert from DataAnalyzer's ColumnStatistics to buffer's ColumnStatistics - let stats = ColumnStatistics { - column_name: analyzer_stats.column_name, - column_type: match analyzer_stats.data_type { - sql_cli::data_analyzer::ColumnType::Integer - | sql_cli::data_analyzer::ColumnType::Float => ColumnType::Numeric, - sql_cli::data_analyzer::ColumnType::String - | sql_cli::data_analyzer::ColumnType::Boolean - | sql_cli::data_analyzer::ColumnType::Date => ColumnType::String, - sql_cli::data_analyzer::ColumnType::Mixed => ColumnType::Mixed, - sql_cli::data_analyzer::ColumnType::Unknown => ColumnType::Mixed, - }, - total_count: analyzer_stats.total_values, - null_count: analyzer_stats.null_values, - unique_count: analyzer_stats.unique_values, - frequency_map: analyzer_stats.frequency_map.clone(), - // For numeric columns, parse the min/max strings to f64 - min: analyzer_stats - .min_value - .as_ref() - .and_then(|s| s.parse::().ok()), - max: analyzer_stats - .max_value - .as_ref() - .and_then(|s| s.parse::().ok()), - sum: analyzer_stats.sum_value, - mean: analyzer_stats.avg_value, - median: analyzer_stats.median_value, - }; - - // Calculate total time - let elapsed = start_total.elapsed(); - - self.buffer_mut().set_column_stats(Some(stats)); - - // Show timing in status message - self.buffer_mut().set_status_message(format!( - "Column stats: {:.1}ms for {} values ({} unique)", - elapsed.as_secs_f64() * 1000.0, - data_to_analyze.len(), - analyzer_stats.unique_values - )); - - self.buffer_mut().set_mode(AppMode::ColumnStats); - } - - fn check_parser_error(&self, query: &str) -> Option { - // Quick check for common parser errors - let mut paren_depth = 0; - let mut in_string = false; - let mut escape_next = false; - - for ch in query.chars() { - if escape_next { - escape_next = false; - continue; - } - - match ch { - '\\' if in_string => escape_next = true, - '\'' => in_string = !in_string, - '(' if !in_string => paren_depth += 1, - ')' if !in_string => { - paren_depth -= 1; - if paren_depth < 0 { - return Some("Extra )".to_string()); - } - } - _ => {} - } - } - - if paren_depth > 0 { - return Some(format!("Missing {} )", paren_depth)); - } - - // Could add more checks here (unclosed strings, etc.) - if in_string { - return Some("Unclosed string".to_string()); - } - - None - } - - fn update_viewport_size(&mut self) { - // Update the stored viewport size based on current terminal size - if let Ok((_, height)) = crossterm::terminal::size() { - let terminal_height = height as usize; - // Match the actual layout calculation: - // - Input area: 3 rows (from input_height) - // - Status bar: 3 rows - // - Results area gets the rest - let input_height = 3; - let status_height = 3; - let results_area_height = terminal_height.saturating_sub(input_height + status_height); - - // Now match EXACTLY what the render function does: - // - 1 row for top border - // - 1 row for header - // - 1 row for bottom border - self.buffer_mut() - .set_last_visible_rows(results_area_height.saturating_sub(3).max(10)); - } - } - - fn goto_last_row(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - let last_row = total_rows - 1; - self.table_state.select(Some(last_row)); - // Position viewport to show the last row at the bottom - let visible_rows = self.buffer().get_last_visible_rows(); - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = last_row.saturating_sub(visible_rows - 1); - self.buffer_mut().set_scroll_offset(offset); - - // Set status to confirm action - self.buffer_mut().set_status_message(format!( - "Jumped to last row ({}/{})", - last_row + 1, - total_rows - )); - } - } - - fn page_down(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - let visible_rows = self.buffer().get_last_visible_rows(); - let current = self.table_state.selected().unwrap_or(0); - let new_position = (current + visible_rows).min(total_rows - 1); - - &mut self.table_state.select(Some(new_position)); - - // Scroll viewport down by a page - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = (offset.0 + visible_rows).min(total_rows.saturating_sub(visible_rows)); - self.buffer_mut().set_scroll_offset(offset); - } - } - - fn page_up(&mut self) { - let visible_rows = self.buffer().get_last_visible_rows(); - let current = self.table_state.selected().unwrap_or(0); - let new_position = current.saturating_sub(visible_rows); - - &mut self.table_state.select(Some(new_position)); - - // Scroll viewport up by a page - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = offset.0.saturating_sub(visible_rows); - self.buffer_mut().set_scroll_offset(offset); - } - - // Search and filter functions - fn perform_search(&mut self) { - if let Some(data) = self.get_current_data() { - self.buffer_mut().set_search_matches(Vec::new()); - - if let Ok(regex) = Regex::new(&self.buffer().get_search_pattern()) { - for (row_idx, row) in data.iter().enumerate() { - for (col_idx, cell) in row.iter().enumerate() { - if regex.is_match(cell) { - let mut matches = self.buffer().get_search_matches(); - matches.push((row_idx, col_idx)); - self.buffer_mut().set_search_matches(matches); - } - } - } - - if !self.buffer().get_search_matches().is_empty() { - self.buffer_mut().set_search_match_index(0); - let matches = self.buffer().get_search_matches(); - self.buffer_mut().set_current_match(Some(matches[0])); - let (row, _) = matches[0]; - &mut self.table_state.select(Some(row)); - self.buffer_mut() - .set_status_message(format!("Found {} matches", matches.len())); - } else { - self.buffer_mut() - .set_status_message("No matches found".to_string()); - } - } else { - self.buffer_mut() - .set_status_message("Invalid regex pattern".to_string()); - } - } - } - - fn next_search_match(&mut self) { - if !self.buffer().get_search_matches().is_empty() { - let matches = self.buffer().get_search_matches(); - let new_index = (self.buffer().get_search_match_index() + 1) % matches.len(); - self.buffer_mut().set_search_match_index(new_index); - let (row, _) = matches[new_index]; - &mut self.table_state.select(Some(row)); - self.buffer_mut() - .set_current_match(Some(matches[new_index])); - self.buffer_mut().set_status_message(format!( - "Match {} of {}", - new_index + 1, - matches.len() - )); - } - } - - fn previous_search_match(&mut self) { - if !self.buffer().get_search_matches().is_empty() { - let matches = self.buffer().get_search_matches(); - let current_index = self.buffer().get_search_match_index(); - let new_index = if current_index == 0 { - matches.len() - 1 - } else { - current_index - 1 - }; - self.buffer_mut().set_search_match_index(new_index); - let (row, _) = matches[new_index]; - &mut self.table_state.select(Some(row)); - self.buffer_mut() - .set_current_match(Some(matches[new_index])); - self.buffer_mut().set_status_message(format!( - "Match {} of {}", - new_index + 1, - matches.len() - )); - } - } - - fn apply_filter(&mut self) { - if self.get_filter_state().pattern.is_empty() { - self.buffer_mut().set_filtered_data(None); - self.get_filter_state_mut().active = false; - self.buffer_mut() - .set_status_message("Filter cleared".to_string()); - return; - } - - if let Some(results) = self.buffer().get_results() { - if let Ok(regex) = Regex::new(&self.get_filter_state().pattern) { - let mut filtered = Vec::new(); - - for item in &results.data { - let mut row = Vec::new(); - let mut matches = false; - - if let Some(obj) = item.as_object() { - for (_, value) in obj { - let cell_str = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "".to_string(), - _ => value.to_string(), - }; - - if regex.is_match(&cell_str) { - matches = true; - } - row.push(cell_str); - } - - if matches { - filtered.push(row); - } - } - } - - let filtered_count = filtered.len(); - self.buffer_mut().set_filtered_data(Some(filtered)); - self.get_filter_state_mut().regex = Some(regex); - self.get_filter_state_mut().active = true; - - // Reset table state but preserve filtered data - self.table_state = TableState::default(); - self.buffer_mut().set_scroll_offset((0, 0)); - self.buffer_mut().set_current_column(0); - - // Clear search state but keep filter state - self.search_state = SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }; - - self.buffer_mut() - .set_status_message(format!("Filtered to {} rows", filtered_count)); - } else { - self.buffer_mut() - .set_status_message("Invalid regex pattern".to_string()); - } - } - } - - fn apply_fuzzy_filter(&mut self) { - if self.buffer().get_fuzzy_filter_pattern().is_empty() { - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); - self.buffer_mut() - .set_status_message("Fuzzy filter cleared".to_string()); - return; - } - - let pattern = self.buffer().get_fuzzy_filter_pattern(); - let mut filtered_indices = Vec::new(); - - // Get the data to filter - either already filtered data or original results - let data_to_filter = - if self.get_filter_state().active && self.buffer().get_filtered_data().is_some() { - // If regex filter is active, fuzzy filter on top of that - self.buffer().get_filtered_data() - } else if let Some(results) = self.buffer().get_results() { - // Otherwise filter original results - let mut rows = Vec::new(); - for item in &results.data { - let mut row = Vec::new(); - if let Some(obj) = item.as_object() { - for (_, value) in obj { - let cell_str = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "".to_string(), - _ => value.to_string(), - }; - row.push(cell_str); - } - rows.push(row); - } - } - self.buffer_mut().set_filtered_data(Some(rows)); - self.buffer().get_filtered_data() - } else { - return; - }; - - if let Some(data) = data_to_filter { - for (index, row) in data.iter().enumerate() { - // Concatenate all columns into a single string for matching - let row_text = row.join(" "); - - // Check if pattern starts with ' for exact matching - let matches = if pattern.starts_with('\'') && pattern.len() > 1 { - // Exact substring matching (case-insensitive) - let exact_pattern = &pattern[1..]; - row_text - .to_lowercase() - .contains(&exact_pattern.to_lowercase()) - } else { - // Fuzzy matching - let matcher = SkimMatcherV2::default(); - if let Some(score) = matcher.fuzzy_match(&row_text, &pattern) { - score > 0 - } else { - false - } - }; - - if matches { - filtered_indices.push(index); - } - } - } - - let match_count = filtered_indices.len(); - let is_active = !filtered_indices.is_empty(); - self.buffer_mut().set_fuzzy_filter_indices(filtered_indices); - self.buffer_mut().set_fuzzy_filter_active(is_active); - - if self.buffer().is_fuzzy_filter_active() { - let filter_type = if pattern.starts_with('\'') { - "Exact" - } else { - "Fuzzy" - }; - self.buffer_mut().set_status_message(format!( - "{} filter: {} matches for '{}' (highlighted in magenta)", - filter_type, match_count, pattern - )); - // Reset table state for new filtered view - self.table_state = TableState::default(); - self.buffer_mut().set_scroll_offset((0, 0)); - } else { - let filter_type = if pattern.starts_with('\'') { - "exact" - } else { - "fuzzy" - }; - self.buffer_mut() - .set_status_message(format!("No {} matches for '{}'", filter_type, pattern)); - } - } - - fn update_column_search(&mut self) { - // Get column headers from the current results - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - // Find matching columns (case-insensitive) - let pattern = self.buffer().get_column_search_pattern().to_lowercase(); - let mut matching_columns = Vec::new(); - - for (index, header) in headers.iter().enumerate() { - if header.to_lowercase().contains(&pattern) { - matching_columns.push((index, header.to_string())); - } - } - - self.buffer_mut() - .set_column_search_matches(matching_columns); - self.buffer_mut().set_column_search_current_match(0); - - // Update status message - if self.buffer().get_column_search_pattern().is_empty() { - self.buffer_mut() - .set_status_message("Enter column name to search".to_string()); - } else if self.buffer().get_column_search_matches().clone().is_empty() { - let pattern = self.buffer().get_column_search_pattern(); - self.buffer_mut() - .set_status_message(format!("No columns match '{}'", pattern)); - } else { - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone()[0].clone(); - let matches_len = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut().set_status_message(format!( - "Column 1 of {}: {} (Tab=next, Enter=select)", - matches_len, column_name - )); - } - } else { - self.buffer_mut() - .set_status_message("No column data available".to_string()); - } - } else { - self.buffer_mut() - .set_status_message("No data available for column search".to_string()); - } - } else { - self.buffer_mut() - .set_status_message("No results available for column search".to_string()); - } - } - - fn sort_by_column(&mut self, column_index: usize) { - let new_order = match &self.sort_state { - SortState { - column: Some(col), - order, - } if *col == column_index => match order { - SortOrder::Ascending => SortOrder::Descending, - SortOrder::Descending => SortOrder::None, - SortOrder::None => SortOrder::Ascending, - }, - _ => SortOrder::Ascending, - }; - - if new_order == SortOrder::None { - // Reset to original order - would need to store original data - self.sort_state = SortState { - column: None, - order: SortOrder::None, - }; - self.buffer_mut() - .set_status_message("Sort cleared".to_string()); - return; - } - - // Sort using original JSON values for proper type-aware comparison - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - if column_index < headers.len() { - let column_name = headers[column_index]; - - // Create a vector of (original_json_row, row_index) pairs for sorting - let mut indexed_rows: Vec<(serde_json::Value, usize)> = results - .data - .iter() - .enumerate() - .map(|(i, row)| (row.clone(), i)) - .collect(); - - // Sort based on the original JSON values - indexed_rows.sort_by(|(row_a, _), (row_b, _)| { - let val_a = row_a.get(column_name); - let val_b = row_b.get(column_name); - - let cmp = match (val_a, val_b) { - ( - Some(serde_json::Value::Number(a)), - Some(serde_json::Value::Number(b)), - ) => { - // Numeric comparison - this handles integers and floats properly - let a_f64 = a.as_f64().unwrap_or(0.0); - let b_f64 = b.as_f64().unwrap_or(0.0); - a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal) - } - ( - Some(serde_json::Value::String(a)), - Some(serde_json::Value::String(b)), - ) => { - // String comparison - a.cmp(b) - } - ( - Some(serde_json::Value::Bool(a)), - Some(serde_json::Value::Bool(b)), - ) => { - // Boolean comparison (false < true) - a.cmp(b) - } - (Some(serde_json::Value::Null), Some(serde_json::Value::Null)) => { - Ordering::Equal - } - (Some(serde_json::Value::Null), Some(_)) => { - // NULL comes first - Ordering::Less - } - (Some(_), Some(serde_json::Value::Null)) => { - // NULL comes first - Ordering::Greater - } - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - // Mixed type comparison - fall back to string representation - (Some(a), Some(b)) => { - let a_str = match a { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - let b_str = match b { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - a_str.cmp(&b_str) - } - }; - - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - }); - - // Rebuild the QueryResponse with sorted data - let sorted_data: Vec = - indexed_rows.into_iter().map(|(row, _)| row).collect(); - - // Update both the results and clear filtered_data to force regeneration - let mut new_results = results.clone(); - new_results.data = sorted_data; - self.buffer_mut().set_results(Some(new_results)); - self.buffer_mut().set_filtered_data(None); // Force regeneration of string data - } - } - } - } else if let Some(data) = self.buffer().get_filtered_data() { - // Fallback to string-based sorting if no JSON data available - // Clone the data, sort it, and set it back - let mut sorted_data = data.clone(); - sorted_data.sort_by(|a, b| { - if column_index >= a.len() || column_index >= b.len() { - return Ordering::Equal; - } - - let cell_a = &a[column_index]; - let cell_b = &b[column_index]; - - // Try numeric comparison first - if let (Ok(num_a), Ok(num_b)) = (cell_a.parse::(), cell_b.parse::()) { - let cmp = num_a.partial_cmp(&num_b).unwrap_or(Ordering::Equal); - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - } else { - // String comparison - let cmp = cell_a.cmp(cell_b); - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - } - }); - self.buffer_mut().set_filtered_data(Some(sorted_data)); - } - - self.sort_state = SortState { - column: Some(column_index), - order: new_order, - }; - - // Reset table state but preserve current column position - let current_column = self.buffer().get_current_column(); - self.reset_table_state(); - self.buffer_mut().set_current_column(current_column); - - self.buffer_mut().set_status_message(format!( - "Sorted by column {} ({}) - type-aware", - column_index + 1, - match new_order { - SortOrder::Ascending => "ascending", - SortOrder::Descending => "descending", - SortOrder::None => "none", - } - )); - } - - fn get_current_data(&self) -> Option>> { - if let Some(filtered) = self.buffer().get_filtered_data() { - Some(filtered.clone()) - } else if let Some(results) = self.buffer().get_results() { - Some(DataExporter::convert_json_to_strings(&results.data)) - } else { - None - } - } - - fn get_row_count(&self) -> usize { - // TODO: Fix row count when fuzzy filter is active - // Currently this returns the count from filtered_data (WHERE clause results) - // but doesn't account for fuzzy_filter_state.filtered_indices - // This causes incorrect row counts in the status line (e.g., showing 1/1513 instead of 1/257) - // This will be fixed when fuzzy_filter_state is migrated to the buffer system - // and we have a single source of truth for visible rows - if let Some(filtered) = self.buffer().get_filtered_data() { - filtered.len() - } else if let Some(results) = self.buffer().get_results() { - results.data.len() - } else { - 0 - } - } - - // Removed get_current_data_mut - sorting now uses immutable data and clones when needed - // Removed convert_json_to_strings - moved to DataExporter module - - fn reset_table_state(&mut self) { - self.table_state = TableState::default(); - self.buffer_mut().set_scroll_offset((0, 0)); - self.buffer_mut().set_current_column(0); - self.buffer_mut().set_last_results_row(None); // Reset saved position for new results - self.buffer_mut().set_last_scroll_offset((0, 0)); // Reset saved scroll offset for new results - - // Clear filter state to prevent old filtered data from persisting - *self.get_filter_state_mut() = FilterState { - pattern: String::new(), - regex: None, - active: false, - }; - - // Clear search state - self.search_state = SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }; - - // Clear fuzzy filter state to prevent it from persisting across queries - { - let buffer = self.buffer_mut(); - buffer.clear_fuzzy_filter(); - buffer.set_fuzzy_filter_pattern(String::new()); - buffer.set_fuzzy_filter_active(false); - buffer.set_fuzzy_filter_indices(Vec::new()); - }; - - // Clear filtered data - self.buffer_mut().set_filtered_data(None); - } - - fn calculate_viewport_column_widths(&mut self, viewport_start: usize, viewport_end: usize) { - // Calculate column widths based only on visible rows in viewport - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let mut widths = Vec::with_capacity(headers.len()); - - // Use compact mode settings - let compact = self.buffer().is_compact_mode(); - let min_width = if compact { 4 } else { 6 }; - let max_width = if compact { 20 } else { 30 }; - let padding = if compact { 1 } else { 2 }; - - // Only check visible rows - let rows_to_check = - &results.data[viewport_start..viewport_end.min(results.data.len())]; - - for header in &headers { - // Start with header width - let mut max_col_width = header.len(); - - // Check only visible rows for this column - for row in rows_to_check { - if let Some(obj) = row.as_object() { - if let Some(value) = obj.get(*header) { - let display_value = if value.is_null() { - "NULL" - } else if let Some(s) = value.as_str() { - s - } else { - &value.to_string() - }; - max_col_width = max_col_width.max(display_value.len()); - } - } - } - - // Apply min/max constraints and padding - let width = (max_col_width + padding).clamp(min_width, max_width) as u16; - widths.push(width); - } - - self.buffer_mut().set_column_widths(widths); - } - } - } - } - - fn update_parser_for_current_buffer(&mut self) { - // Sync the input field with the current buffer's text - if let Some(buffer) = self.buffer_manager.current() { - let text = buffer.get_input_text(); - let cursor_pos = buffer.get_input_cursor_position(); - self.input = tui_input::Input::new(text.clone()).with_cursor(cursor_pos); - debug!(target: "buffer", "Synced input field with buffer text: '{}' (cursor: {})", text, cursor_pos); - } - - // Update the parser's schema based on the current buffer's data source - if let Some(buffer) = self.buffer_manager.current() { - if buffer.is_csv_mode() { - let table_name = buffer.get_table_name(); - if let Some(csv_client) = buffer.get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the full column list from the schema - if let Some(columns) = schema.get(&table_name) { - debug!(target: "buffer", "Updating parser with {} columns for table '{}'", columns.len(), table_name); - self.hybrid_parser - .update_single_table(table_name, columns.clone()); - } - } - } - } else if buffer.is_cache_mode() { - // For cache mode, use cached data schema if available - if let Some(cached_data) = buffer.get_cached_data() { - if let Some(first_row) = cached_data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = obj.keys().map(|k| k.to_string()).collect(); - debug!(target: "buffer", "Updating parser with {} columns for cached data", columns.len()); - self.hybrid_parser - .update_single_table("cached_data".to_string(), columns); - } - } - } - } else if let Some(results) = buffer.get_results() { - // For API mode or when we have results, use the result columns - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = obj.keys().map(|k| k.to_string()).collect(); - let table_name = buffer.get_table_name(); - debug!(target: "buffer", "Updating parser with {} columns for table '{}'", columns.len(), table_name); - self.hybrid_parser.update_single_table(table_name, columns); - } - } - } - } - } - - fn calculate_optimal_column_widths(&mut self) { - use sql_cli::column_manager::ColumnManager; - - if let Some(results) = self.buffer().get_results() { - let widths = ColumnManager::calculate_optimal_widths(&results.data); - if !widths.is_empty() { - self.buffer_mut().set_column_widths(widths); - } - } - } - - fn export_to_csv(&mut self) { - match DataExporter::export_to_csv(self.buffer()) { - Ok(message) => { - self.buffer_mut().set_status_message(message); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Export failed: {}", e)); - } - } - } - - fn yank_cell(&mut self) { - debug!("yank_cell called"); - if let Some(selected_row) = self.table_state.selected() { - let column = self.buffer().get_current_column(); - debug!("Yanking cell at row={}, column={}", selected_row, column); - match YankManager::yank_cell(self.buffer(), selected_row, column) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview.clone())); - let message = format!("Yanked cell: {}", result.full_value); - debug!("Yank successful: {}", message); - self.buffer_mut().set_status_message(message); - } - Err(e) => { - let message = format!("Failed to yank cell: {}", e); - debug!("Yank failed: {}", message); - self.buffer_mut().set_status_message(message); - } - } - } else { - debug!("No row selected for yank"); - } - } - - fn yank_row(&mut self) { - if let Some(selected_row) = self.table_state.selected() { - match YankManager::yank_row(self.buffer(), selected_row) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview)); - self.buffer_mut() - .set_status_message(format!("Yanked {}", result.description)); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to yank row: {}", e)); - } - } - } - } - - fn yank_column(&mut self) { - let column = self.buffer().get_current_column(); - match YankManager::yank_column(self.buffer(), column) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview)); - self.buffer_mut() - .set_status_message(format!("Yanked {}", result.description)); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to yank column: {}", e)); - } - } - } - - fn yank_all(&mut self) { - match YankManager::yank_all(self.buffer()) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview.clone())); - self.buffer_mut().set_status_message(format!( - "Yanked {}: {}", - result.description, result.preview - )); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to yank all: {}", e)); - } - } - } - - /// Yank current query and results as a complete test case (Ctrl+T in debug mode) - fn yank_as_test_case(&mut self) { - let test_case = DebugInfo::generate_test_case(self.buffer()); - - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&test_case) { - Ok(_) => { - self.buffer_mut().set_status_message(format!( - "Copied complete test case to clipboard ({} lines)", - test_case.lines().count() - )); - self.last_yanked = Some(( - "Test Case".to_string(), - format!( - "{}...", - test_case.lines().take(3).collect::>().join("; ") - ), - )); - } - Err(e) => { - self.buffer_mut().set_status_message(format!( - "Failed to copy test case to clipboard: {}", - e - )); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to access clipboard: {}", e)); - } - } - } - - /// Yank debug dump with context for manual test creation (Shift+Y in debug mode) - fn yank_debug_with_context(&mut self) { - let debug_context = DebugInfo::generate_debug_context(self.buffer()); - - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_context) { - Ok(_) => { - self.buffer_mut().set_status_message(format!( - "Copied debug context to clipboard ({} lines)", - debug_context.lines().count() - )); - self.last_yanked = Some(( - "Debug Context".to_string(), - "Query context with data for test creation".to_string(), - )); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to copy debug context: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to access clipboard: {}", e)); - } - } - } - - fn paste_from_clipboard(&mut self) { - // Paste from system clipboard into the current input field - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.get_text() { - Ok(text) => { - match self.buffer().get_mode() { - AppMode::Command => { - // Always use single-line mode paste - // Get current cursor position - let cursor_pos = self.get_input_cursor(); - let current_value = self.get_input_text(); - - // Insert at cursor position - let mut new_value = String::new(); - new_value.push_str(¤t_value[..cursor_pos]); - new_value.push_str(&text); - new_value.push_str(¤t_value[cursor_pos..]); - - self.set_input_text_with_cursor(new_value, cursor_pos + text.len()); - - self.buffer_mut() - .set_status_message(format!("Pasted {} characters", text.len())); - } - AppMode::Filter - | AppMode::FuzzyFilter - | AppMode::Search - | AppMode::ColumnSearch => { - // For search/filter modes, append to current pattern - let cursor_pos = self.get_input_cursor(); - let current_value = self.get_input_text(); - - let mut new_value = String::new(); - new_value.push_str(¤t_value[..cursor_pos]); - new_value.push_str(&text); - new_value.push_str(¤t_value[cursor_pos..]); - - self.set_input_text_with_cursor(new_value, cursor_pos + text.len()); - - // Update the appropriate filter/search state - match self.buffer().get_mode() { - AppMode::Filter => { - self.get_filter_state_mut().pattern = self.get_input_text(); - self.apply_filter(); - } - AppMode::FuzzyFilter => { - let input_text = self.get_input_text(); - self.buffer_mut().set_fuzzy_filter_pattern(input_text); - self.apply_fuzzy_filter(); - } - AppMode::Search => { - let search_text = self.get_input_text(); - self.buffer_mut().set_search_pattern(search_text); - // TODO: self.search_in_results(); - } - AppMode::ColumnSearch => { - let input_text = self.get_input_text(); - self.buffer_mut().set_column_search_pattern(input_text); - // TODO: self.search_columns(); - } - _ => {} - } - } - _ => { - self.buffer_mut() - .set_status_message("Paste not available in this mode".to_string()); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to paste: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - - fn export_to_json(&mut self) { - // Include filtered data if filters are active - let include_filtered = - self.get_filter_state().active || self.buffer().is_fuzzy_filter_active(); - - match DataExporter::export_to_json(self.buffer(), include_filtered) { - Ok(message) => { - self.buffer_mut().set_status_message(message); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Export failed: {}", e)); - } - } - } - - // Removed get_filtered_json_data - moved to YankManager::convert_filtered_to_json - - fn get_horizontal_scroll_offset(&self) -> u16 { - // Delegate to cursor_manager (incremental refactoring) - let (horizontal, _vertical) = self.cursor_manager.scroll_offsets(); - horizontal - } - - fn update_horizontal_scroll(&mut self, terminal_width: u16) { - let inner_width = terminal_width.saturating_sub(3) as usize; // Account for borders + 1 char padding - let cursor_pos = self.get_input_cursor(); - - // Update cursor_manager scroll (incremental refactoring) - self.cursor_manager - .update_horizontal_scroll(cursor_pos, terminal_width.saturating_sub(3)); - - // Keep legacy field in sync for now - if cursor_pos < self.input_scroll_offset as usize { - self.input_scroll_offset = cursor_pos as u16; - } - // If cursor is after the scroll window, scroll right - else if cursor_pos >= self.input_scroll_offset as usize + inner_width { - self.input_scroll_offset = (cursor_pos + 1).saturating_sub(inner_width) as u16; - } - } - - fn get_cursor_token_position(&self) -> (usize, usize) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - TextNavigator::get_cursor_token_position(&query, cursor_pos) - } - - fn get_token_at_cursor(&self) -> Option { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - TextNavigator::get_token_at_cursor(&query, cursor_pos) - } - - fn move_cursor_word_backward(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.move_cursor_word_backward(); - - // Sync for rendering if single-line mode - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - } - - fn move_cursor_word_forward(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - let query_len = query.len(); - - if cursor_pos >= query_len { - return; - } - - // Use our lexer to tokenize the query - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find the next token boundary after the cursor - let mut target_pos = query_len; - for (start, end, _) in &tokens { - if *start > cursor_pos { - target_pos = *start; - break; - } else if *end > cursor_pos { - target_pos = *end; - break; - } - } - - // Update cursor_manager (small incremental step) - self.cursor_manager.set_position(target_pos); - - // Move cursor to new position through buffer - let is_single_line = self.buffer().get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.set_input_text_with_cursor(text, target_pos); - } - } - - // Update status message - self.buffer_mut() - .set_status_message(format!("Moved to position {} (word boundary)", target_pos)); - } - - fn kill_line(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.kill_line(); - - // Sync for rendering if single-line mode - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - } - - fn kill_line_backward(&mut self) { - // Always use single-line mode - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if let Some((killed_text, new_query)) = TextEditor::kill_line_backward(&query, cursor_pos) { - // Save to undo stack before modifying - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - // Save to kill ring before deleting - self.buffer_mut().set_kill_ring(killed_text); - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to beginning - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(0); - // Sync for rendering - self.set_input_text_with_cursor(new_query, 0); - } - } - } - - fn undo(&mut self) { - // Use buffer's high-level undo operation - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.perform_undo() { - self.buffer_mut() - .set_status_message("Undo performed".to_string()); - } else { - self.buffer_mut() - .set_status_message("Nothing to undo".to_string()); - } - } - } - - // Buffer management methods - - fn new_buffer(&mut self) { - let mut new_buffer = - sql_cli::buffer::Buffer::new(self.buffer_manager.all_buffers().len() + 1); - // Apply config settings to the new buffer - new_buffer.set_compact_mode(self.config.display.compact_mode); - new_buffer.set_case_insensitive(self.config.behavior.case_insensitive_default); - new_buffer.set_show_row_numbers(self.config.display.show_row_numbers); - - info!(target: "buffer", "Creating new buffer with config: compact_mode={}, case_insensitive={}, show_row_numbers={}", - self.config.display.compact_mode, - self.config.behavior.case_insensitive_default, - self.config.display.show_row_numbers); - - let index = self.buffer_manager.add_buffer(new_buffer); - self.buffer_mut() - .set_status_message(format!("Created new buffer #{}", index + 1)); - } - - // DataTable buffer creation disabled during revert - // fn new_datatable_buffer(&mut self) { ... } - - /// Debug method to dump current buffer state (disabled to prevent TUI corruption) - #[allow(dead_code)] - fn debug_current_buffer(&self) { - // Debug output disabled - was corrupting TUI display - // Use tracing/logging instead if debugging is needed - } - - fn yank(&mut self) { - if !self.buffer().is_kill_ring_empty() { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - // Get kill ring content and calculate new query - let kill_ring_content = self.buffer().get_kill_ring(); - let before = query.chars().take(cursor_pos).collect::(); - let after = query.chars().skip(cursor_pos).collect::(); - let new_query = format!("{}{}{}", before, kill_ring_content, after); - let new_cursor = cursor_pos + kill_ring_content.len(); - - // Save to undo stack before modifying - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to new position - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(new_cursor); - // Sync for rendering - if self.buffer().get_edit_mode() == EditMode::SingleLine { - self.set_input_text_with_cursor(new_query, new_cursor); - } - } - } - } - - fn jump_to_prev_token(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if cursor_pos == 0 { - return; - } - - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find current token position - let mut in_token = false; - let mut current_token_start = 0; - for (start, end, _) in &tokens { - if cursor_pos > *start && cursor_pos <= *end { - in_token = true; - current_token_start = *start; - break; - } - } - - // Find the previous token start - let mut target_pos = 0; - - if in_token && cursor_pos > current_token_start { - // If we're in the middle of a token, go to its start - target_pos = current_token_start; - } else { - // Otherwise, find the previous token - for (start, _, _) in tokens.iter().rev() { - if *start < cursor_pos { - target_pos = *start; - break; - } - } - } - - // Move cursor through buffer - if target_pos < cursor_pos { - let is_single_line = self.buffer().get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.set_input_text_with_cursor(text, target_pos); - } - } - } - } - - fn jump_to_next_token(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if let Some(target_pos) = TextNavigator::calculate_next_token_position(&query, cursor_pos) { - let is_single_line = self.buffer().get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.set_input_text_with_cursor(text, target_pos); - } - } - } - } - - fn ui(&mut self, f: &mut Frame) { - // Always use single-line mode input height - let input_height = 3; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(input_height), // Command input area - Constraint::Min(0), // Results - Constraint::Length(3), // Status bar - ] - .as_ref(), - ) - .split(f.area()); - - // Update horizontal scroll based on actual terminal width - self.update_horizontal_scroll(chunks[0].width); - - // Command input area - let input_title = match self.buffer().get_mode() { - AppMode::Command => "SQL Query".to_string(), - AppMode::Results => "SQL Query (Results Mode - Press ↑ to edit)".to_string(), - AppMode::Search => "Search Pattern".to_string(), - AppMode::Filter => "Filter Pattern".to_string(), - AppMode::FuzzyFilter => "Fuzzy Filter".to_string(), - AppMode::ColumnSearch => "Column Search".to_string(), - AppMode::Help => "Help".to_string(), - AppMode::History => format!( - "History Search: '{}' (Esc to cancel)", - self.history_state.search_query - ), - AppMode::Debug => "Parser Debug (F5)".to_string(), - AppMode::PrettyQuery => "Pretty Query View (F6)".to_string(), - AppMode::CacheList => "Cache Management (F7)".to_string(), - AppMode::JumpToRow => format!("Jump to row: {}", self.jump_to_row_input), - AppMode::ColumnStats => "Column Statistics (S to close)".to_string(), - }; - - let input_block = Block::default().borders(Borders::ALL).title(input_title); - - // Always get input text through the buffer API for consistency - let input_text_string = self.get_input_text(); - let input_text = match self.buffer().get_mode() { - AppMode::History => &self.history_state.search_query, - _ => &input_text_string, - }; - - let input_paragraph = match self.buffer().get_mode() { - AppMode::Command => { - match self.buffer().get_edit_mode() { - EditMode::SingleLine => { - // Use syntax highlighting for SQL command input with horizontal scrolling - let highlighted_line = - self.sql_highlighter.simple_sql_highlight(input_text); - Paragraph::new(Text::from(vec![highlighted_line])) - .block(input_block) - .scroll((0, self.get_horizontal_scroll_offset())) - } - EditMode::MultiLine => { - // MultiLine mode is no longer supported, always use single-line - let highlighted_line = - self.sql_highlighter.simple_sql_highlight(input_text); - Paragraph::new(Text::from(vec![highlighted_line])) - .block(input_block) - .scroll((0, self.get_horizontal_scroll_offset())) - } - } - } - _ => { - // Plain text for other modes - Paragraph::new(input_text.as_str()) - .block(input_block) - .style(match self.buffer().get_mode() { - AppMode::Results => Style::default().fg(Color::DarkGray), - AppMode::Search => Style::default().fg(Color::Yellow), - AppMode::Filter => Style::default().fg(Color::Cyan), - AppMode::FuzzyFilter => Style::default().fg(Color::Magenta), - AppMode::ColumnSearch => Style::default().fg(Color::Green), - AppMode::Help => Style::default().fg(Color::DarkGray), - AppMode::History => Style::default().fg(Color::Magenta), - AppMode::Debug => Style::default().fg(Color::Yellow), - AppMode::PrettyQuery => Style::default().fg(Color::Green), - AppMode::CacheList => Style::default().fg(Color::Cyan), - AppMode::JumpToRow => Style::default().fg(Color::Magenta), - AppMode::ColumnStats => Style::default().fg(Color::Cyan), - _ => Style::default(), - }) - .scroll((0, self.get_horizontal_scroll_offset())) - } - }; - - // Always render the input paragraph (single-line mode) - f.render_widget(input_paragraph, chunks[0]); - let results_area = chunks[1]; - - // Set cursor position for input modes - match self.buffer().get_mode() { - AppMode::Command => { - // Always use single-line cursor handling - // Calculate cursor position with horizontal scrolling - let inner_width = chunks[0].width.saturating_sub(2) as usize; - let cursor_pos = self.get_visual_cursor().1; // Get column position for single-line - let scroll_offset = self.get_horizontal_scroll_offset() as usize; - - // Calculate visible cursor position - if cursor_pos >= scroll_offset && cursor_pos < scroll_offset + inner_width { - let visible_pos = cursor_pos - scroll_offset; - f.set_cursor_position((chunks[0].x + visible_pos as u16 + 1, chunks[0].y + 1)); - } - } - AppMode::Search => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::Filter => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::FuzzyFilter => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::ColumnSearch => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::JumpToRow => { - f.set_cursor_position(( - chunks[0].x + self.jump_to_row_input.len() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::History => { - f.set_cursor_position(( - chunks[0].x + self.history_state.search_query.len() as u16 + 1, - chunks[0].y + 1, - )); - } - _ => {} - } - - // Results area - render based on mode to reduce complexity - match (&self.buffer().get_mode(), self.show_help) { - (_, true) => self.render_help(f, results_area), - (AppMode::History, false) => self.render_history(f, results_area), - (AppMode::Debug, false) => self.render_debug(f, results_area), - (AppMode::PrettyQuery, false) => self.render_pretty_query(f, results_area), - (AppMode::CacheList, false) => self.render_cache_list(f, results_area), - (AppMode::ColumnStats, false) => self.render_column_stats(f, results_area), - (_, false) if self.buffer().get_results().is_some() => { - // We need to work around the borrow checker here - // Calculate widths needs mutable self, but we also need to pass results - if let Some(results) = self.buffer().get_results() { - // Extract viewport info first - let terminal_height = results_area.height as usize; - let max_visible_rows = terminal_height.saturating_sub(3).max(10); - let total_rows = if let Some(filtered) = self.buffer().get_filtered_data() { - filtered.len() - } else { - results.data.len() - }; - let row_viewport_start = self - .buffer() - .get_scroll_offset() - .0 - .min(total_rows.saturating_sub(1)); - let row_viewport_end = (row_viewport_start + max_visible_rows).min(total_rows); - - // Calculate column widths based on viewport - self.calculate_viewport_column_widths(row_viewport_start, row_viewport_end); - } - - // Now render the table - if let Some(results) = self.buffer().get_results() { - self.render_table_immutable(f, results_area, results); - } - } - _ => { - // Simple placeholder - reduced text to improve rendering speed - let placeholder = Paragraph::new("Enter SQL query and press Enter\n\nTip: Use Tab for completion, Ctrl+R for history") - .block(Block::default().borders(Borders::ALL).title("Results")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(placeholder, results_area); - } - } - - // Render mode-specific status line - self.render_status_line(f, chunks[2]); - } - - fn render_status_line(&self, f: &mut Frame, area: Rect) { - // Determine the mode color - let (status_style, mode_color) = match self.buffer().get_mode() { - AppMode::Command => (Style::default().fg(Color::Green), Color::Green), - AppMode::Results => (Style::default().fg(Color::Blue), Color::Blue), - AppMode::Search => (Style::default().fg(Color::Yellow), Color::Yellow), - AppMode::Filter => (Style::default().fg(Color::Cyan), Color::Cyan), - AppMode::FuzzyFilter => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::ColumnSearch => (Style::default().fg(Color::Green), Color::Green), - AppMode::Help => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::History => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::Debug => (Style::default().fg(Color::Yellow), Color::Yellow), - AppMode::PrettyQuery => (Style::default().fg(Color::Green), Color::Green), - AppMode::CacheList => (Style::default().fg(Color::Cyan), Color::Cyan), - AppMode::JumpToRow => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::ColumnStats => (Style::default().fg(Color::Cyan), Color::Cyan), - }; - - let mode_indicator = match self.buffer().get_mode() { - AppMode::Command => "CMD", - AppMode::Results => "NAV", - AppMode::Search => "SEARCH", - AppMode::Filter => "FILTER", - AppMode::FuzzyFilter => "FUZZY", - AppMode::ColumnSearch => "COL", - AppMode::Help => "HELP", - AppMode::History => "HISTORY", - AppMode::Debug => "DEBUG", - AppMode::PrettyQuery => "PRETTY", - AppMode::CacheList => "CACHE", - AppMode::JumpToRow => "JUMP", - AppMode::ColumnStats => "STATS", - }; - - let mut spans = Vec::new(); - - // Mode indicator with color - spans.push(Span::styled( - format!("[{}]", mode_indicator), - Style::default().fg(mode_color).add_modifier(Modifier::BOLD), - )); - - // Show buffer information - { - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); - - // Show buffer indicator if multiple buffers - if total > 1 { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}/{}]", index + 1, total), - Style::default().fg(Color::Yellow), - )); - } - - // Show current buffer name - if let Some(buffer) = self.buffer_manager.current() { - spans.push(Span::raw(" ")); - let name = buffer.get_name(); - let modified = if buffer.is_modified() { "*" } else { "" }; - spans.push(Span::styled( - format!("{}{}", name, modified), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } - } - - // Get buffer name from the current buffer - let buffer_name = self.buffer().get_name(); - if !buffer_name.is_empty() && buffer_name != "[Buffer 1]" { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - buffer_name, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } else if self.buffer().is_csv_mode() && !self.buffer().get_table_name().is_empty() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - self.buffer().get_table_name(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } - - // Mode-specific information - match self.buffer().get_mode() { - AppMode::Command => { - // In command mode, show editing-related info - if !self.get_input_text().trim().is_empty() { - let (token_pos, total_tokens) = self.get_cursor_token_position(); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Token {}/{}", token_pos, total_tokens), - Style::default().fg(Color::DarkGray), - )); - - // Show current token if available - if let Some(token) = self.get_token_at_cursor() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}]", token), - Style::default().fg(Color::Cyan), - )); - } - - // Check for parser errors - if let Some(error_msg) = self.check_parser_error(&self.get_input_text()) { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("{} {}", self.config.display.icons.warning, error_msg), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )); - } - } - } - AppMode::Results => { - // In results mode, show navigation and data info - let total_rows = self.get_row_count(); - if total_rows > 0 { - let selected = self.table_state.selected().unwrap_or(0) + 1; - spans.push(Span::raw(" | ")); - - // Show selection mode - let mode_text = match self.selection_mode { - SelectionMode::Cell => "CELL", - SelectionMode::Row => "ROW", - }; - spans.push(Span::styled( - format!("[{}]", mode_text), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("Row {}/{}", selected, total_rows), - Style::default().fg(Color::White), - )); - - // Column information - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - if self.buffer().get_current_column() < headers.len() { - spans.push(Span::raw(" | Col: ")); - spans.push(Span::styled( - headers[self.buffer().get_current_column()], - Style::default().fg(Color::Cyan), - )); - - // Show pinned columns count if any - if !self.buffer().get_pinned_columns().clone().is_empty() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!( - "📌{}", - self.buffer().get_pinned_columns().clone().len() - ), - Style::default().fg(Color::Magenta), - )); - } - - // In cell mode, show the current cell value - if self.selection_mode == SelectionMode::Cell { - if let Some(selected_row) = self.table_state.selected() { - if let Some(row_data) = results.data.get(selected_row) { - if let Some(row_obj) = row_data.as_object() { - if let Some(value) = row_obj.get( - headers[self.buffer().get_current_column()], - ) { - let cell_value = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "NULL".to_string(), - other => other.to_string(), - }; - - // Truncate if too long - let display_value = if cell_value.len() > 30 - { - format!("{}...", &cell_value[..27]) - } else { - cell_value - }; - - spans.push(Span::raw(" = ")); - spans.push(Span::styled( - display_value, - Style::default().fg(Color::Yellow), - )); - } - } - } - } - } - } - } - } - } - - // Filter indicators - if self.buffer().is_fuzzy_filter_active() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Fuzzy: {}", self.buffer().get_fuzzy_filter_pattern()), - Style::default().fg(Color::Magenta), - )); - } else if self.get_filter_state().active { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Filter: {}", self.get_filter_state().pattern), - Style::default().fg(Color::Cyan), - )); - } - - // Show last yanked value - if let Some((col, val)) = &self.last_yanked { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - "Yanked: ", - Style::default().fg(Color::DarkGray), - )); - spans.push(Span::styled( - format!("{}={}", col, val), - Style::default().fg(Color::Green), - )); - } - } - } - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // Show the pattern being typed - always use input for consistency - let pattern = self.get_input_text(); - if !pattern.is_empty() { - spans.push(Span::raw(" | Pattern: ")); - spans.push(Span::styled(pattern, Style::default().fg(mode_color))); - } - } - _ => {} - } - - // Data source indicator (shown in all modes) - if let Some(source) = self.buffer().get_last_query_source() { - spans.push(Span::raw(" | ")); - let (icon, label, color) = match source.as_str() { - "cache" => ( - &self.config.display.icons.cache, - "CACHE".to_string(), - Color::Cyan, - ), - "file" | "FileDataSource" => ( - &self.config.display.icons.file, - "FILE".to_string(), - Color::Green, - ), - "SqlServerDataSource" => ( - &self.config.display.icons.database, - "SQL".to_string(), - Color::Blue, - ), - "PublicApiDataSource" => ( - &self.config.display.icons.api, - "API".to_string(), - Color::Yellow, - ), - _ => ( - &self.config.display.icons.api, - source.clone(), - Color::Magenta, - ), - }; - spans.push(Span::raw(format!("{} ", icon))); - spans.push(Span::styled(label, Style::default().fg(color))); - } else if self.buffer().is_csv_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::raw(&self.config.display.icons.file)); - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("CSV: {}", self.buffer().get_table_name()), - Style::default().fg(Color::Green), - )); - } else if self.buffer().is_cache_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::raw(&self.config.display.icons.cache)); - spans.push(Span::raw(" ")); - spans.push(Span::styled("CACHE", Style::default().fg(Color::Cyan))); - } - - // Global indicators (shown when active) - let case_insensitive = self.buffer().is_case_insensitive(); - if case_insensitive { - spans.push(Span::raw(" | ")); - // Use to_string() to ensure we get the actual string value - let icon = self.config.display.icons.case_insensitive.clone(); - spans.push(Span::styled( - format!("{} CASE", icon), - Style::default().fg(Color::Cyan), - )); - } - - if self.buffer().is_compact_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled("COMPACT", Style::default().fg(Color::Green))); - } - - if self.buffer().is_viewport_lock() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - &self.config.display.icons.lock, - Style::default().fg(Color::Magenta), - )); - } - - // Help shortcuts (right side) - let help_text = match self.buffer().get_mode() { - AppMode::Command => "Enter:Run | Tab:Complete | ↓:Results | F1:Help", - AppMode::Results => match self.selection_mode { - SelectionMode::Cell => "v:Row mode | y:Yank cell | ↑:Edit | F1:Help", - SelectionMode::Row => "v:Cell mode | y:Yank | f:Filter | ↑:Edit | F1:Help", - }, - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - "Enter:Apply | Esc:Cancel" - } - AppMode::Help - | AppMode::Debug - | AppMode::PrettyQuery - | AppMode::CacheList - | AppMode::ColumnStats => "Esc:Close", - AppMode::History => "Enter:Select | Esc:Cancel", - AppMode::JumpToRow => "Enter:Jump | Esc:Cancel", - }; - - // Calculate available space for help text - let current_length: usize = spans.iter().map(|s| s.content.len()).sum(); - let available_width = area.width.saturating_sub(4) as usize; // Account for borders - let help_length = help_text.len(); - - if current_length + help_length + 3 < available_width { - // Add spacing to right-align help text - let padding = available_width - current_length - help_length - 3; - spans.push(Span::raw(" ".repeat(padding))); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - help_text, - Style::default().fg(Color::DarkGray), - )); - } - - let status_line = Line::from(spans); - let status = Paragraph::new(status_line) - .block(Block::default().borders(Borders::ALL)) - .style(status_style); - f.render_widget(status, area); - } - - fn render_table_immutable(&self, f: &mut Frame, area: Rect, results: &QueryResponse) { - if results.data.is_empty() { - let empty = Paragraph::new("No results found") - .block(Block::default().borders(Borders::ALL).title("Results")) - .style(Style::default().fg(Color::Yellow)); - f.render_widget(empty, area); - return; - } - - // Get headers from first row - let headers: Vec<&str> = if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - obj.keys().map(|k| k.as_str()).collect() - } else { - vec![] - } - } else { - vec![] - }; - - // Calculate visible columns for virtual scrolling based on actual widths - let terminal_width = area.width as usize; - let available_width = terminal_width.saturating_sub(4); // Account for borders and padding - - // Split columns into pinned and scrollable - let mut pinned_headers: Vec<(usize, &str)> = Vec::new(); - let mut scrollable_indices: Vec = Vec::new(); - - for (i, header) in headers.iter().enumerate() { - if self.buffer().get_pinned_columns().contains(&i) { - pinned_headers.push((i, header)); - } else { - scrollable_indices.push(i); - } - } - - // Calculate space used by pinned columns - let mut pinned_width = 0; - for &(idx, _) in &pinned_headers { - let column_widths = self.buffer().get_column_widths().clone(); - if idx < column_widths.len() { - pinned_width += column_widths[idx] as usize; - } else { - pinned_width += 15; // Default width - } - } - - // Calculate how many scrollable columns can fit in remaining space - let remaining_width = available_width.saturating_sub(pinned_width); - let column_widths = self.buffer().get_column_widths().clone(); - let max_visible_scrollable_cols = if !column_widths.is_empty() { - let mut width_used = 0; - let mut cols_that_fit = 0; - - for &idx in &scrollable_indices { - if idx >= headers.len() { - break; - } - let col_width = if idx < column_widths.len() { - column_widths[idx] as usize - } else { - 15 - }; - if width_used + col_width <= remaining_width { - width_used += col_width; - cols_that_fit += 1; - } else { - break; - } - } - cols_that_fit.max(1) - } else { - // Fallback to old method if no calculated widths - let avg_col_width = 15; - (remaining_width / avg_col_width).max(1) - }; - - // Calculate viewport for scrollable columns based on current_column - let current_in_scrollable = scrollable_indices - .iter() - .position(|&x| x == self.buffer().get_current_column()); - let viewport_start = if let Some(pos) = current_in_scrollable { - if pos < max_visible_scrollable_cols / 2 { - 0 - } else if pos + max_visible_scrollable_cols / 2 >= scrollable_indices.len() { - scrollable_indices - .len() - .saturating_sub(max_visible_scrollable_cols) - } else { - pos.saturating_sub(max_visible_scrollable_cols / 2) - } - } else { - // Current column is pinned, use scroll offset - self.buffer().get_scroll_offset().1.min( - scrollable_indices - .len() - .saturating_sub(max_visible_scrollable_cols), - ) - }; - let viewport_end = - (viewport_start + max_visible_scrollable_cols).min(scrollable_indices.len()); - - // Build final list of visible columns (pinned + scrollable viewport) - let mut visible_columns: Vec<(usize, &str)> = Vec::new(); - visible_columns.extend(pinned_headers.iter().copied()); - for i in viewport_start..viewport_end { - let idx = scrollable_indices[i]; - visible_columns.push((idx, headers[idx])); - } - - // Only work with visible headers - let visible_headers: Vec<&str> = visible_columns.iter().map(|(_, h)| *h).collect(); - - // Calculate viewport dimensions FIRST before processing any data - let terminal_height = area.height as usize; - let max_visible_rows = terminal_height.saturating_sub(3).max(10); - - let total_rows = if let Some(filtered) = self.buffer().get_filtered_data() { - if self.buffer().is_fuzzy_filter_active() - && !self.buffer().get_fuzzy_filter_indices().clone().is_empty() - { - self.buffer().get_fuzzy_filter_indices().clone().len() - } else { - filtered.len() - } - } else { - results.data.len() - }; - - // Calculate row viewport - let row_viewport_start = self - .buffer() - .get_scroll_offset() - .0 - .min(total_rows.saturating_sub(1)); - let row_viewport_end = (row_viewport_start + max_visible_rows).min(total_rows); - - // Prepare table data (only visible rows AND columns) - let data_to_display = if let Some(filtered) = self.buffer().get_filtered_data() { - // Check if fuzzy filter is active - if self.buffer().is_fuzzy_filter_active() - && !self.buffer().get_fuzzy_filter_indices().clone().is_empty() - { - // Apply fuzzy filter on top of existing filter - let mut fuzzy_filtered = Vec::new(); - for &idx in &self.buffer().get_fuzzy_filter_indices().clone() { - if idx < filtered.len() { - fuzzy_filtered.push(filtered[idx].clone()); - } - } - - // Recalculate viewport for fuzzy filtered data - let fuzzy_total = fuzzy_filtered.len(); - let fuzzy_start = self - .buffer() - .get_scroll_offset() - .0 - .min(fuzzy_total.saturating_sub(1)); - let fuzzy_end = (fuzzy_start + max_visible_rows).min(fuzzy_total); - - fuzzy_filtered[fuzzy_start..fuzzy_end] - .iter() - .map(|row| { - visible_columns - .iter() - .map(|(idx, _)| row[*idx].clone()) - .collect() - }) - .collect() - } else { - // Apply both row and column viewport to filtered data - filtered[row_viewport_start..row_viewport_end] - .iter() - .map(|row| { - visible_columns - .iter() - .map(|(idx, _)| row[*idx].clone()) - .collect() - }) - .collect() - } - } else { - // Convert JSON data to string matrix (only visible rows AND columns) - results.data[row_viewport_start..row_viewport_end] - .iter() - .map(|item| { - if let Some(obj) = item.as_object() { - visible_columns - .iter() - .map(|(_, header)| match obj.get(*header) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "".to_string(), - Some(other) => other.to_string(), - None => "".to_string(), - }) - .collect() - } else { - vec![] - } - }) - .collect::>>() - }; - - // Create header row with sort indicators and column selection - let mut header_cells: Vec = Vec::new(); - - // Add row number header if enabled - if self.buffer().is_show_row_numbers() { - header_cells.push( - Cell::from("#").style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - ); - } - - // Add data headers - header_cells.extend(visible_columns.iter().map(|(actual_col_index, header)| { - let sort_indicator = if let Some(col) = self.sort_state.column { - if col == *actual_col_index { - match self.sort_state.order { - SortOrder::Ascending => " ↑", - SortOrder::Descending => " ↓", - SortOrder::None => "", - } - } else { - "" - } - } else { - "" - }; - - let column_indicator = if *actual_col_index == self.buffer().get_current_column() { - " [*]" - } else { - "" - }; - - // Add pin indicator for pinned columns - let pin_indicator = if self - .buffer() - .get_pinned_columns() - .contains(&*actual_col_index) - { - "📌 " - } else { - "" - }; - - let header_text = format!( - "{}{}{}{}", - pin_indicator, header, sort_indicator, column_indicator - ); - let mut style = Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD); - - // Highlight the current column - if *actual_col_index == self.buffer().get_current_column() { - style = style.bg(Color::DarkGray); - } - - Cell::from(header_text).style(style) - })); - - let selected_row = self.table_state.selected().unwrap_or(0); - - // Create data rows (already filtered to visible rows and columns) - let rows: Vec = data_to_display - .iter() - .enumerate() - .map(|(visible_row_idx, row)| { - let actual_row_idx = row_viewport_start + visible_row_idx; - let mut cells: Vec = Vec::new(); - - // Add row number if enabled - if self.buffer().is_show_row_numbers() { - let row_num = actual_row_idx + 1; // 1-based numbering - cells.push( - Cell::from(row_num.to_string()).style(Style::default().fg(Color::Magenta)), - ); - } - - // Add data cells - cells.extend(row.iter().enumerate().map(|(visible_col_idx, cell)| { - let actual_col_idx = visible_columns[visible_col_idx].0; - let mut style = Style::default(); - - // Cell mode highlighting - highlight only the selected cell - let is_selected_row = actual_row_idx == selected_row; - let is_selected_cell = - is_selected_row && actual_col_idx == self.buffer().get_current_column(); - - if self.selection_mode == SelectionMode::Cell { - // In cell mode, only highlight the specific cell - if is_selected_cell { - // Use a highlighted foreground instead of changing background - // This works better with various terminal color schemes - style = style - .fg(Color::Yellow) // Bright, readable color - .add_modifier(Modifier::BOLD | Modifier::UNDERLINED); - } - } else { - // In row mode, highlight the current column for all rows - if actual_col_idx == self.buffer().get_current_column() { - style = style.bg(Color::DarkGray); - } - } - - // Highlight search matches (override column highlight) - if let Some((match_row, match_col)) = self.buffer().get_current_match() { - if actual_row_idx == match_row && actual_col_idx == match_col { - style = style.bg(Color::Yellow).fg(Color::Black); - } - } - - // Highlight filter matches - if self.get_filter_state().active { - if let Some(ref regex) = self.get_filter_state().regex { - if regex.is_match(cell) { - style = style.fg(Color::Cyan); - } - } - } - - // Highlight fuzzy/exact filter matches - if self.buffer().is_fuzzy_filter_active() - && !self.buffer().get_fuzzy_filter_pattern().is_empty() - { - let pattern = &self.buffer().get_fuzzy_filter_pattern(); - let cell_matches = if pattern.starts_with('\'') && pattern.len() > 1 { - // Exact match highlighting - let exact_pattern = &pattern[1..]; - cell.to_lowercase().contains(&exact_pattern.to_lowercase()) - } else { - // Fuzzy match highlighting - check if this cell contributes to the fuzzy match - if let Some(score) = - SkimMatcherV2::default().fuzzy_match(cell, &pattern) - { - score > 0 - } else { - false - } - }; - - if cell_matches { - style = style.fg(Color::Magenta).add_modifier(Modifier::BOLD); - } - } - - Cell::from(cell.as_str()).style(style) - })); - - Row::new(cells) - }) - .collect(); - - // Calculate column constraints using optimal widths (only for visible columns) - let mut constraints: Vec = Vec::new(); - - // Add constraint for row number column if enabled - if self.buffer().is_show_row_numbers() { - // Calculate width needed for row numbers (max row count digits + padding) - let max_row_num = total_rows; - let row_num_width = max_row_num.to_string().len() as u16 + 2; - constraints.push(Constraint::Length(row_num_width.min(8))); // Cap at 8 chars - } - - // Add data column constraints - let column_widths = self.buffer().get_column_widths().clone(); - if !column_widths.is_empty() { - // Use calculated optimal widths for visible columns - constraints.extend(visible_columns.iter().map(|(col_idx, _)| { - if *col_idx < column_widths.len() { - Constraint::Length(column_widths[*col_idx]) - } else { - Constraint::Min(10) // Fallback - } - })); - } else { - // Fallback to minimum width if no calculated widths available - constraints.extend((0..visible_headers.len()).map(|_| Constraint::Min(10))); - } - - // Build the table with conditional row highlighting - let mut table = Table::new(rows, constraints) - .header(Row::new(header_cells).height(1)) - .block(Block::default() - .borders(Borders::ALL) - .title(format!("Results ({} rows) - {} pinned, {} visible of {} | Viewport rows {}-{} (selected: {}) | Use h/l to scroll", - total_rows, - self.buffer().get_pinned_columns().clone().len(), - visible_columns.len(), - headers.len(), - row_viewport_start + 1, - row_viewport_end, - selected_row + 1))); - - // Only apply row highlighting in row mode - if self.selection_mode == SelectionMode::Row { - table = table - .row_highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol("► "); - } else { - // In cell mode, no row highlighting - cell highlighting is handled above - table = table.highlight_symbol(" "); - } - - let mut table_state = self.table_state.clone(); - // Adjust table state to use relative position within the viewport - if let Some(selected) = table_state.selected() { - let relative_position = selected.saturating_sub(row_viewport_start); - table_state.select(Some(relative_position)); - } - f.render_stateful_widget(table, area, &mut table_state); - } - - fn render_help(&self, f: &mut Frame, area: Rect) { - // Create two-column layout - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - // Get help content from HelpText module - let left_content = HelpText::left_column(); - let right_content = HelpText::right_column(); - - // Calculate visible area for scrolling - let visible_height = area.height.saturating_sub(2) as usize; // Account for borders - let left_total_lines = left_content.len(); - let right_total_lines = right_content.len(); - let max_lines = left_total_lines.max(right_total_lines); - - // Apply scroll offset - let scroll_offset = self.help_scroll as usize; - - // Get visible portions with scrolling - let left_visible: Vec = left_content - .into_iter() - .skip(scroll_offset) - .take(visible_height) - .collect(); - - let right_visible: Vec = right_content - .into_iter() - .skip(scroll_offset) - .take(visible_height) - .collect(); - - // Create scroll indicator in title - let scroll_indicator = if max_lines > visible_height { - format!( - " (↓/↑ to scroll, {}/{})", - scroll_offset + 1, - max_lines.saturating_sub(visible_height) + 1 - ) - } else { - String::new() - }; - - // Render left column - let left_paragraph = Paragraph::new(Text::from(left_visible)) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("Help - Commands{}", scroll_indicator)), - ) - .style(Style::default()); - - // Render right column - let right_paragraph = Paragraph::new(Text::from(right_visible)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Help - Navigation & Features"), - ) - .style(Style::default()); - - f.render_widget(left_paragraph, chunks[0]); - f.render_widget(right_paragraph, chunks[1]); - } - - fn render_debug(&self, f: &mut Frame, area: Rect) { - self.debug_widget.render(f, area, AppMode::Debug); - } - - fn render_pretty_query(&self, f: &mut Frame, area: Rect) { - self.debug_widget.render(f, area, AppMode::PrettyQuery); - } - - fn render_history(&self, f: &mut Frame, area: Rect) { - if self.history_state.matches.is_empty() { - let no_history = if self.history_state.search_query.is_empty() { - "No command history found.\nExecute some queries to build history." - } else { - "No matches found for your search.\nTry a different search term." - }; - - let placeholder = Paragraph::new(no_history) - .block( - Block::default() - .borders(Borders::ALL) - .title("Command History"), - ) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(placeholder, area); - return; - } - - // Split the area to show selected command details - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(50), // History list - 50% of space - Constraint::Percentage(50), // Selected command preview - 50% of space - ]) - .split(area); - - self.render_history_list(f, chunks[0]); - self.render_selected_command_preview(f, chunks[1]); - } - - fn render_history_list(&self, f: &mut Frame, area: Rect) { - // Create more compact history list - just show essential info - let history_items: Vec = self - .history_state - .matches - .iter() - .enumerate() - .map(|(i, history_match)| { - let entry = &history_match.entry; - let is_selected = i == self.history_state.selected_index; - - let success_indicator = if entry.success { "✓" } else { "✗" }; - let time_ago = { - let elapsed = chrono::Utc::now() - entry.timestamp; - if elapsed.num_days() > 0 { - format!("{}d", elapsed.num_days()) - } else if elapsed.num_hours() > 0 { - format!("{}h", elapsed.num_hours()) - } else if elapsed.num_minutes() > 0 { - format!("{}m", elapsed.num_minutes()) - } else { - "now".to_string() - } - }; - - // Use more space for the command, less for metadata - let terminal_width = area.width as usize; - let metadata_space = 15; // Reduced metadata: " ✓ 2x 1h" - let available_for_command = terminal_width.saturating_sub(metadata_space).max(50); - - let command_text = if entry.command.len() > available_for_command { - format!( - "{}…", - &entry.command[..available_for_command.saturating_sub(1)] - ) - } else { - entry.command.clone() - }; - - let line_text = format!( - "{} {} {} {}x {}", - if is_selected { "►" } else { " " }, - command_text, - success_indicator, - entry.execution_count, - time_ago - ); - - let mut style = Style::default(); - if is_selected { - style = style.bg(Color::DarkGray).add_modifier(Modifier::BOLD); - } - if !entry.success { - style = style.fg(Color::Red); - } - - // Highlight matching characters for fuzzy search - if !history_match.indices.is_empty() && is_selected { - style = style.fg(Color::Yellow); - } - - Line::from(line_text).style(style) - }) - .collect(); - - let history_paragraph = Paragraph::new(history_items) - .block(Block::default().borders(Borders::ALL).title(format!( - "History ({} matches) - j/k to navigate, Enter to select", - self.history_state.matches.len() - ))) - .wrap(ratatui::widgets::Wrap { trim: false }); - - f.render_widget(history_paragraph, area); - } - - fn render_selected_command_preview(&self, f: &mut Frame, area: Rect) { - if let Some(selected_match) = self - .history_state - .matches - .get(self.history_state.selected_index) - { - let entry = &selected_match.entry; - - // Pretty format the SQL command - adjust compactness based on available space - use crate::recursive_parser::format_sql_pretty_compact; - - // Calculate how many columns we can fit per line - let available_width = area.width.saturating_sub(6) as usize; // Account for indentation and borders - let avg_col_width = 15; // Assume average column name is ~15 chars - let cols_per_line = (available_width / avg_col_width).max(3).min(12); // Between 3-12 columns per line - - let mut pretty_lines = format_sql_pretty_compact(&entry.command, cols_per_line); - - // If too many lines for the area, use a more compact format - let max_lines = area.height.saturating_sub(2) as usize; // Account for borders - if pretty_lines.len() > max_lines && cols_per_line < 12 { - // Try with more columns per line - pretty_lines = format_sql_pretty_compact(&entry.command, 15); - } - - // Convert to Text with syntax highlighting - let mut highlighted_lines = Vec::new(); - for line in pretty_lines { - highlighted_lines.push(self.sql_highlighter.simple_sql_highlight(&line)); - } - - let preview_text = Text::from(highlighted_lines); - - let duration_text = entry - .duration_ms - .map(|d| format!("{}ms", d)) - .unwrap_or_else(|| "?ms".to_string()); - - let success_text = if entry.success { - "✓ Success" - } else { - "✗ Failed" - }; - - let preview = Paragraph::new(preview_text) - .block(Block::default().borders(Borders::ALL).title(format!( - "Pretty SQL Preview: {} | {} | Used {}x", - success_text, duration_text, entry.execution_count - ))) - .scroll((0, 0)); // Allow scrolling if needed - - f.render_widget(preview, area); - } else { - let empty_preview = Paragraph::new("No command selected") - .block(Block::default().borders(Borders::ALL).title("Preview")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(empty_preview, area); - } - } - - fn handle_cache_command(&mut self, command: &str) -> Result<()> { - let parts: Vec<&str> = command.split_whitespace().collect(); - if parts.len() < 2 { - self.buffer_mut().set_status_message( - "Invalid cache command. Use :cache save or :cache load ".to_string(), - ); - return Ok(()); - } - - match parts[1] { - "save" => { - // Save last query results to cache with optional custom ID - if let Some(results) = self.buffer().get_results() { - let data_to_save = results.data.clone(); // Extract the data we need - let _ = results; // Explicitly drop the borrow - - if let Some(ref mut cache) = self.query_cache { - // Check if a custom ID is provided - let (custom_id, query) = if parts.len() > 2 { - // Check if the first word after "save" could be an ID (alphanumeric) - let potential_id = parts[2]; - if potential_id - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-') - && !potential_id.starts_with("SELECT") - && !potential_id.starts_with("select") - { - // First word is likely an ID - let id = Some(potential_id.to_string()); - let query = if parts.len() > 3 { - parts[3..].join(" ") - } else if let Some(last_entry) = - self.command_history.get_last_entry() - { - last_entry.command.clone() - } else { - self.buffer_mut() - .set_status_message("No query to cache".to_string()); - return Ok(()); - }; - (id, query) - } else { - // No ID provided, treat everything as the query - (None, parts[2..].join(" ")) - } - } else if let Some(last_entry) = self.command_history.get_last_entry() { - (None, last_entry.command.clone()) - } else { - self.buffer_mut() - .set_status_message("No query to cache".to_string()); - return Ok(()); - }; - - match cache.save_query(&query, &data_to_save, custom_id) { - Ok(id) => { - self.buffer_mut().set_status_message(format!( - "Query cached with ID: {} ({} rows)", - id, - data_to_save.len() - )); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to cache query: {}", e)); - } - } - } - } else { - self.buffer_mut().set_status_message( - "No results to cache. Execute a query first.".to_string(), - ); - } - } - "load" => { - if parts.len() < 3 { - self.buffer_mut() - .set_status_message("Usage: :cache load ".to_string()); - return Ok(()); - } - - if let Ok(id) = parts[2].parse::() { - if let Some(ref cache) = self.query_cache { - match cache.load_query(id) { - Ok((_query, data)) => { - self.buffer_mut().set_cached_data(Some(data.clone())); - self.buffer_mut().set_cache_mode(true); - self.buffer_mut().set_status_message(format!( - "Loaded cache ID {} with {} rows. Cache mode enabled.", - id, - data.len() - )); - - // Update parser with cached data schema if available - if let Some(first_row) = data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = - obj.keys().map(|k| k.to_string()).collect(); - self.hybrid_parser.update_single_table( - "cached_data".to_string(), - columns, - ); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to load cache: {}", e)); - } - } - } - } else { - self.buffer_mut() - .set_status_message("Invalid cache ID".to_string()); - } - } - "list" => { - self.buffer_mut().set_mode(AppMode::CacheList); - } - "clear" => { - self.buffer_mut().set_cache_mode(false); - self.buffer_mut().set_cached_data(None); - self.buffer_mut() - .set_status_message("Cache mode disabled".to_string()); - } - _ => { - self.buffer_mut().set_status_message( - "Unknown cache command. Use save, load, list, or clear.".to_string(), - ); - } - } - - Ok(()) - } - - fn handle_cache_list_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => { - self.buffer_mut().set_mode(AppMode::Command); - } - _ => {} - } - Ok(false) - } - - fn handle_column_stats_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match self.stats_widget.handle_key(key) { - StatsAction::Quit => return Ok(true), - StatsAction::Close => { - self.buffer_mut().set_column_stats(None); - self.buffer_mut().set_mode(AppMode::Results); - } - StatsAction::Continue | StatsAction::PassThrough => {} - } - Ok(false) - } - - fn handle_jump_to_row_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - self.buffer_mut().set_mode(AppMode::Results); - self.jump_to_row_input.clear(); - self.buffer_mut() - .set_status_message("Jump cancelled".to_string()); - } - KeyCode::Enter => { - if let Ok(row_num) = self.jump_to_row_input.parse::() { - if row_num > 0 { - let target_row = row_num - 1; // Convert to 0-based index - let max_row = self.get_current_data().map(|d| d.len()).unwrap_or(0); - - if target_row < max_row { - &mut self.table_state.select(Some(target_row)); - - // Adjust viewport to center the target row - let visible_rows = self.buffer().get_last_visible_rows(); - if visible_rows > 0 { - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = target_row.saturating_sub(visible_rows / 2); - self.buffer_mut().set_scroll_offset(offset); - } - - self.buffer_mut() - .set_status_message(format!("Jumped to row {}", row_num)); - } else { - self.buffer_mut().set_status_message(format!( - "Row {} out of range (max: {})", - row_num, max_row - )); - } - } - } - self.buffer_mut().set_mode(AppMode::Results); - self.jump_to_row_input.clear(); - } - KeyCode::Backspace => { - self.jump_to_row_input.pop(); - } - KeyCode::Char(c) if c.is_ascii_digit() => { - self.jump_to_row_input.push(c); - } - _ => {} - } - Ok(false) - } - - fn render_cache_list(&self, f: &mut Frame, area: Rect) { - if let Some(ref cache) = self.query_cache { - let cached_queries = cache.list_cached_queries(); - - if cached_queries.is_empty() { - let empty = Paragraph::new("No cached queries found.\n\nUse :cache save after running a query to cache results.") - .block(Block::default().borders(Borders::ALL).title("Cached Queries (F7)")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(empty, area); - return; - } - - // Create table of cached queries - let header_cells = vec!["ID", "Query", "Rows", "Cached At"] - .into_iter() - .map(|h| { - Cell::from(h).style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - }) - .collect::>(); - - let rows: Vec = cached_queries - .iter() - .map(|query| { - let cells = vec![ - Cell::from(query.id.to_string()), - Cell::from(if query.query_text.len() > 50 { - format!("{}...", &query.query_text[..47]) - } else { - query.query_text.clone() - }), - Cell::from(query.row_count.to_string()), - Cell::from(query.timestamp.format("%Y-%m-%d %H:%M:%S").to_string()), - ]; - Row::new(cells) - }) - .collect(); - - let table = Table::new( - rows, - vec![ - Constraint::Length(6), - Constraint::Percentage(50), - Constraint::Length(8), - Constraint::Length(20), - ], - ) - .header(Row::new(header_cells)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Cached Queries (F7) - Use :cache load to load"), - ) - .row_highlight_style(Style::default().bg(Color::DarkGray)); - - f.render_widget(table, area); - } else { - let error = Paragraph::new("Cache not available") - .block(Block::default().borders(Borders::ALL).title("Cache Error")) - .style(Style::default().fg(Color::Red)); - f.render_widget(error, area); - } - } - - fn render_column_stats(&self, f: &mut Frame, area: Rect) { - // Delegate to the stats widget - self.stats_widget.render(f, area, self.buffer()); - } - - // === Editor Widget Helper Methods === - // These methods handle the actions returned by the editor widget - - fn handle_execute_query(&mut self) -> Result { - // Get the current query text and execute it directly - let query = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query: {}", query); - if !query.is_empty() { - // Check for special commands - if query == ":help" { - self.show_help = true; - self.buffer_mut().set_mode(AppMode::Help); - self.buffer_mut() - .set_status_message("Help Mode - Press ESC to return".to_string()); - } else if query == ":exit" || query == ":quit" { - return Ok(true); - } else { - // Execute the SQL query - self.buffer_mut() - .set_status_message(format!("Processing query: '{}'", query)); - if let Err(e) = self.execute_query(&query) { - self.buffer_mut() - .set_status_message(format!("Error executing query: {}", e)); - } - // Don't clear input - preserve query for editing - } - } - Ok(false) // Continue running, don't exit - } - - fn handle_buffer_action(&mut self, action: BufferAction) -> Result { - match action { - BufferAction::NextBuffer => { - let message = self.buffer_handler.next_buffer(&mut self.buffer_manager); - debug!("{}", message); - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - Ok(false) - } - BufferAction::PreviousBuffer => { - let message = self - .buffer_handler - .previous_buffer(&mut self.buffer_manager); - debug!("{}", message); - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - Ok(false) - } - BufferAction::QuickSwitch => { - let message = self.buffer_handler.quick_switch(&mut self.buffer_manager); - debug!("{}", message); - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - Ok(false) - } - BufferAction::NewBuffer => { - let message = self - .buffer_handler - .new_buffer(&mut self.buffer_manager, &self.config); - debug!("{}", message); - Ok(false) - } - BufferAction::CloseBuffer => { - let (success, message) = self.buffer_handler.close_buffer(&mut self.buffer_manager); - debug!("{}", message); - Ok(!success) // Exit if we couldn't close (only one left) - } - BufferAction::ListBuffers => { - let buffer_list = self.buffer_handler.list_buffers(&self.buffer_manager); - // For now, just log the list - later we can show a popup - for line in &buffer_list { - debug!("{}", line); - } - Ok(false) - } - BufferAction::SwitchToBuffer(buffer_index) => { - let message = self - .buffer_handler - .switch_to_buffer(&mut self.buffer_manager, buffer_index); - debug!("{}", message); - - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - - Ok(false) - } - } - } - - fn handle_expand_asterisk(&mut self) -> Result { - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.expand_asterisk(&self.hybrid_parser) { - // Sync for rendering if needed - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - } - } - } - Ok(false) - } - - fn toggle_debug_mode(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - match buffer.get_mode() { - AppMode::Debug => { - buffer.set_mode(AppMode::Command); - } - _ => { - buffer.set_mode(AppMode::Debug); - // Generate full debug information like the original F5 handler - self.debug_current_buffer(); - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; - let query = self.get_input_text(); - - // Collect all needed data before mutable borrow - let buffer_names: Vec = self - .buffer_manager - .all_buffers() - .iter() - .map(|b| b.get_name()) - .collect(); - let buffer_count = self.buffer_manager.all_buffers().len(); - let buffer_index = self.buffer_manager.current_index(); - let api_url = self.api_client.base_url.clone(); - - // Generate debug info directly without buffer reference - let mut debug_info = self - .hybrid_parser - .get_detailed_debug_info(&query, cursor_pos); - - // Add input state - debug_info.push_str(&format!( - "\n========== INPUT STATE ==========\n\ - Input Value Length: {}\n\ - Cursor Position: {}\n\ - Visual Cursor: {}\n\ - Input Mode: Command\n", - query.len(), - cursor_pos, - visual_cursor - )); - - // Add buffer state info - debug_info.push_str(&format!( - "\n========== BUFFER MANAGER STATE ==========\n\ - Number of Buffers: {}\n\ - Current Buffer Index: {}\n\ - Buffer Names: {}\n", - buffer_count, - buffer_index, - buffer_names.join(", ") - )); - - // Add WHERE clause AST if needed - if query.to_lowercase().contains(" where ") { - let where_ast_info = match self.parse_where_clause_ast(&query) { - Ok(ast_str) => ast_str, - Err(e) => format!("\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {}\n", e) - }; - debug_info.push_str(&where_ast_info); - } - - // Add key chord handler debug info - debug_info.push_str("\n"); - debug_info.push_str(&self.key_chord_handler.format_debug_info()); - debug_info.push_str("========================================\n"); - - // Add trace logs from ring buffer - debug_info.push_str("\n========== TRACE LOGS ==========\n"); - debug_info.push_str("(Most recent at bottom, last 100 entries)\n"); - if let Some(ref log_buffer) = self.log_buffer { - let recent_logs = log_buffer.get_recent(100); - for entry in recent_logs { - debug_info.push_str(&entry.format_for_display()); - debug_info.push('\n'); - } - debug_info.push_str(&format!("Total log entries: {}\n", log_buffer.len())); - } else { - debug_info.push_str("Log buffer not initialized\n"); - } - debug_info.push_str("================================\n"); - - // Set the final content in debug widget - self.debug_widget.set_content(debug_info.clone()); - - // Try to copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_info) { - Ok(_) => { - // Verify clipboard write by reading it back - match clipboard.get_text() { - Ok(clipboard_content) => { - let clipboard_len = clipboard_content.len(); - if clipboard_content == debug_info { - self.buffer_mut().set_status_message(format!( - "DEBUG INFO copied to clipboard ({} chars)!", - clipboard_len - )); - } else { - self.buffer_mut().set_status_message(format!( - "Clipboard verification failed! Expected {} chars, got {} chars", - debug_info.len(), clipboard_len - )); - } - } - Err(e) => { - self.buffer_mut().set_status_message(format!( - "Debug info copied but verification failed: {}", - e - )); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - } - } - } - - fn show_pretty_query(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_mode(AppMode::PrettyQuery); - let query = buffer.get_input_text(); - self.debug_widget.generate_pretty_sql(&query); - } - } -} - -pub fn run_enhanced_tui_multi(api_url: &str, data_files: Vec<&str>) -> Result<()> { - let app = if !data_files.is_empty() { - // Load the first file using existing logic - let first_file = data_files[0]; - let extension = std::path::Path::new(first_file) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - - let mut app = match extension.to_lowercase().as_str() { - "csv" => EnhancedTuiApp::new_with_csv(first_file)?, - "json" => EnhancedTuiApp::new_with_json(first_file)?, - _ => { - return Err(anyhow::anyhow!( - "Unsupported file type: {}. Use .csv or .json files.", - first_file - )) - } - }; - - // Set the file path for the first buffer if we have multiple files - if data_files.len() > 1 { - if let Some(buffer) = app.buffer_manager.current_mut() { - buffer.set_file_path(Some(first_file.to_string())); - let filename = std::path::Path::new(first_file) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - buffer.set_name(filename.to_string()); - } - } - - // Load additional files into separate buffers - if data_files.len() > 1 { - for (_index, file_path) in data_files.iter().skip(1).enumerate() { - let extension = std::path::Path::new(file_path) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - - match extension.to_lowercase().as_str() { - "csv" | "json" => { - // Get config value before mutable borrow - let case_insensitive = app.config.behavior.case_insensitive_default; - - // Create a new buffer for each additional file - app.new_buffer(); - - // Get the current buffer and set it up - if let Some(buffer) = app.buffer_manager.current_mut() { - // Create and configure CSV client for this buffer - let mut csv_client = CsvApiClient::new(); - csv_client.set_case_insensitive(case_insensitive); - - // Get table name from file - let raw_name = std::path::Path::new(file_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - let table_name = EnhancedTuiApp::sanitize_table_name(&raw_name); - - // Load the data - if extension.to_lowercase() == "csv" { - if let Err(e) = csv_client.load_csv(file_path, &table_name) { - app.buffer_mut().set_status_message(format!( - "Error loading {}: {}", - file_path, e - )); - continue; - } - } else { - if let Err(e) = csv_client.load_json(file_path, &table_name) { - app.buffer_mut().set_status_message(format!( - "Error loading {}: {}", - file_path, e - )); - continue; - } - } - - // Set the CSV client and metadata in the buffer - buffer.set_csv_client(Some(csv_client)); - buffer.set_csv_mode(true); - buffer.set_table_name(table_name.clone()); - - info!(target: "buffer", "Loaded {} file '{}' into buffer {}: table='{}', case_insensitive={}", - extension.to_uppercase(), file_path, buffer.get_id(), table_name, case_insensitive); - - // Set query - let query = format!("SELECT * FROM {}", table_name); - buffer.set_input_text(query); - - // Store the file path and name - buffer.set_file_path(Some(file_path.to_string())); - let filename = std::path::Path::new(file_path) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - buffer.set_name(filename.to_string()); - } - } - _ => { - app.buffer_mut().set_status_message(format!( - "Skipping unsupported file: {}", - file_path - )); - continue; - } - } - } - - // Switch back to the first buffer - app.buffer_manager.switch_to(0); - - app.buffer_mut().set_status_message(format!( - "Loaded {} files into separate buffers. Use Alt+Tab to switch.", - data_files.len() - )); - } - - app - } else { - EnhancedTuiApp::new(api_url) - }; - - app.run() -} - -pub fn run_enhanced_tui(api_url: &str, data_file: Option<&str>) -> Result<()> { - // For backward compatibility, convert single file to vec and call multi version - let files = if let Some(file) = data_file { - vec![file] - } else { - vec![] - }; - run_enhanced_tui_multi(api_url, files) -} diff --git a/sql-cli/src/enhanced_tui_clean.rs b/sql-cli/src/enhanced_tui_clean.rs deleted file mode 100644 index 25ce92b0..00000000 --- a/sql-cli/src/enhanced_tui_clean.rs +++ /dev/null @@ -1,6059 +0,0 @@ -use crate::parser::SqlParser; -use crate::sql_highlighter::SqlHighlighter; -use anyhow::Result; -use chrono::Local; -use crossterm::{ - event::{ - self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, - }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Wrap}, - Frame, Terminal, -}; -use regex::Regex; -use serde_json::Value; -use sql_cli::api_client::{ApiClient, QueryResponse}; -use sql_cli::buffer::{ - AppMode, BufferAPI, BufferManager, ColumnStatistics, ColumnType, EditMode, SortOrder, SortState, -}; -use sql_cli::buffer_handler::BufferHandler; -use sql_cli::cache::QueryCache; -use sql_cli::config::Config; -use sql_cli::csv_datasource::CsvApiClient; -use sql_cli::cursor_manager::CursorManager; -use sql_cli::data_analyzer::DataAnalyzer; -use sql_cli::data_exporter::DataExporter; -use sql_cli::debug_info::{DebugInfo, DebugView}; -use sql_cli::debug_widget::DebugWidget; -use sql_cli::editor_widget::{BufferAction, EditorAction, EditorWidget}; -use sql_cli::help_text::HelpText; -use sql_cli::history::{CommandHistory, HistoryMatch}; -use sql_cli::hybrid_parser::HybridParser; -use sql_cli::key_chord_handler::{ChordResult, KeyChordHandler}; -use sql_cli::key_dispatcher::KeyDispatcher; -use sql_cli::logging::{get_log_buffer, LogRingBuffer}; -use sql_cli::stats_widget::{StatsAction, StatsWidget}; -use sql_cli::text_navigation::{TextEditor, TextNavigator}; -use sql_cli::where_ast::format_where_ast; -use sql_cli::where_parser::WhereParser; -use sql_cli::yank_manager::YankManager; -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::io; -use tracing::{debug, info, trace, warn}; -use tui_input::{backend::crossterm::EventHandler, Input}; - -// Using AppMode and EditMode from sql_cli::buffer module - -#[derive(Clone, PartialEq, Debug)] -enum SelectionMode { - Row, - Cell, -} - -// Using SortOrder and SortState from sql_cli::buffer module - -struct FuzzyFilterState { - pattern: String, - active: bool, - matcher: SkimMatcherV2, - filtered_indices: Vec, // Indices of rows that match -} - -impl Clone for FuzzyFilterState { - fn clone(&self) -> Self { - Self { - pattern: self.pattern.clone(), - active: self.active, - matcher: SkimMatcherV2::default(), // Create new matcher - filtered_indices: self.filtered_indices.clone(), - } - } -} - -#[derive(Clone)] -struct FilterState { - pattern: String, - regex: Option, - active: bool, -} - -#[derive(Clone)] -struct ColumnSearchState { - pattern: String, - matching_columns: Vec<(usize, String)>, // (index, column_name) - current_match: usize, // Index into matching_columns -} - -#[derive(Clone)] -struct SearchState { - pattern: String, - current_match: Option<(usize, usize)>, // (row, col) - matches: Vec<(usize, usize)>, - match_index: usize, -} - -#[derive(Clone)] -struct CompletionState { - suggestions: Vec, - current_index: usize, - last_query: String, - last_cursor_pos: usize, -} - -#[derive(Clone)] -struct HistoryState { - search_query: String, - matches: Vec, - selected_index: usize, -} - -pub struct EnhancedTuiApp { - api_client: ApiClient, - input: Input, - cursor_manager: CursorManager, // New: manages cursor/navigation logic - data_analyzer: DataAnalyzer, // New: manages data analysis/statistics - // results: Option, // MIGRATED to buffer system - table_state: TableState, - show_help: bool, - sql_parser: SqlParser, - hybrid_parser: HybridParser, - - // Configuration - config: Config, - - // Enhanced features - sort_state: SortState, - filter_state: FilterState, - search_state: SearchState, - completion_state: CompletionState, - history_state: HistoryState, - command_history: CommandHistory, - scroll_offset: (usize, usize), // (row, col) - current_column: usize, // For column-based operations - sql_highlighter: SqlHighlighter, - debug_widget: DebugWidget, - editor_widget: EditorWidget, - stats_widget: StatsWidget, - key_chord_handler: KeyChordHandler, // Manages key sequences and history - key_dispatcher: KeyDispatcher, // Maps keys to actions - help_scroll: u16, // Scroll offset for help page - input_scroll_offset: u16, // Horizontal scroll offset for input - - // Selection and clipboard - selection_mode: SelectionMode, // Row or Cell mode - last_yanked: Option<(String, String)>, // (description, value) of last yanked item - - // Buffer management (new - for supporting multiple files) - buffer_manager: BufferManager, - buffer_handler: BufferHandler, // Handles buffer operations like switching - // Cache - query_cache: Option, - // Data source tracking - - // Undo/redo and kill ring - undo_stack: Vec<(String, usize)>, // (text, cursor_pos) - redo_stack: Vec<(String, usize)>, - - // Viewport tracking - last_visible_rows: usize, // Track the last calculated viewport height - - // Display options - jump_to_row_input: String, // Input buffer for jump to row command - log_buffer: Option, // Ring buffer for debug logs -} - -impl EnhancedTuiApp { - // --- Buffer Compatibility Layer --- - // These methods provide a gradual migration path from direct field access to BufferAPI - - /// Get current buffer if available (for reading) - fn current_buffer(&self) -> Option<&dyn sql_cli::buffer::BufferAPI> { - self.buffer_manager - .current() - .map(|b| b as &dyn sql_cli::buffer::BufferAPI) - } - - /// Get current buffer (panics if none exists) - /// Use this when we know a buffer should always exist - fn buffer(&self) -> &dyn sql_cli::buffer::BufferAPI { - self.current_buffer() - .expect("No buffer available - this should not happen") - } - - // Note: current_buffer_mut removed - use buffer_manager.current_mut() directly - - /// Get current mutable buffer (panics if none exists) - /// Use this when we know a buffer should always exist - fn buffer_mut(&mut self) -> &mut sql_cli::buffer::Buffer { - self.buffer_manager - .current_mut() - .expect("No buffer available - this should not happen") - } - - // Note: edit_mode methods removed - use buffer directly - - // Helper to get input text from buffer or fallback to direct input - fn get_input_text(&self) -> String { - // For special modes that use the input field for their own purposes - match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes temporarily use the input field for their patterns - self.input.value().to_string() // TODO: Migrate to buffer-based input - } - _ => { - // All other modes use the buffer - self.buffer().get_input_text() - } - } - } - - // Helper to get cursor position from buffer or fallback to direct input - fn get_input_cursor(&self) -> usize { - // For special modes that use the input field directly - match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // These modes use the input field for their patterns - self.input.cursor() - } - _ => { - // All other modes use the buffer - self.buffer().get_input_cursor_position() - } - } - } - - // Helper to set input text through buffer and sync input field - fn set_input_text(&mut self, text: String) { - self.buffer_mut().set_input_text(text.clone()); - // Also sync cursor position to end of text - self.buffer_mut().set_input_cursor_position(text.len()); - - // Always update the input field for all modes - // TODO: Eventually migrate special modes to use buffer input - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - } - - // Helper to set input text with specific cursor position - fn set_input_text_with_cursor(&mut self, text: String, cursor_pos: usize) { - self.buffer_mut().set_input_text(text.clone()); - self.buffer_mut().set_input_cursor_position(cursor_pos); - - // Always update the input field for consistency - // TODO: Eventually migrate special modes to use buffer input - self.input = tui_input::Input::new(text).with_cursor(cursor_pos); - } - - // Helper to clear input - fn clear_input(&mut self) { - self.set_input_text(String::new()); - } - - // Helper to handle key events in the input - fn handle_input_key(&mut self, key: KeyEvent) -> bool { - // For special modes that handle input directly - match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - self.input.handle_event(&Event::Key(key)); - false - } - _ => { - // Route to buffer's input handling - self.buffer_mut().handle_input_key(key) - } - } - } - - // Helper to get visual cursor position (for rendering) - fn get_visual_cursor(&self) -> (usize, usize) { - // Get text and cursor from appropriate source based on mode - let (text, cursor) = match self.buffer().get_mode() { - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // Special modes use self.input directly - (self.input.value().to_string(), self.input.cursor()) - } - _ => { - // Other modes use buffer - ( - self.buffer().get_input_text(), - self.buffer().get_input_cursor_position(), - ) - } - }; - - let lines: Vec<&str> = text.split('\n').collect(); - let mut current_pos = 0; - for (row, line) in lines.iter().enumerate() { - if current_pos + line.len() >= cursor { - return (row, cursor - current_pos); - } - current_pos += line.len() + 1; // +1 for newline - } - (0, cursor) - } - - // Note: mode methods removed - use buffer directly - - fn get_filter_state(&self) -> &FilterState { - &self.filter_state - } - - fn get_filter_state_mut(&mut self) -> &mut FilterState { - &mut self.filter_state - } - - fn sanitize_table_name(name: &str) -> String { - // Replace spaces and other problematic characters with underscores - // to create SQL-friendly table names - // Examples: "Business Crime Borough Level" -> "Business_Crime_Borough_Level" - name.trim() - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '_' { - c - } else { - '_' - } - }) - .collect() - } - - pub fn new(api_url: &str) -> Self { - // Load configuration - let config = Config::load().unwrap_or_else(|_e| { - // Config loading error - using defaults - Config::default() - }); - - Self { - api_client: ApiClient::new(api_url), - input: Input::default(), - cursor_manager: CursorManager::new(), - data_analyzer: DataAnalyzer::new(), - // results: None, // MIGRATED to buffer system - table_state: TableState::default(), - show_help: false, - sql_parser: SqlParser::new(), - hybrid_parser: HybridParser::new(), - config: config.clone(), - - sort_state: SortState { - column: None, - order: SortOrder::None, - }, - filter_state: FilterState { - pattern: String::new(), - regex: None, - active: false, - }, - // fuzzy_filter_state: FuzzyFilterState { ... }, // MIGRATED to buffer system - search_state: SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }, - completion_state: CompletionState { - suggestions: Vec::new(), - current_index: 0, - last_query: String::new(), - last_cursor_pos: 0, - }, - history_state: HistoryState { - search_query: String::new(), - matches: Vec::new(), - selected_index: 0, - }, - command_history: CommandHistory::new().unwrap_or_default(), - scroll_offset: (0, 0), - current_column: 0, - sql_highlighter: SqlHighlighter::new(), - debug_widget: DebugWidget::new(), - editor_widget: EditorWidget::new(), - stats_widget: StatsWidget::new(), - key_chord_handler: KeyChordHandler::new(), - key_dispatcher: KeyDispatcher::new(), - help_scroll: 0, - input_scroll_offset: 0, - selection_mode: SelectionMode::Row, // Default to row mode - last_yanked: None, - // CSV fields now in Buffer - buffer_manager: { - // Initialize buffer manager with a default buffer - let mut manager = BufferManager::new(); - let mut buffer = sql_cli::buffer::Buffer::new(1); - // Sync initial settings from config - buffer.set_case_insensitive(config.behavior.case_insensitive_default); - buffer.set_compact_mode(config.display.compact_mode); - buffer.set_show_row_numbers(config.display.show_row_numbers); - manager.add_buffer(buffer); - manager - }, - buffer_handler: BufferHandler::new(), - query_cache: QueryCache::new().ok(), - // Cache fields now in Buffer - undo_stack: Vec::new(), - redo_stack: Vec::new(), - last_visible_rows: 30, // Default estimate - jump_to_row_input: String::new(), - log_buffer: get_log_buffer(), - } - } - - pub fn new_with_csv(csv_path: &str) -> Result { - let mut csv_client = CsvApiClient::new(); - - // First create the app to get its config - let mut app = Self::new(""); // Empty API URL for CSV mode - - // Use the app's config for consistency - csv_client.set_case_insensitive(app.config.behavior.case_insensitive_default); - - let raw_name = std::path::Path::new(csv_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - // Sanitize the table name to be SQL-friendly - let table_name = Self::sanitize_table_name(&raw_name); - - csv_client.load_csv(csv_path, &table_name)?; - - // Get schema from CSV - let schema = csv_client - .get_schema() - .ok_or_else(|| anyhow::anyhow!("Failed to get CSV schema"))?; - - // Replace the default buffer with a CSV buffer - { - // Clear all buffers and add a CSV buffer - app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_csv( - 1, - std::path::PathBuf::from(csv_path), - csv_client, - table_name.clone(), - ); - // Apply config settings to the buffer - use app's config - buffer.set_case_insensitive(app.config.behavior.case_insensitive_default); - buffer.set_compact_mode(app.config.display.compact_mode); - buffer.set_show_row_numbers(app.config.display.show_row_numbers); - - info!(target: "buffer", "Configured CSV buffer with: compact_mode={}, case_insensitive={}, show_row_numbers={}", - app.config.display.compact_mode, - app.config.behavior.case_insensitive_default, - app.config.display.show_row_numbers); - app.buffer_manager.add_buffer(buffer); - } - - // Update parser with CSV columns - if let Some(columns) = schema.get(&table_name) { - // Update the parser with CSV columns - app.hybrid_parser - .update_single_table(table_name.clone(), columns.clone()); - let display_msg = if raw_name != table_name { - format!( - "CSV loaded: '{}' as table '{}' with {} columns", - raw_name, - table_name, - columns.len() - ) - } else { - format!( - "CSV loaded: table '{}' with {} columns", - table_name, - columns.len() - ) - }; - app.buffer_mut().set_status_message(display_msg); - } - - // Auto-execute SELECT * FROM table_name to show data immediately (if configured) - let auto_query = format!("SELECT * FROM {}", table_name); - - // Populate the input field with the query for easy editing - app.set_input_text(auto_query.clone()); - - if app.config.behavior.auto_execute_on_load { - if let Err(e) = app.execute_query(&auto_query) { - // If auto-query fails, just log it in status but don't fail the load - app.buffer_mut().set_status_message(format!( - "CSV loaded: table '{}' ({} columns) - Note: {}", - table_name, - schema.get(&table_name).map(|c| c.len()).unwrap_or(0), - e - )); - } - } - - Ok(app) - } - - pub fn new_with_json(json_path: &str) -> Result { - let mut csv_client = CsvApiClient::new(); - - // First create the app to get its config - let mut app = Self::new(""); // Empty API URL for JSON mode - - // Use the app's config for consistency - csv_client.set_case_insensitive(app.config.behavior.case_insensitive_default); - - let raw_name = std::path::Path::new(json_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - // Sanitize the table name to be SQL-friendly - let table_name = Self::sanitize_table_name(&raw_name); - - csv_client.load_json(json_path, &table_name)?; - - // Get schema from JSON data - let schema = csv_client - .get_schema() - .ok_or_else(|| anyhow::anyhow!("Failed to get JSON schema"))?; - - // Replace the default buffer with a JSON buffer - { - // Clear all buffers and add a JSON buffer - app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_json( - 1, - std::path::PathBuf::from(json_path), - csv_client, - table_name.clone(), - ); - // Apply config settings to the buffer - use app's config - buffer.set_case_insensitive(app.config.behavior.case_insensitive_default); - buffer.set_compact_mode(app.config.display.compact_mode); - buffer.set_show_row_numbers(app.config.display.show_row_numbers); - - info!(target: "buffer", "Configured CSV buffer with: compact_mode={}, case_insensitive={}, show_row_numbers={}", - app.config.display.compact_mode, - app.config.behavior.case_insensitive_default, - app.config.display.show_row_numbers); - app.buffer_manager.add_buffer(buffer); - } - - // Buffer state is now initialized - - // Update parser with JSON columns - if let Some(columns) = schema.get(&table_name) { - app.hybrid_parser - .update_single_table(table_name.clone(), columns.clone()); - let display_msg = if raw_name != table_name { - format!( - "JSON loaded: '{}' as table '{}' with {} columns", - raw_name, - table_name, - columns.len() - ) - } else { - format!( - "JSON loaded: table '{}' with {} columns", - table_name, - columns.len() - ) - }; - app.buffer_mut().set_status_message(display_msg); - } - - // Auto-execute SELECT * FROM table_name to show data immediately (if configured) - let auto_query = format!("SELECT * FROM {}", table_name); - - // Populate the input field with the query for easy editing - app.set_input_text(auto_query.clone()); - - if app.config.behavior.auto_execute_on_load { - if let Err(e) = app.execute_query(&auto_query) { - // If auto-query fails, just log it in status but don't fail the load - app.buffer_mut().set_status_message(format!( - "JSON loaded: table '{}' ({} columns) - Note: {}", - table_name, - schema.get(&table_name).map(|c| c.len()).unwrap_or(0), - e - )); - } - } - - Ok(app) - } - - pub fn run(mut self) -> Result<()> { - // Setup terminal with error handling - if let Err(e) = enable_raw_mode() { - return Err(anyhow::anyhow!( - "Failed to enable raw mode: {}. Try running with --classic flag.", - e - )); - } - - let mut stdout = io::stdout(); - if let Err(e) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) { - let _ = disable_raw_mode(); - return Err(anyhow::anyhow!( - "Failed to setup terminal: {}. Try running with --classic flag.", - e - )); - } - - let backend = CrosstermBackend::new(stdout); - let mut terminal = match Terminal::new(backend) { - Ok(t) => t, - Err(e) => { - let _ = disable_raw_mode(); - return Err(anyhow::anyhow!( - "Failed to create terminal: {}. Try running with --classic flag.", - e - )); - } - }; - - let res = self.run_app(&mut terminal); - - // Always restore terminal, even on error - let _ = disable_raw_mode(); - let _ = execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ); - let _ = terminal.show_cursor(); - - match res { - Ok(_) => Ok(()), - Err(e) => Err(anyhow::anyhow!("TUI error: {}", e)), - } - } - - fn run_app(&mut self, terminal: &mut Terminal) -> Result<()> { - // Initial draw - terminal.draw(|f| self.ui(f))?; - - loop { - // Use blocking read for better performance - only process when there's an actual event - match event::read()? { - Event::Key(key) => { - // On Windows, filter out key release events - only handle key press - // This prevents double-triggering of toggles - if key.kind != crossterm::event::KeyEventKind::Press { - continue; - } - - let should_exit = match self.buffer().get_mode() { - AppMode::Command => self.handle_command_input(key)?, - AppMode::Results => self.handle_results_input(key)?, - AppMode::Search => self.handle_search_input(key)?, - AppMode::Filter => self.handle_filter_input(key)?, - AppMode::FuzzyFilter => self.handle_fuzzy_filter_input(key)?, - AppMode::ColumnSearch => self.handle_column_search_input(key)?, - AppMode::Help => self.handle_help_input(key)?, - AppMode::History => self.handle_history_input(key)?, - AppMode::Debug => self.handle_debug_input(key)?, - AppMode::PrettyQuery => self.handle_pretty_query_input(key)?, - AppMode::CacheList => self.handle_cache_list_input(key)?, - AppMode::JumpToRow => self.handle_jump_to_row_input(key)?, - AppMode::ColumnStats => self.handle_column_stats_input(key)?, - }; - - if should_exit { - break; - } - - // Only redraw after handling a key event - terminal.draw(|f| self.ui(f))?; - } - _ => { - // Ignore other events (mouse, resize, etc.) to reduce CPU - } - } - } - Ok(()) - } - - fn handle_command_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // NEW: Try editor widget first for high-level actions - let key_dispatcher = self.key_dispatcher.clone(); - // Handle editor widget actions by splitting the borrow - let editor_result = if let Some(buffer) = self.buffer_manager.current_mut() { - self.editor_widget - .handle_key(key.clone(), &key_dispatcher, buffer)? - } else { - EditorAction::PassToMainApp(key.clone()) - }; - - match editor_result { - EditorAction::Quit => return Ok(true), - EditorAction::ExecuteQuery => { - // Execute the current query - delegate to existing logic for now - return self.handle_execute_query(); - } - EditorAction::BufferAction(buffer_action) => { - return self.handle_buffer_action(buffer_action); - } - EditorAction::ExpandAsterisk => { - return self.handle_expand_asterisk(); - } - EditorAction::ShowHelp => { - self.show_help = true; - return Ok(false); - } - EditorAction::ShowDebug => { - // This is now handled by passing through to original F5 handler - return Ok(false); - } - EditorAction::ShowPrettyQuery => { - self.show_pretty_query(); - return Ok(false); - } - EditorAction::SwitchMode(mode) => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_mode(mode.clone()); - } - // Special handling for History mode - initialize history search - if mode == AppMode::History { - self.history_state.search_query.clear(); - self.update_history_matches(); - // Debug: log how many history entries we have - let total_entries = self.command_history.get_all().len(); - self.buffer_mut().set_status_message(format!( - "History search: {} total entries", - total_entries - )); - } - return Ok(false); - } - EditorAction::PassToMainApp(_) => { - // Fall through to original logic below - } - EditorAction::Continue => return Ok(false), - } - - // ORIGINAL LOGIC: Keep all existing logic as fallback - // Store old cursor position - let old_cursor = self.get_input_cursor(); - - // Also log to tracing - trace!(target: "input", "Key: {:?} Modifiers: {:?}", key.code, key.modifiers); - - // DON'T process chord handler in Command mode - yanking makes no sense when editing queries! - // The 'y' key should just type 'y' in the query editor. - - // Try dispatcher first for buffer operations and other actions - if let Some(action) = self.key_dispatcher.get_command_action(&key) { - match action { - "quit" => return Ok(true), - "next_buffer" => { - let message = self.buffer_handler.next_buffer(&mut self.buffer_manager); - debug!("{}", message); - return Ok(false); - } - "previous_buffer" => { - let message = self - .buffer_handler - .previous_buffer(&mut self.buffer_manager); - debug!("{}", message); - return Ok(false); - } - "quick_switch_buffer" => { - let message = self.buffer_handler.quick_switch(&mut self.buffer_manager); - debug!("{}", message); - return Ok(false); - } - "new_buffer" => { - let message = self - .buffer_handler - .new_buffer(&mut self.buffer_manager, &self.config); - debug!("{}", message); - return Ok(false); - } - "close_buffer" => { - let (success, message) = - self.buffer_handler.close_buffer(&mut self.buffer_manager); - debug!("{}", message); - return Ok(!success); // Exit if we couldn't close (only one left) - } - "list_buffers" => { - let buffer_list = self.buffer_handler.list_buffers(&self.buffer_manager); - // For now, just log the list - later we can show a popup - for line in &buffer_list { - debug!("{}", line); - } - return Ok(false); - } - action if action.starts_with("switch_to_buffer_") => { - if let Some(buffer_num_str) = action.strip_prefix("switch_to_buffer_") { - if let Ok(buffer_num) = buffer_num_str.parse::() { - let message = self - .buffer_handler - .switch_to_buffer(&mut self.buffer_manager, buffer_num - 1); // Convert to 0-based - debug!("{}", message); - } - } - return Ok(false); - } - "expand_asterisk" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.expand_asterisk(&self.hybrid_parser) { - // Sync for rendering if needed - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - } - } - } - return Ok(false); - } - // "move_to_line_start" and "move_to_line_end" now handled by editor_widget - "delete_word_backward" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.delete_word_backward(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "delete_word_forward" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.delete_word_forward(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "kill_line" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.kill_line(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "kill_line_backward" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - buffer.kill_line_backward(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "move_word_backward" => { - self.move_cursor_word_backward(); - return Ok(false); - } - "move_word_forward" => { - self.move_cursor_word_forward(); - return Ok(false); - } - "jump_to_prev_token" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.jump_to_prev_token(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - "jump_to_next_token" => { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.jump_to_next_token(); - // Sync for rendering - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - return Ok(false); - } - _ => {} // Fall through to hardcoded handling - } - } - - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - // KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::ALT) && key.modifiers.contains(KeyModifiers::SHIFT) => { - // // Alt+Shift+D - new DataTable buffer (for testing) - disabled during revert - // self.new_datatable_buffer(); - // } - KeyCode::F(1) | KeyCode::Char('?') => { - self.show_help = !self.show_help; - let mode = if self.show_help { - AppMode::Help - } else { - AppMode::Command - }; - self.buffer_mut().set_mode(mode); - } - KeyCode::F(3) => { - // F3 no longer toggles modes - always stay in single-line mode - self.buffer_mut().set_status_message( - "Multi-line mode has been removed. Use F6 for pretty print.".to_string(), - ); - } - KeyCode::F(7) => { - // F7 - Toggle cache mode or show cache list - if self.buffer().is_cache_mode() { - self.buffer_mut().set_mode(AppMode::CacheList); - } else { - self.buffer_mut().set_mode(AppMode::CacheList); - } - } - KeyCode::Enter => { - // Always use single-line mode handling - let query = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query: {}", query); - - if !query.is_empty() { - // Check for special commands - if query == ":help" { - self.show_help = true; - self.buffer_mut().set_mode(AppMode::Help); - self.buffer_mut() - .set_status_message("Help Mode - Press ESC to return".to_string()); - } else if query == ":exit" || query == ":quit" { - return Ok(true); - } else if query == ":tui" { - // Already in TUI mode - self.buffer_mut() - .set_status_message("Already in TUI mode".to_string()); - } else if query.starts_with(":cache ") { - self.handle_cache_command(&query)?; - } else { - self.buffer_mut() - .set_status_message(format!("Processing query: '{}'", query)); - self.execute_query(&query)?; - } - } else { - self.buffer_mut() - .set_status_message("Empty query - please enter a SQL command".to_string()); - } - } - KeyCode::Tab => { - // Tab completion works in both modes - // Always use single-line completion - self.apply_completion() - } - // Ctrl+R is now handled by the editor widget above - // History navigation - Ctrl+P or Alt+Up - KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to previous command in history - // Get history entries first, before mutable borrow - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_up(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Debug: show what we got from history - let debug_msg = if text.is_empty() { - "History navigation returned empty text!".to_string() - } else { - format!( - "History: {}", - if text.len() > 50 { - format!("{}...", &text[..50]) - } else { - text.clone() - } - ) - }; - - // Update the appropriate input field based on edit mode - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut().set_status_message(debug_msg); - } - } - } - // History navigation - Ctrl+N or Alt+Down - KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Navigate to next command in history - // Get history entries first, before mutable borrow - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_down(&history_commands) { - // Sync the input field with buffer (for now, until we complete migration) - let text = buffer.get_input_text(); - - // Update the appropriate input field based on edit mode - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut() - .set_status_message("Next command from history".to_string()); - } - } - } - // Alternative: Alt+Up for history previous (in case Ctrl+P is intercepted) - KeyCode::Up if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_up(&history_commands) { - let text = buffer.get_input_text(); - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut() - .set_status_message("Previous command (Alt+Up)".to_string()); - } - } - } - // Alternative: Alt+Down for history next - KeyCode::Down if key.modifiers.contains(KeyModifiers::ALT) => { - let history_entries = self.command_history.get_navigation_entries(); - let history_commands: Vec = - history_entries.iter().map(|e| e.command.clone()).collect(); - - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.navigate_history_down(&history_commands) { - let text = buffer.get_input_text(); - // Always use single-line mode - self.input = tui_input::Input::new(text.clone()).with_cursor(text.len()); - self.buffer_mut() - .set_status_message("Next command (Alt+Down)".to_string()); - } - } - } - KeyCode::F(8) => { - // Toggle case-insensitive string comparisons - let current = self.buffer().is_case_insensitive(); - self.buffer_mut().set_case_insensitive(!current); - - // Update CSV client if in CSV mode - // Update CSV client if in CSV mode - if let Some(csv_client) = self.buffer_mut().get_csv_client_mut() { - csv_client.set_case_insensitive(!current); - } - - self.buffer_mut().set_status_message(format!( - "Case-insensitive string comparisons: {}", - if !current { "ON" } else { "OFF" } - )); - } - KeyCode::F(9) => { - // F9 as alternative for kill line (for terminals that intercept Ctrl+K) - self.kill_line(); - let message = if !self.buffer().is_kill_ring_empty() { - format!( - "Killed to end of line ('{}' saved to kill ring)", - self.buffer().get_kill_ring() - ) - } else { - "Killed to end of line".to_string() - }; - self.buffer_mut().set_status_message(message); - } - KeyCode::F(10) => { - // F10 as alternative for kill line backward (for consistency with F9) - self.kill_line_backward(); - let message = if !self.buffer().is_kill_ring_empty() { - format!( - "Killed to beginning of line ('{}' saved to kill ring)", - self.buffer().get_kill_ring() - ) - } else { - "Killed to beginning of line".to_string() - }; - self.buffer_mut().set_status_message(message); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line - delete from cursor to end of line - self.buffer_mut() - .set_status_message("Ctrl+K pressed - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::ALT) => { - // Alternative: Alt+K for kill line (for terminals that intercept Ctrl+K) - self.buffer_mut() - .set_status_message("Alt+K - killing to end of line".to_string()); - self.kill_line(); - } - KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Kill line backward - delete from cursor to beginning of line - self.kill_line_backward(); - } - // Ctrl+Z (undo) now handled by editor_widget - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Yank - paste from kill ring - self.yank(); - } - KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Paste from system clipboard - self.paste_from_clipboard(); - } - KeyCode::Char('[') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to previous SQL token - self.jump_to_prev_token(); - } - KeyCode::Char(']') if key.modifiers.contains(KeyModifiers::ALT) => { - // Jump to next SQL token - self.jump_to_next_token(); - } - KeyCode::Left if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move backward one word - self.move_cursor_word_backward(); - } - KeyCode::Right if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Move forward one word - self.move_cursor_word_forward(); - } - KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move backward one word (alt+b like in bash) - self.move_cursor_word_backward(); - } - KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::ALT) => { - // Move forward one word (alt+f like in bash) - self.move_cursor_word_forward(); - } - KeyCode::Down - if self.buffer().get_results().is_some() - && self.buffer().get_edit_mode() == EditMode::SingleLine => - { - self.buffer_mut().set_mode(AppMode::Results); - // Restore previous position or default to 0 - let row = self.buffer().get_last_results_row().unwrap_or(0); - &mut self.table_state.select(Some(row)); - - // Restore the exact scroll offset from when we left - let last_offset = self.buffer().get_last_scroll_offset(); - self.buffer_mut().set_scroll_offset(last_offset); - } - KeyCode::F(5) => { - // Generate full debug information - self.debug_current_buffer(); - - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; - let query = self.get_input_text(); - - // Collect all needed data before mutable borrow - let buffer_names: Vec = self - .buffer_manager - .all_buffers() - .iter() - .map(|b| b.get_name()) - .collect(); - let buffer_count = self.buffer_manager.all_buffers().len(); - let buffer_index = self.buffer_manager.current_index(); - let api_url = self.api_client.base_url.clone(); - - // Generate debug info directly without buffer reference - let mut debug_info = self - .hybrid_parser - .get_detailed_debug_info(&query, cursor_pos); - - // Add input state - debug_info.push_str(&format!( - "\n========== INPUT STATE ==========\n\ - Input Value Length: {}\n\ - Cursor Position: {}\n\ - Visual Cursor: {}\n\ - Input Mode: Command\n", - query.len(), - cursor_pos, - visual_cursor - )); - - // Add buffer state info - debug_info.push_str(&format!( - "\n========== BUFFER MANAGER STATE ==========\n\ - Number of Buffers: {}\n\ - Current Buffer Index: {}\n\ - Buffer Names: {}\n", - buffer_count, - buffer_index, - buffer_names.join(", ") - )); - - // Add WHERE clause AST if needed - if query.to_lowercase().contains(" where ") { - let where_ast_info = match self.parse_where_clause_ast(&query) { - Ok(ast_str) => ast_str, - Err(e) => format!("\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {}\n", e) - }; - debug_info.push_str(&where_ast_info); - } - - // Add key chord handler debug info - debug_info.push_str("\n"); - debug_info.push_str(&self.key_chord_handler.format_debug_info()); - debug_info.push_str("========================================\n"); - - // Add trace logs from ring buffer - debug_info.push_str("\n========== TRACE LOGS ==========\n"); - debug_info.push_str("(Most recent at bottom, last 100 entries)\n"); - if let Some(ref log_buffer) = self.log_buffer { - let recent_logs = log_buffer.get_recent(100); - for entry in recent_logs { - debug_info.push_str(&entry.format_for_display()); - debug_info.push('\n'); - } - debug_info.push_str(&format!("Total log entries: {}\n", log_buffer.len())); - } else { - debug_info.push_str("Log buffer not initialized\n"); - } - debug_info.push_str("================================\n"); - - // Set the final content in debug widget - self.debug_widget.set_content(debug_info.clone()); - - // Try to copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_info) { - Ok(_) => { - // Verify clipboard write by reading it back - match clipboard.get_text() { - Ok(clipboard_content) => { - let clipboard_len = clipboard_content.len(); - if clipboard_content == debug_info { - self.buffer_mut().set_status_message(format!( - "DEBUG INFO copied to clipboard ({} chars)!", - clipboard_len - )); - } else { - self.buffer_mut().set_status_message(format!( - "Clipboard verification failed! Expected {} chars, got {} chars", - debug_info.len(), clipboard_len - )); - } - } - Err(e) => { - self.buffer_mut().set_status_message(format!( - "Debug info copied but verification failed: {}", - e - )); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Can't access clipboard: {}", e)); - } - } - - self.buffer_mut().set_mode(AppMode::Debug); - } - KeyCode::F(6) => { - // Pretty print query view - let query = self.get_input_text(); - if !query.trim().is_empty() { - self.debug_widget.generate_pretty_sql(&query); - self.buffer_mut().set_mode(AppMode::PrettyQuery); - self.buffer_mut().set_status_message( - "Pretty query view (press Esc or q to return)".to_string(), - ); - } else { - self.buffer_mut() - .set_status_message("No query to format".to_string()); - } - } - _ => { - // Use the new helper to handle input keys through buffer - self.handle_input_key(key); - - // Clear completion state when typing other characters - self.completion_state.suggestions.clear(); - self.completion_state.current_index = 0; - - // Always use single-line completion - self.handle_completion() - } - } - - // Update horizontal scroll if cursor moved - if self.get_input_cursor() != old_cursor { - self.update_horizontal_scroll(120); // Assume reasonable terminal width, will be adjusted in render - } - - Ok(false) - } - - fn handle_results_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - debug!( - "handle_results_input: key={:?}, selection_mode={:?}", - key, self.selection_mode - ); - - // Debug uppercase G specifically - if matches!(key.code, KeyCode::Char('G')) { - debug!("Detected uppercase G key press!"); - } - - // In cell mode, skip chord handler for 'y' key - handle it directly - // Also skip 'G' as it's a single key action, not a chord - let should_skip_chord = (matches!(self.selection_mode, SelectionMode::Cell) - && matches!(key.code, KeyCode::Char('y'))) - || matches!(key.code, KeyCode::Char('G')); - - let chord_result = if should_skip_chord { - debug!("Skipping chord handler for key {:?}", key.code); - // Still log the key press even when skipping chord handler - self.key_chord_handler.log_key_press(&key); - ChordResult::SingleKey(key.clone()) - } else { - // Process key through chord handler - self.key_chord_handler.process_key(key.clone()) - }; - - // Handle chord results - match chord_result { - ChordResult::CompleteChord(action) => { - // Handle completed chord actions - match action.as_str() { - "yank_row" => { - self.yank_row(); - return Ok(false); - } - "yank_column" => { - self.yank_column(); - return Ok(false); - } - "yank_all" => { - self.yank_all(); - return Ok(false); - } - "yank_cell" => { - self.yank_cell(); - return Ok(false); - } - _ => { - // Unknown action, continue with normal key handling - } - } - } - ChordResult::PartialChord(description) => { - // Update status to show chord mode - self.buffer_mut().set_status_message(description); - return Ok(false); - } - ChordResult::Cancelled => { - self.buffer_mut() - .set_status_message("Chord cancelled".to_string()); - return Ok(false); - } - ChordResult::SingleKey(_) => { - // Continue with normal key handling - } - } - - // Use dispatcher to get action first - if let Some(action) = self.key_dispatcher.get_results_action(&key) { - debug!("Dispatcher returned action '{}' for key {:?}", action, key); - match action { - "quit" => return Ok(true), - "exit_results_mode" => { - // Save current position before switching to Command mode - if let Some(selected) = self.table_state.selected() { - self.buffer_mut().set_last_results_row(Some(selected)); - let scroll_offset = self.buffer().get_scroll_offset(); - self.buffer_mut().set_last_scroll_offset(scroll_offset); - } - self.buffer_mut().set_mode(AppMode::Command); - &mut self.table_state.select(None); - } - "next_row" => self.next_row(), - "previous_row" => self.previous_row(), - "move_column_left" => self.move_column_left(), - "move_column_right" => self.move_column_right(), - "goto_first_row" => self.goto_first_row(), - "goto_last_row" => { - debug!("Executing goto_last_row action"); - self.goto_last_row(); - } - "goto_first_column" => self.goto_first_column(), - "goto_last_column" => self.goto_last_column(), - "page_up" => self.page_up(), - "page_down" => self.page_down(), - "start_search" => { - // Save SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::Search); - self.buffer_mut().set_search_pattern(String::new()); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - "start_column_search" => { - // Save current SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::ColumnSearch); - self.buffer_mut().set_column_search_pattern(String::new()); - self.buffer_mut().set_column_search_matches(Vec::new()); - self.buffer_mut().set_column_search_current_match(0); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - "start_filter" => { - self.buffer_mut().set_mode(AppMode::Filter); - self.get_filter_state_mut().pattern.clear(); - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - "start_fuzzy_filter" => { - self.buffer_mut().set_mode(AppMode::FuzzyFilter); - self.buffer_mut().set_fuzzy_filter_pattern(String::new()); - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - "sort_by_column" => self.sort_by_column(self.buffer().get_current_column()), - "show_column_stats" => self.calculate_column_statistics(), - "next_search_match" => self.next_search_match(), - "previous_search_match" => self.previous_search_match(), - "toggle_compact_mode" => { - let current_mode = self.buffer().is_compact_mode(); - self.buffer_mut().set_compact_mode(!current_mode); - let message = if !current_mode { - "Compact mode: ON (reduced padding, more columns visible)".to_string() - } else { - "Compact mode: OFF (normal padding)".to_string() - }; - self.buffer_mut().set_status_message(message); - } - "toggle_row_numbers" => { - let current_mode = self.buffer().is_show_row_numbers(); - self.buffer_mut().set_show_row_numbers(!current_mode); - let message = if !current_mode { - "Row numbers: ON".to_string() - } else { - "Row numbers: OFF".to_string() - }; - self.buffer_mut().set_status_message(message); - } - "jump_to_row" => { - self.buffer_mut().set_mode(AppMode::JumpToRow); - self.jump_to_row_input.clear(); - self.buffer_mut() - .set_status_message("Enter row number:".to_string()); - } - "pin_column" => self.toggle_column_pin(), - "clear_pins" => self.clear_all_pinned_columns(), - "toggle_selection_mode" => { - self.selection_mode = match self.selection_mode { - SelectionMode::Row => { - self.buffer_mut().set_status_message( - "Cell mode - Navigate to select individual cells".to_string(), - ); - SelectionMode::Cell - } - SelectionMode::Cell => { - self.buffer_mut().set_status_message( - "Row mode - Navigate to select rows".to_string(), - ); - SelectionMode::Row - } - }; - return Ok(false); // Return to prevent duplicate handling - } - "export_to_csv" => self.export_to_csv(), - "export_to_json" => self.export_to_json(), - "toggle_help" => { - if self.buffer().get_mode() == AppMode::Help { - self.buffer_mut().set_mode(AppMode::Results); - } else { - self.buffer_mut().set_mode(AppMode::Help); - } - } - "toggle_debug" => { - // Debug mode - show buffer state and parser information - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; // Get column position for single-line - let query = self.get_input_text(); - - // Create debug info showing buffer state - let mut debug_info = String::new(); - debug_info.push_str("=== Debug Information (Results Mode) ===\n\n"); - - // Add current query info - debug_info.push_str("Current Query:\n"); - debug_info.push_str(&format!(" Query: '{}'\n", query)); - debug_info.push_str(&format!(" Cursor Position: {}\n", cursor_pos)); - debug_info.push_str(&format!(" Visual Cursor: {}\n", visual_cursor)); - debug_info.push_str("\n"); - - // Add buffer manager info - debug_info.push_str("=== Buffer Manager ===\n"); - debug_info.push_str(&format!( - " Total Buffers: {}\n", - self.buffer_manager.all_buffers().len() - )); - debug_info.push_str(&format!( - " Current Buffer Index: {}\n", - self.buffer_manager.current_index() - )); - - // Add current buffer debug dump - if let Some(buffer) = self.buffer_manager.current() { - debug_info.push_str("\n=== Current Buffer State ===\n"); - debug_info.push_str(&buffer.debug_dump()); - } - - self.debug_widget.set_content(debug_info); - self.buffer_mut().set_mode(AppMode::Debug); - self.buffer_mut() - .set_status_message("Debug mode - Press 'q' or ESC to return".to_string()); - } - "toggle_case_insensitive" => { - // Toggle case-insensitive string comparisons - let current = self.buffer().is_case_insensitive(); - self.buffer_mut().set_case_insensitive(!current); - - // Update CSV client if in CSV mode - if let Some(csv_client) = self.buffer_mut().get_csv_client_mut() { - csv_client.set_case_insensitive(!current); - } - - self.buffer_mut().set_status_message(format!( - "Case-insensitive string comparisons: {}", - if !current { "ON" } else { "OFF" } - )); - } - _ => { - // Action not recognized, continue to handle key directly - } - } - } - - // Fall back to direct key handling for special cases not in dispatcher - match key.code { - KeyCode::Char(' ') => { - // Toggle viewport lock with Space - let current_lock = self.buffer().is_viewport_lock(); - self.buffer_mut().set_viewport_lock(!current_lock); - if self.buffer().is_viewport_lock() { - // Lock to current position in viewport (middle of screen) - let visible_rows = self.buffer().get_last_visible_rows(); - self.buffer_mut() - .set_viewport_lock_row(Some(visible_rows / 2)); - self.buffer_mut().set_status_message(format!( - "Viewport lock: ON (anchored at row {} of viewport)", - visible_rows / 2 + 1 - )); - } else { - self.buffer_mut().set_viewport_lock_row(None); - self.buffer_mut() - .set_status_message("Viewport lock: OFF (normal scrolling)".to_string()); - } - } - KeyCode::PageDown | KeyCode::Char('f') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.page_down(); - } - KeyCode::PageUp | KeyCode::Char('b') - if key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.page_up(); - } - // Search functionality - KeyCode::Char('/') => { - // Save SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::Search); - self.buffer_mut().set_search_pattern(String::new()); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - // Column navigation/search functionality (backslash like vim reverse search) - KeyCode::Char('\\') => { - // Save current SQL query before switching modes - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - self.buffer_mut().set_mode(AppMode::ColumnSearch); - self.buffer_mut().set_column_search_pattern(String::new()); - self.buffer_mut().set_column_search_matches(Vec::new()); - self.buffer_mut().set_column_search_current_match(0); - - // Only clear the UI input field, not the buffer's stored text - self.input = tui_input::Input::default(); - } - KeyCode::Char('n') => { - self.next_search_match(); - } - KeyCode::Char('N') if key.modifiers.contains(KeyModifiers::SHIFT) => { - // Only for search navigation when Shift is held - if !self.buffer().get_search_pattern().is_empty() { - self.previous_search_match(); - } else { - // Toggle row numbers display - let current = self.buffer().is_show_row_numbers(); - self.buffer_mut().set_show_row_numbers(!current); - let message = if !current { - "Row numbers: ON (showing line numbers)".to_string() - } else { - "Row numbers: OFF".to_string() - }; - self.buffer_mut().set_status_message(message); - // Recalculate column widths with new mode - self.calculate_optimal_column_widths(); - } - } - // Regex filter functionality (uppercase F) - KeyCode::Char('F') if key.modifiers.contains(KeyModifiers::SHIFT) => { - self.buffer_mut().set_mode(AppMode::Filter); - self.get_filter_state_mut().pattern.clear(); - // Save SQL query and use temporary input for filter display - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - // Fuzzy filter functionality (lowercase f) - KeyCode::Char('f') - if !key.modifiers.contains(KeyModifiers::ALT) - && !key.modifiers.contains(KeyModifiers::CONTROL) => - { - self.buffer_mut().set_mode(AppMode::FuzzyFilter); - self.buffer_mut().set_fuzzy_filter_pattern(String::new()); - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); // Clear active state when entering mode - // Save SQL query and use temporary input for fuzzy filter display - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - self.clear_input(); - } - // Sort functionality (lowercase s) - KeyCode::Char('s') - if !key.modifiers.contains(KeyModifiers::CONTROL) - && !key.modifiers.contains(KeyModifiers::SHIFT) => - { - self.sort_by_column(self.buffer().get_current_column()); - } - // Column statistics (uppercase S) - KeyCode::Char('S') | KeyCode::Char('s') - if key.modifiers.contains(KeyModifiers::SHIFT) => - { - self.calculate_column_statistics(); - } - // Clipboard operations (vim-like yank) - KeyCode::Char('y') => { - debug!("'y' key pressed - selection_mode={:?}", self.selection_mode); - match self.selection_mode { - SelectionMode::Cell => { - // In cell mode, single 'y' yanks the cell directly - debug!("Yanking cell in cell selection mode"); - self.buffer_mut() - .set_status_message("Yanking cell...".to_string()); - self.yank_cell(); - // Status message will be set by yank_cell - } - SelectionMode::Row => { - // In row mode, 'y' is handled by chord handler (yy, yc, ya) - // The chord handler will process the key sequence - debug!("'y' pressed in row mode - waiting for chord completion"); - self.buffer_mut().set_status_message( - "Press second key for chord: yy=row, yc=column, ya=all, yv=cell" - .to_string(), - ); - } - } - } - // Export to CSV - KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_csv(); - } - // Export to JSON - KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.export_to_json(); - } - // Number keys for direct column sorting - KeyCode::Char(c) if c.is_ascii_digit() => { - if let Some(digit) = c.to_digit(10) { - let column_index = (digit as usize).saturating_sub(1); - self.sort_by_column(column_index); - } - } - KeyCode::F(1) | KeyCode::Char('?') => { - self.show_help = true; - self.buffer_mut().set_mode(AppMode::Help); - } - _ => { - // Other keys handled normally - } - } - Ok(false) - } - - fn handle_search_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Enter => { - self.perform_search(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Backspace => { - { - let mut pattern = self.buffer().get_search_pattern(); - pattern.pop(); - self.buffer_mut().set_search_pattern(pattern); - }; - // Update input for rendering - let pattern = self.buffer().get_search_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - KeyCode::Char(c) => { - { - let mut pattern = self.buffer().get_search_pattern(); - pattern.push(c); - self.buffer_mut().set_search_pattern(pattern); - } - // Update input for rendering - let pattern = self.buffer().get_search_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - _ => {} - } - Ok(false) - } - - fn handle_filter_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Enter => { - self.apply_filter(); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Backspace => { - self.get_filter_state_mut().pattern.pop(); - // Update input for rendering - let pattern = self.get_filter_state().pattern.clone(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - KeyCode::Char(c) => { - self.get_filter_state_mut().pattern.push(c); - // Update input for rendering - let pattern = self.get_filter_state().pattern.clone(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - } - _ => {} - } - Ok(false) - } - - fn handle_fuzzy_filter_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Clear fuzzy filter and return to results - self.buffer_mut().set_fuzzy_filter_active(false); - self.buffer_mut().set_fuzzy_filter_pattern(String::new()); - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - self.buffer_mut() - .set_status_message("Fuzzy filter cleared".to_string()); - } - KeyCode::Enter => { - // Apply fuzzy filter and return to results - if !self.buffer().get_fuzzy_filter_pattern().is_empty() { - self.apply_fuzzy_filter(); - self.buffer_mut().set_fuzzy_filter_active(true); - } - // Restore original SQL query - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Backspace => { - { - let mut pattern = self.buffer().get_fuzzy_filter_pattern(); - pattern.pop(); - self.buffer_mut().set_fuzzy_filter_pattern(pattern); - }; - // Update input for rendering - let pattern = self.buffer().get_fuzzy_filter_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - // Re-apply filter in real-time - if !self.buffer().get_fuzzy_filter_pattern().is_empty() { - self.apply_fuzzy_filter(); - } else { - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); - } - } - KeyCode::Char(c) => { - { - let mut pattern = self.buffer().get_fuzzy_filter_pattern(); - pattern.push(c); - self.buffer_mut().set_fuzzy_filter_pattern(pattern); - }; - // Update input for rendering - let pattern = self.buffer().get_fuzzy_filter_pattern(); - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - // Apply filter in real-time as user types - self.apply_fuzzy_filter(); - } - _ => {} - } - Ok(false) - } - - fn handle_column_search_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - // Restore original SQL query from undo stack FIRST - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } else { - // Fallback: restore from buffer's stored text if undo fails - let text = self.buffer().get_input_text(); - let cursor = self.buffer().get_input_cursor_position(); - self.input = tui_input::Input::new(text.clone()).with_cursor(cursor); - } - - // Cancel column search and return to results - self.buffer_mut().set_mode(AppMode::Results); - self.buffer_mut().set_column_search_pattern(String::new()); - self.buffer_mut().set_column_search_matches(Vec::new()); - self.buffer_mut() - .set_status_message("Column search cancelled".to_string()); - } - KeyCode::Enter => { - // Jump to first matching column - if !self.buffer().get_column_search_matches().clone().is_empty() { - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone() - [self.buffer().get_column_search_current_match()] - .clone(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut() - .set_status_message(format!("Jumped to column: {}", column_name)); - } else { - self.buffer_mut() - .set_status_message("No matching columns found".to_string()); - } - - // Restore original SQL query from undo stack - if let Some((original_query, cursor_pos)) = self.buffer_mut().pop_undo() { - self.set_input_text_with_cursor(original_query, cursor_pos); - } else { - // Fallback: restore from buffer's stored text if undo fails - let text = self.buffer().get_input_text(); - let cursor = self.buffer().get_input_cursor_position(); - self.input = tui_input::Input::new(text.clone()).with_cursor(cursor); - } - - self.buffer_mut().set_mode(AppMode::Results); - } - KeyCode::Tab => { - // Next match (Tab only, not 'n' to allow typing 'n' in search) - if !self.buffer().get_column_search_matches().clone().is_empty() { - let matches_len = self.buffer().get_column_search_matches().clone().len(); - let current = self.buffer().get_column_search_current_match(); - self.buffer_mut() - .set_column_search_current_match((current + 1) % matches_len); - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone() - [self.buffer().get_column_search_current_match()] - .clone(); - let current_match = self.buffer().get_column_search_current_match() + 1; - let total_matches = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut().set_status_message(format!( - "Column {} of {}: {}", - current_match, total_matches, column_name - )); - } - } - KeyCode::BackTab => { - // Previous match (Shift+Tab only, not 'N' to allow typing 'N' in search) - if !self.buffer().get_column_search_matches().clone().is_empty() { - let current = self.buffer().get_column_search_current_match(); - if current == 0 { - let matches_len = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut() - .set_column_search_current_match(matches_len - 1); - } else { - self.buffer_mut() - .set_column_search_current_match(current - 1); - } - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone() - [self.buffer().get_column_search_current_match()] - .clone(); - let current_match = self.buffer().get_column_search_current_match() + 1; - let total_matches = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut().set_status_message(format!( - "Column {} of {}: {}", - current_match, total_matches, column_name - )); - } - } - KeyCode::Backspace => { - let mut pattern = self.buffer().get_column_search_pattern(); - pattern.pop(); - self.buffer_mut().set_column_search_pattern(pattern.clone()); - // Also update input to keep it in sync for rendering - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - self.update_column_search(); - } - KeyCode::Char(c) => { - let mut pattern = self.buffer().get_column_search_pattern(); - pattern.push(c); - self.buffer_mut().set_column_search_pattern(pattern.clone()); - // Also update input to keep it in sync for rendering - self.set_input_text_with_cursor(pattern.clone(), pattern.len()); - self.update_column_search(); - } - _ => {} - } - Ok(false) - } - - fn handle_help_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // Use dispatcher to get action - if let Some(action) = self.key_dispatcher.get_help_action(&key) { - match action { - "quit" => return Ok(true), - "exit_help" => self.exit_help(), - "scroll_help_down" => self.scroll_help_down(), - "scroll_help_up" => self.scroll_help_up(), - "help_page_down" => self.help_page_down(), - "help_page_up" => self.help_page_up(), - _ => {} - } - } else { - // Handle any keys not in the dispatcher (like 'j' and 'k' for vim-style) - match key.code { - KeyCode::Char('j') => self.scroll_help_down(), - KeyCode::Char('k') => self.scroll_help_up(), - KeyCode::F(1) => self.exit_help(), - KeyCode::Home => self.help_scroll = 0, - KeyCode::End => { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - self.help_scroll = max_scroll as u16; - } - _ => {} - } - } - Ok(false) - } - - // Helper methods for help mode actions - fn exit_help(&mut self) { - self.show_help = false; - self.help_scroll = 0; - let mode = if self.buffer().get_results().is_some() { - AppMode::Results - } else { - AppMode::Command - }; - self.buffer_mut().set_mode(mode); - } - - fn scroll_help_down(&mut self) { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - if (self.help_scroll as usize) < max_scroll { - self.help_scroll += 1; - } - } - - fn scroll_help_up(&mut self) { - self.help_scroll = self.help_scroll.saturating_sub(1); - } - - fn help_page_down(&mut self) { - let max_lines: usize = 58; - let visible_height: usize = 30; - let max_scroll = max_lines.saturating_sub(visible_height); - self.help_scroll = (self.help_scroll + 10).min(max_scroll as u16); - } - - fn help_page_up(&mut self) { - self.help_scroll = self.help_scroll.saturating_sub(10); - } - - fn handle_history_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc => { - self.buffer_mut().set_mode(AppMode::Command); - } - KeyCode::Enter => { - if !self.history_state.matches.is_empty() - && self.history_state.selected_index < self.history_state.matches.len() - { - let selected_command = self.history_state.matches - [self.history_state.selected_index] - .entry - .command - .clone(); - // Use helper to set text through buffer - self.set_input_text(selected_command); - self.buffer_mut().set_mode(AppMode::Command); - self.buffer_mut() - .set_status_message("Command loaded from history".to_string()); - // Reset scroll to show end of command - self.input_scroll_offset = 0; - self.update_horizontal_scroll(120); // Will be properly updated on next render - } - } - KeyCode::Up | KeyCode::Char('k') => { - if !self.history_state.matches.is_empty() { - self.history_state.selected_index = - self.history_state.selected_index.saturating_sub(1); - } - } - KeyCode::Down | KeyCode::Char('j') => { - if !self.history_state.matches.is_empty() - && self.history_state.selected_index + 1 < self.history_state.matches.len() - { - self.history_state.selected_index += 1; - } - } - KeyCode::Backspace => { - self.history_state.search_query.pop(); - self.update_history_matches(); - } - KeyCode::Char(c) => { - self.history_state.search_query.push(c); - self.update_history_matches(); - } - _ => {} - } - Ok(false) - } - - fn update_history_matches(&mut self) { - // Get current schema columns and data source for better matching - let (current_columns, current_source_str) = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the first (and usually only) table's columns and name - let (cols, table_name) = schema - .iter() - .next() - .map(|(table_name, cols)| (cols.clone(), Some(table_name.clone()))) - .unwrap_or((vec![], None)); - (cols, table_name) - } else { - (vec![], None) - } - } else { - (vec![], None) - } - } else if self.buffer().is_cache_mode() { - (vec![], Some("cache".to_string())) - } else { - (vec![], Some("api".to_string())) - }; - - let current_source = current_source_str.as_deref(); - - self.history_state.matches = self.command_history.search_with_schema( - &self.history_state.search_query, - ¤t_columns, - current_source, - ); - self.history_state.selected_index = 0; - } - - fn handle_debug_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - // Handle special keys for test case generation - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+C to quit - return Ok(true); - } - KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+T: "Yank as Test" - capture current session as test case - self.yank_as_test_case(); - return Ok(false); - } - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::SHIFT) => { - // Shift+Y: Yank debug dump with test context - self.yank_debug_with_context(); - return Ok(false); - } - _ => {} - } - - // Let the widget handle navigation and exit - if self.debug_widget.handle_key(key) { - // Widget returned true - exit debug mode - self.buffer_mut().set_mode(AppMode::Command); - } - Ok(false) - } - - fn handle_pretty_query_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { - return Ok(true); - } - - // Let debug widget handle the key (includes scrolling and exit) - if self.debug_widget.handle_key(key) { - // Widget returned true - exit pretty query mode - self.buffer_mut().set_mode(AppMode::Command); - } - Ok(false) - } - - fn execute_query(&mut self, query: &str) -> Result<()> { - info!(target: "query", "Executing query: {}", query); - self.buffer_mut() - .set_status_message(format!("Executing query: '{}'...", query)); - let start_time = std::time::Instant::now(); - - let result = if self.buffer().is_cache_mode() { - // When in cache mode, use CSV client to query cached data - if let Some(cached_data) = self.buffer().get_cached_data() { - let mut csv_client = CsvApiClient::new(); - csv_client.set_case_insensitive(self.buffer().is_case_insensitive()); - csv_client.load_from_json(cached_data.clone(), "cached_data")?; - - csv_client.query_csv(query).map(|r| QueryResponse { - data: r.data, - count: r.count, - query: sql_cli::api_client::QueryInfo { - select: r.query.select, - where_clause: r.query.where_clause, - order_by: r.query.order_by, - }, - source: Some("cache".to_string()), - table: Some("cached_data".to_string()), - cached: Some(true), - }) - } else { - Err(anyhow::anyhow!("No cached data loaded")) - } - } else if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - // Convert CSV result to match the expected type - csv_client.query_csv(query).map(|r| QueryResponse { - data: r.data, - count: r.count, - query: sql_cli::api_client::QueryInfo { - select: r.query.select, - where_clause: r.query.where_clause, - order_by: r.query.order_by, - }, - source: Some("file".to_string()), - table: Some(self.buffer().get_table_name()), - cached: Some(false), - }) - } else { - Err(anyhow::anyhow!("CSV client not initialized")) - } - } else { - self.api_client - .query_trades(query) - .map_err(|e| anyhow::anyhow!("{}", e)) - }; - - match result { - Ok(response) => { - let duration = start_time.elapsed(); - - // Get schema columns and data source for history - let (schema_columns, data_source) = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the first (and usually only) table's columns - let cols = schema - .iter() - .next() - .map(|(table_name, cols)| (cols.clone(), Some(table_name.clone()))) - .unwrap_or((vec![], None)); - cols - } else { - (vec![], None) - } - } else { - (vec![], None) - } - } else if self.buffer().is_cache_mode() { - (vec![], Some("cache".to_string())) - } else { - (vec![], Some("api".to_string())) - }; - - let _ = self.command_history.add_entry_with_schema( - query.to_string(), - true, - Some(duration.as_millis() as u64), - schema_columns, - data_source, - ); - - // Add debug info about results - let row_count = response.data.len(); - - // Capture the source from the response - self.buffer_mut() - .set_last_query_source(response.source.clone()); - - // Store results in the current buffer - if let Some(buffer) = self.buffer_manager.current_mut() { - let buffer_id = buffer.get_id(); - buffer.set_results(Some(response.clone())); - info!(target: "buffer", "Stored {} results in buffer {}", row_count, buffer_id); - } - self.buffer_mut().set_results(Some(response.clone())); // Keep for compatibility during migration - - // Update parser with the FULL schema if we're in CSV/cache mode - // For CSV mode, get the complete schema from the CSV client, not from query results - if self.buffer().is_csv_mode() { - let table_name = self.buffer().get_table_name(); - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the full column list from the schema - if let Some(columns) = schema.get(&table_name) { - info!(target: "buffer", "Query executed, updating parser with FULL schema ({} columns) for table '{}'", columns.len(), table_name); - self.hybrid_parser - .update_single_table(table_name, columns.clone()); - } - } - } - } else if self.buffer().is_cache_mode() { - // For cache mode, we still use the results columns since cached data might be filtered - if let Some(first_row) = response.data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = obj.keys().map(|k| k.to_string()).collect(); - info!(target: "buffer", "Query executed, updating parser with {} columns for cached table", columns.len()); - self.hybrid_parser - .update_single_table("cached_data".to_string(), columns); - } - } - } - - self.calculate_optimal_column_widths(); - self.reset_table_state(); - - if row_count == 0 { - self.buffer_mut().set_status_message(format!( - "Query executed successfully but returned 0 rows ({}ms)", - duration.as_millis() - )); - } else { - self.buffer_mut().set_status_message(format!("Query executed successfully - {} rows returned ({}ms) - Use ↓ or j/k to navigate", row_count, duration.as_millis())); - } - - self.buffer_mut().set_mode(AppMode::Results); - &mut self.table_state.select(Some(0)); - } - Err(e) => { - let duration = start_time.elapsed(); - - // Get schema columns and data source for history (even for failed queries) - let (schema_columns, data_source) = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the first (and usually only) table's columns - let cols = schema - .iter() - .next() - .map(|(table_name, cols)| (cols.clone(), Some(table_name.clone()))) - .unwrap_or((vec![], None)); - cols - } else { - (vec![], None) - } - } else { - (vec![], None) - } - } else if self.buffer().is_cache_mode() { - (vec![], Some("cache".to_string())) - } else { - (vec![], Some("api".to_string())) - }; - - let _ = self.command_history.add_entry_with_schema( - query.to_string(), - false, - Some(duration.as_millis() as u64), - schema_columns, - data_source, - ); - self.buffer_mut() - .set_status_message(format!("Error: {}", e)); - } - } - Ok(()) - } - - fn parse_where_clause_ast(&self, query: &str) -> Result { - let query_lower = query.to_lowercase(); - if let Some(where_pos) = query_lower.find(" where ") { - let where_clause = &query[where_pos + 7..]; // Skip " where " - - // Get columns from CSV client if available - let columns = if self.buffer().is_csv_mode() { - if let Some(csv_client) = self.buffer().get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - schema - .iter() - .next() - .map(|(_, cols)| cols.clone()) - .unwrap_or_default() - } else { - vec![] - } - } else { - vec![] - } - } else { - vec![] - }; - - match WhereParser::parse_with_options( - where_clause, - columns, - self.buffer().is_case_insensitive(), - ) { - Ok(ast) => { - let tree = format_where_ast(&ast, 0); - Ok(format!( - "\n========== WHERE CLAUSE AST ==========\n\ - Query: {}\n\ - WHERE clause: {}\n\n\ - AST Tree:\n{}\n\n\ - Note: Parentheses in the query control operator precedence.\n\ - The parser respects: OR < AND < NOT < comparisons\n\ - Example: 'a = 1 OR b = 2 AND c = 3' parses as 'a = 1 OR (b = 2 AND c = 3)'\n\ - Use parentheses to override: '(a = 1 OR b = 2) AND c = 3'\n", - query, - where_clause, - tree - )) - } - Err(e) => Err(anyhow::anyhow!("Failed to parse WHERE clause: {}", e)), - } - } else { - Ok( - "\n========== WHERE CLAUSE AST ==========\nNo WHERE clause found in query\n" - .to_string(), - ) - } - } - - fn handle_completion(&mut self) { - let cursor_pos = self.get_input_cursor(); - let query_str = self.get_input_text(); - let query = query_str.as_str(); - - let hybrid_result = self.hybrid_parser.get_completions(query, cursor_pos); - if !hybrid_result.suggestions.is_empty() { - self.buffer_mut().set_status_message(format!( - "Suggestions: {}", - hybrid_result.suggestions.join(", ") - )); - } - } - - fn apply_completion(&mut self) { - let cursor_pos = self.get_input_cursor(); - let query = self.get_input_text(); - - // Check if this is a continuation of the same completion session - let is_same_context = query == self.completion_state.last_query - && cursor_pos == self.completion_state.last_cursor_pos; - - if !is_same_context { - // New completion context - get fresh suggestions - let hybrid_result = self.hybrid_parser.get_completions(&query, cursor_pos); - if hybrid_result.suggestions.is_empty() { - self.buffer_mut() - .set_status_message("No completions available".to_string()); - return; - } - - self.completion_state.suggestions = hybrid_result.suggestions; - self.completion_state.current_index = 0; - } else if !self.completion_state.suggestions.is_empty() { - // Cycle to next suggestion - self.completion_state.current_index = - (self.completion_state.current_index + 1) % self.completion_state.suggestions.len(); - } else { - self.buffer_mut() - .set_status_message("No completions available".to_string()); - return; - } - - // Apply the current suggestion (clone to avoid borrow issues) - let suggestion = - self.completion_state.suggestions[self.completion_state.current_index].clone(); - let partial_word = self.extract_partial_word_at_cursor(&query, cursor_pos); - - if let Some(partial) = partial_word { - // Replace the partial word with the suggestion - let before_partial = &query[..cursor_pos - partial.len()]; - let after_cursor = &query[cursor_pos..]; - - // Handle quoted identifiers - if both partial and suggestion start with quotes, - // we need to avoid double quotes - let suggestion_to_use = if partial.starts_with('"') && suggestion.starts_with('"') { - // The partial already includes the opening quote, so use suggestion without its quote - if suggestion.len() > 1 { - suggestion[1..].to_string() - } else { - suggestion.clone() - } - } else { - suggestion.clone() - }; - - let new_query = format!("{}{}{}", before_partial, suggestion_to_use, after_cursor); - - // Update input and cursor position - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let cursor_pos = if suggestion_to_use.ends_with("('')") { - // Position cursor between the quotes - before_partial.len() + suggestion_to_use.len() - 2 - } else { - before_partial.len() + suggestion_to_use.len() - }; - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to correct position - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(cursor_pos); - // Sync for rendering - if self.buffer().get_edit_mode() == EditMode::SingleLine { - self.set_input_text_with_cursor(new_query.clone(), cursor_pos); - } - } - - // Update completion state for next tab press - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = cursor_pos; - - let suggestion_info = if self.completion_state.suggestions.len() > 1 { - format!( - "Completed: {} ({}/{} - Tab for next)", - suggestion, - self.completion_state.current_index + 1, - self.completion_state.suggestions.len() - ) - } else { - format!("Completed: {}", suggestion) - }; - self.buffer_mut().set_status_message(suggestion_info); - } else { - // Just insert the suggestion at cursor position - let before_cursor = &query[..cursor_pos]; - let after_cursor = &query[cursor_pos..]; - let new_query = format!("{}{}{}", before_cursor, suggestion, after_cursor); - - // Special case: if we completed a string method like Contains(''), position cursor inside quotes - let cursor_pos_new = if suggestion.ends_with("('')") { - // Position cursor between the quotes - cursor_pos + suggestion.len() - 2 - } else { - cursor_pos + suggestion.len() - }; - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to correct position - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(cursor_pos_new); - // Sync for rendering - if self.buffer().get_edit_mode() == EditMode::SingleLine { - self.input = - tui_input::Input::new(new_query.clone()).with_cursor(cursor_pos_new); - } - } - - // Update completion state - self.completion_state.last_query = new_query; - self.completion_state.last_cursor_pos = cursor_pos_new; - - self.buffer_mut() - .set_status_message(format!("Inserted: {}", suggestion)); - } - } - - // Note: expand_asterisk and get_table_columns removed - moved to Buffer and use hybrid_parser directly - - fn extract_partial_word_at_cursor(&self, query: &str, cursor_pos: usize) -> Option { - if cursor_pos == 0 || cursor_pos > query.len() { - return None; - } - - let chars: Vec = query.chars().collect(); - let mut start = cursor_pos; - let end = cursor_pos; - - // Check if we might be in a quoted identifier - let mut in_quote = false; - - // Find start of word (go backward) - while start > 0 { - let prev_char = chars[start - 1]; - if prev_char == '"' { - // Found a quote, include it and stop - start -= 1; - in_quote = true; - break; - } else if prev_char.is_alphanumeric() - || prev_char == '_' - || (prev_char == ' ' && in_quote) - { - start -= 1; - } else { - break; - } - } - - // If we found a quote but are in a quoted identifier, - // we need to continue backwards to include the identifier content - if in_quote && start > 0 { - // We've already moved past the quote, now get the content before it - // Actually, we want to include everything from the quote forward - // The logic above is correct - we stop at the quote - } - - // Convert back to byte positions - let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum(); - let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum(); - - if start_byte < end_byte { - Some(query[start_byte..end_byte].to_string()) - } else { - None - } - } - - // Helper to get estimated visible rows based on terminal size - - // Navigation functions - fn next_row(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - // Update viewport size before navigation - self.update_viewport_size(); - - let current = self.table_state.selected().unwrap_or(0); - if current >= total_rows - 1 { - return; - } // Already at bottom - - let new_position = current + 1; - &mut self.table_state.select(Some(new_position)); - - // Update viewport based on lock mode - if self.buffer().is_viewport_lock() { - // In lock mode, keep cursor at fixed viewport position - if let Some(lock_row) = self.buffer().get_viewport_lock_row() { - // Adjust viewport so cursor stays at lock_row position - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = new_position.saturating_sub(lock_row); - self.buffer_mut().set_scroll_offset(offset); - } - } else { - // Normal scrolling behavior - let visible_rows = self.buffer().get_last_visible_rows(); - - // Check if cursor would be below the last visible row - let offset = self.buffer().get_scroll_offset(); - if new_position > offset.0 + visible_rows - 1 { - // Cursor moved below viewport - scroll down by one - self.buffer_mut() - .set_scroll_offset((offset.0 + 1, offset.1)); - } - } - } - } - - fn previous_row(&mut self) { - let current = self.table_state.selected().unwrap_or(0); - if current == 0 { - return; - } // Already at top - - let new_position = current - 1; - &mut self.table_state.select(Some(new_position)); - - // Update viewport based on lock mode - if self.buffer().is_viewport_lock() { - // In lock mode, keep cursor at fixed viewport position - if let Some(lock_row) = self.buffer().get_viewport_lock_row() { - // Adjust viewport so cursor stays at lock_row position - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = new_position.saturating_sub(lock_row); - self.buffer_mut().set_scroll_offset(offset); - } - } else { - // Normal scrolling behavior - let mut offset = self.buffer().get_scroll_offset(); - if new_position < offset.0 { - // Cursor moved above viewport - scroll up - offset.0 = new_position; - self.buffer_mut().set_scroll_offset(offset); - } - } - } - - fn move_column_left(&mut self) { - // Update cursor_manager for table navigation (incremental step) - let (_row, _col) = self.cursor_manager.table_position(); - self.cursor_manager.move_table_left(); - - // Keep existing logic for now - let new_column = self.buffer().get_current_column().saturating_sub(1); - self.buffer_mut().set_current_column(new_column); - let mut offset = self.buffer().get_scroll_offset(); - offset.1 = offset.1.saturating_sub(1); - let column_num = self.buffer().get_current_column() + 1; - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message(format!("Column {} selected", column_num)); - } - - fn move_column_right(&mut self) { - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let max_columns = obj.len(); - - // Update cursor_manager for table navigation (incremental step) - self.cursor_manager.move_table_right(max_columns); - - // Keep existing logic for now - let current_column = self.buffer().get_current_column(); - if current_column + 1 < max_columns { - self.buffer_mut().set_current_column(current_column + 1); - let mut offset = self.buffer().get_scroll_offset(); - offset.1 += 1; - let column_num = self.buffer().get_current_column() + 1; - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message(format!("Column {} selected", column_num)); - } - } - } - } - } - - fn goto_first_column(&mut self) { - self.buffer_mut().set_current_column(0); - let mut offset = self.buffer().get_scroll_offset(); - offset.1 = 0; - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message("First column selected".to_string()); - } - - fn goto_last_column(&mut self) { - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let max_columns = obj.len(); - if max_columns > 0 { - self.buffer_mut().set_current_column(max_columns - 1); - // Update horizontal scroll to show the last column - // This ensures the last column is visible in the viewport - let mut offset = self.buffer().get_scroll_offset(); - let column = self.buffer().get_current_column(); - offset.1 = column.saturating_sub(5); // Keep some context - self.buffer_mut().set_scroll_offset(offset); - self.buffer_mut() - .set_status_message(format!("Last column selected ({})", column + 1)); - } - } - } - } - } - - fn goto_first_row(&mut self) { - self.table_state.select(Some(0)); - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = 0; // Reset viewport to top - self.buffer_mut().set_scroll_offset(offset); - - let total_rows = self.get_row_count(); - if total_rows > 0 { - self.buffer_mut() - .set_status_message(format!("Jumped to first row (1/{})", total_rows)); - } - } - - fn toggle_column_pin(&mut self) { - // Pin or unpin the current column - let current_col = self.buffer().get_current_column(); - if self.buffer().get_pinned_columns().contains(¤t_col) { - // Column is already pinned, unpin it - self.buffer_mut().remove_pinned_column(current_col); - self.buffer_mut() - .set_status_message(format!("Column {} unpinned", current_col + 1)); - } else { - // Pin the column (max 4 pinned columns) - if self.buffer().get_pinned_columns().clone().len() < 4 { - self.buffer_mut().add_pinned_column(current_col); - self.buffer_mut() - .set_status_message(format!("Column {} pinned 📌", current_col + 1)); - } else { - self.buffer_mut() - .set_status_message("Maximum 4 pinned columns allowed".to_string()); - } - } - } - - fn clear_all_pinned_columns(&mut self) { - self.buffer_mut().clear_pinned_columns(); - self.buffer_mut() - .set_status_message("All columns unpinned".to_string()); - } - - fn calculate_column_statistics(&mut self) { - use std::time::Instant; - - let start_total = Instant::now(); - - // Collect all data first, then drop the buffer reference before calling analyzer - let (column_name, data_to_analyze) = { - // Get the current column name and data - let results = match self.buffer().get_results() { - Some(r) if !r.data.is_empty() => r, - _ => return, - }; - - // Get column names from first row - let headers: Vec = if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - obj.keys().map(|k| k.to_string()).collect() - } else { - return; - } - } else { - return; - }; - - let current_column = self.buffer().get_current_column(); - if current_column >= headers.len() { - return; - } - - let column_name = headers[current_column].clone(); - - // Extract column data more efficiently - avoid cloning strings when possible - let data_to_analyze: Vec = - if let Some(filtered) = self.buffer().get_filtered_data() { - // For filtered data, we already have strings - let mut string_data = Vec::new(); - for row in filtered { - if current_column < row.len() { - string_data.push(row[current_column].clone()); - } - } - string_data - } else { - // For JSON data, we need to convert to owned strings - results - .data - .iter() - .filter_map(|row| { - if let Some(obj) = row.as_object() { - obj.get(&column_name).map(|v| match v { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => String::new(), - _ => v.to_string(), - }) - } else { - None - } - }) - .collect() - }; - - (column_name, data_to_analyze) - }; - - // Convert to references for the analyzer - let data_refs: Vec<&str> = data_to_analyze.iter().map(|s| s.as_str()).collect(); - - // Use DataAnalyzer to calculate statistics - let analyzer_stats = self - .data_analyzer - .calculate_column_statistics(&column_name, &data_refs); - - // Convert from DataAnalyzer's ColumnStatistics to buffer's ColumnStatistics - let stats = ColumnStatistics { - column_name: analyzer_stats.column_name, - column_type: match analyzer_stats.data_type { - sql_cli::data_analyzer::ColumnType::Integer - | sql_cli::data_analyzer::ColumnType::Float => ColumnType::Numeric, - sql_cli::data_analyzer::ColumnType::String - | sql_cli::data_analyzer::ColumnType::Boolean - | sql_cli::data_analyzer::ColumnType::Date => ColumnType::String, - sql_cli::data_analyzer::ColumnType::Mixed => ColumnType::Mixed, - sql_cli::data_analyzer::ColumnType::Unknown => ColumnType::Mixed, - }, - total_count: analyzer_stats.total_values, - null_count: analyzer_stats.null_values, - unique_count: analyzer_stats.unique_values, - frequency_map: analyzer_stats.frequency_map.clone(), - // For numeric columns, parse the min/max strings to f64 - min: analyzer_stats - .min_value - .as_ref() - .and_then(|s| s.parse::().ok()), - max: analyzer_stats - .max_value - .as_ref() - .and_then(|s| s.parse::().ok()), - sum: analyzer_stats.sum_value, - mean: analyzer_stats.avg_value, - median: analyzer_stats.median_value, - }; - - // Calculate total time - let elapsed = start_total.elapsed(); - - self.buffer_mut().set_column_stats(Some(stats)); - - // Show timing in status message - self.buffer_mut().set_status_message(format!( - "Column stats: {:.1}ms for {} values ({} unique)", - elapsed.as_secs_f64() * 1000.0, - data_to_analyze.len(), - analyzer_stats.unique_values - )); - - self.buffer_mut().set_mode(AppMode::ColumnStats); - } - - fn check_parser_error(&self, query: &str) -> Option { - // Quick check for common parser errors - let mut paren_depth = 0; - let mut in_string = false; - let mut escape_next = false; - - for ch in query.chars() { - if escape_next { - escape_next = false; - continue; - } - - match ch { - '\\' if in_string => escape_next = true, - '\'' => in_string = !in_string, - '(' if !in_string => paren_depth += 1, - ')' if !in_string => { - paren_depth -= 1; - if paren_depth < 0 { - return Some("Extra )".to_string()); - } - } - _ => {} - } - } - - if paren_depth > 0 { - return Some(format!("Missing {} )", paren_depth)); - } - - // Could add more checks here (unclosed strings, etc.) - if in_string { - return Some("Unclosed string".to_string()); - } - - None - } - - fn update_viewport_size(&mut self) { - // Update the stored viewport size based on current terminal size - if let Ok((_, height)) = crossterm::terminal::size() { - let terminal_height = height as usize; - // Match the actual layout calculation: - // - Input area: 3 rows (from input_height) - // - Status bar: 3 rows - // - Results area gets the rest - let input_height = 3; - let status_height = 3; - let results_area_height = terminal_height.saturating_sub(input_height + status_height); - - // Now match EXACTLY what the render function does: - // - 1 row for top border - // - 1 row for header - // - 1 row for bottom border - self.buffer_mut() - .set_last_visible_rows(results_area_height.saturating_sub(3).max(10)); - } - } - - fn goto_last_row(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - let last_row = total_rows - 1; - self.table_state.select(Some(last_row)); - // Position viewport to show the last row at the bottom - let visible_rows = self.buffer().get_last_visible_rows(); - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = last_row.saturating_sub(visible_rows - 1); - self.buffer_mut().set_scroll_offset(offset); - - // Set status to confirm action - self.buffer_mut().set_status_message(format!( - "Jumped to last row ({}/{})", - last_row + 1, - total_rows - )); - } - } - - fn page_down(&mut self) { - let total_rows = self.get_row_count(); - if total_rows > 0 { - let visible_rows = self.buffer().get_last_visible_rows(); - let current = self.table_state.selected().unwrap_or(0); - let new_position = (current + visible_rows).min(total_rows - 1); - - &mut self.table_state.select(Some(new_position)); - - // Scroll viewport down by a page - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = (offset.0 + visible_rows).min(total_rows.saturating_sub(visible_rows)); - self.buffer_mut().set_scroll_offset(offset); - } - } - - fn page_up(&mut self) { - let visible_rows = self.buffer().get_last_visible_rows(); - let current = self.table_state.selected().unwrap_or(0); - let new_position = current.saturating_sub(visible_rows); - - &mut self.table_state.select(Some(new_position)); - - // Scroll viewport up by a page - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = offset.0.saturating_sub(visible_rows); - self.buffer_mut().set_scroll_offset(offset); - } - - // Search and filter functions - fn perform_search(&mut self) { - if let Some(data) = self.get_current_data() { - self.buffer_mut().set_search_matches(Vec::new()); - - if let Ok(regex) = Regex::new(&self.buffer().get_search_pattern()) { - for (row_idx, row) in data.iter().enumerate() { - for (col_idx, cell) in row.iter().enumerate() { - if regex.is_match(cell) { - let mut matches = self.buffer().get_search_matches(); - matches.push((row_idx, col_idx)); - self.buffer_mut().set_search_matches(matches); - } - } - } - - if !self.buffer().get_search_matches().is_empty() { - self.buffer_mut().set_search_match_index(0); - let matches = self.buffer().get_search_matches(); - self.buffer_mut().set_current_match(Some(matches[0])); - let (row, _) = matches[0]; - &mut self.table_state.select(Some(row)); - self.buffer_mut() - .set_status_message(format!("Found {} matches", matches.len())); - } else { - self.buffer_mut() - .set_status_message("No matches found".to_string()); - } - } else { - self.buffer_mut() - .set_status_message("Invalid regex pattern".to_string()); - } - } - } - - fn next_search_match(&mut self) { - if !self.buffer().get_search_matches().is_empty() { - let matches = self.buffer().get_search_matches(); - let new_index = (self.buffer().get_search_match_index() + 1) % matches.len(); - self.buffer_mut().set_search_match_index(new_index); - let (row, _) = matches[new_index]; - &mut self.table_state.select(Some(row)); - self.buffer_mut() - .set_current_match(Some(matches[new_index])); - self.buffer_mut().set_status_message(format!( - "Match {} of {}", - new_index + 1, - matches.len() - )); - } - } - - fn previous_search_match(&mut self) { - if !self.buffer().get_search_matches().is_empty() { - let matches = self.buffer().get_search_matches(); - let current_index = self.buffer().get_search_match_index(); - let new_index = if current_index == 0 { - matches.len() - 1 - } else { - current_index - 1 - }; - self.buffer_mut().set_search_match_index(new_index); - let (row, _) = matches[new_index]; - &mut self.table_state.select(Some(row)); - self.buffer_mut() - .set_current_match(Some(matches[new_index])); - self.buffer_mut().set_status_message(format!( - "Match {} of {}", - new_index + 1, - matches.len() - )); - } - } - - fn apply_filter(&mut self) { - if self.get_filter_state().pattern.is_empty() { - self.buffer_mut().set_filtered_data(None); - self.get_filter_state_mut().active = false; - self.buffer_mut() - .set_status_message("Filter cleared".to_string()); - return; - } - - if let Some(results) = self.buffer().get_results() { - if let Ok(regex) = Regex::new(&self.get_filter_state().pattern) { - let mut filtered = Vec::new(); - - for item in &results.data { - let mut row = Vec::new(); - let mut matches = false; - - if let Some(obj) = item.as_object() { - for (_, value) in obj { - let cell_str = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "".to_string(), - _ => value.to_string(), - }; - - if regex.is_match(&cell_str) { - matches = true; - } - row.push(cell_str); - } - - if matches { - filtered.push(row); - } - } - } - - let filtered_count = filtered.len(); - self.buffer_mut().set_filtered_data(Some(filtered)); - self.get_filter_state_mut().regex = Some(regex); - self.get_filter_state_mut().active = true; - - // Reset table state but preserve filtered data - self.table_state = TableState::default(); - self.buffer_mut().set_scroll_offset((0, 0)); - self.buffer_mut().set_current_column(0); - - // Clear search state but keep filter state - self.search_state = SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }; - - self.buffer_mut() - .set_status_message(format!("Filtered to {} rows", filtered_count)); - } else { - self.buffer_mut() - .set_status_message("Invalid regex pattern".to_string()); - } - } - } - - fn apply_fuzzy_filter(&mut self) { - if self.buffer().get_fuzzy_filter_pattern().is_empty() { - self.buffer_mut().set_fuzzy_filter_indices(Vec::new()); - self.buffer_mut().set_fuzzy_filter_active(false); - self.buffer_mut() - .set_status_message("Fuzzy filter cleared".to_string()); - return; - } - - let pattern = self.buffer().get_fuzzy_filter_pattern(); - let mut filtered_indices = Vec::new(); - - // Get the data to filter - either already filtered data or original results - let data_to_filter = - if self.get_filter_state().active && self.buffer().get_filtered_data().is_some() { - // If regex filter is active, fuzzy filter on top of that - self.buffer().get_filtered_data() - } else if let Some(results) = self.buffer().get_results() { - // Otherwise filter original results - let mut rows = Vec::new(); - for item in &results.data { - let mut row = Vec::new(); - if let Some(obj) = item.as_object() { - for (_, value) in obj { - let cell_str = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "".to_string(), - _ => value.to_string(), - }; - row.push(cell_str); - } - rows.push(row); - } - } - self.buffer_mut().set_filtered_data(Some(rows)); - self.buffer().get_filtered_data() - } else { - return; - }; - - if let Some(data) = data_to_filter { - for (index, row) in data.iter().enumerate() { - // Concatenate all columns into a single string for matching - let row_text = row.join(" "); - - // Check if pattern starts with ' for exact matching - let matches = if pattern.starts_with('\'') && pattern.len() > 1 { - // Exact substring matching (case-insensitive) - let exact_pattern = &pattern[1..]; - row_text - .to_lowercase() - .contains(&exact_pattern.to_lowercase()) - } else { - // Fuzzy matching - let matcher = SkimMatcherV2::default(); - if let Some(score) = matcher.fuzzy_match(&row_text, &pattern) { - score > 0 - } else { - false - } - }; - - if matches { - filtered_indices.push(index); - } - } - } - - let match_count = filtered_indices.len(); - let is_active = !filtered_indices.is_empty(); - self.buffer_mut().set_fuzzy_filter_indices(filtered_indices); - self.buffer_mut().set_fuzzy_filter_active(is_active); - - if self.buffer().is_fuzzy_filter_active() { - let filter_type = if pattern.starts_with('\'') { - "Exact" - } else { - "Fuzzy" - }; - self.buffer_mut().set_status_message(format!( - "{} filter: {} matches for '{}' (highlighted in magenta)", - filter_type, match_count, pattern - )); - // Reset table state for new filtered view - self.table_state = TableState::default(); - self.buffer_mut().set_scroll_offset((0, 0)); - } else { - let filter_type = if pattern.starts_with('\'') { - "exact" - } else { - "fuzzy" - }; - self.buffer_mut() - .set_status_message(format!("No {} matches for '{}'", filter_type, pattern)); - } - } - - fn update_column_search(&mut self) { - // Get column headers from the current results - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - // Find matching columns (case-insensitive) - let pattern = self.buffer().get_column_search_pattern().to_lowercase(); - let mut matching_columns = Vec::new(); - - for (index, header) in headers.iter().enumerate() { - if header.to_lowercase().contains(&pattern) { - matching_columns.push((index, header.to_string())); - } - } - - self.buffer_mut() - .set_column_search_matches(matching_columns); - self.buffer_mut().set_column_search_current_match(0); - - // Update status message - if self.buffer().get_column_search_pattern().is_empty() { - self.buffer_mut() - .set_status_message("Enter column name to search".to_string()); - } else if self.buffer().get_column_search_matches().clone().is_empty() { - let pattern = self.buffer().get_column_search_pattern(); - self.buffer_mut() - .set_status_message(format!("No columns match '{}'", pattern)); - } else { - let (column_index, column_name) = - self.buffer().get_column_search_matches().clone()[0].clone(); - let matches_len = self.buffer().get_column_search_matches().clone().len(); - self.buffer_mut().set_current_column(column_index); - self.buffer_mut().set_status_message(format!( - "Column 1 of {}: {} (Tab=next, Enter=select)", - matches_len, column_name - )); - } - } else { - self.buffer_mut() - .set_status_message("No column data available".to_string()); - } - } else { - self.buffer_mut() - .set_status_message("No data available for column search".to_string()); - } - } else { - self.buffer_mut() - .set_status_message("No results available for column search".to_string()); - } - } - - fn sort_by_column(&mut self, column_index: usize) { - let new_order = match &self.sort_state { - SortState { - column: Some(col), - order, - } if *col == column_index => match order { - SortOrder::Ascending => SortOrder::Descending, - SortOrder::Descending => SortOrder::None, - SortOrder::None => SortOrder::Ascending, - }, - _ => SortOrder::Ascending, - }; - - if new_order == SortOrder::None { - // Reset to original order - would need to store original data - self.sort_state = SortState { - column: None, - order: SortOrder::None, - }; - self.buffer_mut() - .set_status_message("Sort cleared".to_string()); - return; - } - - // Sort using original JSON values for proper type-aware comparison - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - - if column_index < headers.len() { - let column_name = headers[column_index]; - - // Create a vector of (original_json_row, row_index) pairs for sorting - let mut indexed_rows: Vec<(serde_json::Value, usize)> = results - .data - .iter() - .enumerate() - .map(|(i, row)| (row.clone(), i)) - .collect(); - - // Sort based on the original JSON values - indexed_rows.sort_by(|(row_a, _), (row_b, _)| { - let val_a = row_a.get(column_name); - let val_b = row_b.get(column_name); - - let cmp = match (val_a, val_b) { - ( - Some(serde_json::Value::Number(a)), - Some(serde_json::Value::Number(b)), - ) => { - // Numeric comparison - this handles integers and floats properly - let a_f64 = a.as_f64().unwrap_or(0.0); - let b_f64 = b.as_f64().unwrap_or(0.0); - a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal) - } - ( - Some(serde_json::Value::String(a)), - Some(serde_json::Value::String(b)), - ) => { - // String comparison - a.cmp(b) - } - ( - Some(serde_json::Value::Bool(a)), - Some(serde_json::Value::Bool(b)), - ) => { - // Boolean comparison (false < true) - a.cmp(b) - } - (Some(serde_json::Value::Null), Some(serde_json::Value::Null)) => { - Ordering::Equal - } - (Some(serde_json::Value::Null), Some(_)) => { - // NULL comes first - Ordering::Less - } - (Some(_), Some(serde_json::Value::Null)) => { - // NULL comes first - Ordering::Greater - } - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - // Mixed type comparison - fall back to string representation - (Some(a), Some(b)) => { - let a_str = match a { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - let b_str = match b { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - a_str.cmp(&b_str) - } - }; - - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - }); - - // Rebuild the QueryResponse with sorted data - let sorted_data: Vec = - indexed_rows.into_iter().map(|(row, _)| row).collect(); - - // Update both the results and clear filtered_data to force regeneration - let mut new_results = results.clone(); - new_results.data = sorted_data; - self.buffer_mut().set_results(Some(new_results)); - self.buffer_mut().set_filtered_data(None); // Force regeneration of string data - } - } - } - } else if let Some(data) = self.buffer().get_filtered_data() { - // Fallback to string-based sorting if no JSON data available - // Clone the data, sort it, and set it back - let mut sorted_data = data.clone(); - sorted_data.sort_by(|a, b| { - if column_index >= a.len() || column_index >= b.len() { - return Ordering::Equal; - } - - let cell_a = &a[column_index]; - let cell_b = &b[column_index]; - - // Try numeric comparison first - if let (Ok(num_a), Ok(num_b)) = (cell_a.parse::(), cell_b.parse::()) { - let cmp = num_a.partial_cmp(&num_b).unwrap_or(Ordering::Equal); - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - } else { - // String comparison - let cmp = cell_a.cmp(cell_b); - match new_order { - SortOrder::Ascending => cmp, - SortOrder::Descending => cmp.reverse(), - SortOrder::None => Ordering::Equal, - } - } - }); - self.buffer_mut().set_filtered_data(Some(sorted_data)); - } - - self.sort_state = SortState { - column: Some(column_index), - order: new_order, - }; - - // Reset table state but preserve current column position - let current_column = self.buffer().get_current_column(); - self.reset_table_state(); - self.buffer_mut().set_current_column(current_column); - - self.buffer_mut().set_status_message(format!( - "Sorted by column {} ({}) - type-aware", - column_index + 1, - match new_order { - SortOrder::Ascending => "ascending", - SortOrder::Descending => "descending", - SortOrder::None => "none", - } - )); - } - - fn get_current_data(&self) -> Option>> { - if let Some(filtered) = self.buffer().get_filtered_data() { - Some(filtered.clone()) - } else if let Some(results) = self.buffer().get_results() { - Some(DataExporter::convert_json_to_strings(&results.data)) - } else { - None - } - } - - fn get_row_count(&self) -> usize { - // TODO: Fix row count when fuzzy filter is active - // Currently this returns the count from filtered_data (WHERE clause results) - // but doesn't account for fuzzy_filter_state.filtered_indices - // This causes incorrect row counts in the status line (e.g., showing 1/1513 instead of 1/257) - // This will be fixed when fuzzy_filter_state is migrated to the buffer system - // and we have a single source of truth for visible rows - if let Some(filtered) = self.buffer().get_filtered_data() { - filtered.len() - } else if let Some(results) = self.buffer().get_results() { - results.data.len() - } else { - 0 - } - } - - // Removed get_current_data_mut - sorting now uses immutable data and clones when needed - // Removed convert_json_to_strings - moved to DataExporter module - - fn reset_table_state(&mut self) { - self.table_state = TableState::default(); - self.buffer_mut().set_scroll_offset((0, 0)); - self.buffer_mut().set_current_column(0); - self.buffer_mut().set_last_results_row(None); // Reset saved position for new results - self.buffer_mut().set_last_scroll_offset((0, 0)); // Reset saved scroll offset for new results - - // Clear filter state to prevent old filtered data from persisting - *self.get_filter_state_mut() = FilterState { - pattern: String::new(), - regex: None, - active: false, - }; - - // Clear search state - self.search_state = SearchState { - pattern: String::new(), - current_match: None, - matches: Vec::new(), - match_index: 0, - }; - - // Clear fuzzy filter state to prevent it from persisting across queries - { - let buffer = self.buffer_mut(); - buffer.clear_fuzzy_filter(); - buffer.set_fuzzy_filter_pattern(String::new()); - buffer.set_fuzzy_filter_active(false); - buffer.set_fuzzy_filter_indices(Vec::new()); - }; - - // Clear filtered data - self.buffer_mut().set_filtered_data(None); - } - - fn calculate_viewport_column_widths(&mut self, viewport_start: usize, viewport_end: usize) { - // Calculate column widths based only on visible rows in viewport - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let mut widths = Vec::with_capacity(headers.len()); - - // Use compact mode settings - let compact = self.buffer().is_compact_mode(); - let min_width = if compact { 4 } else { 6 }; - let max_width = if compact { 20 } else { 30 }; - let padding = if compact { 1 } else { 2 }; - - // Only check visible rows - let rows_to_check = - &results.data[viewport_start..viewport_end.min(results.data.len())]; - - for header in &headers { - // Start with header width - let mut max_col_width = header.len(); - - // Check only visible rows for this column - for row in rows_to_check { - if let Some(obj) = row.as_object() { - if let Some(value) = obj.get(*header) { - let display_value = if value.is_null() { - "NULL" - } else if let Some(s) = value.as_str() { - s - } else { - &value.to_string() - }; - max_col_width = max_col_width.max(display_value.len()); - } - } - } - - // Apply min/max constraints and padding - let width = (max_col_width + padding).clamp(min_width, max_width) as u16; - widths.push(width); - } - - self.buffer_mut().set_column_widths(widths); - } - } - } - } - - fn update_parser_for_current_buffer(&mut self) { - // Sync the input field with the current buffer's text - if let Some(buffer) = self.buffer_manager.current() { - let text = buffer.get_input_text(); - let cursor_pos = buffer.get_input_cursor_position(); - self.input = tui_input::Input::new(text.clone()).with_cursor(cursor_pos); - debug!(target: "buffer", "Synced input field with buffer text: '{}' (cursor: {})", text, cursor_pos); - } - - // Update the parser's schema based on the current buffer's data source - if let Some(buffer) = self.buffer_manager.current() { - if buffer.is_csv_mode() { - let table_name = buffer.get_table_name(); - if let Some(csv_client) = buffer.get_csv_client() { - if let Some(schema) = csv_client.get_schema() { - // Get the full column list from the schema - if let Some(columns) = schema.get(&table_name) { - debug!(target: "buffer", "Updating parser with {} columns for table '{}'", columns.len(), table_name); - self.hybrid_parser - .update_single_table(table_name, columns.clone()); - } - } - } - } else if buffer.is_cache_mode() { - // For cache mode, use cached data schema if available - if let Some(cached_data) = buffer.get_cached_data() { - if let Some(first_row) = cached_data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = obj.keys().map(|k| k.to_string()).collect(); - debug!(target: "buffer", "Updating parser with {} columns for cached data", columns.len()); - self.hybrid_parser - .update_single_table("cached_data".to_string(), columns); - } - } - } - } else if let Some(results) = buffer.get_results() { - // For API mode or when we have results, use the result columns - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = obj.keys().map(|k| k.to_string()).collect(); - let table_name = buffer.get_table_name(); - debug!(target: "buffer", "Updating parser with {} columns for table '{}'", columns.len(), table_name); - self.hybrid_parser.update_single_table(table_name, columns); - } - } - } - } - } - - fn calculate_optimal_column_widths(&mut self) { - use sql_cli::column_manager::ColumnManager; - - if let Some(results) = self.buffer().get_results() { - let widths = ColumnManager::calculate_optimal_widths(&results.data); - if !widths.is_empty() { - self.buffer_mut().set_column_widths(widths); - } - } - } - - fn export_to_csv(&mut self) { - match DataExporter::export_to_csv(self.buffer()) { - Ok(message) => { - self.buffer_mut().set_status_message(message); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Export failed: {}", e)); - } - } - } - - fn yank_cell(&mut self) { - debug!("yank_cell called"); - if let Some(selected_row) = self.table_state.selected() { - let column = self.buffer().get_current_column(); - debug!("Yanking cell at row={}, column={}", selected_row, column); - match YankManager::yank_cell(self.buffer(), selected_row, column) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview.clone())); - let message = format!("Yanked cell: {}", result.full_value); - debug!("Yank successful: {}", message); - self.buffer_mut().set_status_message(message); - } - Err(e) => { - let message = format!("Failed to yank cell: {}", e); - debug!("Yank failed: {}", message); - self.buffer_mut().set_status_message(message); - } - } - } else { - debug!("No row selected for yank"); - } - } - - fn yank_row(&mut self) { - if let Some(selected_row) = self.table_state.selected() { - match YankManager::yank_row(self.buffer(), selected_row) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview)); - self.buffer_mut() - .set_status_message(format!("Yanked {}", result.description)); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to yank row: {}", e)); - } - } - } - } - - fn yank_column(&mut self) { - let column = self.buffer().get_current_column(); - match YankManager::yank_column(self.buffer(), column) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview)); - self.buffer_mut() - .set_status_message(format!("Yanked {}", result.description)); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to yank column: {}", e)); - } - } - } - - fn yank_all(&mut self) { - match YankManager::yank_all(self.buffer()) { - Ok(result) => { - self.last_yanked = Some((result.description.clone(), result.preview.clone())); - self.buffer_mut().set_status_message(format!( - "Yanked {}: {}", - result.description, result.preview - )); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to yank all: {}", e)); - } - } - } - - /// Yank current query and results as a complete test case (Ctrl+T in debug mode) - fn yank_as_test_case(&mut self) { - let test_case = DebugInfo::generate_test_case(self.buffer()); - - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&test_case) { - Ok(_) => { - self.buffer_mut().set_status_message(format!( - "Copied complete test case to clipboard ({} lines)", - test_case.lines().count() - )); - self.last_yanked = Some(( - "Test Case".to_string(), - format!( - "{}...", - test_case.lines().take(3).collect::>().join("; ") - ), - )); - } - Err(e) => { - self.buffer_mut().set_status_message(format!( - "Failed to copy test case to clipboard: {}", - e - )); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to access clipboard: {}", e)); - } - } - } - - /// Yank debug dump with context for manual test creation (Shift+Y in debug mode) - fn yank_debug_with_context(&mut self) { - let debug_context = DebugInfo::generate_debug_context(self.buffer()); - - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_context) { - Ok(_) => { - self.buffer_mut().set_status_message(format!( - "Copied debug context to clipboard ({} lines)", - debug_context.lines().count() - )); - self.last_yanked = Some(( - "Debug Context".to_string(), - "Query context with data for test creation".to_string(), - )); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to copy debug context: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to access clipboard: {}", e)); - } - } - } - - fn paste_from_clipboard(&mut self) { - // Paste from system clipboard into the current input field - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.get_text() { - Ok(text) => { - match self.buffer().get_mode() { - AppMode::Command => { - // Always use single-line mode paste - // Get current cursor position - let cursor_pos = self.get_input_cursor(); - let current_value = self.get_input_text(); - - // Insert at cursor position - let mut new_value = String::new(); - new_value.push_str(¤t_value[..cursor_pos]); - new_value.push_str(&text); - new_value.push_str(¤t_value[cursor_pos..]); - - self.set_input_text_with_cursor(new_value, cursor_pos + text.len()); - - self.buffer_mut() - .set_status_message(format!("Pasted {} characters", text.len())); - } - AppMode::Filter - | AppMode::FuzzyFilter - | AppMode::Search - | AppMode::ColumnSearch => { - // For search/filter modes, append to current pattern - let cursor_pos = self.get_input_cursor(); - let current_value = self.get_input_text(); - - let mut new_value = String::new(); - new_value.push_str(¤t_value[..cursor_pos]); - new_value.push_str(&text); - new_value.push_str(¤t_value[cursor_pos..]); - - self.set_input_text_with_cursor(new_value, cursor_pos + text.len()); - - // Update the appropriate filter/search state - match self.buffer().get_mode() { - AppMode::Filter => { - self.get_filter_state_mut().pattern = self.get_input_text(); - self.apply_filter(); - } - AppMode::FuzzyFilter => { - let input_text = self.get_input_text(); - self.buffer_mut().set_fuzzy_filter_pattern(input_text); - self.apply_fuzzy_filter(); - } - AppMode::Search => { - let search_text = self.get_input_text(); - self.buffer_mut().set_search_pattern(search_text); - // TODO: self.search_in_results(); - } - AppMode::ColumnSearch => { - let input_text = self.get_input_text(); - self.buffer_mut().set_column_search_pattern(input_text); - // TODO: self.search_columns(); - } - _ => {} - } - } - _ => { - self.buffer_mut() - .set_status_message("Paste not available in this mode".to_string()); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to paste: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - - fn export_to_json(&mut self) { - // Include filtered data if filters are active - let include_filtered = - self.get_filter_state().active || self.buffer().is_fuzzy_filter_active(); - - match DataExporter::export_to_json(self.buffer(), include_filtered) { - Ok(message) => { - self.buffer_mut().set_status_message(message); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Export failed: {}", e)); - } - } - } - - // Removed get_filtered_json_data - moved to YankManager::convert_filtered_to_json - - fn get_horizontal_scroll_offset(&self) -> u16 { - // Delegate to cursor_manager (incremental refactoring) - let (horizontal, _vertical) = self.cursor_manager.scroll_offsets(); - horizontal - } - - fn update_horizontal_scroll(&mut self, terminal_width: u16) { - let inner_width = terminal_width.saturating_sub(3) as usize; // Account for borders + 1 char padding - let cursor_pos = self.get_input_cursor(); - - // Update cursor_manager scroll (incremental refactoring) - self.cursor_manager - .update_horizontal_scroll(cursor_pos, terminal_width.saturating_sub(3)); - - // Keep legacy field in sync for now - if cursor_pos < self.input_scroll_offset as usize { - self.input_scroll_offset = cursor_pos as u16; - } - // If cursor is after the scroll window, scroll right - else if cursor_pos >= self.input_scroll_offset as usize + inner_width { - self.input_scroll_offset = (cursor_pos + 1).saturating_sub(inner_width) as u16; - } - } - - fn get_cursor_token_position(&self) -> (usize, usize) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - TextNavigator::get_cursor_token_position(&query, cursor_pos) - } - - fn get_token_at_cursor(&self) -> Option { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - TextNavigator::get_token_at_cursor(&query, cursor_pos) - } - - fn move_cursor_word_backward(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.move_cursor_word_backward(); - - // Sync for rendering if single-line mode - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - } - - fn move_cursor_word_forward(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - let query_len = query.len(); - - if cursor_pos >= query_len { - return; - } - - // Use our lexer to tokenize the query - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find the next token boundary after the cursor - let mut target_pos = query_len; - for (start, end, _) in &tokens { - if *start > cursor_pos { - target_pos = *start; - break; - } else if *end > cursor_pos { - target_pos = *end; - break; - } - } - - // Update cursor_manager (small incremental step) - self.cursor_manager.set_position(target_pos); - - // Move cursor to new position through buffer - let is_single_line = self.buffer().get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.set_input_text_with_cursor(text, target_pos); - } - } - - // Update status message - self.buffer_mut() - .set_status_message(format!("Moved to position {} (word boundary)", target_pos)); - } - - fn kill_line(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.kill_line(); - - // Sync for rendering if single-line mode - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - self.cursor_manager.set_position(cursor); - } - } - } - - fn kill_line_backward(&mut self) { - // Always use single-line mode - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if let Some((killed_text, new_query)) = TextEditor::kill_line_backward(&query, cursor_pos) { - // Save to undo stack before modifying - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - // Save to kill ring before deleting - self.buffer_mut().set_kill_ring(killed_text); - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to beginning - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(0); - // Sync for rendering - self.set_input_text_with_cursor(new_query, 0); - } - } - } - - fn undo(&mut self) { - // Use buffer's high-level undo operation - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.perform_undo() { - self.buffer_mut() - .set_status_message("Undo performed".to_string()); - } else { - self.buffer_mut() - .set_status_message("Nothing to undo".to_string()); - } - } - } - - // Buffer management methods - - fn new_buffer(&mut self) { - let mut new_buffer = - sql_cli::buffer::Buffer::new(self.buffer_manager.all_buffers().len() + 1); - // Apply config settings to the new buffer - new_buffer.set_compact_mode(self.config.display.compact_mode); - new_buffer.set_case_insensitive(self.config.behavior.case_insensitive_default); - new_buffer.set_show_row_numbers(self.config.display.show_row_numbers); - - info!(target: "buffer", "Creating new buffer with config: compact_mode={}, case_insensitive={}, show_row_numbers={}", - self.config.display.compact_mode, - self.config.behavior.case_insensitive_default, - self.config.display.show_row_numbers); - - let index = self.buffer_manager.add_buffer(new_buffer); - self.buffer_mut() - .set_status_message(format!("Created new buffer #{}", index + 1)); - } - - // DataTable buffer creation disabled during revert - // fn new_datatable_buffer(&mut self) { ... } - - /// Debug method to dump current buffer state (disabled to prevent TUI corruption) - #[allow(dead_code)] - fn debug_current_buffer(&self) { - // Debug output disabled - was corrupting TUI display - // Use tracing/logging instead if debugging is needed - } - - fn yank(&mut self) { - if !self.buffer().is_kill_ring_empty() { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - // Get kill ring content and calculate new query - let kill_ring_content = self.buffer().get_kill_ring(); - let before = query.chars().take(cursor_pos).collect::(); - let after = query.chars().skip(cursor_pos).collect::(); - let new_query = format!("{}{}{}", before, kill_ring_content, after); - let new_cursor = cursor_pos + kill_ring_content.len(); - - // Save to undo stack before modifying - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.save_state_for_undo(); - } - - // Use helper to set text through buffer - self.set_input_text(new_query.clone()); - // Set cursor to new position - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(new_cursor); - // Sync for rendering - if self.buffer().get_edit_mode() == EditMode::SingleLine { - self.set_input_text_with_cursor(new_query, new_cursor); - } - } - } - } - - fn jump_to_prev_token(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if cursor_pos == 0 { - return; - } - - use crate::recursive_parser::Lexer; - let mut lexer = Lexer::new(&query); - let tokens = lexer.tokenize_all_with_positions(); - - // Find current token position - let mut in_token = false; - let mut current_token_start = 0; - for (start, end, _) in &tokens { - if cursor_pos > *start && cursor_pos <= *end { - in_token = true; - current_token_start = *start; - break; - } - } - - // Find the previous token start - let mut target_pos = 0; - - if in_token && cursor_pos > current_token_start { - // If we're in the middle of a token, go to its start - target_pos = current_token_start; - } else { - // Otherwise, find the previous token - for (start, _, _) in tokens.iter().rev() { - if *start < cursor_pos { - target_pos = *start; - break; - } - } - } - - // Move cursor through buffer - if target_pos < cursor_pos { - let is_single_line = self.buffer().get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.set_input_text_with_cursor(text, target_pos); - } - } - } - } - - fn jump_to_next_token(&mut self) { - let query = self.get_input_text(); - let cursor_pos = self.get_input_cursor(); - - if let Some(target_pos) = TextNavigator::calculate_next_token_position(&query, cursor_pos) { - let is_single_line = self.buffer().get_edit_mode() == EditMode::SingleLine; - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_input_cursor_position(target_pos); - // Sync for rendering - if is_single_line { - let text = buffer.get_input_text(); - self.set_input_text_with_cursor(text, target_pos); - } - } - } - } - - fn ui(&mut self, f: &mut Frame) { - // Always use single-line mode input height - let input_height = 3; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(input_height), // Command input area - Constraint::Min(0), // Results - Constraint::Length(3), // Status bar - ] - .as_ref(), - ) - .split(f.area()); - - // Update horizontal scroll based on actual terminal width - self.update_horizontal_scroll(chunks[0].width); - - // Command input area - let input_title = match self.buffer().get_mode() { - AppMode::Command => "SQL Query".to_string(), - AppMode::Results => "SQL Query (Results Mode - Press ↑ to edit)".to_string(), - AppMode::Search => "Search Pattern".to_string(), - AppMode::Filter => "Filter Pattern".to_string(), - AppMode::FuzzyFilter => "Fuzzy Filter".to_string(), - AppMode::ColumnSearch => "Column Search".to_string(), - AppMode::Help => "Help".to_string(), - AppMode::History => format!( - "History Search: '{}' (Esc to cancel)", - self.history_state.search_query - ), - AppMode::Debug => "Parser Debug (F5)".to_string(), - AppMode::PrettyQuery => "Pretty Query View (F6)".to_string(), - AppMode::CacheList => "Cache Management (F7)".to_string(), - AppMode::JumpToRow => format!("Jump to row: {}", self.jump_to_row_input), - AppMode::ColumnStats => "Column Statistics (S to close)".to_string(), - }; - - let input_block = Block::default().borders(Borders::ALL).title(input_title); - - // Always get input text through the buffer API for consistency - let input_text_string = self.get_input_text(); - let input_text = match self.buffer().get_mode() { - AppMode::History => &self.history_state.search_query, - _ => &input_text_string, - }; - - let input_paragraph = match self.buffer().get_mode() { - AppMode::Command => { - match self.buffer().get_edit_mode() { - EditMode::SingleLine => { - // Use syntax highlighting for SQL command input with horizontal scrolling - let highlighted_line = - self.sql_highlighter.simple_sql_highlight(input_text); - Paragraph::new(Text::from(vec![highlighted_line])) - .block(input_block) - .scroll((0, self.get_horizontal_scroll_offset())) - } - EditMode::MultiLine => { - // MultiLine mode is no longer supported, always use single-line - let highlighted_line = - self.sql_highlighter.simple_sql_highlight(input_text); - Paragraph::new(Text::from(vec![highlighted_line])) - .block(input_block) - .scroll((0, self.get_horizontal_scroll_offset())) - } - } - } - _ => { - // Plain text for other modes - Paragraph::new(input_text.as_str()) - .block(input_block) - .style(match self.buffer().get_mode() { - AppMode::Results => Style::default().fg(Color::DarkGray), - AppMode::Search => Style::default().fg(Color::Yellow), - AppMode::Filter => Style::default().fg(Color::Cyan), - AppMode::FuzzyFilter => Style::default().fg(Color::Magenta), - AppMode::ColumnSearch => Style::default().fg(Color::Green), - AppMode::Help => Style::default().fg(Color::DarkGray), - AppMode::History => Style::default().fg(Color::Magenta), - AppMode::Debug => Style::default().fg(Color::Yellow), - AppMode::PrettyQuery => Style::default().fg(Color::Green), - AppMode::CacheList => Style::default().fg(Color::Cyan), - AppMode::JumpToRow => Style::default().fg(Color::Magenta), - AppMode::ColumnStats => Style::default().fg(Color::Cyan), - _ => Style::default(), - }) - .scroll((0, self.get_horizontal_scroll_offset())) - } - }; - - // Always render the input paragraph (single-line mode) - f.render_widget(input_paragraph, chunks[0]); - let results_area = chunks[1]; - - // Set cursor position for input modes - match self.buffer().get_mode() { - AppMode::Command => { - // Always use single-line cursor handling - // Calculate cursor position with horizontal scrolling - let inner_width = chunks[0].width.saturating_sub(2) as usize; - let cursor_pos = self.get_visual_cursor().1; // Get column position for single-line - let scroll_offset = self.get_horizontal_scroll_offset() as usize; - - // Calculate visible cursor position - if cursor_pos >= scroll_offset && cursor_pos < scroll_offset + inner_width { - let visible_pos = cursor_pos - scroll_offset; - f.set_cursor_position((chunks[0].x + visible_pos as u16 + 1, chunks[0].y + 1)); - } - } - AppMode::Search => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::Filter => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::FuzzyFilter => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::ColumnSearch => { - f.set_cursor_position(( - chunks[0].x + self.get_input_cursor() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::JumpToRow => { - f.set_cursor_position(( - chunks[0].x + self.jump_to_row_input.len() as u16 + 1, - chunks[0].y + 1, - )); - } - AppMode::History => { - f.set_cursor_position(( - chunks[0].x + self.history_state.search_query.len() as u16 + 1, - chunks[0].y + 1, - )); - } - _ => {} - } - - // Results area - render based on mode to reduce complexity - match (&self.buffer().get_mode(), self.show_help) { - (_, true) => self.render_help(f, results_area), - (AppMode::History, false) => self.render_history(f, results_area), - (AppMode::Debug, false) => self.render_debug(f, results_area), - (AppMode::PrettyQuery, false) => self.render_pretty_query(f, results_area), - (AppMode::CacheList, false) => self.render_cache_list(f, results_area), - (AppMode::ColumnStats, false) => self.render_column_stats(f, results_area), - (_, false) if self.buffer().get_results().is_some() => { - // We need to work around the borrow checker here - // Calculate widths needs mutable self, but we also need to pass results - if let Some(results) = self.buffer().get_results() { - // Extract viewport info first - let terminal_height = results_area.height as usize; - let max_visible_rows = terminal_height.saturating_sub(3).max(10); - let total_rows = if let Some(filtered) = self.buffer().get_filtered_data() { - filtered.len() - } else { - results.data.len() - }; - let row_viewport_start = self - .buffer() - .get_scroll_offset() - .0 - .min(total_rows.saturating_sub(1)); - let row_viewport_end = (row_viewport_start + max_visible_rows).min(total_rows); - - // Calculate column widths based on viewport - self.calculate_viewport_column_widths(row_viewport_start, row_viewport_end); - } - - // Now render the table - if let Some(results) = self.buffer().get_results() { - self.render_table_immutable(f, results_area, results); - } - } - _ => { - // Simple placeholder - reduced text to improve rendering speed - let placeholder = Paragraph::new("Enter SQL query and press Enter\n\nTip: Use Tab for completion, Ctrl+R for history") - .block(Block::default().borders(Borders::ALL).title("Results")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(placeholder, results_area); - } - } - - // Render mode-specific status line - self.render_status_line(f, chunks[2]); - } - - fn render_status_line(&self, f: &mut Frame, area: Rect) { - // Determine the mode color - let (status_style, mode_color) = match self.buffer().get_mode() { - AppMode::Command => (Style::default().fg(Color::Green), Color::Green), - AppMode::Results => (Style::default().fg(Color::Blue), Color::Blue), - AppMode::Search => (Style::default().fg(Color::Yellow), Color::Yellow), - AppMode::Filter => (Style::default().fg(Color::Cyan), Color::Cyan), - AppMode::FuzzyFilter => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::ColumnSearch => (Style::default().fg(Color::Green), Color::Green), - AppMode::Help => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::History => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::Debug => (Style::default().fg(Color::Yellow), Color::Yellow), - AppMode::PrettyQuery => (Style::default().fg(Color::Green), Color::Green), - AppMode::CacheList => (Style::default().fg(Color::Cyan), Color::Cyan), - AppMode::JumpToRow => (Style::default().fg(Color::Magenta), Color::Magenta), - AppMode::ColumnStats => (Style::default().fg(Color::Cyan), Color::Cyan), - }; - - let mode_indicator = match self.buffer().get_mode() { - AppMode::Command => "CMD", - AppMode::Results => "NAV", - AppMode::Search => "SEARCH", - AppMode::Filter => "FILTER", - AppMode::FuzzyFilter => "FUZZY", - AppMode::ColumnSearch => "COL", - AppMode::Help => "HELP", - AppMode::History => "HISTORY", - AppMode::Debug => "DEBUG", - AppMode::PrettyQuery => "PRETTY", - AppMode::CacheList => "CACHE", - AppMode::JumpToRow => "JUMP", - AppMode::ColumnStats => "STATS", - }; - - let mut spans = Vec::new(); - - // Mode indicator with color - spans.push(Span::styled( - format!("[{}]", mode_indicator), - Style::default().fg(mode_color).add_modifier(Modifier::BOLD), - )); - - // Show buffer information - { - let index = self.buffer_manager.current_index(); - let total = self.buffer_manager.all_buffers().len(); - - // Show buffer indicator if multiple buffers - if total > 1 { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}/{}]", index + 1, total), - Style::default().fg(Color::Yellow), - )); - } - - // Show current buffer name - if let Some(buffer) = self.buffer_manager.current() { - spans.push(Span::raw(" ")); - let name = buffer.get_name(); - let modified = if buffer.is_modified() { "*" } else { "" }; - spans.push(Span::styled( - format!("{}{}", name, modified), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } - } - - // Get buffer name from the current buffer - let buffer_name = self.buffer().get_name(); - if !buffer_name.is_empty() && buffer_name != "[Buffer 1]" { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - buffer_name, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } else if self.buffer().is_csv_mode() && !self.buffer().get_table_name().is_empty() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - self.buffer().get_table_name(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - } - - // Mode-specific information - match self.buffer().get_mode() { - AppMode::Command => { - // In command mode, show editing-related info - if !self.get_input_text().trim().is_empty() { - let (token_pos, total_tokens) = self.get_cursor_token_position(); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Token {}/{}", token_pos, total_tokens), - Style::default().fg(Color::DarkGray), - )); - - // Show current token if available - if let Some(token) = self.get_token_at_cursor() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("[{}]", token), - Style::default().fg(Color::Cyan), - )); - } - - // Check for parser errors - if let Some(error_msg) = self.check_parser_error(&self.get_input_text()) { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("{} {}", self.config.display.icons.warning, error_msg), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )); - } - } - } - AppMode::Results => { - // In results mode, show navigation and data info - let total_rows = self.get_row_count(); - if total_rows > 0 { - let selected = self.table_state.selected().unwrap_or(0) + 1; - spans.push(Span::raw(" | ")); - - // Show selection mode - let mode_text = match self.selection_mode { - SelectionMode::Cell => "CELL", - SelectionMode::Row => "ROW", - }; - spans.push(Span::styled( - format!("[{}]", mode_text), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )); - - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("Row {}/{}", selected, total_rows), - Style::default().fg(Color::White), - )); - - // Column information - if let Some(results) = self.buffer().get_results() { - if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - let headers: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - if self.buffer().get_current_column() < headers.len() { - spans.push(Span::raw(" | Col: ")); - spans.push(Span::styled( - headers[self.buffer().get_current_column()], - Style::default().fg(Color::Cyan), - )); - - // Show pinned columns count if any - if !self.buffer().get_pinned_columns().clone().is_empty() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!( - "📌{}", - self.buffer().get_pinned_columns().clone().len() - ), - Style::default().fg(Color::Magenta), - )); - } - - // In cell mode, show the current cell value - if self.selection_mode == SelectionMode::Cell { - if let Some(selected_row) = self.table_state.selected() { - if let Some(row_data) = results.data.get(selected_row) { - if let Some(row_obj) = row_data.as_object() { - if let Some(value) = row_obj.get( - headers[self.buffer().get_current_column()], - ) { - let cell_value = match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "NULL".to_string(), - other => other.to_string(), - }; - - // Truncate if too long - let display_value = if cell_value.len() > 30 - { - format!("{}...", &cell_value[..27]) - } else { - cell_value - }; - - spans.push(Span::raw(" = ")); - spans.push(Span::styled( - display_value, - Style::default().fg(Color::Yellow), - )); - } - } - } - } - } - } - } - } - } - - // Filter indicators - if self.buffer().is_fuzzy_filter_active() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Fuzzy: {}", self.buffer().get_fuzzy_filter_pattern()), - Style::default().fg(Color::Magenta), - )); - } else if self.get_filter_state().active { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - format!("Filter: {}", self.get_filter_state().pattern), - Style::default().fg(Color::Cyan), - )); - } - - // Show last yanked value - if let Some((col, val)) = &self.last_yanked { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - "Yanked: ", - Style::default().fg(Color::DarkGray), - )); - spans.push(Span::styled( - format!("{}={}", col, val), - Style::default().fg(Color::Green), - )); - } - } - } - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - // Show the pattern being typed - always use input for consistency - let pattern = self.get_input_text(); - if !pattern.is_empty() { - spans.push(Span::raw(" | Pattern: ")); - spans.push(Span::styled(pattern, Style::default().fg(mode_color))); - } - } - _ => {} - } - - // Data source indicator (shown in all modes) - if let Some(source) = self.buffer().get_last_query_source() { - spans.push(Span::raw(" | ")); - let (icon, label, color) = match source.as_str() { - "cache" => ( - &self.config.display.icons.cache, - "CACHE".to_string(), - Color::Cyan, - ), - "file" | "FileDataSource" => ( - &self.config.display.icons.file, - "FILE".to_string(), - Color::Green, - ), - "SqlServerDataSource" => ( - &self.config.display.icons.database, - "SQL".to_string(), - Color::Blue, - ), - "PublicApiDataSource" => ( - &self.config.display.icons.api, - "API".to_string(), - Color::Yellow, - ), - _ => ( - &self.config.display.icons.api, - source.clone(), - Color::Magenta, - ), - }; - spans.push(Span::raw(format!("{} ", icon))); - spans.push(Span::styled(label, Style::default().fg(color))); - } else if self.buffer().is_csv_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::raw(&self.config.display.icons.file)); - spans.push(Span::raw(" ")); - spans.push(Span::styled( - format!("CSV: {}", self.buffer().get_table_name()), - Style::default().fg(Color::Green), - )); - } else if self.buffer().is_cache_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::raw(&self.config.display.icons.cache)); - spans.push(Span::raw(" ")); - spans.push(Span::styled("CACHE", Style::default().fg(Color::Cyan))); - } - - // Global indicators (shown when active) - let case_insensitive = self.buffer().is_case_insensitive(); - if case_insensitive { - spans.push(Span::raw(" | ")); - // Use to_string() to ensure we get the actual string value - let icon = self.config.display.icons.case_insensitive.clone(); - spans.push(Span::styled( - format!("{} CASE", icon), - Style::default().fg(Color::Cyan), - )); - } - - if self.buffer().is_compact_mode() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled("COMPACT", Style::default().fg(Color::Green))); - } - - if self.buffer().is_viewport_lock() { - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - &self.config.display.icons.lock, - Style::default().fg(Color::Magenta), - )); - } - - // Help shortcuts (right side) - let help_text = match self.buffer().get_mode() { - AppMode::Command => "Enter:Run | Tab:Complete | ↓:Results | F1:Help", - AppMode::Results => match self.selection_mode { - SelectionMode::Cell => "v:Row mode | y:Yank cell | ↑:Edit | F1:Help", - SelectionMode::Row => "v:Cell mode | y:Yank | f:Filter | ↑:Edit | F1:Help", - }, - AppMode::Search | AppMode::Filter | AppMode::FuzzyFilter | AppMode::ColumnSearch => { - "Enter:Apply | Esc:Cancel" - } - AppMode::Help - | AppMode::Debug - | AppMode::PrettyQuery - | AppMode::CacheList - | AppMode::ColumnStats => "Esc:Close", - AppMode::History => "Enter:Select | Esc:Cancel", - AppMode::JumpToRow => "Enter:Jump | Esc:Cancel", - }; - - // Calculate available space for help text - let current_length: usize = spans.iter().map(|s| s.content.len()).sum(); - let available_width = area.width.saturating_sub(4) as usize; // Account for borders - let help_length = help_text.len(); - - if current_length + help_length + 3 < available_width { - // Add spacing to right-align help text - let padding = available_width - current_length - help_length - 3; - spans.push(Span::raw(" ".repeat(padding))); - spans.push(Span::raw(" | ")); - spans.push(Span::styled( - help_text, - Style::default().fg(Color::DarkGray), - )); - } - - let status_line = Line::from(spans); - let status = Paragraph::new(status_line) - .block(Block::default().borders(Borders::ALL)) - .style(status_style); - f.render_widget(status, area); - } - - fn render_table_immutable(&self, f: &mut Frame, area: Rect, results: &QueryResponse) { - if results.data.is_empty() { - let empty = Paragraph::new("No results found") - .block(Block::default().borders(Borders::ALL).title("Results")) - .style(Style::default().fg(Color::Yellow)); - f.render_widget(empty, area); - return; - } - - // Get headers from first row - let headers: Vec<&str> = if let Some(first_row) = results.data.first() { - if let Some(obj) = first_row.as_object() { - obj.keys().map(|k| k.as_str()).collect() - } else { - vec![] - } - } else { - vec![] - }; - - // Calculate visible columns for virtual scrolling based on actual widths - let terminal_width = area.width as usize; - let available_width = terminal_width.saturating_sub(4); // Account for borders and padding - - // Split columns into pinned and scrollable - let mut pinned_headers: Vec<(usize, &str)> = Vec::new(); - let mut scrollable_indices: Vec = Vec::new(); - - for (i, header) in headers.iter().enumerate() { - if self.buffer().get_pinned_columns().contains(&i) { - pinned_headers.push((i, header)); - } else { - scrollable_indices.push(i); - } - } - - // Calculate space used by pinned columns - let mut pinned_width = 0; - for &(idx, _) in &pinned_headers { - let column_widths = self.buffer().get_column_widths().clone(); - if idx < column_widths.len() { - pinned_width += column_widths[idx] as usize; - } else { - pinned_width += 15; // Default width - } - } - - // Calculate how many scrollable columns can fit in remaining space - let remaining_width = available_width.saturating_sub(pinned_width); - let column_widths = self.buffer().get_column_widths().clone(); - let max_visible_scrollable_cols = if !column_widths.is_empty() { - let mut width_used = 0; - let mut cols_that_fit = 0; - - for &idx in &scrollable_indices { - if idx >= headers.len() { - break; - } - let col_width = if idx < column_widths.len() { - column_widths[idx] as usize - } else { - 15 - }; - if width_used + col_width <= remaining_width { - width_used += col_width; - cols_that_fit += 1; - } else { - break; - } - } - cols_that_fit.max(1) - } else { - // Fallback to old method if no calculated widths - let avg_col_width = 15; - (remaining_width / avg_col_width).max(1) - }; - - // Calculate viewport for scrollable columns based on current_column - let current_in_scrollable = scrollable_indices - .iter() - .position(|&x| x == self.buffer().get_current_column()); - let viewport_start = if let Some(pos) = current_in_scrollable { - if pos < max_visible_scrollable_cols / 2 { - 0 - } else if pos + max_visible_scrollable_cols / 2 >= scrollable_indices.len() { - scrollable_indices - .len() - .saturating_sub(max_visible_scrollable_cols) - } else { - pos.saturating_sub(max_visible_scrollable_cols / 2) - } - } else { - // Current column is pinned, use scroll offset - self.buffer().get_scroll_offset().1.min( - scrollable_indices - .len() - .saturating_sub(max_visible_scrollable_cols), - ) - }; - let viewport_end = - (viewport_start + max_visible_scrollable_cols).min(scrollable_indices.len()); - - // Build final list of visible columns (pinned + scrollable viewport) - let mut visible_columns: Vec<(usize, &str)> = Vec::new(); - visible_columns.extend(pinned_headers.iter().copied()); - for i in viewport_start..viewport_end { - let idx = scrollable_indices[i]; - visible_columns.push((idx, headers[idx])); - } - - // Only work with visible headers - let visible_headers: Vec<&str> = visible_columns.iter().map(|(_, h)| *h).collect(); - - // Calculate viewport dimensions FIRST before processing any data - let terminal_height = area.height as usize; - let max_visible_rows = terminal_height.saturating_sub(3).max(10); - - let total_rows = if let Some(filtered) = self.buffer().get_filtered_data() { - if self.buffer().is_fuzzy_filter_active() - && !self.buffer().get_fuzzy_filter_indices().clone().is_empty() - { - self.buffer().get_fuzzy_filter_indices().clone().len() - } else { - filtered.len() - } - } else { - results.data.len() - }; - - // Calculate row viewport - let row_viewport_start = self - .buffer() - .get_scroll_offset() - .0 - .min(total_rows.saturating_sub(1)); - let row_viewport_end = (row_viewport_start + max_visible_rows).min(total_rows); - - // Prepare table data (only visible rows AND columns) - let data_to_display = if let Some(filtered) = self.buffer().get_filtered_data() { - // Check if fuzzy filter is active - if self.buffer().is_fuzzy_filter_active() - && !self.buffer().get_fuzzy_filter_indices().clone().is_empty() - { - // Apply fuzzy filter on top of existing filter - let mut fuzzy_filtered = Vec::new(); - for &idx in &self.buffer().get_fuzzy_filter_indices().clone() { - if idx < filtered.len() { - fuzzy_filtered.push(filtered[idx].clone()); - } - } - - // Recalculate viewport for fuzzy filtered data - let fuzzy_total = fuzzy_filtered.len(); - let fuzzy_start = self - .buffer() - .get_scroll_offset() - .0 - .min(fuzzy_total.saturating_sub(1)); - let fuzzy_end = (fuzzy_start + max_visible_rows).min(fuzzy_total); - - fuzzy_filtered[fuzzy_start..fuzzy_end] - .iter() - .map(|row| { - visible_columns - .iter() - .map(|(idx, _)| row[*idx].clone()) - .collect() - }) - .collect() - } else { - // Apply both row and column viewport to filtered data - filtered[row_viewport_start..row_viewport_end] - .iter() - .map(|row| { - visible_columns - .iter() - .map(|(idx, _)| row[*idx].clone()) - .collect() - }) - .collect() - } - } else { - // Convert JSON data to string matrix (only visible rows AND columns) - results.data[row_viewport_start..row_viewport_end] - .iter() - .map(|item| { - if let Some(obj) = item.as_object() { - visible_columns - .iter() - .map(|(_, header)| match obj.get(*header) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Null) => "".to_string(), - Some(other) => other.to_string(), - None => "".to_string(), - }) - .collect() - } else { - vec![] - } - }) - .collect::>>() - }; - - // Create header row with sort indicators and column selection - let mut header_cells: Vec = Vec::new(); - - // Add row number header if enabled - if self.buffer().is_show_row_numbers() { - header_cells.push( - Cell::from("#").style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - ); - } - - // Add data headers - header_cells.extend(visible_columns.iter().map(|(actual_col_index, header)| { - let sort_indicator = if let Some(col) = self.sort_state.column { - if col == *actual_col_index { - match self.sort_state.order { - SortOrder::Ascending => " ↑", - SortOrder::Descending => " ↓", - SortOrder::None => "", - } - } else { - "" - } - } else { - "" - }; - - let column_indicator = if *actual_col_index == self.buffer().get_current_column() { - " [*]" - } else { - "" - }; - - // Add pin indicator for pinned columns - let pin_indicator = if self - .buffer() - .get_pinned_columns() - .contains(&*actual_col_index) - { - "📌 " - } else { - "" - }; - - let header_text = format!( - "{}{}{}{}", - pin_indicator, header, sort_indicator, column_indicator - ); - let mut style = Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD); - - // Highlight the current column - if *actual_col_index == self.buffer().get_current_column() { - style = style.bg(Color::DarkGray); - } - - Cell::from(header_text).style(style) - })); - - let selected_row = self.table_state.selected().unwrap_or(0); - - // Create data rows (already filtered to visible rows and columns) - let rows: Vec = data_to_display - .iter() - .enumerate() - .map(|(visible_row_idx, row)| { - let actual_row_idx = row_viewport_start + visible_row_idx; - let mut cells: Vec = Vec::new(); - - // Add row number if enabled - if self.buffer().is_show_row_numbers() { - let row_num = actual_row_idx + 1; // 1-based numbering - cells.push( - Cell::from(row_num.to_string()).style(Style::default().fg(Color::Magenta)), - ); - } - - // Add data cells - cells.extend(row.iter().enumerate().map(|(visible_col_idx, cell)| { - let actual_col_idx = visible_columns[visible_col_idx].0; - let mut style = Style::default(); - - // Cell mode highlighting - highlight only the selected cell - let is_selected_row = actual_row_idx == selected_row; - let is_selected_cell = - is_selected_row && actual_col_idx == self.buffer().get_current_column(); - - if self.selection_mode == SelectionMode::Cell { - // In cell mode, only highlight the specific cell - if is_selected_cell { - // Use a highlighted foreground instead of changing background - // This works better with various terminal color schemes - style = style - .fg(Color::Yellow) // Bright, readable color - .add_modifier(Modifier::BOLD | Modifier::UNDERLINED); - } - } else { - // In row mode, highlight the current column for all rows - if actual_col_idx == self.buffer().get_current_column() { - style = style.bg(Color::DarkGray); - } - } - - // Highlight search matches (override column highlight) - if let Some((match_row, match_col)) = self.buffer().get_current_match() { - if actual_row_idx == match_row && actual_col_idx == match_col { - style = style.bg(Color::Yellow).fg(Color::Black); - } - } - - // Highlight filter matches - if self.get_filter_state().active { - if let Some(ref regex) = self.get_filter_state().regex { - if regex.is_match(cell) { - style = style.fg(Color::Cyan); - } - } - } - - // Highlight fuzzy/exact filter matches - if self.buffer().is_fuzzy_filter_active() - && !self.buffer().get_fuzzy_filter_pattern().is_empty() - { - let pattern = &self.buffer().get_fuzzy_filter_pattern(); - let cell_matches = if pattern.starts_with('\'') && pattern.len() > 1 { - // Exact match highlighting - let exact_pattern = &pattern[1..]; - cell.to_lowercase().contains(&exact_pattern.to_lowercase()) - } else { - // Fuzzy match highlighting - check if this cell contributes to the fuzzy match - if let Some(score) = - SkimMatcherV2::default().fuzzy_match(cell, &pattern) - { - score > 0 - } else { - false - } - }; - - if cell_matches { - style = style.fg(Color::Magenta).add_modifier(Modifier::BOLD); - } - } - - Cell::from(cell.as_str()).style(style) - })); - - Row::new(cells) - }) - .collect(); - - // Calculate column constraints using optimal widths (only for visible columns) - let mut constraints: Vec = Vec::new(); - - // Add constraint for row number column if enabled - if self.buffer().is_show_row_numbers() { - // Calculate width needed for row numbers (max row count digits + padding) - let max_row_num = total_rows; - let row_num_width = max_row_num.to_string().len() as u16 + 2; - constraints.push(Constraint::Length(row_num_width.min(8))); // Cap at 8 chars - } - - // Add data column constraints - let column_widths = self.buffer().get_column_widths().clone(); - if !column_widths.is_empty() { - // Use calculated optimal widths for visible columns - constraints.extend(visible_columns.iter().map(|(col_idx, _)| { - if *col_idx < column_widths.len() { - Constraint::Length(column_widths[*col_idx]) - } else { - Constraint::Min(10) // Fallback - } - })); - } else { - // Fallback to minimum width if no calculated widths available - constraints.extend((0..visible_headers.len()).map(|_| Constraint::Min(10))); - } - - // Build the table with conditional row highlighting - let mut table = Table::new(rows, constraints) - .header(Row::new(header_cells).height(1)) - .block(Block::default() - .borders(Borders::ALL) - .title(format!("Results ({} rows) - {} pinned, {} visible of {} | Viewport rows {}-{} (selected: {}) | Use h/l to scroll", - total_rows, - self.buffer().get_pinned_columns().clone().len(), - visible_columns.len(), - headers.len(), - row_viewport_start + 1, - row_viewport_end, - selected_row + 1))); - - // Only apply row highlighting in row mode - if self.selection_mode == SelectionMode::Row { - table = table - .row_highlight_style(Style::default().bg(Color::DarkGray)) - .highlight_symbol("► "); - } else { - // In cell mode, no row highlighting - cell highlighting is handled above - table = table.highlight_symbol(" "); - } - - let mut table_state = self.table_state.clone(); - // Adjust table state to use relative position within the viewport - if let Some(selected) = table_state.selected() { - let relative_position = selected.saturating_sub(row_viewport_start); - table_state.select(Some(relative_position)); - } - f.render_stateful_widget(table, area, &mut table_state); - } - - fn render_help(&self, f: &mut Frame, area: Rect) { - // Create two-column layout - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - // Get help content from HelpText module - let left_content = HelpText::left_column(); - let right_content = HelpText::right_column(); - - // Calculate visible area for scrolling - let visible_height = area.height.saturating_sub(2) as usize; // Account for borders - let left_total_lines = left_content.len(); - let right_total_lines = right_content.len(); - let max_lines = left_total_lines.max(right_total_lines); - - // Apply scroll offset - let scroll_offset = self.help_scroll as usize; - - // Get visible portions with scrolling - let left_visible: Vec = left_content - .into_iter() - .skip(scroll_offset) - .take(visible_height) - .collect(); - - let right_visible: Vec = right_content - .into_iter() - .skip(scroll_offset) - .take(visible_height) - .collect(); - - // Create scroll indicator in title - let scroll_indicator = if max_lines > visible_height { - format!( - " (↓/↑ to scroll, {}/{})", - scroll_offset + 1, - max_lines.saturating_sub(visible_height) + 1 - ) - } else { - String::new() - }; - - // Render left column - let left_paragraph = Paragraph::new(Text::from(left_visible)) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("Help - Commands{}", scroll_indicator)), - ) - .style(Style::default()); - - // Render right column - let right_paragraph = Paragraph::new(Text::from(right_visible)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Help - Navigation & Features"), - ) - .style(Style::default()); - - f.render_widget(left_paragraph, chunks[0]); - f.render_widget(right_paragraph, chunks[1]); - } - - fn render_debug(&self, f: &mut Frame, area: Rect) { - self.debug_widget.render(f, area, AppMode::Debug); - } - - fn render_pretty_query(&self, f: &mut Frame, area: Rect) { - self.debug_widget.render(f, area, AppMode::PrettyQuery); - } - - fn render_history(&self, f: &mut Frame, area: Rect) { - if self.history_state.matches.is_empty() { - let no_history = if self.history_state.search_query.is_empty() { - "No command history found.\nExecute some queries to build history." - } else { - "No matches found for your search.\nTry a different search term." - }; - - let placeholder = Paragraph::new(no_history) - .block( - Block::default() - .borders(Borders::ALL) - .title("Command History"), - ) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(placeholder, area); - return; - } - - // Split the area to show selected command details - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(50), // History list - 50% of space - Constraint::Percentage(50), // Selected command preview - 50% of space - ]) - .split(area); - - self.render_history_list(f, chunks[0]); - self.render_selected_command_preview(f, chunks[1]); - } - - fn render_history_list(&self, f: &mut Frame, area: Rect) { - // Create more compact history list - just show essential info - let history_items: Vec = self - .history_state - .matches - .iter() - .enumerate() - .map(|(i, history_match)| { - let entry = &history_match.entry; - let is_selected = i == self.history_state.selected_index; - - let success_indicator = if entry.success { "✓" } else { "✗" }; - let time_ago = { - let elapsed = chrono::Utc::now() - entry.timestamp; - if elapsed.num_days() > 0 { - format!("{}d", elapsed.num_days()) - } else if elapsed.num_hours() > 0 { - format!("{}h", elapsed.num_hours()) - } else if elapsed.num_minutes() > 0 { - format!("{}m", elapsed.num_minutes()) - } else { - "now".to_string() - } - }; - - // Use more space for the command, less for metadata - let terminal_width = area.width as usize; - let metadata_space = 15; // Reduced metadata: " ✓ 2x 1h" - let available_for_command = terminal_width.saturating_sub(metadata_space).max(50); - - let command_text = if entry.command.len() > available_for_command { - format!( - "{}…", - &entry.command[..available_for_command.saturating_sub(1)] - ) - } else { - entry.command.clone() - }; - - let line_text = format!( - "{} {} {} {}x {}", - if is_selected { "►" } else { " " }, - command_text, - success_indicator, - entry.execution_count, - time_ago - ); - - let mut style = Style::default(); - if is_selected { - style = style.bg(Color::DarkGray).add_modifier(Modifier::BOLD); - } - if !entry.success { - style = style.fg(Color::Red); - } - - // Highlight matching characters for fuzzy search - if !history_match.indices.is_empty() && is_selected { - style = style.fg(Color::Yellow); - } - - Line::from(line_text).style(style) - }) - .collect(); - - let history_paragraph = Paragraph::new(history_items) - .block(Block::default().borders(Borders::ALL).title(format!( - "History ({} matches) - j/k to navigate, Enter to select", - self.history_state.matches.len() - ))) - .wrap(ratatui::widgets::Wrap { trim: false }); - - f.render_widget(history_paragraph, area); - } - - fn render_selected_command_preview(&self, f: &mut Frame, area: Rect) { - if let Some(selected_match) = self - .history_state - .matches - .get(self.history_state.selected_index) - { - let entry = &selected_match.entry; - - // Pretty format the SQL command - adjust compactness based on available space - use crate::recursive_parser::format_sql_pretty_compact; - - // Calculate how many columns we can fit per line - let available_width = area.width.saturating_sub(6) as usize; // Account for indentation and borders - let avg_col_width = 15; // Assume average column name is ~15 chars - let cols_per_line = (available_width / avg_col_width).max(3).min(12); // Between 3-12 columns per line - - let mut pretty_lines = format_sql_pretty_compact(&entry.command, cols_per_line); - - // If too many lines for the area, use a more compact format - let max_lines = area.height.saturating_sub(2) as usize; // Account for borders - if pretty_lines.len() > max_lines && cols_per_line < 12 { - // Try with more columns per line - pretty_lines = format_sql_pretty_compact(&entry.command, 15); - } - - // Convert to Text with syntax highlighting - let mut highlighted_lines = Vec::new(); - for line in pretty_lines { - highlighted_lines.push(self.sql_highlighter.simple_sql_highlight(&line)); - } - - let preview_text = Text::from(highlighted_lines); - - let duration_text = entry - .duration_ms - .map(|d| format!("{}ms", d)) - .unwrap_or_else(|| "?ms".to_string()); - - let success_text = if entry.success { - "✓ Success" - } else { - "✗ Failed" - }; - - let preview = Paragraph::new(preview_text) - .block(Block::default().borders(Borders::ALL).title(format!( - "Pretty SQL Preview: {} | {} | Used {}x", - success_text, duration_text, entry.execution_count - ))) - .scroll((0, 0)); // Allow scrolling if needed - - f.render_widget(preview, area); - } else { - let empty_preview = Paragraph::new("No command selected") - .block(Block::default().borders(Borders::ALL).title("Preview")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(empty_preview, area); - } - } - - fn handle_cache_command(&mut self, command: &str) -> Result<()> { - let parts: Vec<&str> = command.split_whitespace().collect(); - if parts.len() < 2 { - self.buffer_mut().set_status_message( - "Invalid cache command. Use :cache save or :cache load ".to_string(), - ); - return Ok(()); - } - - match parts[1] { - "save" => { - // Save last query results to cache with optional custom ID - if let Some(results) = self.buffer().get_results() { - let data_to_save = results.data.clone(); // Extract the data we need - let _ = results; // Explicitly drop the borrow - - if let Some(ref mut cache) = self.query_cache { - // Check if a custom ID is provided - let (custom_id, query) = if parts.len() > 2 { - // Check if the first word after "save" could be an ID (alphanumeric) - let potential_id = parts[2]; - if potential_id - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-') - && !potential_id.starts_with("SELECT") - && !potential_id.starts_with("select") - { - // First word is likely an ID - let id = Some(potential_id.to_string()); - let query = if parts.len() > 3 { - parts[3..].join(" ") - } else if let Some(last_entry) = - self.command_history.get_last_entry() - { - last_entry.command.clone() - } else { - self.buffer_mut() - .set_status_message("No query to cache".to_string()); - return Ok(()); - }; - (id, query) - } else { - // No ID provided, treat everything as the query - (None, parts[2..].join(" ")) - } - } else if let Some(last_entry) = self.command_history.get_last_entry() { - (None, last_entry.command.clone()) - } else { - self.buffer_mut() - .set_status_message("No query to cache".to_string()); - return Ok(()); - }; - - match cache.save_query(&query, &data_to_save, custom_id) { - Ok(id) => { - self.buffer_mut().set_status_message(format!( - "Query cached with ID: {} ({} rows)", - id, - data_to_save.len() - )); - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to cache query: {}", e)); - } - } - } - } else { - self.buffer_mut().set_status_message( - "No results to cache. Execute a query first.".to_string(), - ); - } - } - "load" => { - if parts.len() < 3 { - self.buffer_mut() - .set_status_message("Usage: :cache load ".to_string()); - return Ok(()); - } - - if let Ok(id) = parts[2].parse::() { - if let Some(ref cache) = self.query_cache { - match cache.load_query(id) { - Ok((_query, data)) => { - self.buffer_mut().set_cached_data(Some(data.clone())); - self.buffer_mut().set_cache_mode(true); - self.buffer_mut().set_status_message(format!( - "Loaded cache ID {} with {} rows. Cache mode enabled.", - id, - data.len() - )); - - // Update parser with cached data schema if available - if let Some(first_row) = data.first() { - if let Some(obj) = first_row.as_object() { - let columns: Vec = - obj.keys().map(|k| k.to_string()).collect(); - self.hybrid_parser.update_single_table( - "cached_data".to_string(), - columns, - ); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Failed to load cache: {}", e)); - } - } - } - } else { - self.buffer_mut() - .set_status_message("Invalid cache ID".to_string()); - } - } - "list" => { - self.buffer_mut().set_mode(AppMode::CacheList); - } - "clear" => { - self.buffer_mut().set_cache_mode(false); - self.buffer_mut().set_cached_data(None); - self.buffer_mut() - .set_status_message("Cache mode disabled".to_string()); - } - _ => { - self.buffer_mut().set_status_message( - "Unknown cache command. Use save, load, list, or clear.".to_string(), - ); - } - } - - Ok(()) - } - - fn handle_cache_list_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true), - KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => { - self.buffer_mut().set_mode(AppMode::Command); - } - _ => {} - } - Ok(false) - } - - fn handle_column_stats_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match self.stats_widget.handle_key(key) { - StatsAction::Quit => return Ok(true), - StatsAction::Close => { - self.buffer_mut().set_column_stats(None); - self.buffer_mut().set_mode(AppMode::Results); - } - StatsAction::Continue | StatsAction::PassThrough => {} - } - Ok(false) - } - - fn handle_jump_to_row_input(&mut self, key: crossterm::event::KeyEvent) -> Result { - match key.code { - KeyCode::Esc => { - self.buffer_mut().set_mode(AppMode::Results); - self.jump_to_row_input.clear(); - self.buffer_mut() - .set_status_message("Jump cancelled".to_string()); - } - KeyCode::Enter => { - if let Ok(row_num) = self.jump_to_row_input.parse::() { - if row_num > 0 { - let target_row = row_num - 1; // Convert to 0-based index - let max_row = self.get_current_data().map(|d| d.len()).unwrap_or(0); - - if target_row < max_row { - &mut self.table_state.select(Some(target_row)); - - // Adjust viewport to center the target row - let visible_rows = self.buffer().get_last_visible_rows(); - if visible_rows > 0 { - let mut offset = self.buffer().get_scroll_offset(); - offset.0 = target_row.saturating_sub(visible_rows / 2); - self.buffer_mut().set_scroll_offset(offset); - } - - self.buffer_mut() - .set_status_message(format!("Jumped to row {}", row_num)); - } else { - self.buffer_mut().set_status_message(format!( - "Row {} out of range (max: {})", - row_num, max_row - )); - } - } - } - self.buffer_mut().set_mode(AppMode::Results); - self.jump_to_row_input.clear(); - } - KeyCode::Backspace => { - self.jump_to_row_input.pop(); - } - KeyCode::Char(c) if c.is_ascii_digit() => { - self.jump_to_row_input.push(c); - } - _ => {} - } - Ok(false) - } - - fn render_cache_list(&self, f: &mut Frame, area: Rect) { - if let Some(ref cache) = self.query_cache { - let cached_queries = cache.list_cached_queries(); - - if cached_queries.is_empty() { - let empty = Paragraph::new("No cached queries found.\n\nUse :cache save after running a query to cache results.") - .block(Block::default().borders(Borders::ALL).title("Cached Queries (F7)")) - .style(Style::default().fg(Color::DarkGray)); - f.render_widget(empty, area); - return; - } - - // Create table of cached queries - let header_cells = vec!["ID", "Query", "Rows", "Cached At"] - .into_iter() - .map(|h| { - Cell::from(h).style( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - }) - .collect::>(); - - let rows: Vec = cached_queries - .iter() - .map(|query| { - let cells = vec![ - Cell::from(query.id.to_string()), - Cell::from(if query.query_text.len() > 50 { - format!("{}...", &query.query_text[..47]) - } else { - query.query_text.clone() - }), - Cell::from(query.row_count.to_string()), - Cell::from(query.timestamp.format("%Y-%m-%d %H:%M:%S").to_string()), - ]; - Row::new(cells) - }) - .collect(); - - let table = Table::new( - rows, - vec![ - Constraint::Length(6), - Constraint::Percentage(50), - Constraint::Length(8), - Constraint::Length(20), - ], - ) - .header(Row::new(header_cells)) - .block( - Block::default() - .borders(Borders::ALL) - .title("Cached Queries (F7) - Use :cache load to load"), - ) - .row_highlight_style(Style::default().bg(Color::DarkGray)); - - f.render_widget(table, area); - } else { - let error = Paragraph::new("Cache not available") - .block(Block::default().borders(Borders::ALL).title("Cache Error")) - .style(Style::default().fg(Color::Red)); - f.render_widget(error, area); - } - } - - fn render_column_stats(&self, f: &mut Frame, area: Rect) { - // Delegate to the stats widget - self.stats_widget.render(f, area, self.buffer()); - } - - // === Editor Widget Helper Methods === - // These methods handle the actions returned by the editor widget - - fn handle_execute_query(&mut self) -> Result { - // Get the current query text and execute it directly - let query = self.get_input_text().trim().to_string(); - debug!(target: "action", "Executing query: {}", query); - if !query.is_empty() { - // Check for special commands - if query == ":help" { - self.show_help = true; - self.buffer_mut().set_mode(AppMode::Help); - self.buffer_mut() - .set_status_message("Help Mode - Press ESC to return".to_string()); - } else if query == ":exit" || query == ":quit" { - return Ok(true); - } else { - // Execute the SQL query - self.buffer_mut() - .set_status_message(format!("Processing query: '{}'", query)); - if let Err(e) = self.execute_query(&query) { - self.buffer_mut() - .set_status_message(format!("Error executing query: {}", e)); - } - // Don't clear input - preserve query for editing - } - } - Ok(false) // Continue running, don't exit - } - - fn handle_buffer_action(&mut self, action: BufferAction) -> Result { - match action { - BufferAction::NextBuffer => { - let message = self.buffer_handler.next_buffer(&mut self.buffer_manager); - debug!("{}", message); - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - Ok(false) - } - BufferAction::PreviousBuffer => { - let message = self - .buffer_handler - .previous_buffer(&mut self.buffer_manager); - debug!("{}", message); - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - Ok(false) - } - BufferAction::QuickSwitch => { - let message = self.buffer_handler.quick_switch(&mut self.buffer_manager); - debug!("{}", message); - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - Ok(false) - } - BufferAction::NewBuffer => { - let message = self - .buffer_handler - .new_buffer(&mut self.buffer_manager, &self.config); - debug!("{}", message); - Ok(false) - } - BufferAction::CloseBuffer => { - let (success, message) = self.buffer_handler.close_buffer(&mut self.buffer_manager); - debug!("{}", message); - Ok(!success) // Exit if we couldn't close (only one left) - } - BufferAction::ListBuffers => { - let buffer_list = self.buffer_handler.list_buffers(&self.buffer_manager); - // For now, just log the list - later we can show a popup - for line in &buffer_list { - debug!("{}", line); - } - Ok(false) - } - BufferAction::SwitchToBuffer(buffer_index) => { - let message = self - .buffer_handler - .switch_to_buffer(&mut self.buffer_manager, buffer_index); - debug!("{}", message); - - // Update parser schema for the new buffer - self.update_parser_for_current_buffer(); - - Ok(false) - } - } - } - - fn handle_expand_asterisk(&mut self) -> Result { - if let Some(buffer) = self.buffer_manager.current_mut() { - if buffer.expand_asterisk(&self.hybrid_parser) { - // Sync for rendering if needed - if buffer.get_edit_mode() == EditMode::SingleLine { - let text = buffer.get_input_text(); - let cursor = buffer.get_input_cursor_position(); - self.set_input_text_with_cursor(text, cursor); - } - } - } - Ok(false) - } - - fn toggle_debug_mode(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - match buffer.get_mode() { - AppMode::Debug => { - buffer.set_mode(AppMode::Command); - } - _ => { - buffer.set_mode(AppMode::Debug); - // Generate full debug information like the original F5 handler - self.debug_current_buffer(); - let cursor_pos = self.get_input_cursor(); - let visual_cursor = self.get_visual_cursor().1; - let query = self.get_input_text(); - - // Collect all needed data before mutable borrow - let buffer_names: Vec = self - .buffer_manager - .all_buffers() - .iter() - .map(|b| b.get_name()) - .collect(); - let buffer_count = self.buffer_manager.all_buffers().len(); - let buffer_index = self.buffer_manager.current_index(); - let api_url = self.api_client.base_url.clone(); - - // Generate debug info directly without buffer reference - let mut debug_info = self - .hybrid_parser - .get_detailed_debug_info(&query, cursor_pos); - - // Add input state - debug_info.push_str(&format!( - "\n========== INPUT STATE ==========\n\ - Input Value Length: {}\n\ - Cursor Position: {}\n\ - Visual Cursor: {}\n\ - Input Mode: Command\n", - query.len(), - cursor_pos, - visual_cursor - )); - - // Add buffer state info - debug_info.push_str(&format!( - "\n========== BUFFER MANAGER STATE ==========\n\ - Number of Buffers: {}\n\ - Current Buffer Index: {}\n\ - Buffer Names: {}\n", - buffer_count, - buffer_index, - buffer_names.join(", ") - )); - - // Add WHERE clause AST if needed - if query.to_lowercase().contains(" where ") { - let where_ast_info = match self.parse_where_clause_ast(&query) { - Ok(ast_str) => ast_str, - Err(e) => format!("\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {}\n", e) - }; - debug_info.push_str(&where_ast_info); - } - - // Add key chord handler debug info - debug_info.push_str("\n"); - debug_info.push_str(&self.key_chord_handler.format_debug_info()); - debug_info.push_str("========================================\n"); - - // Add trace logs from ring buffer - debug_info.push_str("\n========== TRACE LOGS ==========\n"); - debug_info.push_str("(Most recent at bottom, last 100 entries)\n"); - if let Some(ref log_buffer) = self.log_buffer { - let recent_logs = log_buffer.get_recent(100); - for entry in recent_logs { - debug_info.push_str(&entry.format_for_display()); - debug_info.push('\n'); - } - debug_info.push_str(&format!("Total log entries: {}\n", log_buffer.len())); - } else { - debug_info.push_str("Log buffer not initialized\n"); - } - debug_info.push_str("================================\n"); - - // Set the final content in debug widget - self.debug_widget.set_content(debug_info.clone()); - - // Try to copy to clipboard - match arboard::Clipboard::new() { - Ok(mut clipboard) => match clipboard.set_text(&debug_info) { - Ok(_) => { - // Verify clipboard write by reading it back - match clipboard.get_text() { - Ok(clipboard_content) => { - let clipboard_len = clipboard_content.len(); - if clipboard_content == debug_info { - self.buffer_mut().set_status_message(format!( - "DEBUG INFO copied to clipboard ({} chars)!", - clipboard_len - )); - } else { - self.buffer_mut().set_status_message(format!( - "Clipboard verification failed! Expected {} chars, got {} chars", - debug_info.len(), clipboard_len - )); - } - } - Err(e) => { - self.buffer_mut().set_status_message(format!( - "Debug info copied but verification failed: {}", - e - )); - } - } - } - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Clipboard error: {}", e)); - } - }, - Err(e) => { - self.buffer_mut() - .set_status_message(format!("Can't access clipboard: {}", e)); - } - } - } - } - } - } - - fn show_pretty_query(&mut self) { - if let Some(buffer) = self.buffer_manager.current_mut() { - buffer.set_mode(AppMode::PrettyQuery); - let query = buffer.get_input_text(); - self.debug_widget.generate_pretty_sql(&query); - } - } -} - -pub fn run_enhanced_tui_multi(api_url: &str, data_files: Vec<&str>) -> Result<()> { - let app = if !data_files.is_empty() { - // Load the first file using existing logic - let first_file = data_files[0]; - let extension = std::path::Path::new(first_file) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - - let mut app = match extension.to_lowercase().as_str() { - "csv" => EnhancedTuiApp::new_with_csv(first_file)?, - "json" => EnhancedTuiApp::new_with_json(first_file)?, - _ => { - return Err(anyhow::anyhow!( - "Unsupported file type: {}. Use .csv or .json files.", - first_file - )) - } - }; - - // Set the file path for the first buffer if we have multiple files - if data_files.len() > 1 { - if let Some(buffer) = app.buffer_manager.current_mut() { - buffer.set_file_path(Some(first_file.to_string())); - let filename = std::path::Path::new(first_file) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - buffer.set_name(filename.to_string()); - } - } - - // Load additional files into separate buffers - if data_files.len() > 1 { - for (_index, file_path) in data_files.iter().skip(1).enumerate() { - let extension = std::path::Path::new(file_path) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - - match extension.to_lowercase().as_str() { - "csv" | "json" => { - // Get config value before mutable borrow - let case_insensitive = app.config.behavior.case_insensitive_default; - - // Create a new buffer for each additional file - app.new_buffer(); - - // Get the current buffer and set it up - if let Some(buffer) = app.buffer_manager.current_mut() { - // Create and configure CSV client for this buffer - let mut csv_client = CsvApiClient::new(); - csv_client.set_case_insensitive(case_insensitive); - - // Get table name from file - let raw_name = std::path::Path::new(file_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - let table_name = EnhancedTuiApp::sanitize_table_name(&raw_name); - - // Load the data - if extension.to_lowercase() == "csv" { - if let Err(e) = csv_client.load_csv(file_path, &table_name) { - app.buffer_mut().set_status_message(format!( - "Error loading {}: {}", - file_path, e - )); - continue; - } - } else { - if let Err(e) = csv_client.load_json(file_path, &table_name) { - app.buffer_mut().set_status_message(format!( - "Error loading {}: {}", - file_path, e - )); - continue; - } - } - - // Set the CSV client and metadata in the buffer - buffer.set_csv_client(Some(csv_client)); - buffer.set_csv_mode(true); - buffer.set_table_name(table_name.clone()); - - info!(target: "buffer", "Loaded {} file '{}' into buffer {}: table='{}', case_insensitive={}", - extension.to_uppercase(), file_path, buffer.get_id(), table_name, case_insensitive); - - // Set query - let query = format!("SELECT * FROM {}", table_name); - buffer.set_input_text(query); - - // Store the file path and name - buffer.set_file_path(Some(file_path.to_string())); - let filename = std::path::Path::new(file_path) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - buffer.set_name(filename.to_string()); - } - } - _ => { - app.buffer_mut().set_status_message(format!( - "Skipping unsupported file: {}", - file_path - )); - continue; - } - } - } - - // Switch back to the first buffer - app.buffer_manager.switch_to(0); - - app.buffer_mut().set_status_message(format!( - "Loaded {} files into separate buffers. Use Alt+Tab to switch.", - data_files.len() - )); - } - - app - } else { - EnhancedTuiApp::new(api_url) - }; - - app.run() -} - -pub fn run_enhanced_tui(api_url: &str, data_file: Option<&str>) -> Result<()> { - // For backward compatibility, convert single file to vec and call multi version - let files = if let Some(file) = data_file { - vec![file] - } else { - vec![] - }; - run_enhanced_tui_multi(api_url, files) -} diff --git a/sql-cli/src/global_state.rs b/sql-cli/src/global_state.rs index 2bff266b..8a5ce624 100644 --- a/sql-cli/src/global_state.rs +++ b/sql-cli/src/global_state.rs @@ -1,6 +1,6 @@ use crate::api_client::ApiClient; use crate::cache::QueryCache; -use crate::config::Config; +use crate::config::config::Config; use crate::history::CommandHistory; use crate::hybrid_parser::HybridParser; use crate::parser::SqlParser; diff --git a/sql-cli/src/lib.rs b/sql-cli/src/lib.rs index a5dd36e6..db2fa4ec 100644 --- a/sql-cli/src/lib.rs +++ b/sql-cli/src/lib.rs @@ -1,52 +1,64 @@ +// New module structure (gradually moving files here) +pub mod api; +pub mod config; +pub mod core; +pub mod data; +pub mod sql; +pub mod state; +pub mod ui; +pub mod utils; +pub mod widgets; + +// Existing flat structure (to be gradually moved to modules above) pub mod api_client; -pub mod app_paths; +// pub mod app_paths; // Moved to utils/ pub mod buffer; pub mod buffer_handler; -pub mod cache; +// pub mod cache; // Moved to sql/ pub mod cell_renderer; -pub mod config; -pub mod csv_datasource; -pub mod csv_fixes; -pub mod cursor_aware_parser; +// pub mod config; // Moved to config module +// pub mod csv_datasource; // Moved to data/ +// pub mod csv_fixes; // Moved to data/ +// pub mod cursor_aware_parser; // Moved to sql/ pub mod cursor_operations; -pub mod data_exporter; -pub mod data_provider; -pub mod datasource_adapter; -pub mod datasource_trait; -pub mod datatable; -pub mod datatable_buffer; -pub mod datatable_converter; -pub mod datatable_loaders; -pub mod datatable_view; -pub mod debouncer; -pub mod debug_info; -pub mod debug_service; -pub mod debug_widget; -pub mod dual_logging; +// pub mod data_exporter; // Moved to data/ +// pub mod data_provider; // Moved to data/ +// pub mod datasource_adapter; // Moved to data/ +// pub mod datasource_trait; // Moved to data/ +// pub mod datatable; // Moved to data/ +// pub mod datatable_buffer; // Moved to data/ +// pub mod datatable_converter; // Moved to data/ +// pub mod datatable_loaders; // Moved to data/ +// pub mod datatable_view; // Moved to data/ +// pub mod debouncer; // Moved to utils/ +// pub mod debug_info; // Moved to utils/ +// pub mod debug_service; // Moved to utils/ +// pub mod debug_widget; // Moved to widgets/ +// pub mod dual_logging; // Moved to utils/ pub mod dynamic_schema; -pub mod editor_widget; +// pub mod editor_widget; // Moved to widgets/ pub mod global_state; -pub mod help_widget; +// pub mod help_widget; // Moved to widgets/ pub mod history; pub mod history_protection; -pub mod history_widget; -pub mod hybrid_parser; +// pub mod history_widget; // Moved to widgets/ +// pub mod hybrid_parser; // Moved to sql/ pub mod input_manager; pub mod key_indicator; -pub mod logging; -pub mod modern_input; -pub mod modern_tui; -pub mod parser; -pub mod recursive_parser; -pub mod schema_config; -pub mod search_modes_widget; +// pub mod logging; // Moved to utils/ +// pub mod modern_input; // Removed - experimental +// pub mod modern_tui; // Moved to ui/ +// pub mod parser; // Moved to sql/ +// pub mod recursive_parser; // Moved to sql/ +// pub mod schema_config; // Moved to config/ +// pub mod search_modes_widget; // Moved to widgets/ pub mod service_container; -pub mod sql_highlighter; +// pub mod sql_highlighter; // Moved to sql/ pub mod state_manager; -pub mod stats_widget; +// pub mod stats_widget; // Moved to widgets/ pub mod virtual_table; -pub mod where_ast; -pub mod where_parser; +// pub mod where_ast; // Moved to sql/ +// pub mod where_parser; // Moved to sql/ pub mod widget_traits; pub mod yank_manager; @@ -56,14 +68,68 @@ pub mod app_state_container; pub mod column_manager; pub mod completion_manager; pub mod cursor_manager; -pub mod data_analyzer; +// pub mod data_analyzer; // Moved to data/ pub mod help_text; pub mod history_manager; -pub mod key_bindings; +// pub mod key_bindings; // Moved to config/ pub mod key_chord_handler; -pub mod key_dispatcher; +// pub mod key_dispatcher; // Moved to ui/ pub mod search_filter; pub mod text_navigation; -pub mod tui_renderer; -pub mod tui_state; +// pub mod tui_renderer; // Moved to ui/ +// pub mod tui_state; // Moved to ui/ // pub mod data_manager; // TODO: Fix QueryResponse field access + +// Re-export widgets for backward compatibility +pub use widgets::debug_widget; +pub use widgets::editor_widget; +pub use widgets::help_widget; +pub use widgets::history_widget; +pub use widgets::search_modes_widget; +pub use widgets::stats_widget; + +// Re-export data modules for backward compatibility +pub use data::csv_datasource; +pub use data::csv_fixes; +pub use data::data_analyzer; +pub use data::data_exporter; +pub use data::data_provider; +pub use data::datasource_adapter; +pub use data::datasource_trait; +pub use data::datatable; +pub use data::datatable_buffer; +pub use data::datatable_converter; +pub use data::datatable_loaders; +pub use data::datatable_view; + +// Re-export UI modules for backward compatibility +pub use ui::enhanced_tui; +pub use ui::key_dispatcher; +pub use ui::tui_app; +pub use ui::tui_renderer; +pub use ui::tui_state; + +// Re-export SQL modules for backward compatibility +pub use sql::cache; +pub use sql::cursor_aware_parser; +pub use sql::hybrid_parser; +pub use sql::parser; +pub use sql::recursive_parser; +pub use sql::smart_parser; +pub use sql::sql_highlighter; +pub use sql::where_ast; +pub use sql::where_parser; + +// Re-export utils modules for backward compatibility +pub use utils::app_paths; +pub use utils::debouncer; +pub use utils::debug_helpers; +pub use utils::debug_info; +pub use utils::debug_service; +pub use utils::dual_logging; +pub use utils::logging; + +// Re-export config modules for backward compatibility +pub use config::config as config_module; +pub use config::key_bindings; +pub use config::schema_config; diff --git a/sql-cli/src/main.rs b/sql-cli/src/main.rs index 4b105795..71f8d2a7 100644 --- a/sql-cli/src/main.rs +++ b/sql-cli/src/main.rs @@ -4,25 +4,14 @@ use reedline::{ MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, ReedlineMenu, Signal, ValidationResult, Validator, }; -use sql_cli::app_paths::AppPaths; +use sql_cli::utils::app_paths::AppPaths; use std::{borrow::Cow, io}; mod completer; -mod csv_fixes; -mod cursor_aware_parser; -mod enhanced_tui; -mod modern_tui_main; -mod parser; -mod recursive_parser; -mod schema_config; -mod smart_parser; -mod sql_highlighter; mod table_display; -mod tui_app; -mod virtual_table; use completer::SqlCompleter; -use parser::{ParseState, SqlParser}; +use sql_cli::sql::parser::{ParseState, SqlParser}; use sql_cli::api_client::ApiClient; use table_display::{display_results, export_to_csv}; @@ -105,10 +94,6 @@ fn print_help() { ); println!(" {} - Use classic CLI mode", "--classic".green()); println!(" {} - Use simple TUI mode", "--simple".green()); - println!( - " {} - Use experimental modern TUI", - "--modern".green() - ); println!(); println!("{}", "Commands:".yellow()); println!(" {} - Execute query and fetch results", "Enter".green()); @@ -150,10 +135,10 @@ fn execute_query(client: &ApiClient, query: &str) -> Result<(), Box io::Result<()> { // Initialize unified logging (tracing + dual logging) - sql_cli::logging::init_tracing_with_dual_logging(); + sql_cli::utils::logging::init_tracing_with_dual_logging(); // Get the dual logger to show the log path - if let Some(dual_logger) = sql_cli::dual_logging::get_dual_logger() { + if let Some(dual_logger) = sql_cli::utils::dual_logging::get_dual_logger() { eprintln!("📝 Debug logs will be written to:"); eprintln!(" {}", dual_logger.log_path().display()); eprintln!(" Tail with: tail -f {}", dual_logger.log_path().display()); @@ -165,7 +150,7 @@ fn main() -> io::Result<()> { // Check for config initialization if args.contains(&"--init-config".to_string()) { - match sql_cli::config::Config::init_wizard() { + match sql_cli::config::config::Config::init_wizard() { Ok(config) => { println!("\nConfiguration initialized successfully!"); if !config.display.use_glyphs { @@ -182,9 +167,9 @@ fn main() -> io::Result<()> { // Check for config file generation if args.contains(&"--generate-config".to_string()) { - match sql_cli::config::Config::get_config_path() { + match sql_cli::config::config::Config::get_config_path() { Ok(path) => { - let config_content = sql_cli::config::Config::create_default_with_comments(); + let config_content = sql_cli::config::config::Config::create_default_with_comments(); if let Some(parent) = path.parent() { if let Err(e) = std::fs::create_dir_all(parent) { eprintln!("Error creating config directory: {}", e); @@ -232,7 +217,7 @@ fn main() -> io::Result<()> { if use_tui { if use_classic_tui { println!("Starting simple TUI mode... (use --enhanced for csvlens-style features)"); - if let Err(e) = tui_app::run_tui_app() { + if let Err(e) = sql_cli::ui::tui_app::run_tui_app() { eprintln!("TUI Error: {}", e); std::process::exit(1); } @@ -249,39 +234,22 @@ fn main() -> io::Result<()> { ); } else { println!( - "Starting enhanced TUI mode... (use --modern for experimental TUI, --simple for basic TUI, --classic for CLI)" + "Starting enhanced TUI mode... (use --simple for basic TUI, --classic for CLI)" ); } let api_url = std::env::var("TRADE_API_URL") .unwrap_or_else(|_| "http://localhost:5000".to_string()); - // Check if user wants modern TUI (experimental) - let use_modern = args.contains(&"--modern".to_string()); - - let result = if use_modern { - // Use the experimental modern TUI - if data_files.len() > 1 { - let file_refs: Vec<&str> = data_files.iter().map(|s| s.as_str()).collect(); - modern_tui_main::run_modern_tui_multi(&api_url, file_refs) - } else { - modern_tui_main::run_modern_tui(&api_url, data_file.as_deref()) - } + // Use the enhanced TUI by default + let result = if data_files.len() > 1 { + let file_refs: Vec<&str> = data_files.iter().map(|s| s.as_str()).collect(); + sql_cli::ui::enhanced_tui::run_enhanced_tui_multi(&api_url, file_refs) } else { - // Use the enhanced TUI by default (stable and feature-complete) - if data_files.len() > 1 { - let file_refs: Vec<&str> = data_files.iter().map(|s| s.as_str()).collect(); - enhanced_tui::run_enhanced_tui_multi(&api_url, file_refs) - } else { - enhanced_tui::run_enhanced_tui(&api_url, data_file.as_deref()) - } + sql_cli::ui::enhanced_tui::run_enhanced_tui(&api_url, data_file.as_deref()) }; if let Err(e) = result { - if use_modern { - eprintln!("Modern TUI Error: {}", e); - } else { - eprintln!("Enhanced TUI Error: {}", e); - } + eprintln!("Enhanced TUI Error: {}", e); eprintln!("Falling back to classic CLI mode..."); eprintln!(""); // Don't exit, fall through to classic mode diff --git a/sql-cli/src/modern_input.rs b/sql-cli/src/modern_input.rs deleted file mode 100644 index 80d570ec..00000000 --- a/sql-cli/src/modern_input.rs +++ /dev/null @@ -1,861 +0,0 @@ -use crate::history::{CommandHistory, HistoryMatch}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::style::{Color, Style}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Paragraph}; -use std::collections::VecDeque; - -/// Modern, clean input system without legacy baggage -/// Preserves all the excellent features of the original but with cleaner architecture -pub struct ModernInput { - /// Current input text - text: String, - - /// Cursor position (byte offset) - cursor: usize, - - /// History management - history: CommandHistory, - history_navigation: HistoryNavigation, - - /// Current schema context for history search - schema_columns: Vec, - data_source: Option, - - /// Input mode - mode: InputMode, - - /// Search state for fuzzy history search - search_state: SearchState, - - /// Undo/redo stack - undo_stack: VecDeque, - redo_stack: VecDeque, - max_undo: usize, -} - -/// Input modes -#[derive(Debug, Clone, PartialEq)] -pub enum InputMode { - Normal, // Regular typing - HistorySearch, // Ctrl+R fuzzy search through history - HistoryNav, // Up/Down arrow navigation -} - -/// History navigation state -#[derive(Debug, Clone)] -struct HistoryNavigation { - entries: Vec, - current_index: Option, - original_input: Option, -} - -/// Search state for Ctrl+R fuzzy search -#[derive(Debug, Clone)] -struct SearchState { - query: String, - matches: Vec, - selected_index: usize, - original_input: String, -} - -/// Snapshot for undo/redo -#[derive(Debug, Clone)] -struct InputSnapshot { - text: String, - cursor: usize, -} - -impl ModernInput { - /// Create a new modern input - pub fn new() -> Self { - Self { - text: String::new(), - cursor: 0, - history: CommandHistory::default(), - history_navigation: HistoryNavigation { - entries: Vec::new(), - current_index: None, - original_input: None, - }, - schema_columns: Vec::new(), - data_source: None, - mode: InputMode::Normal, - search_state: SearchState { - query: String::new(), - matches: Vec::new(), - selected_index: 0, - original_input: String::new(), - }, - undo_stack: VecDeque::new(), - redo_stack: VecDeque::new(), - max_undo: 50, - } - } - - /// Create with initial text - pub fn with_text(text: String) -> Self { - let cursor = text.len(); - Self { - text, - cursor, - ..Self::new() - } - } - - /// Set schema context for better history matching - pub fn set_schema_context(&mut self, columns: Vec, data_source: Option) { - self.schema_columns = columns; - self.data_source = data_source; - } - - /// Get current text - pub fn text(&self) -> &str { - &self.text - } - - /// Get current cursor position - pub fn cursor_position(&self) -> usize { - self.cursor - } - - /// Get current input mode - pub fn mode(&self) -> &InputMode { - &self.mode - } - - /// Clear the input - pub fn clear(&mut self) { - self.save_snapshot(); - self.text.clear(); - self.cursor = 0; - self.exit_special_modes(); - } - - /// Set text and cursor to end - pub fn set_text(&mut self, text: String) { - self.save_snapshot(); - self.cursor = text.len(); - self.text = text; - self.exit_special_modes(); - } - - /// Handle key events - returns true if input was consumed - pub fn handle_key_event(&mut self, key: KeyEvent) -> bool { - match &self.mode { - InputMode::Normal => self.handle_normal_mode(key), - InputMode::HistorySearch => self.handle_history_search_mode(key), - InputMode::HistoryNav => self.handle_history_nav_mode(key), - } - } - - /// Add command to history - pub fn add_to_history(&mut self, command: String, success: bool, duration_ms: Option) { - if let Err(e) = self.history.add_entry_with_schema( - command, - success, - duration_ms, - self.schema_columns.clone(), - self.data_source.clone(), - ) { - eprintln!("Failed to add to history: {}", e); - } - - // Update navigation entries - self.update_navigation_entries(); - } - - /// Handle normal input mode - fn handle_normal_mode(&mut self, key: KeyEvent) -> bool { - match key.code { - // History search (Ctrl+R) - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.enter_history_search_mode(); - true - } - - // History navigation (Up/Down) - KeyCode::Up => { - self.enter_history_nav_mode(); - self.history_nav_previous(); - true - } - KeyCode::Down => { - if self.mode == InputMode::HistoryNav { - self.history_nav_next(); - } else { - self.enter_history_nav_mode(); - self.history_nav_next(); - } - true - } - - // Undo/Redo - KeyCode::Char('z') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.undo(); - true - } - KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.redo(); - true - } - - // Regular editing - _ => self.handle_edit_keys(key), - } - } - - /// Handle history search mode (Ctrl+R) - fn handle_history_search_mode(&mut self, key: KeyEvent) -> bool { - match key.code { - // Continue search - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.search_next_match(); - true - } - - // Accept current match - KeyCode::Enter => { - self.accept_search_match(); - true - } - - // Cancel search - KeyCode::Esc => { - self.cancel_search(); - true - } - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.cancel_search(); - true - } - - // Navigate matches - KeyCode::Up => { - self.search_prev_match(); - true - } - KeyCode::Down => { - self.search_next_match(); - true - } - - // Edit search query - KeyCode::Char(c) => { - self.search_add_char(c); - true - } - KeyCode::Backspace => { - self.search_delete_char(); - true - } - - _ => false, - } - } - - /// Handle history navigation mode (Up/Down arrows) - fn handle_history_nav_mode(&mut self, key: KeyEvent) -> bool { - match key.code { - KeyCode::Up => { - self.history_nav_previous(); - true - } - KeyCode::Down => { - self.history_nav_next(); - true - } - KeyCode::Esc => { - self.exit_special_modes(); - true - } - // Any other key exits navigation mode - _ => { - self.exit_special_modes(); - self.handle_edit_keys(key) - } - } - } - - /// Handle regular editing keys - fn handle_edit_keys(&mut self, key: KeyEvent) -> bool { - self.save_snapshot(); - - match key.code { - KeyCode::Char(c) => { - if key.modifiers.contains(KeyModifiers::CONTROL) { - self.handle_control_char(c) - } else { - self.insert_char(c); - true - } - } - KeyCode::Backspace => { - self.delete_char_backward(); - true - } - KeyCode::Delete => { - self.delete_char_forward(); - true - } - KeyCode::Left => { - if key.modifiers.contains(KeyModifiers::CONTROL) { - self.move_word_backward(); - } else { - self.move_cursor_left(); - } - true - } - KeyCode::Right => { - if key.modifiers.contains(KeyModifiers::CONTROL) { - self.move_word_forward(); - } else { - self.move_cursor_right(); - } - true - } - KeyCode::Home => { - self.move_cursor_home(); - true - } - KeyCode::End => { - self.move_cursor_end(); - true - } - _ => false, - } - } - - /// Handle Ctrl+key combinations - fn handle_control_char(&mut self, c: char) -> bool { - match c { - 'a' => { - self.move_cursor_home(); - true - } - 'e' => { - self.move_cursor_end(); - true - } - 'f' => { - self.move_cursor_right(); - true - } - 'b' => { - self.move_cursor_left(); - true - } - 'd' => { - self.delete_char_forward(); - true - } - 'h' => { - self.delete_char_backward(); - true - } - 'k' => { - self.delete_to_end_of_line(); - true - } - 'u' => { - self.delete_to_start_of_line(); - true - } - 'w' => { - self.delete_word_backward(); - true - } - 'l' => { - self.clear(); - true - } - _ => false, - } - } - - // === Text editing operations === - - fn insert_char(&mut self, c: char) { - self.text - .insert(self.char_index_to_byte_index(self.cursor), c); - self.cursor += 1; - } - - fn delete_char_backward(&mut self) { - if self.cursor > 0 { - self.cursor -= 1; - let byte_index = self.char_index_to_byte_index(self.cursor); - self.text.remove(byte_index); - } - } - - fn delete_char_forward(&mut self) { - if self.cursor < self.text.chars().count() { - let byte_index = self.char_index_to_byte_index(self.cursor); - self.text.remove(byte_index); - } - } - - fn delete_to_end_of_line(&mut self) { - let byte_index = self.char_index_to_byte_index(self.cursor); - self.text.truncate(byte_index); - } - - fn delete_to_start_of_line(&mut self) { - let byte_index = self.char_index_to_byte_index(self.cursor); - self.text.drain(0..byte_index); - self.cursor = 0; - } - - fn delete_word_backward(&mut self) { - let original_cursor = self.cursor; - self.move_word_backward(); - let start_byte = self.char_index_to_byte_index(self.cursor); - let end_byte = self.char_index_to_byte_index(original_cursor); - self.text.drain(start_byte..end_byte); - } - - // === Cursor movement === - - fn move_cursor_left(&mut self) { - if self.cursor > 0 { - self.cursor -= 1; - } - } - - fn move_cursor_right(&mut self) { - let char_count = self.text.chars().count(); - if self.cursor < char_count { - self.cursor += 1; - } - } - - fn move_cursor_home(&mut self) { - self.cursor = 0; - } - - fn move_cursor_end(&mut self) { - self.cursor = self.text.chars().count(); - } - - fn move_word_backward(&mut self) { - while self.cursor > 0 - && self - .char_at_cursor_minus_1() - .map_or(false, |c| c.is_whitespace()) - { - self.cursor -= 1; - } - while self.cursor > 0 - && self - .char_at_cursor_minus_1() - .map_or(false, |c| !c.is_whitespace()) - { - self.cursor -= 1; - } - } - - fn move_word_forward(&mut self) { - let char_count = self.text.chars().count(); - while self.cursor < char_count - && self.char_at_cursor().map_or(false, |c| !c.is_whitespace()) - { - self.cursor += 1; - } - while self.cursor < char_count && self.char_at_cursor().map_or(false, |c| c.is_whitespace()) - { - self.cursor += 1; - } - } - - // === History search mode === - - fn enter_history_search_mode(&mut self) { - self.search_state = SearchState { - query: String::new(), - matches: Vec::new(), - selected_index: 0, - original_input: self.text.clone(), - }; - self.mode = InputMode::HistorySearch; - self.update_search_matches(); - } - - fn search_add_char(&mut self, c: char) { - self.search_state.query.push(c); - self.update_search_matches(); - } - - fn search_delete_char(&mut self) { - self.search_state.query.pop(); - self.update_search_matches(); - } - - fn search_next_match(&mut self) { - if !self.search_state.matches.is_empty() { - self.search_state.selected_index = - (self.search_state.selected_index + 1) % self.search_state.matches.len(); - self.update_input_from_search(); - } - } - - fn search_prev_match(&mut self) { - if !self.search_state.matches.is_empty() { - self.search_state.selected_index = if self.search_state.selected_index == 0 { - self.search_state.matches.len() - 1 - } else { - self.search_state.selected_index - 1 - }; - self.update_input_from_search(); - } - } - - fn accept_search_match(&mut self) { - self.mode = InputMode::Normal; - self.cursor = self.text.chars().count(); - } - - fn cancel_search(&mut self) { - self.text = self.search_state.original_input.clone(); - self.cursor = self.text.chars().count(); - self.mode = InputMode::Normal; - } - - fn update_search_matches(&mut self) { - self.search_state.matches = self.history.search_with_schema( - &self.search_state.query, - &self.schema_columns, - self.data_source.as_deref(), - ); - - if self.search_state.selected_index >= self.search_state.matches.len() { - self.search_state.selected_index = 0; - } - - self.update_input_from_search(); - } - - fn update_input_from_search(&mut self) { - if let Some(selected_match) = self - .search_state - .matches - .get(self.search_state.selected_index) - { - self.text = selected_match.entry.command.clone(); - } else if self.search_state.query.is_empty() { - self.text = self.search_state.original_input.clone(); - } else { - // No matches, show original input - self.text = self.search_state.original_input.clone(); - } - self.cursor = self.text.chars().count(); - } - - // === History navigation mode === - - fn enter_history_nav_mode(&mut self) { - if self.mode != InputMode::HistoryNav { - self.history_navigation.original_input = Some(self.text.clone()); - self.history_navigation.current_index = None; - self.mode = InputMode::HistoryNav; - } - } - - fn history_nav_previous(&mut self) { - if self.history_navigation.entries.is_empty() { - return; - } - - let new_index = match self.history_navigation.current_index { - None => Some(self.history_navigation.entries.len() - 1), - Some(0) => Some(0), // Stay at oldest - Some(i) => Some(i - 1), - }; - - if let Some(index) = new_index { - if let Some(entry) = self.history_navigation.entries.get(index) { - self.text = entry.clone(); - self.cursor = self.text.chars().count(); - self.history_navigation.current_index = Some(index); - } - } - } - - fn history_nav_next(&mut self) { - match self.history_navigation.current_index { - None => return, // Not navigating - Some(i) if i >= self.history_navigation.entries.len() - 1 => { - // Go back to original input - if let Some(original) = &self.history_navigation.original_input { - self.text = original.clone(); - self.cursor = self.text.chars().count(); - } - self.history_navigation.current_index = None; - } - Some(i) => { - if let Some(entry) = self.history_navigation.entries.get(i + 1) { - self.text = entry.clone(); - self.cursor = self.text.chars().count(); - self.history_navigation.current_index = Some(i + 1); - } - } - } - } - - fn update_navigation_entries(&mut self) { - self.history_navigation.entries = self - .history - .get_navigation_entries() - .into_iter() - .map(|e| e.command) - .collect(); - } - - // === Mode management === - - fn exit_special_modes(&mut self) { - self.mode = InputMode::Normal; - self.history_navigation.current_index = None; - self.history_navigation.original_input = None; - } - - // === Undo/Redo === - - fn save_snapshot(&mut self) { - let snapshot = InputSnapshot { - text: self.text.clone(), - cursor: self.cursor, - }; - - self.undo_stack.push_back(snapshot); - if self.undo_stack.len() > self.max_undo { - self.undo_stack.pop_front(); - } - - // Clear redo stack on new action - self.redo_stack.clear(); - } - - fn undo(&mut self) { - if let Some(snapshot) = self.undo_stack.pop_back() { - let current = InputSnapshot { - text: self.text.clone(), - cursor: self.cursor, - }; - self.redo_stack.push_back(current); - - self.text = snapshot.text; - self.cursor = snapshot.cursor; - } - } - - fn redo(&mut self) { - if let Some(snapshot) = self.redo_stack.pop_back() { - let current = InputSnapshot { - text: self.text.clone(), - cursor: self.cursor, - }; - self.undo_stack.push_back(current); - - self.text = snapshot.text; - self.cursor = snapshot.cursor; - } - } - - // === Utility methods === - - fn char_index_to_byte_index(&self, char_index: usize) -> usize { - self.text - .char_indices() - .nth(char_index) - .map(|(byte_index, _)| byte_index) - .unwrap_or(self.text.len()) - } - - fn char_at_cursor(&self) -> Option { - self.text.chars().nth(self.cursor) - } - - fn char_at_cursor_minus_1(&self) -> Option { - if self.cursor > 0 { - self.text.chars().nth(self.cursor - 1) - } else { - None - } - } - - // === Rendering === - - /// Create a widget for rendering the input - pub fn create_widget(&self) -> Paragraph<'_> { - match &self.mode { - InputMode::Normal | InputMode::HistoryNav => { - let title = if self.mode == InputMode::HistoryNav { - "Query (History Navigation)" - } else { - "Query" - }; - - Paragraph::new(self.text.as_str()) - .block(Block::default().borders(Borders::ALL).title(title)) - } - InputMode::HistorySearch => self.create_search_widget(), - } - } - - fn create_search_widget(&self) -> Paragraph<'_> { - let mut spans = vec![ - Span::styled("(reverse-i-search)`", Style::default().fg(Color::Cyan)), - Span::styled(&self.search_state.query, Style::default().fg(Color::Yellow)), - Span::styled("': ", Style::default().fg(Color::Cyan)), - Span::raw(&self.text), - ]; - - // Show match count - if !self.search_state.matches.is_empty() { - let match_info = format!( - " [{}/{}]", - self.search_state.selected_index + 1, - self.search_state.matches.len() - ); - spans.push(Span::styled( - match_info, - Style::default().fg(Color::DarkGray), - )); - } - - Paragraph::new(Line::from(spans)).block( - Block::default() - .borders(Borders::ALL) - .title("History Search (Ctrl+R: next, Enter: select, Esc: cancel)") - .style(Style::default().fg(Color::Cyan)), - ) - } - - /// Get cursor position for rendering - pub fn visual_cursor_position(&self) -> usize { - match &self.mode { - InputMode::Normal | InputMode::HistoryNav => self.cursor, - InputMode::HistorySearch => { - // In search mode, cursor is after the search query - self.search_state.query.chars().count() + 20 // Offset for prompt - } - } - } - - /// Get status information - pub fn get_status(&self) -> String { - match &self.mode { - InputMode::Normal => String::new(), - InputMode::HistoryNav => { - if let Some(index) = self.history_navigation.current_index { - format!( - "History: {}/{}", - index + 1, - self.history_navigation.entries.len() - ) - } else { - "History navigation".to_string() - } - } - InputMode::HistorySearch => { - if self.search_state.matches.is_empty() { - format!("Search: '{}' (no matches)", self.search_state.query) - } else { - format!( - "Search: '{}' ({}/{})", - self.search_state.query, - self.search_state.selected_index + 1, - self.search_state.matches.len() - ) - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crossterm::event::KeyModifiers; - - #[test] - fn test_basic_input() { - let mut input = ModernInput::new(); - - // Type some text - let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE); - input.handle_key_event(key); - - assert_eq!(input.text(), "h"); - assert_eq!(input.cursor_position(), 1); - } - - #[test] - fn test_cursor_movement() { - let mut input = ModernInput::with_text("hello world".to_string()); - - // Move to beginning - let key = KeyEvent::new(KeyCode::Home, KeyModifiers::NONE); - input.handle_key_event(key); - assert_eq!(input.cursor_position(), 0); - - // Move right by word - let key = KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL); - input.handle_key_event(key); - assert_eq!(input.cursor_position(), 6); // After "hello " (includes space) - } - - #[test] - fn test_deletion() { - let mut input = ModernInput::with_text("hello world".to_string()); - - // Delete word backward from end - let key = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL); - input.handle_key_event(key); - assert_eq!(input.text(), "hello "); - } - - #[test] - fn test_history_search_mode() { - let mut input = ModernInput::new(); - - // Enter search mode - let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL); - let handled = input.handle_key_event(key); - assert!(handled); - assert_eq!(input.mode(), &InputMode::HistorySearch); - - // Cancel search - let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE); - let handled = input.handle_key_event(key); - assert!(handled); - assert_eq!(input.mode(), &InputMode::Normal); - } - - #[test] - fn test_undo_redo() { - let mut input = ModernInput::new(); - - // Type and save snapshot - let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE); - input.handle_key_event(key); - - let key = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE); - input.handle_key_event(key); - - assert_eq!(input.text(), "ab"); - - // Undo - let key = KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL); - input.handle_key_event(key); - - assert_eq!(input.text(), "a"); - - // Redo - let key = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL); - input.handle_key_event(key); - - assert_eq!(input.text(), "ab"); - } -} diff --git a/sql-cli/src/modern_tui.rs b/sql-cli/src/modern_tui.rs deleted file mode 100644 index d30b4f26..00000000 --- a/sql-cli/src/modern_tui.rs +++ /dev/null @@ -1,516 +0,0 @@ -use crate::datatable::DataTable; -use crate::datatable_view::{DataTableView, SortOrder, ViewMode}; -use crate::modern_input::{InputMode, ModernInput}; -use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; -use ratatui::backend::Backend; -use ratatui::layout::{Alignment, Constraint, Direction, Layout}; -use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use ratatui::{Frame, Terminal}; -use std::io; - -/// The main modern TUI application - stripped down and focused -pub struct ModernTui { - /// The current view we're displaying - view: DataTableView, - - /// Modern input system - input: ModernInput, - - /// Terminal state - should_quit: bool, - - /// Current mode - mode: TuiMode, - - /// Viewport dimensions (for virtualization) - viewport_height: u16, - viewport_width: u16, -} - -/// TUI application modes -#[derive(Debug, Clone, PartialEq)] -enum TuiMode { - Query, // User is typing/editing queries - Results, // User is viewing/navigating results -} - -impl ModernTui { - /// Create a new modern TUI with a DataTable - pub fn new(table: DataTable) -> Self { - let mut input = ModernInput::new(); - - // Set schema context for better history matching - let columns: Vec = table.column_names(); - let table_name = table.name.clone(); - input.set_schema_context(columns, Some(table_name.clone())); - - // Set initial query like enhanced TUI does - input.set_text(format!("SELECT * FROM {}", table_name)); - - Self { - view: DataTableView::new(table), - input, - should_quit: false, - mode: TuiMode::Results, // Start in Results mode since we have data - viewport_height: 20, - viewport_width: 80, - } - } - - /// Main run loop - pub fn run(&mut self, terminal: &mut Terminal) -> io::Result<()> { - loop { - // Draw the UI - terminal.draw(|f| { - // Update viewport before drawing - let size = f.area(); - self.view.update_viewport(size.width, size.height); - self.draw(f) - })?; - - // Handle input - if event::poll(std::time::Duration::from_millis(100))? { - if let Event::Key(key) = event::read()? { - if self.handle_key_event(key) { - break; - } - } - } - - if self.should_quit { - break; - } - } - Ok(()) - } - - /// Handle keyboard input - fn handle_key_event(&mut self, key: KeyEvent) -> bool { - // Global quit keys - if matches!(key.code, KeyCode::Char('q')) && key.modifiers.contains(KeyModifiers::CONTROL) { - self.should_quit = true; - return true; - } - - // Mode switching - match key.code { - KeyCode::Tab => { - self.toggle_mode(); - return false; - } - KeyCode::Esc => { - // Always go back to query mode on Esc - self.mode = TuiMode::Query; - self.view.exit_special_mode(); - return false; - } - _ => {} - } - - // Handle based on current mode - match self.mode { - TuiMode::Query => self.handle_query_mode(key), - TuiMode::Results => self.handle_results_mode(key), - } - } - - /// Toggle between query and results mode - fn toggle_mode(&mut self) { - self.mode = match self.mode { - TuiMode::Query => TuiMode::Results, - TuiMode::Results => TuiMode::Query, - }; - } - - /// Handle keys in query mode - fn handle_query_mode(&mut self, key: KeyEvent) -> bool { - // Let input handle most keys - if self.input.handle_key_event(key) { - return false; - } - - // Handle keys not handled by input - match key.code { - KeyCode::Enter => { - // Execute query (for now just switch to results) - let query = self.input.text().to_string(); - if !query.trim().is_empty() { - // TODO: Actually execute the query and update the view - // For now, just add to history and switch modes - self.input.add_to_history(query, true, None); - self.mode = TuiMode::Results; - } - false - } - _ => false, - } - } - - /// Handle keys in results mode - fn handle_results_mode(&mut self, key: KeyEvent) -> bool { - // Handle view-specific keys first - match self.view.mode() { - ViewMode::Normal => self.handle_results_normal_mode(key), - ViewMode::Filtering => { - self.view.handle_filter_input(key); - false - } - ViewMode::Searching => { - self.view.handle_search_input(key); - false - } - ViewMode::Sorting => { - // TODO: Implement sort column selection - self.view.exit_special_mode(); - false - } - } - } - - /// Handle keys in results normal mode - fn handle_results_normal_mode(&mut self, key: KeyEvent) -> bool { - match key.code { - // Navigation - KeyCode::Up - | KeyCode::Down - | KeyCode::Left - | KeyCode::Right - | KeyCode::PageUp - | KeyCode::PageDown - | KeyCode::Home - | KeyCode::End => { - self.view.handle_navigation(key); - } - - // Enter filter mode - KeyCode::Char('f') if key.modifiers.is_empty() => { - self.view.enter_filter_mode(); - } - - // Enter search mode - KeyCode::Char('/') => { - self.view.enter_search_mode(); - } - - // Search navigation - KeyCode::Char('n') if key.modifiers.is_empty() => { - self.view.next_search_match(); - } - KeyCode::Char('N') if key.modifiers.is_empty() => { - self.view.prev_search_match(); - } - - // Sort by current column - KeyCode::Char('s') if key.modifiers.is_empty() => { - // Sort ascending by current column - self.view - .apply_sort(self.get_selected_column(), SortOrder::Ascending); - } - KeyCode::Char('S') if key.modifiers.is_empty() => { - // Sort descending by current column - self.view - .apply_sort(self.get_selected_column(), SortOrder::Descending); - } - - // Clear filters/search/sort - KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { - self.view.clear_filter(); - self.view.clear_search(); - self.view.clear_sort(); - } - - // Clear filter only - KeyCode::Char('F') if key.modifiers.is_empty() => { - self.view.clear_filter(); - } - - // Quit - KeyCode::Char('q') if key.modifiers.is_empty() => { - self.should_quit = true; - return true; - } - - _ => {} - } - - false - } - - /// Draw the UI - fn draw(&mut self, f: &mut Frame) { - let size = f.area(); - self.viewport_height = size.height.saturating_sub(6); // Reserve space for input and status - self.viewport_width = size.width; - - // Create main layout - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Query input area - Constraint::Min(5), // Main table area - Constraint::Length(3), // Status area - Constraint::Length(1), // Help line - ]) - .split(size); - - // Draw query input - highlight if active - let input_widget = if self.mode == TuiMode::Query { - self.input.create_widget().block( - Block::default() - .borders(Borders::ALL) - .title("Query") - .style(Style::default().fg(Color::Yellow)), - ) - } else { - self.input.create_widget().block( - Block::default() - .borders(Borders::ALL) - .title("Query") - .style(Style::default().fg(Color::DarkGray)), - ) - }; - f.render_widget(input_widget, chunks[0]); - - // Draw the main table - highlight if active - let table_widget = if self.mode == TuiMode::Results { - self.view.create_table_widget().block( - Block::default() - .borders(Borders::ALL) - .title("Data") - .style(Style::default()), - ) - } else { - self.view.create_table_widget().block( - Block::default() - .borders(Borders::ALL) - .title("Data") - .style(Style::default().fg(Color::DarkGray)), - ) - }; - f.render_widget(table_widget, chunks[1]); - - // Draw status information - let mut status_lines = Vec::new(); - - // Current mode - let mode_text = match self.mode { - TuiMode::Query => "Query Mode", - TuiMode::Results => "Results Mode", - }; - status_lines.push(format!("Mode: {}", mode_text)); - - // Input status (history search, etc.) - let input_status = self.input.get_status(); - if !input_status.is_empty() { - status_lines.push(input_status); - } - - // View status (filters, search, etc.) - let view_status = self.view.get_status_info(); - if !view_status.is_empty() { - status_lines.push(view_status); - } - - let status_text = status_lines.join(" | "); - let status = Paragraph::new(status_text) - .block(Block::default().borders(Borders::ALL).title("Status")) - .alignment(Alignment::Left); - f.render_widget(status, chunks[2]); - - // Draw help line - let help_text = self.get_help_text(); - let help = Paragraph::new(help_text) - .style(Style::default().fg(Color::DarkGray)) - .alignment(Alignment::Left); - f.render_widget(help, chunks[3]); - - // Draw input overlay if view is in special mode - if let Some(input_widget) = self.view.create_input_widget() { - self.draw_input_overlay(f, input_widget); - } - - // Draw selected cell value if in results mode and available - if self.mode == TuiMode::Results { - if let Some(value) = self.view.get_selected_value() { - self.draw_cell_inspector(f, value); - } - } - } - - /// Draw an input overlay for filter/search modes - fn draw_input_overlay(&self, f: &mut Frame, widget: Paragraph) { - let size = f.area(); - - // Create a centered popup area - let popup_area = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(40), - Constraint::Length(3), - Constraint::Percentage(40), - ]) - .split(size)[1]; - - let popup_area = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(20), - Constraint::Percentage(60), - Constraint::Percentage(20), - ]) - .split(popup_area)[1]; - - // Clear the area and render the widget - f.render_widget(Clear, popup_area); - f.render_widget(widget, popup_area); - } - - /// Draw cell value inspector (small overlay showing full cell content) - fn draw_cell_inspector(&self, f: &mut Frame, value: &crate::datatable::DataValue) { - let size = f.area(); - - // Only show if the value is long enough to be worth inspecting - let value_str = value.to_string(); - if value_str.len() <= 20 { - return; - } - - // Create inspector area in bottom right - let inspector_width = 40.min(size.width / 3); - let inspector_height = 5.min(size.height / 4); - - let inspector_area = ratatui::layout::Rect { - x: size.width.saturating_sub(inspector_width + 1), - y: size.height.saturating_sub(inspector_height + 1), - width: inspector_width, - height: inspector_height, - }; - - let inspector = Paragraph::new(value_str) - .block( - Block::default() - .borders(Borders::ALL) - .title("Cell Value") - .style(Style::default().bg(Color::DarkGray)), - ) - .wrap(ratatui::widgets::Wrap { trim: true }); - - f.render_widget(Clear, inspector_area); - f.render_widget(inspector, inspector_area); - } - - /// Get help text based on current mode - fn get_help_text(&self) -> String { - match (&self.mode, &self.view.mode(), &self.input.mode()) { - // Query mode help - (TuiMode::Query, _, InputMode::Normal) => { - "Query Mode: Type SQL | ↑↓: History | Ctrl+R: Search History | Enter: Execute | Tab: Switch to Results | Ctrl+Q: Quit".to_string() - } - (TuiMode::Query, _, InputMode::HistorySearch) => { - "History Search: Ctrl+R: Next | Enter: Select | Esc: Cancel | Type to search...".to_string() - } - (TuiMode::Query, _, InputMode::HistoryNav) => { - "History Navigation: ↑↓: Navigate | Enter: Select | Esc: Cancel | Any key: Edit".to_string() - } - - // Results mode help - (TuiMode::Results, ViewMode::Normal, _) => { - "Results Mode: ↑↓←→: Navigate | f: Filter | /: Search | s/S: Sort | Tab: Switch to Query | Esc: Query Mode".to_string() - } - (TuiMode::Results, ViewMode::Filtering, _) => { - "Filter Mode: Enter: Apply | Esc: Cancel | Type to filter...".to_string() - } - (TuiMode::Results, ViewMode::Searching, _) => { - "Search Mode: Enter: Apply | Esc: Cancel | n/N: Next/Prev | Type to search...".to_string() - } - (TuiMode::Results, ViewMode::Sorting, _) => { - "Sort Mode: Select column | Esc: Cancel".to_string() - } - } - } - - /// Get the currently selected column index - fn get_selected_column(&self) -> usize { - self.view.get_selected_column() - } -} - -/// Create and run the modern TUI with a DataTable -pub fn run_modern_tui(table: DataTable) -> io::Result<()> { - // Setup terminal - crossterm::terminal::enable_raw_mode()?; - let mut stdout = io::stdout(); - crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen)?; - - let backend = ratatui::backend::CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // Run the app - let mut app = ModernTui::new(table); - let result = app.run(&mut terminal); - - // Cleanup - crossterm::terminal::disable_raw_mode()?; - crossterm::execute!( - terminal.backend_mut(), - crossterm::terminal::LeaveAlternateScreen - )?; - - result -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::datatable::{DataColumn, DataRow, DataType, DataValue}; - - fn create_test_table() -> DataTable { - let mut table = DataTable::new("test"); - - table.add_column(DataColumn::new("id").with_type(DataType::Integer)); - table.add_column(DataColumn::new("name").with_type(DataType::String)); - - table - .add_row(DataRow::new(vec![ - DataValue::Integer(1), - DataValue::String("Alice".to_string()), - ])) - .unwrap(); - - table - } - - #[test] - fn test_modern_tui_creation() { - let table = create_test_table(); - let tui = ModernTui::new(table); - - assert!(!tui.should_quit); - assert_eq!(tui.view.visible_row_count(), 1); - } - - #[test] - fn test_key_handling() { - let table = create_test_table(); - let mut tui = ModernTui::new(table); - - // Test quit key - let quit_key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE); - let should_quit = tui.handle_key_event(quit_key); - assert!(should_quit); - assert!(tui.should_quit); - } - - #[test] - fn test_filter_mode() { - let table = create_test_table(); - let mut tui = ModernTui::new(table); - - // Enter filter mode - let filter_key = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE); - tui.handle_key_event(filter_key); - - assert_eq!(tui.view.mode(), ViewMode::Filtering); - } -} diff --git a/sql-cli/src/modern_tui_main.rs b/sql-cli/src/modern_tui_main.rs deleted file mode 100644 index baf9c09a..00000000 --- a/sql-cli/src/modern_tui_main.rs +++ /dev/null @@ -1,138 +0,0 @@ -use anyhow::{Context, Result}; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -}; -use ratatui::backend::CrosstermBackend; -use ratatui::Terminal; -use sql_cli::datatable_loaders::{load_csv_to_datatable, load_json_to_datatable}; -use sql_cli::modern_tui::ModernTui; -use std::io::stdout; -use std::path::Path; - -/// Run the modern TUI with a single data file -pub fn run_modern_tui(_api_url: &str, data_file: Option<&str>) -> Result<()> { - // For now, we'll focus on file-based data since that's what we have working - // TODO: Integrate with API client later - - let table = if let Some(file_path) = data_file { - load_data_file(file_path)? - } else { - // Create empty table or show help - return show_usage(); - }; - - run_tui_with_table(table) -} - -/// Run the modern TUI with multiple data files -pub fn run_modern_tui_multi(_api_url: &str, data_files: Vec<&str>) -> Result<()> { - if data_files.is_empty() { - return show_usage(); - } - - // For now, just use the first file - // TODO: Implement multi-file support (tabs, switching, etc.) - let table = load_data_file(data_files[0])?; - - if data_files.len() > 1 { - println!( - "Note: Multi-file support coming soon. Using first file: {}", - data_files[0] - ); - } - - run_tui_with_table(table) -} - -/// Load a data file into a DataTable -fn load_data_file(file_path: &str) -> Result { - let path = Path::new(file_path); - - if !path.exists() { - return Err(anyhow::anyhow!("File not found: {}", file_path)); - } - - let table_name = path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("data") - .to_string(); - - let extension = path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_lowercase(); - - match extension.as_str() { - "json" => load_json_to_datatable(path, &table_name) - .with_context(|| format!("Failed to load JSON file: {}", file_path)), - "csv" => load_csv_to_datatable(path, &table_name) - .with_context(|| format!("Failed to load CSV file: {}", file_path)), - _ => Err(anyhow::anyhow!("Unsupported file type: {}", extension)), - } -} - -/// Run the TUI with a DataTable -fn run_tui_with_table(table: sql_cli::datatable::DataTable) -> Result<()> { - // Print loading info - println!( - "Loading {} with {} rows and {} columns...", - table.name, - table.row_count(), - table.column_count() - ); - println!("Starting Modern SQL CLI...\n"); - - // Setup terminal - enable_raw_mode().context("Failed to enable raw mode")?; - let mut stdout = stdout(); - execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?; - - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).context("Failed to create terminal")?; - - // Run the app - let mut app = ModernTui::new(table); - let result = app.run(&mut terminal); - - // Cleanup - disable_raw_mode().context("Failed to disable raw mode")?; - execute!(terminal.backend_mut(), LeaveAlternateScreen) - .context("Failed to leave alternate screen")?; - - result.context("TUI execution failed") -} - -/// Show usage information -fn show_usage() -> Result<()> { - println!("SQL CLI - Modern TUI"); - println!(""); - println!("Usage:"); - println!(" sql-cli Load JSON file"); - println!(" sql-cli Load CSV file"); - println!(""); - println!("Controls:"); - println!(" Tab Switch between Query/Results mode"); - println!(" Esc Return to Query mode"); - println!(" Ctrl+Q Quit"); - println!(""); - println!("Query Mode:"); - println!(" ↑↓ Navigate history"); - println!(" Ctrl+R Fuzzy search history"); - println!(" Enter Execute query (switch to Results)"); - println!(" Ctrl+A/E Beginning/End of line"); - println!(" Ctrl+W Delete word backward"); - println!(" Ctrl+K Delete to end of line"); - println!(""); - println!("Results Mode:"); - println!(" ↑↓←→ Navigate data"); - println!(" f Filter"); - println!(" / Search"); - println!(" s/S Sort ascending/descending"); - println!(" n/N Next/previous search match"); - println!(" c Clear filters/search"); - - Ok(()) -} diff --git a/sql-cli/src/cache.rs b/sql-cli/src/sql/cache.rs similarity index 100% rename from sql-cli/src/cache.rs rename to sql-cli/src/sql/cache.rs diff --git a/sql-cli/src/cursor_aware_parser.rs b/sql-cli/src/sql/cursor_aware_parser.rs similarity index 100% rename from sql-cli/src/cursor_aware_parser.rs rename to sql-cli/src/sql/cursor_aware_parser.rs diff --git a/sql-cli/src/hybrid_parser.rs b/sql-cli/src/sql/hybrid_parser.rs similarity index 100% rename from sql-cli/src/hybrid_parser.rs rename to sql-cli/src/sql/hybrid_parser.rs diff --git a/sql-cli/src/sql/mod.rs b/sql-cli/src/sql/mod.rs new file mode 100644 index 00000000..ab4d7193 --- /dev/null +++ b/sql-cli/src/sql/mod.rs @@ -0,0 +1,14 @@ +//! SQL parsing, execution, and optimization +//! +//! This module handles all SQL-related functionality including +//! parsing, query optimization, execution, and caching. + +pub mod cache; +pub mod cursor_aware_parser; +pub mod hybrid_parser; +pub mod parser; +pub mod recursive_parser; +pub mod smart_parser; +pub mod sql_highlighter; +pub mod where_ast; +pub mod where_parser; diff --git a/sql-cli/src/parser.rs b/sql-cli/src/sql/parser.rs similarity index 100% rename from sql-cli/src/parser.rs rename to sql-cli/src/sql/parser.rs diff --git a/sql-cli/src/recursive_parser.rs b/sql-cli/src/sql/recursive_parser.rs similarity index 100% rename from sql-cli/src/recursive_parser.rs rename to sql-cli/src/sql/recursive_parser.rs diff --git a/sql-cli/src/smart_parser.rs b/sql-cli/src/sql/smart_parser.rs similarity index 100% rename from sql-cli/src/smart_parser.rs rename to sql-cli/src/sql/smart_parser.rs diff --git a/sql-cli/src/sql_highlighter.rs b/sql-cli/src/sql/sql_highlighter.rs similarity index 100% rename from sql-cli/src/sql_highlighter.rs rename to sql-cli/src/sql/sql_highlighter.rs diff --git a/sql-cli/src/where_ast.rs b/sql-cli/src/sql/where_ast.rs similarity index 100% rename from sql-cli/src/where_ast.rs rename to sql-cli/src/sql/where_ast.rs diff --git a/sql-cli/src/where_parser.rs b/sql-cli/src/sql/where_parser.rs similarity index 100% rename from sql-cli/src/where_parser.rs rename to sql-cli/src/sql/where_parser.rs diff --git a/sql-cli/src/state/mod.rs b/sql-cli/src/state/mod.rs new file mode 100644 index 00000000..eac6c0bb --- /dev/null +++ b/sql-cli/src/state/mod.rs @@ -0,0 +1,16 @@ +//! State management components +//! +//! This module contains individual state components that are +//! orchestrated by the AppStateContainer. + +// State components to be extracted from app_state_container.rs: +// - selection_state.rs +// - filter_state.rs +// - sort_state.rs +// - search_state.rs +// - column_search_state.rs +// - clipboard_state.rs +// - chord_state.rs +// - undo_redo_state.rs +// - navigation_state.rs +// - completion_state.rs diff --git a/sql-cli/src/enhanced_tui.rs b/sql-cli/src/ui/enhanced_tui.rs similarity index 99% rename from sql-cli/src/enhanced_tui.rs rename to sql-cli/src/ui/enhanced_tui.rs index 023e0123..78b74646 100644 --- a/sql-cli/src/enhanced_tui.rs +++ b/sql-cli/src/ui/enhanced_tui.rs @@ -1,5 +1,38 @@ +use crate::api_client::{ApiClient, QueryResponse}; +use crate::app_state_container::{AppStateContainer, SelectionMode}; +use crate::buffer::{ + AppMode, BufferAPI, BufferManager, ColumnStatistics, ColumnType, EditMode, SortOrder, SortState, +}; +use crate::buffer_handler::BufferHandler; +use crate::cell_renderer::CellRenderer; +use crate::config::config::Config; +use crate::cursor_manager::CursorManager; +use crate::data::csv_datasource::CsvApiClient; +use crate::data::data_analyzer::DataAnalyzer; +use crate::data::data_exporter::DataExporter; +use crate::help_text::HelpText; +use crate::history::{CommandHistory, HistoryMatch}; +use crate::key_chord_handler::{ChordResult, KeyChordHandler}; +use crate::key_indicator::{format_key_for_display, KeyPressIndicator}; use crate::parser::SqlParser; +use crate::service_container::ServiceContainer; +use crate::sql::cache::QueryCache; +use crate::sql::hybrid_parser::HybridParser; use crate::sql_highlighter::SqlHighlighter; +use crate::text_navigation::TextNavigator; +use crate::ui::key_dispatcher::KeyDispatcher; +use crate::utils::debug_info::DebugInfo; +use crate::utils::logging::{get_log_buffer, LogRingBuffer}; +use crate::where_ast::format_where_ast; +use crate::where_parser::WhereParser; +use crate::widget_traits::DebugInfoProvider; +use crate::widgets::debug_widget::DebugWidget; +use crate::widgets::editor_widget::{BufferAction, EditorAction, EditorWidget}; +use crate::widgets::help_widget::{HelpAction, HelpWidget}; +use crate::widgets::search_modes_widget::{SearchMode, SearchModesAction, SearchModesWidget}; +use crate::widgets::stats_widget::{StatsAction, StatsWidget}; +use crate::yank_manager::YankManager; +use crate::{buffer, data_analyzer, dual_logging}; use anyhow::Result; use crossterm::{ event::{ @@ -20,38 +53,6 @@ use ratatui::{ }; use regex::Regex; use serde_json::Value; -use sql_cli::api_client::{ApiClient, QueryResponse}; -use sql_cli::app_state_container::{AppStateContainer, SelectionMode}; -use sql_cli::buffer::{ - AppMode, BufferAPI, BufferManager, ColumnStatistics, ColumnType, EditMode, SortOrder, SortState, -}; -use sql_cli::buffer_handler::BufferHandler; -use sql_cli::cache::QueryCache; -use sql_cli::cell_renderer::CellRenderer; -use sql_cli::config::Config; -use sql_cli::csv_datasource::CsvApiClient; -use sql_cli::cursor_manager::CursorManager; -use sql_cli::data_analyzer::DataAnalyzer; -use sql_cli::data_exporter::DataExporter; -use sql_cli::debug_info::DebugInfo; -use sql_cli::debug_widget::DebugWidget; -use sql_cli::editor_widget::{BufferAction, EditorAction, EditorWidget}; -use sql_cli::help_text::HelpText; -use sql_cli::help_widget::{HelpAction, HelpWidget}; -use sql_cli::history::{CommandHistory, HistoryMatch}; -use sql_cli::hybrid_parser::HybridParser; -use sql_cli::key_chord_handler::{ChordResult, KeyChordHandler}; -use sql_cli::key_dispatcher::KeyDispatcher; -use sql_cli::key_indicator::{format_key_for_display, KeyPressIndicator}; -use sql_cli::logging::{get_log_buffer, LogRingBuffer}; -use sql_cli::search_modes_widget::{SearchMode, SearchModesAction, SearchModesWidget}; -use sql_cli::service_container::ServiceContainer; -use sql_cli::stats_widget::{StatsAction, StatsWidget}; -use sql_cli::text_navigation::TextNavigator; -use sql_cli::where_ast::format_where_ast; -use sql_cli::where_parser::WhereParser; -use sql_cli::widget_traits::DebugInfoProvider; -use sql_cli::yank_manager::YankManager; use std::io; use std::sync::Arc; use tracing::{debug, error, info, trace, warn}; @@ -208,15 +209,15 @@ impl EnhancedTuiApp { // These methods provide a gradual migration path from direct field access to BufferAPI /// Get current buffer if available (for reading) - fn current_buffer(&self) -> Option<&dyn sql_cli::buffer::BufferAPI> { + fn current_buffer(&self) -> Option<&dyn buffer::BufferAPI> { self.buffer_manager .current() - .map(|b| b as &dyn sql_cli::buffer::BufferAPI) + .map(|b| b as &dyn buffer::BufferAPI) } /// Get current buffer (panics if none exists) /// Use this when we know a buffer should always exist - fn buffer(&self) -> &dyn sql_cli::buffer::BufferAPI { + fn buffer(&self) -> &dyn buffer::BufferAPI { self.current_buffer() .expect("No buffer available - this should not happen") } @@ -225,7 +226,7 @@ impl EnhancedTuiApp { /// Get current mutable buffer (panics if none exists) /// Use this when we know a buffer should always exist - fn buffer_mut(&mut self) -> &mut sql_cli::buffer::Buffer { + fn buffer_mut(&mut self) -> &mut buffer::Buffer { self.buffer_manager .current_mut() .expect("No buffer available - this should not happen") @@ -393,7 +394,7 @@ impl EnhancedTuiApp { }); // Log initialization - if let Some(logger) = sql_cli::dual_logging::get_dual_logger() { + if let Some(logger) = dual_logging::get_dual_logger() { logger.log( "INFO", "EnhancedTuiApp", @@ -403,7 +404,7 @@ impl EnhancedTuiApp { // Create buffer manager first let mut buffer_manager = BufferManager::new(); - let mut buffer = sql_cli::buffer::Buffer::new(1); + let mut buffer = buffer::Buffer::new(1); // Sync initial settings from config buffer.set_case_insensitive(config.behavior.case_insensitive_default); buffer.set_compact_mode(config.display.compact_mode); @@ -412,7 +413,7 @@ impl EnhancedTuiApp { // Create a second buffer manager for the state container (temporary during migration) let mut container_buffer_manager = BufferManager::new(); - let mut container_buffer = sql_cli::buffer::Buffer::new(1); + let mut container_buffer = buffer::Buffer::new(1); container_buffer.set_case_insensitive(config.behavior.case_insensitive_default); container_buffer.set_compact_mode(config.display.compact_mode); container_buffer.set_show_row_numbers(config.display.show_row_numbers); @@ -465,8 +466,7 @@ impl EnhancedTuiApp { buffer_manager, buffer_handler: BufferHandler::new(), query_cache: QueryCache::new().ok(), - log_buffer: sql_cli::dual_logging::get_dual_logger() - .map(|logger| logger.ring_buffer().clone()), + log_buffer: dual_logging::get_dual_logger().map(|logger| logger.ring_buffer().clone()), cell_renderer: CellRenderer::new(config.theme.cell_selection_style.clone()), key_indicator: { let mut indicator = KeyPressIndicator::new(); @@ -507,7 +507,7 @@ impl EnhancedTuiApp { { // Clear all buffers and add a CSV buffer app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_csv( + let mut buffer = buffer::Buffer::from_csv( 1, std::path::PathBuf::from(csv_path), csv_client, @@ -597,7 +597,7 @@ impl EnhancedTuiApp { { // Clear all buffers and add a JSON buffer app.buffer_manager.clear_all(); - let mut buffer = sql_cli::buffer::Buffer::from_json( + let mut buffer = buffer::Buffer::from_json( 1, std::path::PathBuf::from(json_path), csv_client, @@ -2714,7 +2714,7 @@ impl EnhancedTuiApp { csv_client.query_csv(query).map(|r| QueryResponse { data: r.data, count: r.count, - query: sql_cli::api_client::QueryInfo { + query: crate::api_client::QueryInfo { select: r.query.select, where_clause: r.query.where_clause, order_by: r.query.order_by, @@ -2732,7 +2732,7 @@ impl EnhancedTuiApp { csv_client.query_csv(query).map(|r| QueryResponse { data: r.data, count: r.count, - query: sql_cli::api_client::QueryInfo { + query: crate::api_client::QueryInfo { select: r.query.select, where_clause: r.query.where_clause, order_by: r.query.order_by, @@ -3508,13 +3508,14 @@ impl EnhancedTuiApp { let stats = ColumnStatistics { column_name: analyzer_stats.column_name, column_type: match analyzer_stats.data_type { - sql_cli::data_analyzer::ColumnType::Integer - | sql_cli::data_analyzer::ColumnType::Float => ColumnType::Numeric, - sql_cli::data_analyzer::ColumnType::String - | sql_cli::data_analyzer::ColumnType::Boolean - | sql_cli::data_analyzer::ColumnType::Date => ColumnType::String, - sql_cli::data_analyzer::ColumnType::Mixed => ColumnType::Mixed, - sql_cli::data_analyzer::ColumnType::Unknown => ColumnType::Mixed, + data_analyzer::ColumnType::Integer | data_analyzer::ColumnType::Float => { + ColumnType::Numeric + } + data_analyzer::ColumnType::String + | data_analyzer::ColumnType::Boolean + | data_analyzer::ColumnType::Date => ColumnType::String, + data_analyzer::ColumnType::Mixed => ColumnType::Mixed, + data_analyzer::ColumnType::Unknown => ColumnType::Mixed, }, total_count: analyzer_stats.total_values, null_count: analyzer_stats.null_values, @@ -4419,7 +4420,7 @@ impl EnhancedTuiApp { } fn calculate_optimal_column_widths(&mut self) { - use sql_cli::column_manager::ColumnManager; + use crate::column_manager::ColumnManager; if let Some(results) = self.buffer().get_results() { let widths = ColumnManager::calculate_optimal_widths(&results.data); @@ -4771,8 +4772,7 @@ impl EnhancedTuiApp { // Buffer management methods fn new_buffer(&mut self) { - let mut new_buffer = - sql_cli::buffer::Buffer::new(self.buffer_manager.all_buffers().len() + 1); + let mut new_buffer = buffer::Buffer::new(self.buffer_manager.all_buffers().len() + 1); // Apply config settings to the new buffer new_buffer.set_compact_mode(self.config.display.compact_mode); new_buffer.set_case_insensitive(self.config.behavior.case_insensitive_default); diff --git a/sql-cli/src/key_dispatcher.rs b/sql-cli/src/ui/key_dispatcher.rs similarity index 100% rename from sql-cli/src/key_dispatcher.rs rename to sql-cli/src/ui/key_dispatcher.rs diff --git a/sql-cli/src/ui/mod.rs b/sql-cli/src/ui/mod.rs new file mode 100644 index 00000000..f742351e --- /dev/null +++ b/sql-cli/src/ui/mod.rs @@ -0,0 +1,9 @@ +//! User interface layer +//! +//! This module contains the main TUI application and related UI components. + +pub mod enhanced_tui; +pub mod key_dispatcher; +pub mod tui_app; +pub mod tui_renderer; +pub mod tui_state; diff --git a/sql-cli/src/tui_app.rs b/sql-cli/src/ui/tui_app.rs similarity index 99% rename from sql-cli/src/tui_app.rs rename to sql-cli/src/ui/tui_app.rs index ab020b8a..779d0eec 100644 --- a/sql-cli/src/tui_app.rs +++ b/sql-cli/src/ui/tui_app.rs @@ -1,3 +1,4 @@ +use crate::api_client::{ApiClient, QueryResponse}; use crate::cursor_aware_parser::CursorAwareParser; use crate::parser::SqlParser; use anyhow::Result; @@ -14,7 +15,6 @@ use ratatui::{ widgets::{Block, Borders, Clear, Paragraph}, Frame, Terminal, }; -use sql_cli::api_client::{ApiClient, QueryResponse}; use std::io; use tui_input::{backend::crossterm::EventHandler, Input}; diff --git a/sql-cli/src/tui_renderer.rs b/sql-cli/src/ui/tui_renderer.rs similarity index 99% rename from sql-cli/src/tui_renderer.rs rename to sql-cli/src/ui/tui_renderer.rs index 68fba591..ca95058b 100644 --- a/sql-cli/src/tui_renderer.rs +++ b/sql-cli/src/ui/tui_renderer.rs @@ -1,7 +1,7 @@ use crate::api_client::QueryResponse; use crate::buffer::SortOrder; use crate::buffer::{AppMode, BufferAPI}; -use crate::config::Config; +use crate::config::config::Config; use crate::sql_highlighter::SqlHighlighter; use crate::tui_state::SelectionMode; use ratatui::{ diff --git a/sql-cli/src/tui_state.rs b/sql-cli/src/ui/tui_state.rs similarity index 100% rename from sql-cli/src/tui_state.rs rename to sql-cli/src/ui/tui_state.rs diff --git a/sql-cli/src/app_paths.rs b/sql-cli/src/utils/app_paths.rs similarity index 100% rename from sql-cli/src/app_paths.rs rename to sql-cli/src/utils/app_paths.rs diff --git a/sql-cli/src/debouncer.rs b/sql-cli/src/utils/debouncer.rs similarity index 100% rename from sql-cli/src/debouncer.rs rename to sql-cli/src/utils/debouncer.rs diff --git a/sql-cli/src/debug_helpers.rs b/sql-cli/src/utils/debug_helpers.rs similarity index 54% rename from sql-cli/src/debug_helpers.rs rename to sql-cli/src/utils/debug_helpers.rs index 5fb4978e..4219858e 100644 --- a/sql-cli/src/debug_helpers.rs +++ b/sql-cli/src/utils/debug_helpers.rs @@ -1,10 +1,9 @@ use std::fs::OpenOptions; use std::io::Write; use std::sync::Mutex; +use std::sync::OnceLock; -lazy_static::lazy_static! { - static ref DEBUG_FILE: Mutex> = Mutex::new(None); -} +pub static DEBUG_FILE: OnceLock>> = OnceLock::new(); pub fn init_debug_log() { let file = OpenOptions::new() @@ -13,8 +12,8 @@ pub fn init_debug_log() { .append(true) .open("tui_debug.log") .ok(); - - *DEBUG_FILE.lock().unwrap() = file; + + let _ = DEBUG_FILE.set(Mutex::new(file)); } #[macro_export] @@ -22,12 +21,14 @@ macro_rules! debug_log { ($($arg:tt)*) => { #[cfg(debug_assertions)] { - if let Ok(mut guard) = $crate::debug_helpers::DEBUG_FILE.lock() { - if let Some(ref mut file) = *guard { - let _ = writeln!(file, "[{}] {}", - chrono::Local::now().format("%H:%M:%S%.3f"), - format!($($arg)*)); - let _ = file.flush(); + if let Some(debug_file) = $crate::utils::debug_helpers::DEBUG_FILE.get() { + if let Ok(mut guard) = debug_file.lock() { + if let Some(ref mut file) = *guard { + let _ = writeln!(file, "[{}] {}", + chrono::Local::now().format("%H:%M:%S%.3f"), + format!($($arg)*)); + let _ = file.flush(); + } } } } @@ -38,9 +39,9 @@ pub fn debug_breakpoint(label: &str) { #[cfg(debug_assertions)] { debug_log!("BREAKPOINT: {}", label); - + // This allows you to set a breakpoint here in RustRover // The label will be logged so you know which point was hit let _debug_marker = format!("Debug point: {}", label); } -} \ No newline at end of file +} diff --git a/sql-cli/src/debug_info.rs b/sql-cli/src/utils/debug_info.rs similarity index 100% rename from sql-cli/src/debug_info.rs rename to sql-cli/src/utils/debug_info.rs diff --git a/sql-cli/src/debug_service.rs b/sql-cli/src/utils/debug_service.rs similarity index 96% rename from sql-cli/src/debug_service.rs rename to sql-cli/src/utils/debug_service.rs index 22e8763d..c648ef2f 100644 --- a/sql-cli/src/debug_service.rs +++ b/sql-cli/src/utils/debug_service.rs @@ -219,13 +219,14 @@ impl DebugService { } } -/// Macro for easy debug logging -#[macro_export] -macro_rules! debug_log { - ($service:expr, $component:expr, $($arg:tt)*) => { - $service.info($component, format!($($arg)*)) - }; -} +// Commented out - duplicate with debug_helpers.rs +// /// Macro for easy debug logging +// #[macro_export] +// macro_rules! debug_log { +// ($service:expr, $component:expr, $($arg:tt)*) => { +// $service.info($component, format!($($arg)*)) +// }; +// } #[macro_export] macro_rules! debug_trace { diff --git a/sql-cli/src/dual_logging.rs b/sql-cli/src/utils/dual_logging.rs similarity index 100% rename from sql-cli/src/dual_logging.rs rename to sql-cli/src/utils/dual_logging.rs diff --git a/sql-cli/src/logging.rs b/sql-cli/src/utils/logging.rs similarity index 100% rename from sql-cli/src/logging.rs rename to sql-cli/src/utils/logging.rs diff --git a/sql-cli/src/utils/mod.rs b/sql-cli/src/utils/mod.rs new file mode 100644 index 00000000..7dfdf178 --- /dev/null +++ b/sql-cli/src/utils/mod.rs @@ -0,0 +1,12 @@ +//! Utility functions and helpers +//! +//! This module contains various utility functions, formatters, +//! and helper components used throughout the application. + +pub mod app_paths; +pub mod debouncer; +pub mod debug_helpers; +pub mod debug_info; +pub mod debug_service; +pub mod dual_logging; +pub mod logging; diff --git a/sql-cli/src/debug_widget.rs b/sql-cli/src/widgets/debug_widget.rs similarity index 100% rename from sql-cli/src/debug_widget.rs rename to sql-cli/src/widgets/debug_widget.rs diff --git a/sql-cli/src/editor_widget.rs b/sql-cli/src/widgets/editor_widget.rs similarity index 100% rename from sql-cli/src/editor_widget.rs rename to sql-cli/src/widgets/editor_widget.rs diff --git a/sql-cli/src/help_widget.rs b/sql-cli/src/widgets/help_widget.rs similarity index 100% rename from sql-cli/src/help_widget.rs rename to sql-cli/src/widgets/help_widget.rs diff --git a/sql-cli/src/history_widget.rs b/sql-cli/src/widgets/history_widget.rs similarity index 100% rename from sql-cli/src/history_widget.rs rename to sql-cli/src/widgets/history_widget.rs diff --git a/sql-cli/src/widgets/mod.rs b/sql-cli/src/widgets/mod.rs new file mode 100644 index 00000000..5fccc02d --- /dev/null +++ b/sql-cli/src/widgets/mod.rs @@ -0,0 +1,11 @@ +//! UI widgets for the TUI application +//! +//! This module contains all reusable UI components/widgets +//! used by the TUI for rendering different parts of the interface. + +pub mod debug_widget; +pub mod editor_widget; +pub mod help_widget; +pub mod history_widget; +pub mod search_modes_widget; +pub mod stats_widget; diff --git a/sql-cli/src/search_modes_widget.rs b/sql-cli/src/widgets/search_modes_widget.rs similarity index 100% rename from sql-cli/src/search_modes_widget.rs rename to sql-cli/src/widgets/search_modes_widget.rs diff --git a/sql-cli/src/stats_widget.rs b/sql-cli/src/widgets/stats_widget.rs similarity index 100% rename from sql-cli/src/stats_widget.rs rename to sql-cli/src/widgets/stats_widget.rs