use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CopyModeCursor {
pub x: u16,
pub y: i32,
}
impl CopyModeCursor {
pub fn new(x: u16, y: i32) -> Self {
Self { x, y }
}
pub fn origin() -> Self {
Self { x: 0, y: 0 }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Selection {
pub anchor: CopyModeCursor,
pub active: CopyModeCursor,
}
impl Selection {
pub fn new(anchor: CopyModeCursor, active: CopyModeCursor) -> Self {
Self { anchor, active }
}
pub fn at_point(cursor: CopyModeCursor) -> Self {
Self {
anchor: cursor,
active: cursor,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SelectionMode {
#[default]
None,
Cell,
Line,
Block,
Word,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CopyModeState {
pub active: bool,
pub cursor: CopyModeCursor,
pub selection: Option<Selection>,
pub selection_mode: SelectionMode,
pub viewport_offset: i32,
}
impl CopyModeState {
pub fn new() -> Self {
Self::default()
}
pub fn activate(&mut self, cursor: CopyModeCursor) {
self.active = true;
self.cursor = cursor;
self.selection = None;
self.selection_mode = SelectionMode::None;
}
pub fn deactivate(&mut self) {
self.active = false;
self.selection = None;
self.selection_mode = SelectionMode::None;
}
pub fn start_selection(&mut self, mode: SelectionMode) {
self.selection_mode = mode;
self.selection = Some(Selection::at_point(self.cursor));
}
pub fn clear_selection(&mut self) {
self.selection = None;
self.selection_mode = SelectionMode::None;
}
pub fn update_selection(&mut self) {
if let Some(ref mut selection) = self.selection {
selection.active = self.cursor;
}
}
pub fn toggle_selection(&mut self, mode: SelectionMode) {
if self.selection_mode == mode {
self.clear_selection();
} else {
self.start_selection(mode);
}
}
pub fn swap_selection_ends(&mut self) {
if let Some(ref mut selection) = self.selection {
std::mem::swap(&mut selection.anchor, &mut selection.active);
self.cursor = selection.active;
}
}
pub fn move_left(&mut self) {
if self.cursor.x > 0 {
self.cursor.x -= 1;
}
}
pub fn move_right(&mut self, max_cols: u16) {
if self.cursor.x < max_cols.saturating_sub(1) {
self.cursor.x += 1;
}
}
pub fn move_up(&mut self, min_y: i32) {
if self.cursor.y > min_y {
self.cursor.y -= 1;
}
}
pub fn move_down(&mut self, max_y: i32) {
if self.cursor.y < max_y {
self.cursor.y += 1;
}
}
pub fn move_to_line_start(&mut self) {
self.cursor.x = 0;
}
pub fn move_to_line_end(&mut self, line_length: u16) {
self.cursor.x = line_length.saturating_sub(1);
}
pub fn move_to_top(&mut self, min_y: i32) {
self.cursor.y = min_y;
}
pub fn move_to_bottom(&mut self, max_y: i32) {
self.cursor.y = max_y;
}
pub fn toggle_cell_selection(&mut self) {
self.toggle_selection(SelectionMode::Cell);
}
pub fn toggle_line_selection(&mut self) {
self.toggle_selection(SelectionMode::Line);
}
pub fn toggle_block_selection(&mut self) {
self.toggle_selection(SelectionMode::Block);
}
pub fn toggle_word_selection(&mut self) {
self.toggle_selection(SelectionMode::Word);
}
pub fn get_selection_text<F>(&self, get_line: F) -> Option<String>
where
F: Fn(i32) -> Option<String>,
{
let selection = self.selection.as_ref()?;
let (start, end) = normalize_selection(selection);
let mut result = String::new();
match self.selection_mode {
SelectionMode::None => return None,
SelectionMode::Cell => {
if start.y == end.y {
if let Some(line) = get_line(start.y) {
let start_x = start.x as usize;
let end_x = (end.x as usize + 1).min(line.len());
if start_x < line.len() {
result.push_str(&line[start_x..end_x]);
}
}
} else {
for y in start.y..=end.y {
if let Some(line) = get_line(y) {
if y == start.y {
let start_x = start.x as usize;
if start_x < line.len() {
result.push_str(&line[start_x..]);
}
} else if y == end.y {
let end_x = (end.x as usize + 1).min(line.len());
result.push_str(&line[..end_x]);
} else {
result.push_str(&line);
}
if y < end.y {
result.push('\n');
}
}
}
}
}
SelectionMode::Line => {
for y in start.y..=end.y {
if let Some(line) = get_line(y) {
result.push_str(&line);
if y < end.y {
result.push('\n');
}
}
}
}
SelectionMode::Block => {
let min_x = start.x.min(end.x);
let max_x = start.x.max(end.x);
for y in start.y..=end.y {
if let Some(line) = get_line(y) {
let start_x = min_x as usize;
let end_x = (max_x as usize + 1).min(line.len());
if start_x < line.len() {
result.push_str(&line[start_x..end_x]);
}
if y < end.y {
result.push('\n');
}
}
}
}
SelectionMode::Word => {
if start.y == end.y {
if let Some(line) = get_line(start.y) {
let start_x = start.x as usize;
let end_x = (end.x as usize + 1).min(line.len());
if start_x < line.len() {
result.push_str(&line[start_x..end_x]);
}
}
}
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SearchState {
pub active: bool,
pub query: String,
pub direction: SearchDirection,
pub matches: Vec<SearchMatch>,
pub current_match: Option<usize>,
}
impl SearchState {
pub fn new() -> Self {
Self::default()
}
pub fn start_search(&mut self, direction: SearchDirection) {
self.active = true;
self.direction = direction;
self.query.clear();
self.matches.clear();
self.current_match = None;
}
pub fn update_query(&mut self, query: String, matches: Vec<SearchMatch>) {
self.query = query;
let is_empty = matches.is_empty();
self.matches = matches;
self.current_match = if is_empty { None } else { Some(0) };
}
pub fn next_match(&mut self) {
if let Some(current) = self.current_match {
if !self.matches.is_empty() {
self.current_match = Some((current + 1) % self.matches.len());
}
}
}
pub fn prev_match(&mut self) {
if let Some(current) = self.current_match {
if !self.matches.is_empty() {
self.current_match = Some(if current == 0 {
self.matches.len() - 1
} else {
current - 1
});
}
}
}
pub fn current(&self) -> Option<&SearchMatch> {
self.current_match.and_then(|idx| self.matches.get(idx))
}
pub fn deactivate(&mut self) {
self.active = false;
self.query.clear();
self.matches.clear();
self.current_match = None;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum SearchDirection {
#[default]
Forward,
Backward,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SearchMatch {
pub start: CopyModeCursor,
pub end: CopyModeCursor,
}
impl SearchMatch {
pub fn new(start: CopyModeCursor, end: CopyModeCursor) -> Self {
Self { start, end }
}
}
pub fn find_matches<F>(query: &str, get_line: F, min_y: i32, max_y: i32) -> Vec<SearchMatch>
where
F: Fn(i32) -> Option<String>,
{
let mut matches = Vec::new();
if query.is_empty() {
return matches;
}
let query_lower = query.to_lowercase();
for y in min_y..=max_y {
if let Some(line) = get_line(y) {
let line_lower = line.to_lowercase();
let mut start_pos = 0;
while let Some(match_pos) = line_lower[start_pos..].find(&query_lower) {
let absolute_pos = start_pos + match_pos;
let start = CopyModeCursor::new(absolute_pos as u16, y);
let end = CopyModeCursor::new((absolute_pos + query.len() - 1) as u16, y);
matches.push(SearchMatch::new(start, end));
start_pos = absolute_pos + 1;
}
}
}
matches
}
pub fn normalize_selection(selection: &Selection) -> (CopyModeCursor, CopyModeCursor) {
let a = selection.anchor;
let b = selection.active;
if a.y < b.y || (a.y == b.y && a.x <= b.x) {
(a, b)
} else {
(b, a)
}
}
pub fn get_selection_bounds(selection: &Selection) -> (u16, i32, u16, i32) {
let (start, end) = normalize_selection(selection);
let min_x = start.x.min(end.x);
let max_x = start.x.max(end.x);
let min_y = start.y.min(end.y);
let max_y = start.y.max(end.y);
(min_x, min_y, max_x, max_y)
}
pub fn find_word_bounds(x: u16, line: &str) -> (u16, u16) {
let x_pos = x as usize;
if line.is_empty() || x_pos >= line.len() {
return (x, x);
}
let chars: Vec<char> = line.chars().collect();
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
if x_pos >= chars.len() {
return (x, x);
}
let current_char = chars[x_pos];
if !is_word_char(current_char) {
return (x, x);
}
let mut start = x_pos;
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1;
}
let mut end = x_pos;
while end < chars.len() - 1 && is_word_char(chars[end + 1]) {
end += 1;
}
(start as u16, end as u16)
}
use crate::status_bar::{Color, RenderItem};
pub fn copy_mode_indicator(state: &CopyModeState, search_active: bool) -> Vec<RenderItem> {
if !state.active {
return vec![];
}
let mode_text = if search_active {
"SEARCH"
} else {
match state.selection {
None => "COPY",
Some(_) => match state.selection_mode {
SelectionMode::None => "COPY",
SelectionMode::Cell => "VISUAL",
SelectionMode::Line => "V-LINE",
SelectionMode::Block => "V-BLOCK",
SelectionMode::Word => "V-WORD",
},
}
};
vec![
RenderItem::Background(Color::Rgb(255, 158, 100)), RenderItem::Foreground(Color::Rgb(26, 27, 38)), RenderItem::Bold,
RenderItem::Text(format!(" {} ", mode_text)),
RenderItem::ResetAttributes,
]
}
pub fn copy_mode_position_indicator(state: &CopyModeState) -> Vec<RenderItem> {
if !state.active {
return vec![];
}
vec![RenderItem::Text(format!(
" L{},C{} ",
state.cursor.y + 1,
state.cursor.x + 1
))]
}
pub fn search_match_indicator(search: &SearchState) -> Vec<RenderItem> {
if !search.active || search.matches.is_empty() {
return vec![];
}
let current = search.current_match.map(|i| i + 1).unwrap_or(0);
vec![RenderItem::Text(format!(
" {}/{} ",
current,
search.matches.len()
))]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_creation() {
let cursor = CopyModeCursor::new(10, 5);
assert_eq!(cursor.x, 10);
assert_eq!(cursor.y, 5);
}
#[test]
fn test_cursor_origin() {
let cursor = CopyModeCursor::origin();
assert_eq!(cursor.x, 0);
assert_eq!(cursor.y, 0);
}
#[test]
fn test_cursor_negative_y() {
let cursor = CopyModeCursor::new(5, -100);
assert_eq!(cursor.y, -100);
}
#[test]
fn test_selection_normalization() {
let sel = Selection {
anchor: CopyModeCursor::new(0, 0),
active: CopyModeCursor::new(10, 5),
};
let (start, end) = normalize_selection(&sel);
assert_eq!(start.x, 0);
assert_eq!(start.y, 0);
assert_eq!(end.x, 10);
assert_eq!(end.y, 5);
let sel = Selection {
anchor: CopyModeCursor::new(10, 5),
active: CopyModeCursor::new(5, 3),
};
let (start, end) = normalize_selection(&sel);
assert_eq!(start.x, 5);
assert_eq!(start.y, 3);
assert_eq!(end.x, 10);
assert_eq!(end.y, 5);
let sel = Selection {
anchor: CopyModeCursor::new(10, 5),
active: CopyModeCursor::new(3, 5),
};
let (start, end) = normalize_selection(&sel);
assert_eq!(start.x, 3);
assert_eq!(start.y, 5);
assert_eq!(end.x, 10);
assert_eq!(end.y, 5);
}
#[test]
fn test_selection_bounds() {
let sel = Selection {
anchor: CopyModeCursor::new(5, 3),
active: CopyModeCursor::new(10, 7),
};
let (min_x, min_y, max_x, max_y) = get_selection_bounds(&sel);
assert_eq!(min_x, 5);
assert_eq!(min_y, 3);
assert_eq!(max_x, 10);
assert_eq!(max_y, 7);
}
#[test]
fn test_selection_bounds_reversed() {
let sel = Selection {
anchor: CopyModeCursor::new(10, 7),
active: CopyModeCursor::new(5, 3),
};
let (min_x, min_y, max_x, max_y) = get_selection_bounds(&sel);
assert_eq!(min_x, 5);
assert_eq!(min_y, 3);
assert_eq!(max_x, 10);
assert_eq!(max_y, 7);
}
#[test]
fn test_selection_bounds_single_line() {
let sel = Selection {
anchor: CopyModeCursor::new(3, 5),
active: CopyModeCursor::new(10, 5),
};
let (min_x, min_y, max_x, max_y) = get_selection_bounds(&sel);
assert_eq!(min_x, 3);
assert_eq!(min_y, 5);
assert_eq!(max_x, 10);
assert_eq!(max_y, 5);
}
#[test]
fn test_copy_mode_state_activation() {
let mut state = CopyModeState::new();
assert!(!state.active);
state.activate(CopyModeCursor::new(5, 10));
assert!(state.active);
assert_eq!(state.cursor.x, 5);
assert_eq!(state.cursor.y, 10);
state.deactivate();
assert!(!state.active);
}
#[test]
fn test_copy_mode_selection_toggle() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.toggle_selection(SelectionMode::Cell);
assert_eq!(state.selection_mode, SelectionMode::Cell);
assert!(state.selection.is_some());
state.toggle_selection(SelectionMode::Cell);
assert_eq!(state.selection_mode, SelectionMode::None);
assert!(state.selection.is_none());
}
#[test]
fn test_copy_mode_swap_selection_ends() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.start_selection(SelectionMode::Cell);
state.cursor = CopyModeCursor::new(10, 10);
state.update_selection();
let selection = state.selection.as_ref().unwrap();
assert_eq!(selection.anchor, CopyModeCursor::new(5, 5));
assert_eq!(selection.active, CopyModeCursor::new(10, 10));
state.swap_selection_ends();
let selection = state.selection.as_ref().unwrap();
assert_eq!(selection.anchor, CopyModeCursor::new(10, 10));
assert_eq!(selection.active, CopyModeCursor::new(5, 5));
assert_eq!(state.cursor, CopyModeCursor::new(5, 5));
}
#[test]
fn test_search_state_navigation() {
let mut search = SearchState::new();
let matches = vec![
SearchMatch::new(CopyModeCursor::new(0, 0), CopyModeCursor::new(5, 0)),
SearchMatch::new(CopyModeCursor::new(10, 0), CopyModeCursor::new(15, 0)),
SearchMatch::new(CopyModeCursor::new(0, 1), CopyModeCursor::new(5, 1)),
];
search.update_query("test".to_string(), matches);
assert_eq!(search.current_match, Some(0));
search.next_match();
assert_eq!(search.current_match, Some(1));
search.next_match();
assert_eq!(search.current_match, Some(2));
search.next_match();
assert_eq!(search.current_match, Some(0));
search.prev_match();
assert_eq!(search.current_match, Some(2));
}
#[test]
fn test_search_state_no_matches() {
let mut search = SearchState::new();
search.update_query("notfound".to_string(), vec![]);
assert_eq!(search.current_match, None);
search.next_match();
assert_eq!(search.current_match, None);
search.prev_match();
assert_eq!(search.current_match, None);
}
#[test]
fn test_find_matches() {
let get_line = |y: i32| match y {
0 => Some("Hello world, hello Rust!".to_string()),
1 => Some("Another HELLO here".to_string()),
2 => Some("No match on this line".to_string()),
_ => None,
};
let matches = find_matches("hello", get_line, 0, 2);
assert_eq!(matches.len(), 3);
assert_eq!(matches[0].start, CopyModeCursor::new(0, 0));
assert_eq!(matches[0].end, CopyModeCursor::new(4, 0));
assert_eq!(matches[1].start, CopyModeCursor::new(13, 0));
assert_eq!(matches[1].end, CopyModeCursor::new(17, 0));
assert_eq!(matches[2].start, CopyModeCursor::new(8, 1));
assert_eq!(matches[2].end, CopyModeCursor::new(12, 1));
}
#[test]
fn test_find_matches_empty_query() {
let get_line = |_y: i32| Some("Some text".to_string());
let matches = find_matches("", get_line, 0, 5);
assert_eq!(matches.len(), 0);
}
#[test]
fn test_find_matches_no_matches() {
let get_line = |_y: i32| Some("No matches here".to_string());
let matches = find_matches("xyz", get_line, 0, 5);
assert_eq!(matches.len(), 0);
}
#[test]
fn test_selection_mode_default() {
let mode = SelectionMode::default();
assert_eq!(mode, SelectionMode::None);
}
#[test]
fn test_search_direction_default() {
let direction = SearchDirection::default();
assert_eq!(direction, SearchDirection::Forward);
}
#[test]
fn test_move_left() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.move_left();
assert_eq!(state.cursor.x, 4);
state.move_left();
state.move_left();
state.move_left();
state.move_left();
assert_eq!(state.cursor.x, 0);
state.move_left();
assert_eq!(state.cursor.x, 0);
}
#[test]
fn test_move_right() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.move_right(80);
assert_eq!(state.cursor.x, 6);
state.cursor.x = 79;
state.move_right(80);
assert_eq!(state.cursor.x, 79);
}
#[test]
fn test_move_up() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 10);
state.move_up(0);
assert_eq!(state.cursor.y, 9);
for _ in 0..10 {
state.move_up(0);
}
assert_eq!(state.cursor.y, 0);
state.move_up(0);
assert_eq!(state.cursor.y, 0);
}
#[test]
fn test_move_down() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.move_down(24);
assert_eq!(state.cursor.y, 1);
state.cursor.y = 23;
state.move_down(24);
assert_eq!(state.cursor.y, 24);
state.move_down(24);
assert_eq!(state.cursor.y, 24);
}
#[test]
fn test_move_up_with_scrollback() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.move_up(-100);
assert_eq!(state.cursor.y, -1);
state.cursor.y = -50;
state.move_up(-100);
assert_eq!(state.cursor.y, -51);
state.cursor.y = -100;
state.move_up(-100);
assert_eq!(state.cursor.y, -100);
}
#[test]
fn test_move_to_line_start() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(42, 5);
state.move_to_line_start();
assert_eq!(state.cursor.x, 0);
assert_eq!(state.cursor.y, 5); }
#[test]
fn test_move_to_line_end() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 3);
state.move_to_line_end(80);
assert_eq!(state.cursor.x, 79);
assert_eq!(state.cursor.y, 3); }
#[test]
fn test_move_to_top() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 10);
state.move_to_top(-100);
assert_eq!(state.cursor.x, 5); assert_eq!(state.cursor.y, -100);
}
#[test]
fn test_move_to_bottom() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, -50);
state.move_to_bottom(24);
assert_eq!(state.cursor.x, 5); assert_eq!(state.cursor.y, 24);
}
#[test]
fn test_toggle_cell_selection() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.toggle_cell_selection();
assert_eq!(state.selection_mode, SelectionMode::Cell);
assert!(state.selection.is_some());
state.toggle_cell_selection();
assert_eq!(state.selection_mode, SelectionMode::None);
assert!(state.selection.is_none());
}
#[test]
fn test_toggle_line_selection() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.toggle_line_selection();
assert_eq!(state.selection_mode, SelectionMode::Line);
assert!(state.selection.is_some());
state.toggle_line_selection();
assert_eq!(state.selection_mode, SelectionMode::None);
assert!(state.selection.is_none());
}
#[test]
fn test_toggle_block_selection() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.toggle_block_selection();
assert_eq!(state.selection_mode, SelectionMode::Block);
assert!(state.selection.is_some());
state.toggle_block_selection();
assert_eq!(state.selection_mode, SelectionMode::None);
assert!(state.selection.is_none());
}
#[test]
fn test_toggle_word_selection() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.toggle_word_selection();
assert_eq!(state.selection_mode, SelectionMode::Word);
assert!(state.selection.is_some());
state.toggle_word_selection();
assert_eq!(state.selection_mode, SelectionMode::None);
assert!(state.selection.is_none());
}
#[test]
fn test_get_selection_text_cell_single_line() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.start_selection(SelectionMode::Cell);
state.cursor = CopyModeCursor::new(9, 0);
state.update_selection();
let get_line = |y: i32| {
if y == 0 {
Some("Hello, World!".to_string())
} else {
None
}
};
let text = state.get_selection_text(get_line);
assert_eq!(text, Some(", Wor".to_string()));
}
#[test]
fn test_get_selection_text_cell_multi_line() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.start_selection(SelectionMode::Cell);
state.cursor = CopyModeCursor::new(5, 2);
state.update_selection();
let get_line = |y: i32| match y {
0 => Some("Line 0".to_string()),
1 => Some("Line 1".to_string()),
2 => Some("Line 2".to_string()),
_ => None,
};
let text = state.get_selection_text(get_line);
assert_eq!(text, Some("0\nLine 1\nLine 2".to_string()));
}
#[test]
fn test_get_selection_text_line_mode() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 0);
state.start_selection(SelectionMode::Line);
state.cursor = CopyModeCursor::new(10, 2);
state.update_selection();
let get_line = |y: i32| match y {
0 => Some("First line".to_string()),
1 => Some("Second line".to_string()),
2 => Some("Third line".to_string()),
_ => None,
};
let text = state.get_selection_text(get_line);
assert_eq!(
text,
Some("First line\nSecond line\nThird line".to_string())
);
}
#[test]
fn test_get_selection_text_block_mode() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(2, 0);
state.start_selection(SelectionMode::Block);
state.cursor = CopyModeCursor::new(5, 2);
state.update_selection();
let get_line = |y: i32| match y {
0 => Some("ABCDEFGH".to_string()),
1 => Some("12345678".to_string()),
2 => Some("abcdefgh".to_string()),
_ => None,
};
let text = state.get_selection_text(get_line);
assert_eq!(text, Some("CDEF\n3456\ncdef".to_string()));
}
#[test]
fn test_get_selection_text_no_selection() {
let state = CopyModeState::new();
let get_line = |_y: i32| Some("Line".to_string());
let text = state.get_selection_text(get_line);
assert_eq!(text, None);
}
#[test]
fn test_movement_with_selection_update() {
let mut state = CopyModeState::new();
state.cursor = CopyModeCursor::new(5, 5);
state.start_selection(SelectionMode::Cell);
state.move_right(80);
state.update_selection();
let selection = state.selection.as_ref().unwrap();
assert_eq!(selection.anchor, CopyModeCursor::new(5, 5));
assert_eq!(selection.active, CopyModeCursor::new(6, 5));
state.move_down(24);
state.update_selection();
let selection = state.selection.as_ref().unwrap();
assert_eq!(selection.anchor, CopyModeCursor::new(5, 5));
assert_eq!(selection.active, CopyModeCursor::new(6, 6));
}
#[test]
fn test_find_word_bounds() {
let line = "Hello world, this is a test";
let (start, end) = find_word_bounds(2, line);
assert_eq!(start, 0);
assert_eq!(end, 4);
let (start, end) = find_word_bounds(6, line);
assert_eq!(start, 6);
assert_eq!(end, 10);
let (start, end) = find_word_bounds(24, line);
assert_eq!(start, 23);
assert_eq!(end, 26);
let (start, end) = find_word_bounds(5, line);
assert_eq!(start, 5);
assert_eq!(end, 5);
}
#[test]
fn test_find_word_bounds_empty_line() {
let line = "";
let (start, end) = find_word_bounds(0, line);
assert_eq!(start, 0);
assert_eq!(end, 0);
}
#[test]
fn test_find_word_bounds_with_underscores() {
let line = "hello_world test_case";
let (start, end) = find_word_bounds(6, line);
assert_eq!(start, 0);
assert_eq!(end, 10); }
#[test]
fn test_copy_mode_indicator() {
let mut state = CopyModeState::new();
let items = copy_mode_indicator(&state, false);
assert_eq!(items.len(), 0);
state.active = true;
let items = copy_mode_indicator(&state, false);
assert!(items.len() > 0);
match &items[3] {
RenderItem::Text(s) => assert_eq!(s, " COPY "),
_ => panic!("Expected text item"),
}
state.selection = Some(Selection::at_point(CopyModeCursor::new(0, 0)));
state.selection_mode = SelectionMode::Cell;
let items = copy_mode_indicator(&state, false);
match &items[3] {
RenderItem::Text(s) => assert_eq!(s, " VISUAL "),
_ => panic!("Expected text item"),
}
state.selection_mode = SelectionMode::Line;
let items = copy_mode_indicator(&state, false);
match &items[3] {
RenderItem::Text(s) => assert_eq!(s, " V-LINE "),
_ => panic!("Expected text item"),
}
let items = copy_mode_indicator(&state, true);
match &items[3] {
RenderItem::Text(s) => assert_eq!(s, " SEARCH "),
_ => panic!("Expected text item"),
}
}
#[test]
fn test_copy_mode_position_indicator() {
let mut state = CopyModeState::new();
let items = copy_mode_position_indicator(&state);
assert_eq!(items.len(), 0);
state.active = true;
state.cursor = CopyModeCursor::new(5, 10);
let items = copy_mode_position_indicator(&state);
assert_eq!(items.len(), 1);
match &items[0] {
RenderItem::Text(s) => assert_eq!(s, " L11,C6 "),
_ => panic!("Expected text item"),
}
}
#[test]
fn test_search_match_indicator() {
let mut search = SearchState::new();
let items = search_match_indicator(&search);
assert_eq!(items.len(), 0);
search.active = true;
let items = search_match_indicator(&search);
assert_eq!(items.len(), 0);
search.matches = vec![
SearchMatch::new(CopyModeCursor::new(0, 0), CopyModeCursor::new(5, 0)),
SearchMatch::new(CopyModeCursor::new(0, 1), CopyModeCursor::new(5, 1)),
SearchMatch::new(CopyModeCursor::new(0, 2), CopyModeCursor::new(5, 2)),
];
search.current_match = Some(1);
let items = search_match_indicator(&search);
assert_eq!(items.len(), 1);
match &items[0] {
RenderItem::Text(s) => assert_eq!(s, " 2/3 "),
_ => panic!("Expected text item"),
}
}
}