use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::keybindings::Context;
use crate::ui::state::{AppState, Mode, Overlay};
use crate::ui::tabs::{CellEdit, RowChange, TabKind, WorkspaceTab};
use super::Action;
pub(super) fn handle_tab_data_grid(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let is_script = matches!(state.tabs[tab_idx].kind, TabKind::Script { .. });
if is_script {
let tab = &mut state.tabs[tab_idx];
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len() {
let rt = &tab.result_tabs[idx];
tab.query_result = Some(rt.result.clone());
tab.grid_scroll_row = rt.scroll_row;
tab.grid_selected_row = rt.selected_row;
tab.grid_selected_col = rt.selected_col;
tab.grid_selection_anchor = rt.selection_anchor;
}
}
let b = &state.bindings;
let action_name: Option<&'static str> = if b.matches(Context::Grid, "scroll_down", &key) {
Some("scroll_down")
} else if b.matches(Context::Grid, "scroll_up", &key) {
Some("scroll_up")
} else if b.matches(Context::Grid, "scroll_left", &key) {
Some("scroll_left")
} else if b.matches(Context::Grid, "scroll_right", &key) {
Some("scroll_right")
} else if b.matches(Context::Grid, "next_cell", &key) {
Some("next_cell")
} else if b.matches(Context::Grid, "prev_cell", &key) {
Some("prev_cell")
} else if b.matches(Context::Grid, "scroll_top", &key) {
Some("scroll_top")
} else if b.matches(Context::Grid, "scroll_bottom", &key) {
Some("scroll_bottom")
} else if b.matches(Context::Grid, "half_page_down", &key) {
Some("half_page_down")
} else if b.matches(Context::Grid, "half_page_up", &key) {
Some("half_page_up")
} else if b.matches(Context::Grid, "toggle_visual", &key) {
Some("toggle_visual")
} else if b.matches(Context::Grid, "yank", &key) {
Some("yank")
} else if b.matches(Context::Grid, "refresh_data", &key) {
Some("refresh_data")
} else if b.matches(Context::Grid, "toggle_auto_refresh", &key) {
Some("toggle_auto_refresh")
} else if b.matches(Context::Grid, "edit_cell", &key) {
Some("edit_cell")
} else if b.matches(Context::Grid, "new_row", &key) {
Some("new_row")
} else if b.matches(Context::Grid, "delete_pending", &key) {
Some("delete_pending")
} else if b.matches(Context::Grid, "undo_changes", &key) {
Some("undo_changes")
} else if b.matches(Context::Grid, "save_changes", &key) {
Some("save_changes")
} else if b.matches(Context::Grid, "exit_grid", &key) {
Some("exit_grid")
} else {
None
};
let tab = &mut state.tabs[tab_idx];
if tab.grid_editing.is_some() && state.mode == Mode::Insert {
return handle_grid_cell_edit(state, key);
}
if tab.grid_error_editor.is_some() && action_name == Some("exit_grid") {
tab.grid_error_editor = None;
tab.grid_query_editor = None;
return Action::Render;
}
let is_table_tab = matches!(tab.kind, TabKind::Table { .. });
let row_count = tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
let col_count = tab
.query_result
.as_ref()
.map(|r| r.columns.len())
.unwrap_or(0);
let vh = tab.grid_visible_height.max(1);
let visual = tab.grid_visual_mode;
let action = match action_name {
Some("edit_cell") if is_table_tab && !visual => {
if row_count > 0 && col_count > 0 {
let row = tab.grid_selected_row;
let col = tab.grid_selected_col;
let val = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
let val = if val == "NULL" { String::new() } else { val };
let cursor = val.len();
tab.grid_editing = Some((row, col));
tab.grid_edit_buffer = val;
tab.grid_edit_cursor = cursor;
state.mode = Mode::Insert;
}
return Action::Render;
}
Some("new_row") if is_table_tab && !visual => {
if let Some(ref mut qr) = tab.query_result {
let new_row: Vec<String> = qr.columns.iter().map(|_| "NULL".to_string()).collect();
let insert_pos = (tab.grid_selected_row + 1).min(qr.rows.len());
qr.rows.insert(insert_pos, new_row.clone());
let mut shifted: std::collections::HashMap<usize, _> =
std::collections::HashMap::new();
for (k, v) in tab.grid_changes.drain() {
if k >= insert_pos {
shifted.insert(k + 1, v);
} else {
shifted.insert(k, v);
}
}
tab.grid_changes = shifted;
tab.grid_changes
.insert(insert_pos, RowChange::New { values: new_row });
tab.grid_selected_row = insert_pos;
tab.grid_selected_col = 0;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh - 1);
}
}
return Action::Render;
}
Some("delete_pending") if is_table_tab && !visual => {
if state.pending_d {
state.pending_d = false;
if row_count > 0 {
let row = tab.grid_selected_row;
if matches!(tab.grid_changes.get(&row), Some(RowChange::New { .. })) {
tab.grid_changes.remove(&row);
if let Some(ref mut qr) = tab.query_result
&& row < qr.rows.len()
{
qr.rows.remove(row);
}
let mut shifted: std::collections::HashMap<usize, _> =
std::collections::HashMap::new();
for (k, v) in tab.grid_changes.drain() {
if k > row {
shifted.insert(k - 1, v);
} else {
shifted.insert(k, v);
}
}
tab.grid_changes = shifted;
let new_count =
tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
if tab.grid_selected_row >= new_count && new_count > 0 {
tab.grid_selected_row = new_count - 1;
}
} else {
tab.grid_changes.insert(row, RowChange::Deleted);
}
}
return Action::Render;
} else {
state.pending_d = true;
return Action::Render;
}
}
Some("undo_changes") if is_table_tab && !visual => {
if !tab.grid_changes.is_empty() {
tab.grid_changes.clear();
state.status_message = "Changes discarded".to_string();
return Action::ReloadTableData;
}
return Action::Render;
}
Some("refresh_data") if !visual => {
if !tab.grid_changes.is_empty() {
state.status_message =
"Pending changes — save with Ctrl+s or discard with u first".to_string();
return Action::Render;
}
if is_script {
let idx = tab.active_result_idx;
if let Some(rt) = tab.result_tabs.get(idx)
&& !rt.source_query.is_empty()
{
let query = rt.source_query.clone();
let start_line = rt.source_start_line;
let tab_id = tab.id;
state.status_message = "Refreshing query...".to_string();
return Action::ExecuteQuery {
tab_id,
query,
start_line,
};
}
state.status_message = "No source query to refresh".to_string();
return Action::Render;
}
state.status_message = "Refreshing...".to_string();
return Action::ReloadTableData;
}
Some("toggle_auto_refresh") if !visual && is_script => {
use crate::ui::tabs::AutoRefresh;
let idx = tab.active_result_idx;
if let Some(rt) = tab.result_tabs.get_mut(idx) {
if rt.source_query.is_empty() {
state.status_message = "No source query to auto-refresh".to_string();
return Action::Render;
}
let next = match rt.auto_refresh.as_ref().map(|a| a.interval.as_secs()) {
None => Some(2),
Some(2) => Some(5),
Some(5) => Some(10),
Some(10) => Some(30),
Some(30) | Some(_) => None,
};
rt.auto_refresh = next.map(|secs| AutoRefresh {
interval: std::time::Duration::from_secs(secs),
next_at: std::time::Instant::now() + std::time::Duration::from_secs(secs),
in_flight: false,
});
state.status_message = match next {
Some(s) => format!("Auto-refresh: every {s}s"),
None => "Auto-refresh: off".to_string(),
};
}
return Action::Render;
}
Some("save_changes") if is_table_tab => {
if !tab.grid_changes.is_empty() {
let modified = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::Modified { .. }))
.count();
let new = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::New { .. }))
.count();
let deleted = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::Deleted))
.count();
state.status_message =
format!("Save: {modified} modified, {new} new, {deleted} deleted — y/n?");
state.overlay = Some(Overlay::SaveGridChanges);
} else {
state.status_message = "No pending changes".to_string();
}
return Action::Render;
}
Some("toggle_visual") => {
if visual {
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
tab.grid_anchor_on_header = false;
} else {
tab.grid_visual_mode = true;
tab.grid_selection_anchor = Some((tab.grid_selected_row, tab.grid_selected_col));
tab.grid_anchor_on_header = tab.grid_on_header;
}
Action::Render
}
Some("scroll_down") => {
if tab.grid_on_header {
tab.grid_on_header = false;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_row += 1;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row - vh + 1;
}
}
Action::Render
}
Some("scroll_up") => {
if tab.grid_on_header {
} else if tab.grid_selected_row > 0 {
tab.grid_selected_row -= 1;
if tab.grid_selected_row < tab.grid_scroll_row {
tab.grid_scroll_row = tab.grid_selected_row;
}
} else {
tab.grid_on_header = true;
}
Action::Render
}
Some("scroll_left") => {
if tab.grid_selected_col > 0 {
tab.grid_selected_col -= 1;
}
Action::Render
}
Some("scroll_right") => {
if col_count > 0 && tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
}
Action::Render
}
Some("next_cell") => {
if col_count > 0 {
if tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_col = 0;
tab.grid_selected_row += 1;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row - vh + 1;
}
}
}
Action::Render
}
Some("prev_cell") => {
if tab.grid_selected_col > 0 {
tab.grid_selected_col -= 1;
} else if tab.grid_selected_row > 0 {
tab.grid_selected_row -= 1;
tab.grid_selected_col = col_count.saturating_sub(1);
if tab.grid_selected_row < tab.grid_scroll_row {
tab.grid_scroll_row = tab.grid_selected_row;
}
}
Action::Render
}
Some("half_page_down") => {
let half = vh / 2;
tab.grid_selected_row = (tab.grid_selected_row + half).min(row_count.saturating_sub(1));
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh / 2);
Action::Render
}
Some("half_page_up") => {
let half = vh / 2;
tab.grid_selected_row = tab.grid_selected_row.saturating_sub(half);
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh / 2);
Action::Render
}
Some("scroll_top") => {
tab.grid_selected_row = 0;
tab.grid_selected_col = 0;
tab.grid_scroll_row = 0;
tab.grid_on_header = true;
Action::Render
}
Some("scroll_bottom") => {
tab.grid_on_header = false;
if row_count > 0 {
tab.grid_selected_row = row_count - 1;
tab.grid_scroll_row = row_count.saturating_sub(vh);
}
Action::Render
}
Some("yank") => {
grid_yank(tab);
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
tab.grid_anchor_on_header = false;
Action::Render
}
Some("exit_grid") => {
if visual {
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
} else {
tab.grid_focused = false;
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
}
Action::Render
}
_ => Action::None,
};
if action_name == Some("yank") {
state.status_message = "Copied to clipboard".to_string();
}
if is_script {
let tab = &mut state.tabs[tab_idx];
sync_grid_to_result_tab(tab);
}
action
}
pub(super) fn handle_grid_cell_edit(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let tab = &mut state.tabs[tab_idx];
match key.code {
KeyCode::Esc | KeyCode::Enter => {
if let Some((row, col)) = tab.grid_editing.take() {
let new_val = if tab.grid_edit_buffer.is_empty() {
"NULL".to_string()
} else {
tab.grid_edit_buffer.clone()
};
let original = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
if let Some(ref mut qr) = tab.query_result
&& let Some(r) = qr.rows.get_mut(row)
&& let Some(cell) = r.get_mut(col)
{
*cell = new_val.clone();
}
if new_val != original {
match tab.grid_changes.get_mut(&row) {
Some(RowChange::Modified { edits }) => {
if let Some(e) = edits.iter_mut().find(|e| e.col == col) {
e.value = new_val;
} else {
edits.push(CellEdit {
col,
original,
value: new_val,
});
}
}
Some(RowChange::New { values }) => {
if let Some(v) = values.get_mut(col) {
*v = new_val;
}
}
Some(RowChange::Deleted) => {}
None => {
tab.grid_changes.insert(
row,
RowChange::Modified {
edits: vec![CellEdit {
col,
original,
value: new_val,
}],
},
);
}
}
}
}
tab.grid_edit_buffer.clear();
tab.grid_edit_cursor = 0;
state.mode = Mode::Normal;
Action::Render
}
KeyCode::Tab => {
let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let _ = handle_grid_cell_edit(state, esc_event);
let tab = &mut state.tabs[tab_idx];
let col_count = tab
.query_result
.as_ref()
.map(|r| r.columns.len())
.unwrap_or(0);
let row_count = tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
if col_count > 0 && tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_col = 0;
tab.grid_selected_row += 1;
}
let row = tab.grid_selected_row;
let col = tab.grid_selected_col;
let val = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
let val = if val == "NULL" { String::new() } else { val };
let cursor = val.len();
tab.grid_editing = Some((row, col));
tab.grid_edit_buffer = val;
tab.grid_edit_cursor = cursor;
state.mode = Mode::Insert;
Action::Render
}
KeyCode::Backspace => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor > 0 {
tab.grid_edit_cursor -= 1;
tab.grid_edit_buffer.remove(tab.grid_edit_cursor);
}
Action::Render
}
KeyCode::Delete => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor < tab.grid_edit_buffer.len() {
tab.grid_edit_buffer.remove(tab.grid_edit_cursor);
}
Action::Render
}
KeyCode::Left => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor > 0 {
tab.grid_edit_cursor -= 1;
}
Action::Render
}
KeyCode::Right => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor < tab.grid_edit_buffer.len() {
tab.grid_edit_cursor += 1;
}
Action::Render
}
KeyCode::Home => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_cursor = 0;
Action::Render
}
KeyCode::End => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_cursor = tab.grid_edit_buffer.len();
Action::Render
}
KeyCode::Char(c) => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_buffer.insert(tab.grid_edit_cursor, c);
tab.grid_edit_cursor += 1;
Action::Render
}
_ => Action::None,
}
}
pub(super) fn handle_table_error_editor(
state: &mut AppState,
key: KeyEvent,
is_query: bool,
) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
if key.code == KeyCode::Esc {
let tab = &mut state.tabs[tab_idx];
let editor = if is_query {
tab.grid_query_editor.as_ref()
} else {
tab.grid_error_editor.as_ref()
};
let in_normal =
editor.is_some_and(|e| matches!(e.mode, vimltui::VimMode::Normal) && !e.search.active);
if in_normal {
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
return Action::Render;
}
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('l') | KeyCode::Right => {
if !is_query {
let tab = &mut state.tabs[tab_idx];
if tab.grid_query_editor.is_some() {
tab.sub_focus = crate::ui::tabs::SubFocus::QueryView;
}
}
return Action::Render;
}
KeyCode::Char('h') | KeyCode::Left => {
if is_query {
let tab = &mut state.tabs[tab_idx];
tab.sub_focus = crate::ui::tabs::SubFocus::Results;
}
return Action::Render;
}
KeyCode::Char('k') | KeyCode::Up => {
let tab = &mut state.tabs[tab_idx];
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
return Action::Render;
}
_ => {}
}
}
let tab = &mut state.tabs[tab_idx];
let editor = if is_query {
tab.grid_query_editor.as_mut()
} else {
tab.grid_error_editor.as_mut()
};
if let Some(ed) = editor {
let _ = ed.handle_key(key);
}
Action::Render
}
pub(super) fn handle_save_grid_confirm(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
state.overlay = None;
Action::SaveGridChanges
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.overlay = None;
state.status_message = "Save cancelled".to_string();
Action::Render
}
_ => Action::None,
}
}
pub(super) fn sync_grid_to_result_tab(tab: &mut WorkspaceTab) {
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len() {
tab.result_tabs[idx].scroll_row = tab.grid_scroll_row;
tab.result_tabs[idx].selected_row = tab.grid_selected_row;
tab.result_tabs[idx].selected_col = tab.grid_selected_col;
tab.result_tabs[idx].selection_anchor = tab.grid_selection_anchor;
}
}
pub(super) fn grid_yank(tab: &WorkspaceTab) {
let text = build_yank_text(tab);
if !text.is_empty() {
copy_to_clipboard(&text);
}
}
pub(super) fn build_yank_text(tab: &WorkspaceTab) -> String {
let result = match &tab.query_result {
Some(r) => r,
None => return String::new(),
};
let include_header = tab.grid_on_header || tab.grid_anchor_on_header;
if tab.grid_on_header && tab.grid_selection_anchor.is_none() {
let vals: Vec<&str> = result.columns.iter().map(|c| c.as_str()).collect();
return vals.join(" ");
}
let last_col = result.columns.len().saturating_sub(1);
let (sr, sc, er, ec) = match tab.grid_selection_anchor {
Some((ar, ac)) => {
let r1 = ar.min(tab.grid_selected_row);
let r2 = ar.max(tab.grid_selected_row);
let c1 = ac.min(tab.grid_selected_col);
let c2 = ac.max(tab.grid_selected_col);
(r1, c1, r2, c2)
}
None => (tab.grid_selected_row, 0, tab.grid_selected_row, last_col),
};
let mut text = String::new();
if include_header {
let vals: Vec<&str> = (sc..=ec)
.filter_map(|c| result.columns.get(c).map(|v| v.as_str()))
.collect();
text.push_str(&vals.join(" "));
}
if !result.rows.is_empty() {
for row_idx in sr..=er {
if let Some(row_data) = result.rows.get(row_idx) {
if !text.is_empty() {
text.push('\n');
}
let vals: Vec<&str> = (sc..=ec)
.filter_map(|c| row_data.get(c).map(|v| v.as_str()))
.collect();
text.push_str(&vals.join(" "));
}
}
}
text
}
pub(super) fn copy_to_clipboard(text: &str) {
use std::io::Write;
let b64 = simple_base64_encode(text.as_bytes());
let osc = format!("\x1b]52;c;{b64}\x07");
let _ = std::io::stdout().write_all(osc.as_bytes());
let _ = std::io::stdout().flush();
let cmds: &[(&str, &[&str])] = &[
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["--clipboard", "--input"]),
];
for (cmd, args) in cmds {
if let Ok(mut child) = std::process::Command::new(cmd)
.args(*args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
return;
}
}
}
fn simple_base64_encode(input: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
out.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
out.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(CHARS[(n & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::models::QueryResult;
use crate::ui::tabs::{TabId, WorkspaceTab};
fn test_tab() -> WorkspaceTab {
let mut tab =
WorkspaceTab::new_script(TabId(1), "test".to_string(), Some("conn".to_string()), None);
tab.query_result = Some(QueryResult {
columns: vec!["id".to_string(), "name".to_string(), "age".to_string()],
rows: vec![
vec!["1".to_string(), "Alice".to_string(), "30".to_string()],
vec!["2".to_string(), "Bob".to_string(), "25".to_string()],
vec!["3".to_string(), "Carol".to_string(), "40".to_string()],
],
elapsed: None,
});
tab
}
#[test]
fn yank_header_only_when_cursor_on_header_no_selection() {
let mut tab = test_tab();
tab.grid_on_header = true;
assert_eq!(build_yank_text(&tab), "id name age");
}
#[test]
fn yank_visual_from_header_top_right_going_down() {
let mut tab = test_tab();
tab.grid_on_header = true;
tab.grid_selected_row = 0;
tab.grid_selected_col = 2; tab.grid_visual_mode = true;
tab.grid_selection_anchor = Some((tab.grid_selected_row, tab.grid_selected_col));
tab.grid_anchor_on_header = tab.grid_on_header; tab.grid_on_header = false;
tab.grid_selected_row = 1;
let text = build_yank_text(&tab);
assert_eq!(text, "age\n30\n25");
}
#[test]
fn yank_visual_from_header_top_right_going_down_and_left() {
let mut tab = test_tab();
tab.grid_on_header = true;
tab.grid_selected_row = 0;
tab.grid_selected_col = 2;
tab.grid_visual_mode = true;
tab.grid_selection_anchor = Some((0, 2));
tab.grid_anchor_on_header = true;
tab.grid_on_header = false;
tab.grid_selected_row = 2;
tab.grid_selected_col = 0;
let text = build_yank_text(&tab);
assert_eq!(text, "id name age\n1 Alice 30\n2 Bob 25\n3 Carol 40");
}
#[test]
fn yank_visual_from_bottom_going_up_into_header() {
let mut tab = test_tab();
tab.grid_on_header = false;
tab.grid_selected_row = 2;
tab.grid_selected_col = 0;
tab.grid_visual_mode = true;
tab.grid_selection_anchor = Some((2, 0));
tab.grid_anchor_on_header = false;
tab.grid_selected_row = 0;
tab.grid_on_header = true;
let text = build_yank_text(&tab);
assert_eq!(text, "id\n1\n2\n3");
}
}