use crate::api_client::QueryResponse; use crate::csv_datasource::CsvApiClient; use crate::cursor_operations::CursorOperations;
use crate::data::data_view::DataView;
use crate::data::datatable::DataTable;
use crate::hybrid_parser::HybridParser;
use crate::input_manager::{create_from_input, create_single_line, InputManager};
use anyhow::Result;
use crossterm::event::KeyEvent;
use fuzzy_matcher::skim::SkimMatcherV2;
use ratatui::style::Color;
use ratatui::widgets::TableState;
use regex::Regex;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::debug;
use tui_input::Input;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum AppMode {
Command,
Results,
Search,
Filter,
FuzzyFilter,
ColumnSearch,
Help,
History,
Debug,
PrettyQuery,
JumpToRow,
ColumnStats,
}
#[derive(Clone, PartialEq, Debug)]
pub enum EditMode {
SingleLine,
MultiLine,
}
#[derive(Clone, PartialEq, Copy, Debug)]
pub enum SortOrder {
Ascending,
Descending,
None,
}
#[derive(Clone)]
pub struct SortState {
pub column: Option<usize>,
pub order: SortOrder,
}
#[derive(Clone, Debug, Default)]
pub struct FilterState {
pub pattern: String,
pub regex: Option<Regex>,
pub active: bool,
}
#[derive(Default)]
pub struct FuzzyFilterState {
pub pattern: String,
pub active: bool,
pub matcher: SkimMatcherV2,
pub filtered_indices: Vec<usize>,
}
impl std::fmt::Debug for FuzzyFilterState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FuzzyFilterState")
.field("pattern", &self.pattern)
.field("active", &self.active)
.field("matcher", &"SkimMatcherV2")
.field("filtered_indices", &self.filtered_indices)
.finish()
}
}
impl Clone for FuzzyFilterState {
fn clone(&self) -> Self {
Self {
pattern: self.pattern.clone(),
active: self.active,
matcher: SkimMatcherV2::default(), filtered_indices: self.filtered_indices.clone(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct SearchState {
pub pattern: String,
pub current_match: Option<(usize, usize)>,
pub matches: Vec<(usize, usize)>,
pub match_index: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SelectionMode {
Row,
Cell,
Column,
}
#[derive(Clone, Debug)]
pub struct ViewState {
pub crosshair_row: usize,
pub crosshair_col: usize,
pub scroll_offset: (usize, usize),
pub selection_mode: SelectionMode,
pub selected_cells: Vec<(usize, usize)>,
pub selection_anchor: Option<(usize, usize)>,
pub viewport_lock: bool,
pub cursor_lock: bool,
pub navigation_history: Vec<(usize, usize)>,
pub history_index: usize,
pub viewport_rows: usize,
pub viewport_columns: usize,
pub total_rows: usize,
pub total_columns: usize,
}
impl Default for ViewState {
fn default() -> Self {
Self {
crosshair_row: 0,
crosshair_col: 0,
scroll_offset: (0, 0),
selection_mode: SelectionMode::Row,
selected_cells: Vec::new(),
selection_anchor: None,
viewport_lock: false,
cursor_lock: false,
navigation_history: Vec::new(),
history_index: 0,
viewport_rows: 0,
viewport_columns: 0,
total_rows: 0,
total_columns: 0,
}
}
}
#[derive(Clone, Debug)]
pub enum ColumnType {
String,
Numeric,
Mixed,
}
#[derive(Clone)]
pub struct ColumnStatistics {
pub column_name: String,
pub column_type: ColumnType,
pub total_count: usize,
pub null_count: usize,
pub unique_count: usize,
pub frequency_map: Option<BTreeMap<String, usize>>,
pub min: Option<f64>,
pub max: Option<f64>,
pub sum: Option<f64>,
pub mean: Option<f64>,
pub median: Option<f64>,
}
pub trait BufferAPI: Send + Sync {
fn get_id(&self) -> usize;
fn get_query(&self) -> String;
fn set_query(&mut self, query: String);
fn get_last_query(&self) -> String;
fn set_last_query(&mut self, query: String);
fn get_datatable(&self) -> Option<&DataTable>;
fn get_datatable_mut(&mut self) -> Option<&mut DataTable>;
fn has_datatable(&self) -> bool;
fn set_datatable(&mut self, datatable: Option<Arc<DataTable>>);
fn get_original_source(&self) -> Option<&DataTable>;
fn set_results_as_datatable(&mut self, response: Option<QueryResponse>) -> Result<(), String>;
fn get_dataview(&self) -> Option<&DataView>;
fn get_dataview_mut(&mut self) -> Option<&mut DataView>;
fn set_dataview(&mut self, dataview: Option<DataView>);
fn has_dataview(&self) -> bool;
fn get_mode(&self) -> AppMode;
fn set_mode(&mut self, mode: AppMode);
fn get_edit_mode(&self) -> EditMode;
fn set_edit_mode(&mut self, mode: EditMode);
fn get_status_message(&self) -> String;
fn set_status_message(&mut self, message: String);
fn get_selected_row(&self) -> Option<usize>;
fn set_selected_row(&mut self, row: Option<usize>);
fn get_current_column(&self) -> usize;
fn set_current_column(&mut self, col: usize);
fn get_scroll_offset(&self) -> (usize, usize);
fn set_scroll_offset(&mut self, offset: (usize, usize));
fn get_last_results_row(&self) -> Option<usize>;
fn set_last_results_row(&mut self, row: Option<usize>);
fn get_last_scroll_offset(&self) -> (usize, usize);
fn set_last_scroll_offset(&mut self, offset: (usize, usize));
fn get_filter_pattern(&self) -> String;
fn set_filter_pattern(&mut self, pattern: String);
fn is_filter_active(&self) -> bool;
fn set_filter_active(&mut self, active: bool);
fn get_fuzzy_filter_pattern(&self) -> String;
fn set_fuzzy_filter_pattern(&mut self, pattern: String);
fn is_fuzzy_filter_active(&self) -> bool;
fn set_fuzzy_filter_active(&mut self, active: bool);
fn get_fuzzy_filter_indices(&self) -> &Vec<usize>;
fn set_fuzzy_filter_indices(&mut self, indices: Vec<usize>);
fn clear_fuzzy_filter(&mut self);
fn get_search_pattern(&self) -> String;
fn set_search_pattern(&mut self, pattern: String);
fn get_search_matches(&self) -> Vec<(usize, usize)>;
fn set_search_matches(&mut self, matches: Vec<(usize, usize)>);
fn get_current_match(&self) -> Option<(usize, usize)>;
fn set_current_match(&mut self, match_pos: Option<(usize, usize)>);
fn get_search_match_index(&self) -> usize;
fn set_search_match_index(&mut self, index: usize);
fn clear_search_state(&mut self);
fn get_column_stats(&self) -> Option<&ColumnStatistics>;
fn set_column_stats(&mut self, stats: Option<ColumnStatistics>);
fn get_sort_column(&self) -> Option<usize>;
fn set_sort_column(&mut self, column: Option<usize>);
fn get_sort_order(&self) -> SortOrder;
fn set_sort_order(&mut self, order: SortOrder);
fn is_compact_mode(&self) -> bool;
fn set_compact_mode(&mut self, compact: bool);
fn is_show_row_numbers(&self) -> bool;
fn set_show_row_numbers(&mut self, show: bool);
fn is_viewport_lock(&self) -> bool;
fn set_viewport_lock(&mut self, locked: bool);
fn get_viewport_lock_row(&self) -> Option<usize>;
fn set_viewport_lock_row(&mut self, row: Option<usize>);
fn get_column_widths(&self) -> &Vec<u16>;
fn set_column_widths(&mut self, widths: Vec<u16>);
fn is_case_insensitive(&self) -> bool;
fn set_case_insensitive(&mut self, case_insensitive: bool);
fn get_name(&self) -> String;
fn set_name(&mut self, name: String);
fn get_file_path(&self) -> Option<&PathBuf>;
fn set_file_path(&mut self, path: Option<String>);
fn is_modified(&self) -> bool;
fn set_modified(&mut self, modified: bool);
fn get_last_query_source(&self) -> Option<String>;
fn set_last_query_source(&mut self, source: Option<String>);
fn get_input_value(&self) -> String;
fn set_input_value(&mut self, value: String);
fn get_input_cursor(&self) -> usize;
fn set_input_cursor(&mut self, pos: usize);
fn apply_filter(&mut self) -> Result<()>;
fn apply_sort(&mut self) -> Result<()>;
fn search(&mut self) -> Result<()>;
fn clear_filters(&mut self);
fn get_row_count(&self) -> usize;
fn get_column_count(&self) -> usize;
fn get_column_names(&self) -> Vec<String>;
fn get_undo_stack(&self) -> &Vec<(String, usize)>;
fn push_undo(&mut self, state: (String, usize));
fn pop_undo(&mut self) -> Option<(String, usize)>;
fn get_redo_stack(&self) -> &Vec<(String, usize)>;
fn push_redo(&mut self, state: (String, usize));
fn pop_redo(&mut self) -> Option<(String, usize)>;
fn clear_redo(&mut self);
fn get_kill_ring(&self) -> String;
fn set_kill_ring(&mut self, text: String);
fn is_kill_ring_empty(&self) -> bool;
fn perform_undo(&mut self) -> bool;
fn perform_redo(&mut self) -> bool;
fn save_state_for_undo(&mut self);
fn get_last_visible_rows(&self) -> usize;
fn set_last_visible_rows(&mut self, rows: usize);
fn debug_dump(&self) -> String;
fn get_input_text(&self) -> String;
fn set_input_text(&mut self, text: String);
fn handle_input_key(&mut self, event: KeyEvent) -> bool;
fn switch_input_mode(&mut self, multiline: bool);
fn get_input_cursor_position(&self) -> usize;
fn set_input_cursor_position(&mut self, position: usize);
fn is_input_multiline(&self) -> bool;
fn navigate_history_up(&mut self, history: &[String]) -> bool;
fn navigate_history_down(&mut self, history: &[String]) -> bool;
fn reset_history_navigation(&mut self);
fn clear_results(&mut self);
}
pub struct Buffer {
pub id: usize,
pub file_path: Option<PathBuf>,
pub name: String,
pub modified: bool,
pub datatable: Option<Arc<DataTable>>,
pub original_source: Option<Arc<DataTable>>,
pub dataview: Option<DataView>,
pub mode: AppMode,
pub edit_mode: EditMode,
pub input: Input, pub input_manager: Box<dyn InputManager>, pub table_state: TableState,
pub last_results_row: Option<usize>,
pub last_scroll_offset: (usize, usize),
pub last_query: String,
pub status_message: String,
pub sort_state: SortState,
pub filter_state: FilterState,
pub fuzzy_filter_state: FuzzyFilterState,
pub search_state: SearchState,
pub column_stats: Option<ColumnStatistics>,
pub view_state: ViewState,
pub column_widths: Vec<u16>,
pub compact_mode: bool,
pub show_row_numbers: bool,
pub case_insensitive: bool,
pub undo_stack: Vec<(String, usize)>,
pub redo_stack: Vec<(String, usize)>,
pub kill_ring: String,
pub last_visible_rows: usize,
pub last_query_source: Option<String>,
pub highlighted_text_cache: Option<Vec<(String, Color)>>, pub last_highlighted_text: String,
pub saved_input_state: Option<(String, usize)>, }
impl BufferAPI for Buffer {
fn get_id(&self) -> usize {
self.id
}
fn get_query(&self) -> String {
self.input_manager.get_text()
}
fn set_query(&mut self, query: String) {
self.input_manager.set_text(query.clone());
self.input = Input::new(query.clone()).with_cursor(query.len());
}
fn get_last_query(&self) -> String {
self.last_query.clone()
}
fn set_last_query(&mut self, query: String) {
self.last_query = query;
}
fn get_datatable(&self) -> Option<&DataTable> {
self.datatable.as_ref().map(std::convert::AsRef::as_ref)
}
fn get_datatable_mut(&mut self) -> Option<&mut DataTable> {
None
}
fn has_datatable(&self) -> bool {
self.datatable.is_some()
}
fn get_original_source(&self) -> Option<&DataTable> {
self.original_source
.as_ref()
.map(std::convert::AsRef::as_ref)
}
fn set_datatable(&mut self, datatable: Option<Arc<DataTable>>) {
debug!(
"V50: Setting DataTable with {} rows, {} columns",
datatable.as_ref().map_or(0, |d| d.row_count()),
datatable.as_ref().map_or(0, |d| d.column_count())
);
if let Some(ref current) = self.datatable {
debug!(
"V50: Current DataTable has {} columns: {:?}",
current.column_count(),
current.column_names()
);
}
if let Some(ref original) = self.original_source {
debug!(
"V50: Original source has {} columns: {:?}",
original.column_count(),
original.column_names()
);
}
if datatable.is_some() && self.original_source.is_none() {
self.original_source = datatable.clone();
debug!(
"V50: Preserving original source DataTable with {} columns",
datatable.as_ref().map_or(0, |d| d.column_count())
);
}
if let Some(dt) = &datatable {
let mut view = crate::data::data_view::DataView::new(dt.clone());
if let Some(old_view) = &self.dataview {
let old_all_cols = old_view.source().column_names();
let old_visible_cols = old_view.column_names();
for col_name in &old_all_cols {
if !old_visible_cols.contains(col_name) {
view.hide_column_by_name(col_name);
}
}
}
view.shrink_to_fit();
self.dataview = Some(view);
} else {
self.dataview = None;
}
if let Some(ref original) = self.original_source {
if let Some(ref new_dt) = datatable {
if new_dt.column_count() < original.column_count() {
debug!(
"V50: WARNING - Attempted to replace datatable with fewer columns ({} < {}). Keeping original.",
new_dt.column_count(),
original.column_count()
);
return;
}
}
}
self.datatable = datatable;
}
fn set_results_as_datatable(&mut self, response: Option<QueryResponse>) -> Result<(), String> {
if let Some(ref resp) = response {
debug!("V50: Converting QueryResponse to DataTable");
let table_name = resp.table.as_deref().unwrap_or("data");
match DataTable::from_query_response(resp, table_name) {
Ok(datatable) => {
debug!(
"V50: Stored DataTable with {} rows, {} columns",
datatable.row_count(),
datatable.column_count()
);
self.datatable = Some(Arc::new(datatable));
Ok(())
}
Err(e) => {
let err_msg = format!("V50: Failed to create DataTable: {e}");
debug!("{}", err_msg);
self.datatable = None;
Err(err_msg)
}
}
} else {
self.datatable = None;
Ok(())
}
}
fn get_dataview(&self) -> Option<&DataView> {
self.dataview.as_ref()
}
fn get_dataview_mut(&mut self) -> Option<&mut DataView> {
self.dataview.as_mut()
}
fn set_dataview(&mut self, dataview: Option<DataView>) {
debug!(
"V51: Setting DataView with {} rows",
dataview
.as_ref()
.map_or(0, super::data::data_view::DataView::row_count)
);
self.dataview = dataview;
}
fn has_dataview(&self) -> bool {
self.dataview.is_some()
}
fn get_mode(&self) -> AppMode {
self.mode.clone()
}
fn set_mode(&mut self, mode: AppMode) {
self.mode = mode;
}
fn get_edit_mode(&self) -> EditMode {
self.edit_mode.clone()
}
fn set_edit_mode(&mut self, mode: EditMode) {
self.edit_mode = mode;
}
fn get_status_message(&self) -> String {
self.status_message.clone()
}
fn set_status_message(&mut self, message: String) {
self.status_message = message;
}
fn get_selected_row(&self) -> Option<usize> {
self.table_state.selected()
}
fn set_selected_row(&mut self, row: Option<usize>) {
if let Some(r) = row {
self.view_state.crosshair_row = r;
self.table_state.select(Some(r));
} else {
self.view_state.crosshair_row = 0;
self.table_state.select(None);
}
}
fn get_current_column(&self) -> usize {
self.view_state.crosshair_col
}
fn set_current_column(&mut self, col: usize) {
self.view_state.crosshair_col = col;
}
fn get_scroll_offset(&self) -> (usize, usize) {
self.view_state.scroll_offset
}
fn set_scroll_offset(&mut self, offset: (usize, usize)) {
self.view_state.scroll_offset = offset;
}
fn get_last_results_row(&self) -> Option<usize> {
self.last_results_row
}
fn set_last_results_row(&mut self, row: Option<usize>) {
self.last_results_row = row;
}
fn get_last_scroll_offset(&self) -> (usize, usize) {
self.last_scroll_offset
}
fn set_last_scroll_offset(&mut self, offset: (usize, usize)) {
self.last_scroll_offset = offset;
}
fn get_filter_pattern(&self) -> String {
self.filter_state.pattern.clone()
}
fn set_filter_pattern(&mut self, pattern: String) {
self.filter_state.pattern = pattern;
}
fn is_filter_active(&self) -> bool {
self.filter_state.active
}
fn set_filter_active(&mut self, active: bool) {
self.filter_state.active = active;
}
fn get_fuzzy_filter_pattern(&self) -> String {
self.fuzzy_filter_state.pattern.clone()
}
fn set_fuzzy_filter_pattern(&mut self, pattern: String) {
self.fuzzy_filter_state.pattern = pattern;
}
fn is_fuzzy_filter_active(&self) -> bool {
self.fuzzy_filter_state.active
}
fn set_fuzzy_filter_active(&mut self, active: bool) {
self.fuzzy_filter_state.active = active;
}
fn get_fuzzy_filter_indices(&self) -> &Vec<usize> {
&self.fuzzy_filter_state.filtered_indices
}
fn set_fuzzy_filter_indices(&mut self, indices: Vec<usize>) {
self.fuzzy_filter_state.filtered_indices = indices;
}
fn clear_fuzzy_filter(&mut self) {
self.fuzzy_filter_state.pattern.clear();
self.fuzzy_filter_state.active = false;
self.fuzzy_filter_state.filtered_indices.clear();
}
fn get_search_pattern(&self) -> String {
self.search_state.pattern.clone()
}
fn set_search_pattern(&mut self, pattern: String) {
self.search_state.pattern = pattern;
}
fn get_search_matches(&self) -> Vec<(usize, usize)> {
self.search_state.matches.clone()
}
fn set_search_matches(&mut self, matches: Vec<(usize, usize)>) {
self.search_state.matches = matches;
}
fn get_current_match(&self) -> Option<(usize, usize)> {
self.search_state.current_match
}
fn set_current_match(&mut self, match_pos: Option<(usize, usize)>) {
self.search_state.current_match = match_pos;
}
fn get_search_match_index(&self) -> usize {
self.search_state.match_index
}
fn set_search_match_index(&mut self, index: usize) {
self.search_state.match_index = index;
}
fn clear_search_state(&mut self) {
self.search_state.pattern.clear();
self.search_state.matches.clear();
self.search_state.current_match = None;
self.search_state.match_index = 0;
}
fn get_column_stats(&self) -> Option<&ColumnStatistics> {
self.column_stats.as_ref()
}
fn set_column_stats(&mut self, stats: Option<ColumnStatistics>) {
self.column_stats = stats;
}
fn get_sort_column(&self) -> Option<usize> {
self.sort_state.column
}
fn set_sort_column(&mut self, column: Option<usize>) {
self.sort_state.column = column;
}
fn get_sort_order(&self) -> SortOrder {
self.sort_state.order
}
fn set_sort_order(&mut self, order: SortOrder) {
self.sort_state.order = order;
}
fn is_compact_mode(&self) -> bool {
self.compact_mode
}
fn set_compact_mode(&mut self, compact: bool) {
self.compact_mode = compact;
}
fn is_show_row_numbers(&self) -> bool {
self.show_row_numbers
}
fn set_show_row_numbers(&mut self, show: bool) {
self.show_row_numbers = show;
}
fn is_viewport_lock(&self) -> bool {
self.view_state.viewport_lock
}
fn set_viewport_lock(&mut self, locked: bool) {
self.view_state.viewport_lock = locked;
}
fn get_viewport_lock_row(&self) -> Option<usize> {
if self.view_state.viewport_lock {
Some(self.view_state.crosshair_row)
} else {
None
}
}
fn set_viewport_lock_row(&mut self, row: Option<usize>) {
if let Some(r) = row {
self.view_state.crosshair_row = r;
self.view_state.viewport_lock = true;
}
}
fn get_column_widths(&self) -> &Vec<u16> {
&self.column_widths
}
fn set_column_widths(&mut self, widths: Vec<u16>) {
self.column_widths = widths;
}
fn is_case_insensitive(&self) -> bool {
self.case_insensitive
}
fn set_case_insensitive(&mut self, case_insensitive: bool) {
self.case_insensitive = case_insensitive;
}
fn get_name(&self) -> String {
self.name.clone()
}
fn set_name(&mut self, name: String) {
self.name = name;
}
fn get_file_path(&self) -> Option<&PathBuf> {
self.file_path.as_ref()
}
fn set_file_path(&mut self, path: Option<String>) {
self.file_path = path.map(PathBuf::from);
}
fn is_modified(&self) -> bool {
self.modified
}
fn set_modified(&mut self, modified: bool) {
self.modified = modified;
}
fn get_last_query_source(&self) -> Option<String> {
self.last_query_source.clone()
}
fn set_last_query_source(&mut self, source: Option<String>) {
self.last_query_source = source;
}
fn get_input_value(&self) -> String {
self.input.value().to_string()
}
fn set_input_value(&mut self, value: String) {
let cursor = value.len();
self.input = Input::new(value).with_cursor(cursor);
}
fn get_input_cursor(&self) -> usize {
self.input.cursor()
}
fn set_input_cursor(&mut self, pos: usize) {
let value = self.input.value().to_string();
self.input = Input::new(value).with_cursor(pos);
}
fn apply_filter(&mut self) -> Result<()> {
Ok(())
}
fn apply_sort(&mut self) -> Result<()> {
Ok(())
}
fn search(&mut self) -> Result<()> {
Ok(())
}
fn clear_filters(&mut self) {
self.filter_state.active = false;
self.filter_state.pattern.clear();
self.fuzzy_filter_state.active = false;
self.fuzzy_filter_state.pattern.clear();
}
fn get_row_count(&self) -> usize {
if let Some(dataview) = &self.dataview {
dataview.row_count()
} else if let Some(datatable) = &self.datatable {
datatable.row_count()
} else {
0
}
}
fn get_column_count(&self) -> usize {
if let Some(datatable) = &self.datatable {
return datatable.column_count();
}
0
}
fn get_column_names(&self) -> Vec<String> {
if let Some(datatable) = &self.datatable {
return datatable.column_names();
}
Vec::new()
}
fn get_undo_stack(&self) -> &Vec<(String, usize)> {
&self.undo_stack
}
fn push_undo(&mut self, state: (String, usize)) {
self.undo_stack.push(state);
if self.undo_stack.len() > 100 {
self.undo_stack.remove(0);
}
}
fn pop_undo(&mut self) -> Option<(String, usize)> {
self.undo_stack.pop()
}
fn get_redo_stack(&self) -> &Vec<(String, usize)> {
&self.redo_stack
}
fn push_redo(&mut self, state: (String, usize)) {
self.redo_stack.push(state);
}
fn pop_redo(&mut self) -> Option<(String, usize)> {
self.redo_stack.pop()
}
fn clear_redo(&mut self) {
self.redo_stack.clear();
}
fn perform_undo(&mut self) -> bool {
if let Some((prev_text, prev_cursor)) = self.pop_undo() {
let current_state = (self.get_input_text(), self.get_input_cursor_position());
self.push_redo(current_state);
self.set_input_text(prev_text);
self.set_input_cursor_position(prev_cursor);
true
} else {
false
}
}
fn perform_redo(&mut self) -> bool {
if let Some((next_text, next_cursor)) = self.pop_redo() {
let current_state = (self.get_input_text(), self.get_input_cursor_position());
self.push_undo(current_state);
self.set_input_text(next_text);
self.set_input_cursor_position(next_cursor);
true
} else {
false
}
}
fn save_state_for_undo(&mut self) {
let current_state = (self.get_input_text(), self.get_input_cursor_position());
self.push_undo(current_state);
self.clear_redo();
}
fn get_kill_ring(&self) -> String {
self.kill_ring.clone()
}
fn set_kill_ring(&mut self, text: String) {
self.kill_ring = text;
}
fn is_kill_ring_empty(&self) -> bool {
self.kill_ring.is_empty()
}
fn get_last_visible_rows(&self) -> usize {
self.last_visible_rows
}
fn set_last_visible_rows(&mut self, rows: usize) {
self.last_visible_rows = rows;
}
fn debug_dump(&self) -> String {
let mut output = String::new();
output.push_str("=== BUFFER DEBUG DUMP ===\n");
output.push_str(&format!("Buffer ID: {}\n", self.id));
output.push_str(&format!("Name: {}\n", self.name));
output.push_str(&format!("File Path: {:?}\n", self.file_path));
output.push_str(&format!("Modified: {}\n", self.modified));
output.push_str("\n--- Modes ---\n");
output.push_str(&format!("App Mode: {:?}\n", self.mode));
output.push_str(&format!("Edit Mode: {:?}\n", self.edit_mode));
output.push_str("\n--- Query State ---\n");
output.push_str(&format!("Current Input: '{}'\n", self.input.value()));
output.push_str(&format!("Input Cursor: {}\n", self.input.cursor()));
output.push_str(&format!("Last Query: '{}'\n", self.last_query));
output.push_str(&format!("Status Message: '{}'\n", self.status_message));
output.push_str(&format!(
"Last Query Source: {:?}\n",
self.last_query_source
));
output.push_str("\n--- Results ---\n");
output.push_str(&format!("Has DataTable: {}\n", self.datatable.is_some()));
output.push_str(&format!("Row Count: {}\n", self.get_row_count()));
output.push_str(&format!("Column Count: {}\n", self.get_column_count()));
output.push_str(&format!(
"Selected Row: {:?}\n",
self.table_state.selected()
));
output.push_str(&format!(
"Current Column: {}\n",
self.view_state.crosshair_col
));
output.push_str(&format!(
"Scroll Offset: {:?}\n",
self.view_state.scroll_offset
));
output.push_str("\n--- Filtering ---\n");
output.push_str(&format!("Filter Active: {}\n", self.filter_state.active));
output.push_str(&format!(
"Filter Pattern: '{}'\n",
self.filter_state.pattern
));
output.push_str("Filtering: Handled by DataView\n");
output.push_str(&format!(
"Fuzzy Filter Active: {}\n",
self.fuzzy_filter_state.active
));
output.push_str(&format!(
"Fuzzy Pattern: '{}'\n",
self.fuzzy_filter_state.pattern
));
output.push_str("\n--- Search ---\n");
output.push_str(&format!(
"Search Pattern: '{}'\n",
self.search_state.pattern
));
output.push_str(&format!(
"Search Matches: {} found\n",
self.search_state.matches.len()
));
output.push_str(&format!(
"Current Match: {:?}\n",
self.search_state.current_match
));
output.push_str(&format!("Match Index: {}\n", self.search_state.match_index));
output.push_str("\n--- Column Search ---\n");
output.push_str(&format!(
"Column Search Pattern: '{}'\n",
"<migrated>" ));
output.push_str(&format!(
"Matching Columns: {:?}\n",
Vec::<(usize, String)>::new() ));
output.push_str("\n--- Sorting ---\n");
output.push_str(&format!("Sort Column: {:?}\n", self.sort_state.column));
output.push_str(&format!("Sort Order: {:?}\n", self.sort_state.order));
output.push_str("\n--- Display Options ---\n");
output.push_str(&format!("Compact Mode: {}\n", self.compact_mode));
output.push_str(&format!("Show Row Numbers: {}\n", self.show_row_numbers));
output.push_str(&format!("Case Insensitive: {}\n", self.case_insensitive));
if let Some(ref dataview) = self.dataview {
output.push_str(&format!(
"Pinned Columns: {:?}\n",
dataview.get_pinned_column_names()
));
} else {
output.push_str("Pinned Columns: []\n");
}
output.push_str(&format!("Column Widths: {:?}\n", self.column_widths));
output.push_str(&format!("ViewState: {:?}\n", self.view_state));
output.push_str("\n--- Data Source ---\n");
output.push_str("Legacy CSV/Cache fields removed - using DataTable/DataView\n");
output.push_str("\n--- Undo/Redo ---\n");
output.push_str(&format!("Undo Stack Size: {}\n", self.undo_stack.len()));
output.push_str(&format!("Redo Stack Size: {}\n", self.redo_stack.len()));
output.push_str(&format!(
"Kill Ring: '{}'\n",
if self.kill_ring.len() > 50 {
format!(
"{}... ({} chars)",
&self.kill_ring[..50],
self.kill_ring.len()
)
} else {
self.kill_ring.clone()
}
));
output.push_str("\n--- Stats ---\n");
output.push_str(&format!(
"Has Column Stats: {}\n",
self.column_stats.is_some()
));
output.push_str(&format!("Last Visible Rows: {}\n", self.last_visible_rows));
output.push_str(&format!("Last Results Row: {:?}\n", self.last_results_row));
output.push_str(&format!(
"Last Scroll Offset: {:?}\n",
self.last_scroll_offset
));
output.push_str("\n=== END BUFFER DEBUG ===\n");
output
}
fn get_input_text(&self) -> String {
self.input_manager.get_text()
}
fn set_input_text(&mut self, text: String) {
self.input_manager.set_text(text.clone());
self.input = Input::new(text.clone()).with_cursor(text.len());
}
fn handle_input_key(&mut self, event: KeyEvent) -> bool {
let result = self.input_manager.handle_key_event(event);
self.sync_from_input_manager();
result
}
fn switch_input_mode(&mut self, _multiline: bool) {
let current_text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
self.edit_mode = EditMode::SingleLine;
self.input_manager = create_single_line(current_text.clone());
self.input =
Input::new(current_text.clone()).with_cursor(cursor_pos.min(current_text.len()));
self.input_manager.set_cursor_position(cursor_pos);
}
fn get_input_cursor_position(&self) -> usize {
self.input_manager.get_cursor_position()
}
fn set_input_cursor_position(&mut self, position: usize) {
self.input_manager.set_cursor_position(position);
if self.edit_mode == EditMode::SingleLine {
let text = self.input.value().to_string();
self.input = Input::new(text).with_cursor(position);
}
}
fn is_input_multiline(&self) -> bool {
self.input_manager.is_multiline()
}
fn navigate_history_up(&mut self, history: &[String]) -> bool {
self.input_manager.set_history(history.to_vec());
let navigated = self.input_manager.history_previous();
if navigated {
self.sync_from_input_manager();
}
navigated
}
fn navigate_history_down(&mut self, history: &[String]) -> bool {
self.input_manager.set_history(history.to_vec());
let navigated = self.input_manager.history_next();
if navigated {
self.sync_from_input_manager();
}
navigated
}
fn reset_history_navigation(&mut self) {
self.input_manager.reset_history_position();
}
fn clear_results(&mut self) {
self.datatable = None;
self.table_state.select(None);
self.last_results_row = None;
self.view_state.scroll_offset = (0, 0);
self.last_scroll_offset = (0, 0);
self.column_widths.clear();
self.status_message = "Results cleared".to_string();
self.filter_state.active = false;
self.filter_state.pattern.clear();
self.search_state.pattern.clear();
self.search_state.matches.clear();
self.search_state.current_match = None;
}
}
impl Buffer {
#[must_use]
pub fn new(id: usize) -> Self {
Self {
id,
file_path: None,
name: format!("[Buffer {id}]"),
modified: false,
datatable: None,
original_source: None,
dataview: None,
mode: AppMode::Command,
edit_mode: EditMode::SingleLine,
input: Input::default(),
input_manager: create_single_line(String::new()),
table_state: TableState::default(),
last_results_row: None,
last_scroll_offset: (0, 0),
last_query: String::new(),
status_message: String::new(),
sort_state: SortState {
column: None,
order: SortOrder::None,
},
filter_state: FilterState::default(),
fuzzy_filter_state: FuzzyFilterState::default(),
search_state: SearchState::default(),
column_stats: None,
view_state: ViewState::default(),
column_widths: Vec::new(),
compact_mode: false,
show_row_numbers: false,
case_insensitive: false,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
kill_ring: String::new(),
last_visible_rows: 30,
last_query_source: None,
highlighted_text_cache: None,
last_highlighted_text: String::new(),
saved_input_state: None,
}
}
#[must_use]
pub fn from_csv(
id: usize,
path: PathBuf,
_csv_client: CsvApiClient, _table_name: String, ) -> Self {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.csv")
.to_string();
let mut buffer = Self::new(id);
buffer.file_path = Some(path);
buffer.name = name;
buffer
}
#[must_use]
pub fn from_json(
id: usize,
path: PathBuf,
_csv_client: CsvApiClient, _table_name: String, ) -> Self {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.json")
.to_string();
let mut buffer = Self::new(id);
buffer.file_path = Some(path);
buffer.name = name;
buffer
}
pub fn display_name(&self) -> String {
if self.modified {
format!("{}*", self.name)
} else {
self.name.clone()
}
}
pub fn short_name(&self, max_len: usize) -> String {
let display = self.display_name();
if display.len() <= max_len {
display
} else {
format!("{}...", &display[..max_len.saturating_sub(3)])
}
}
pub fn has_file(&self, path: &PathBuf) -> bool {
self.file_path.as_ref() == Some(path)
}
fn sync_from_input_manager(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let text_len = text.len();
self.input = Input::new(text).with_cursor(cursor_pos.min(text_len));
}
fn sync_to_input_manager(&mut self) {
let _text = self.input.value().to_string();
self.input_manager = create_from_input(self.input.clone());
}
pub fn move_cursor_word_backward(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let new_pos = CursorOperations::find_word_boundary_backward(&text, cursor_pos);
self.input_manager.set_cursor_position(new_pos);
self.sync_from_input_manager();
self.status_message = format!("Moved to position {new_pos} (word boundary)");
}
pub fn move_cursor_word_forward(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let new_pos = CursorOperations::find_word_boundary_forward(&text, cursor_pos);
self.input_manager.set_cursor_position(new_pos);
self.sync_from_input_manager();
}
pub fn delete_word_backward(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let (new_text, new_cursor) = CursorOperations::delete_word_backward(&text, cursor_pos);
if cursor_pos > new_cursor {
self.kill_ring = text[new_cursor..cursor_pos].to_string();
}
self.input_manager.set_text(new_text);
self.input_manager.set_cursor_position(new_cursor);
self.sync_from_input_manager();
}
pub fn delete_word_forward(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let (new_text, new_cursor) = CursorOperations::delete_word_forward(&text, cursor_pos);
let word_end = CursorOperations::find_word_boundary_forward(&text, cursor_pos);
if word_end > cursor_pos {
self.kill_ring = text[cursor_pos..word_end].to_string();
}
self.input_manager.set_text(new_text);
self.input_manager.set_cursor_position(new_cursor);
self.sync_from_input_manager();
}
pub fn kill_line(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let (new_text, killed) = CursorOperations::kill_line(&text, cursor_pos);
self.kill_ring = killed;
self.input_manager.set_text(new_text);
self.sync_from_input_manager();
}
pub fn kill_line_backward(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let (new_text, killed, new_cursor) =
CursorOperations::kill_line_backward(&text, cursor_pos);
self.kill_ring = killed;
self.input_manager.set_text(new_text);
self.input_manager.set_cursor_position(new_cursor);
self.sync_from_input_manager();
}
pub fn jump_to_prev_token(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let new_pos = CursorOperations::jump_to_prev_token(&text, cursor_pos);
self.input_manager.set_cursor_position(new_pos);
self.sync_from_input_manager();
}
pub fn jump_to_next_token(&mut self) {
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let new_pos = CursorOperations::jump_to_next_token(&text, cursor_pos);
self.input_manager.set_cursor_position(new_pos);
self.sync_from_input_manager();
}
pub fn yank(&mut self) {
if !self.kill_ring.is_empty() {
self.save_state_for_undo();
let text = self.input_manager.get_text();
let cursor_pos = self.input_manager.get_cursor_position();
let before = text.chars().take(cursor_pos).collect::<String>();
let after = text.chars().skip(cursor_pos).collect::<String>();
let new_text = format!("{}{}{}", before, &self.kill_ring, after);
let new_cursor = cursor_pos + self.kill_ring.len();
self.input_manager.set_text(new_text);
self.input_manager.set_cursor_position(new_cursor);
self.sync_from_input_manager();
}
}
pub fn expand_asterisk(&mut self, parser: &HybridParser) -> bool {
let query = self.input_manager.get_text();
let query_upper = query.to_uppercase();
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;
if let Some(from_rel_pos) = query_upper[star_abs_pos..].find("FROM") {
let from_abs_pos = star_abs_pos + from_rel_pos;
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() {
let columns = parser.get_table_columns(table_name);
if columns.is_empty() {
self.status_message =
format!("No columns found for table '{table_name}'");
} else {
let columns_str = columns.join(", ");
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}");
self.input_manager.set_text(new_query.clone());
self.input_manager.set_cursor_position(new_query.len());
self.sync_from_input_manager();
self.status_message =
format!("Expanded * to {} columns", columns.len());
return true;
}
}
}
}
}
self.status_message = "No SELECT * pattern found to expand".to_string();
false
}
pub fn expand_asterisk_visible(&mut self) -> bool {
let query = self.input_manager.get_text();
let query_upper = query.to_uppercase();
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;
if let Some(dataview) = &self.dataview {
let visible_columns = dataview.get_display_column_names();
if !visible_columns.is_empty() {
let columns_str = visible_columns.join(", ");
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}");
self.input_manager.set_text(new_query.clone());
self.input_manager.set_cursor_position(new_query.len());
self.sync_from_input_manager();
self.status_message =
format!("Expanded * to {} visible columns", visible_columns.len());
return true;
}
self.status_message = "No visible columns available".to_string();
} else {
self.status_message = "No data loaded to expand from".to_string();
}
}
}
self.status_message = "No SELECT * pattern found to expand".to_string();
false
}
}
impl Clone for Buffer {
fn clone(&self) -> Self {
let input_manager = create_from_input(self.input.clone());
Self {
id: self.id,
file_path: self.file_path.clone(),
name: self.name.clone(),
modified: self.modified,
datatable: self.datatable.clone(),
original_source: self.original_source.clone(),
dataview: self.dataview.clone(),
mode: self.mode.clone(),
edit_mode: self.edit_mode.clone(),
input: self.input.clone(),
input_manager,
table_state: self.table_state.clone(),
last_results_row: self.last_results_row,
last_scroll_offset: self.last_scroll_offset,
last_query: self.last_query.clone(),
status_message: self.status_message.clone(),
sort_state: self.sort_state.clone(),
filter_state: self.filter_state.clone(),
fuzzy_filter_state: self.fuzzy_filter_state.clone(),
search_state: self.search_state.clone(),
column_stats: self.column_stats.clone(),
view_state: self.view_state.clone(),
column_widths: self.column_widths.clone(),
compact_mode: self.compact_mode,
show_row_numbers: self.show_row_numbers,
case_insensitive: self.case_insensitive,
undo_stack: self.undo_stack.clone(),
redo_stack: self.redo_stack.clone(),
kill_ring: self.kill_ring.clone(),
last_visible_rows: self.last_visible_rows,
last_query_source: self.last_query_source.clone(),
highlighted_text_cache: self.highlighted_text_cache.clone(),
last_highlighted_text: self.last_highlighted_text.clone(),
saved_input_state: self.saved_input_state.clone(),
}
}
}
pub struct BufferManager {
buffers: Vec<Buffer>,
current_buffer_index: usize,
next_buffer_id: usize,
}
impl Default for BufferManager {
fn default() -> Self {
Self::new()
}
}
impl BufferManager {
#[must_use]
pub fn new() -> Self {
Self {
buffers: Vec::new(),
current_buffer_index: 0,
next_buffer_id: 1,
}
}
pub fn add_buffer(&mut self, mut buffer: Buffer) -> usize {
buffer.id = self.next_buffer_id;
self.next_buffer_id += 1;
let index = self.buffers.len();
self.buffers.push(buffer);
self.current_buffer_index = index;
index
}
#[must_use]
pub fn current(&self) -> Option<&Buffer> {
self.buffers.get(self.current_buffer_index)
}
pub fn current_mut(&mut self) -> Option<&mut Buffer> {
self.buffers.get_mut(self.current_buffer_index)
}
pub fn next_buffer(&mut self) {
if !self.buffers.is_empty() {
self.current_buffer_index = (self.current_buffer_index + 1) % self.buffers.len();
}
}
pub fn prev_buffer(&mut self) {
if !self.buffers.is_empty() {
if self.current_buffer_index == 0 {
self.current_buffer_index = self.buffers.len() - 1;
} else {
self.current_buffer_index -= 1;
}
}
}
pub fn switch_to(&mut self, index: usize) {
if index < self.buffers.len() {
self.current_buffer_index = index;
}
}
pub fn close_current(&mut self) -> bool {
if self.buffers.len() <= 1 {
return false; }
self.buffers.remove(self.current_buffer_index);
if self.current_buffer_index >= self.buffers.len() {
self.current_buffer_index = self.buffers.len() - 1;
}
true
}
#[must_use]
pub fn find_by_path(&self, path: &PathBuf) -> Option<usize> {
self.buffers.iter().position(|b| b.has_file(path))
}
#[must_use]
pub fn all_buffers(&self) -> &[Buffer] {
&self.buffers
}
#[must_use]
pub fn current_index(&self) -> usize {
self.current_buffer_index
}
#[must_use]
pub fn has_multiple(&self) -> bool {
self.buffers.len() > 1
}
pub fn clear_all(&mut self) {
self.buffers.clear();
self.current_buffer_index = 0;
}
}