use ratatui::text::Span;
use crate::app::Selection;
pub const SELECTION_BG_COLOR: ratatui::style::Color = ratatui::style::Color::Rgb(60, 60, 100);
pub fn get_line_selection_range(selection: &Option<Selection>, line_idx: usize) -> Option<(usize, usize)> {
let sel = selection.as_ref()?;
let (start, end) = if sel.start.row < sel.end.row
|| (sel.start.row == sel.end.row && sel.start.col <= sel.end.col)
{
(sel.start, sel.end)
} else {
(sel.end, sel.start)
};
if line_idx < start.row || line_idx > end.row {
return None;
}
let start_col = if line_idx == start.row { start.col } else { 0 };
let end_col = if line_idx == end.row { end.col } else { usize::MAX };
Some((start_col, end_col))
}
pub fn apply_selection_to_span(
span: Span<'static>,
char_offset: usize,
sel_start: usize,
sel_end: usize,
) -> Vec<Span<'static>> {
let text = span.content.to_string();
let text_len = text.len();
let text_start = char_offset;
let text_end = char_offset + text_len;
if text_end <= sel_start || text_start >= sel_end {
return vec![span];
}
let mut result = Vec::new();
let base_style = span.style;
let selected_style = base_style.bg(SELECTION_BG_COLOR);
if text_start < sel_start {
let before_end = (sel_start - text_start).min(text_len);
let before_text: String = text.chars().take(before_end).collect();
if !before_text.is_empty() {
result.push(Span::styled(before_text, base_style));
}
}
let sel_in_text_start = sel_start.saturating_sub(text_start);
let sel_in_text_end = (sel_end - text_start).min(text_len);
if sel_in_text_start < sel_in_text_end {
let selected_text: String = text.chars()
.skip(sel_in_text_start)
.take(sel_in_text_end - sel_in_text_start)
.collect();
if !selected_text.is_empty() {
result.push(Span::styled(selected_text, selected_style));
}
}
if text_end > sel_end {
let after_start = sel_end.saturating_sub(text_start);
let after_text: String = text.chars().skip(after_start).collect();
if !after_text.is_empty() {
result.push(Span::styled(after_text, base_style));
}
}
if result.is_empty() {
vec![span]
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::{Position, Selection};
use ratatui::style::Style;
fn selection(start_row: usize, start_col: usize, end_row: usize, end_col: usize) -> Selection {
Selection {
start: Position { row: start_row, col: start_col },
end: Position { row: end_row, col: end_col },
active: true,
}
}
#[test]
fn line_selection_returns_none_when_no_selection() {
assert_eq!(get_line_selection_range(&None, 5), None);
}
#[test]
fn line_selection_returns_none_when_line_before_selection() {
let sel = selection(5, 0, 10, 0);
assert_eq!(get_line_selection_range(&Some(sel), 3), None);
}
#[test]
fn line_selection_returns_none_when_line_after_selection() {
let sel = selection(5, 0, 10, 0);
assert_eq!(get_line_selection_range(&Some(sel), 15), None);
}
#[test]
fn line_selection_single_line_returns_exact_columns() {
let sel = selection(5, 3, 5, 10);
assert_eq!(get_line_selection_range(&Some(sel), 5), Some((3, 10)));
}
#[test]
fn line_selection_normalizes_backwards_selection() {
let sel = selection(5, 10, 5, 3);
assert_eq!(get_line_selection_range(&Some(sel), 5), Some((3, 10)));
}
#[test]
fn line_selection_first_line_of_multiline() {
let sel = selection(5, 8, 10, 4);
assert_eq!(get_line_selection_range(&Some(sel), 5), Some((8, usize::MAX)));
}
#[test]
fn line_selection_last_line_of_multiline() {
let sel = selection(5, 8, 10, 4);
assert_eq!(get_line_selection_range(&Some(sel), 10), Some((0, 4)));
}
#[test]
fn line_selection_middle_line_of_multiline() {
let sel = selection(5, 8, 10, 4);
assert_eq!(get_line_selection_range(&Some(sel), 7), Some((0, usize::MAX)));
}
#[test]
fn span_no_overlap_before_selection() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span.clone(), 0, 10, 15);
assert_eq!(result.len(), 1);
assert_eq!(result[0].content, "hello");
}
#[test]
fn span_no_overlap_after_selection() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span.clone(), 20, 10, 15);
assert_eq!(result.len(), 1);
assert_eq!(result[0].content, "hello");
}
#[test]
fn span_fully_inside_selection() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span, 10, 5, 20);
assert_eq!(result.len(), 1);
assert_eq!(result[0].content, "hello");
assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
}
#[test]
fn span_selection_at_start() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span, 0, 0, 2);
assert_eq!(result.len(), 2);
assert_eq!(result[0].content, "he");
assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
assert_eq!(result[1].content, "llo");
assert_eq!(result[1].style.bg, None);
}
#[test]
fn span_selection_at_end() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span, 0, 3, 10);
assert_eq!(result.len(), 2);
assert_eq!(result[0].content, "hel");
assert_eq!(result[0].style.bg, None);
assert_eq!(result[1].content, "lo");
assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
}
#[test]
fn span_selection_in_middle() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span, 0, 1, 4);
assert_eq!(result.len(), 3);
assert_eq!(result[0].content, "h");
assert_eq!(result[0].style.bg, None);
assert_eq!(result[1].content, "ell");
assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
assert_eq!(result[2].content, "o");
assert_eq!(result[2].style.bg, None);
}
#[test]
fn span_with_char_offset() {
let span = Span::styled("world", Style::default());
let result = apply_selection_to_span(span, 10, 12, 14);
assert_eq!(result.len(), 3);
assert_eq!(result[0].content, "wo");
assert_eq!(result[1].content, "rl");
assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
assert_eq!(result[2].content, "d");
}
#[test]
fn span_empty_string() {
let span = Span::styled("", Style::default());
let result = apply_selection_to_span(span, 0, 0, 10);
assert_eq!(result.len(), 1);
assert_eq!(result[0].content, "");
}
#[test]
fn span_unicode_content() {
let span = Span::styled("héllo wörld", Style::default());
let result = apply_selection_to_span(span.clone(), 0, 0, 11);
assert_eq!(result.len(), 1, "Entire string should be one selected span");
assert_eq!(result[0].content, "héllo wörld");
assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
let span2 = Span::styled("héllo wörld", Style::default());
let result2 = apply_selection_to_span(span2, 0, 0, 5);
assert_eq!(result2.len(), 2, "Should split into selected and unselected");
assert_eq!(result2[0].content, "héllo");
assert_eq!(result2[0].style.bg, Some(SELECTION_BG_COLOR));
assert_eq!(result2[1].content, " wörld");
let span3 = Span::styled("héllo wörld", Style::default());
let result3 = apply_selection_to_span(span3, 0, 6, 9); assert_eq!(result3.len(), 3);
assert_eq!(result3[0].content, "héllo ");
assert_eq!(result3[1].content, "wör");
assert_eq!(result3[1].style.bg, Some(SELECTION_BG_COLOR));
assert_eq!(result3[2].content, "ld");
}
#[test]
fn span_selection_exact_boundaries() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span, 5, 5, 10);
assert_eq!(result.len(), 1);
assert_eq!(result[0].content, "hello");
assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
}
#[test]
fn line_selection_normalizes_multiline_backwards() {
let sel = selection(10, 4, 5, 8);
assert_eq!(get_line_selection_range(&Some(sel.clone()), 7), Some((0, usize::MAX)));
assert_eq!(get_line_selection_range(&Some(sel.clone()), 5), Some((8, usize::MAX)));
assert_eq!(get_line_selection_range(&Some(sel), 10), Some((0, 4)));
}
#[test]
fn line_selection_at_boundary() {
let sel = selection(5, 0, 5, 0);
assert_eq!(get_line_selection_range(&Some(sel), 5), Some((0, 0)));
}
#[test]
fn span_preserves_original_style_fg() {
use ratatui::style::Color;
let base_style = Style::default().fg(Color::Red);
let span = Span::styled("hello", base_style);
let result = apply_selection_to_span(span, 0, 0, 5);
assert_eq!(result[0].style.fg, Some(Color::Red));
assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
}
#[test]
fn span_single_char_selection() {
let span = Span::styled("hello", Style::default());
let result = apply_selection_to_span(span, 0, 2, 3);
assert_eq!(result.len(), 3);
assert_eq!(result[0].content, "he");
assert_eq!(result[1].content, "l");
assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
assert_eq!(result[2].content, "lo");
}
}