1use ratatui::text::Span;
2
3use crate::app::Selection;
4
5pub const SELECTION_BG_COLOR: ratatui::style::Color = ratatui::style::Color::Rgb(60, 60, 100);
6
7pub fn get_line_selection_range(selection: &Option<Selection>, line_idx: usize) -> Option<(usize, usize)> {
10 let sel = selection.as_ref()?;
11
12 let (start, end) = if sel.start.row < sel.end.row
14 || (sel.start.row == sel.end.row && sel.start.col <= sel.end.col)
15 {
16 (sel.start, sel.end)
17 } else {
18 (sel.end, sel.start)
19 };
20
21 if line_idx < start.row || line_idx > end.row {
23 return None;
24 }
25
26 let start_col = if line_idx == start.row { start.col } else { 0 };
28 let end_col = if line_idx == end.row { end.col } else { usize::MAX };
29
30 Some((start_col, end_col))
31}
32
33pub fn apply_selection_to_span(
35 span: Span<'static>,
36 char_offset: usize,
37 sel_start: usize,
38 sel_end: usize,
39) -> Vec<Span<'static>> {
40 let text = span.content.to_string();
41 let text_len = text.len();
42 let text_start = char_offset;
43 let text_end = char_offset + text_len;
44
45 if text_end <= sel_start || text_start >= sel_end {
47 return vec![span];
49 }
50
51 let mut result = Vec::new();
52 let base_style = span.style;
53 let selected_style = base_style.bg(SELECTION_BG_COLOR);
54
55 if text_start < sel_start {
57 let before_end = (sel_start - text_start).min(text_len);
58 let before_text: String = text.chars().take(before_end).collect();
59 if !before_text.is_empty() {
60 result.push(Span::styled(before_text, base_style));
61 }
62 }
63
64 let sel_in_text_start = sel_start.saturating_sub(text_start);
66 let sel_in_text_end = (sel_end - text_start).min(text_len);
67 if sel_in_text_start < sel_in_text_end {
68 let selected_text: String = text.chars()
69 .skip(sel_in_text_start)
70 .take(sel_in_text_end - sel_in_text_start)
71 .collect();
72 if !selected_text.is_empty() {
73 result.push(Span::styled(selected_text, selected_style));
74 }
75 }
76
77 if text_end > sel_end {
79 let after_start = sel_end.saturating_sub(text_start);
80 let after_text: String = text.chars().skip(after_start).collect();
81 if !after_text.is_empty() {
82 result.push(Span::styled(after_text, base_style));
83 }
84 }
85
86 if result.is_empty() {
87 vec![span]
88 } else {
89 result
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::app::{Position, Selection};
97 use ratatui::style::Style;
98
99 fn selection(start_row: usize, start_col: usize, end_row: usize, end_col: usize) -> Selection {
100 Selection {
101 start: Position { row: start_row, col: start_col },
102 end: Position { row: end_row, col: end_col },
103 active: true,
104 }
105 }
106
107 #[test]
110 fn line_selection_returns_none_when_no_selection() {
111 assert_eq!(get_line_selection_range(&None, 5), None);
112 }
113
114 #[test]
115 fn line_selection_returns_none_when_line_before_selection() {
116 let sel = selection(5, 0, 10, 0);
117 assert_eq!(get_line_selection_range(&Some(sel), 3), None);
118 }
119
120 #[test]
121 fn line_selection_returns_none_when_line_after_selection() {
122 let sel = selection(5, 0, 10, 0);
123 assert_eq!(get_line_selection_range(&Some(sel), 15), None);
124 }
125
126 #[test]
127 fn line_selection_single_line_returns_exact_columns() {
128 let sel = selection(5, 3, 5, 10);
129 assert_eq!(get_line_selection_range(&Some(sel), 5), Some((3, 10)));
130 }
131
132 #[test]
133 fn line_selection_normalizes_backwards_selection() {
134 let sel = selection(5, 10, 5, 3);
136 assert_eq!(get_line_selection_range(&Some(sel), 5), Some((3, 10)));
137 }
138
139 #[test]
140 fn line_selection_first_line_of_multiline() {
141 let sel = selection(5, 8, 10, 4);
142 assert_eq!(get_line_selection_range(&Some(sel), 5), Some((8, usize::MAX)));
144 }
145
146 #[test]
147 fn line_selection_last_line_of_multiline() {
148 let sel = selection(5, 8, 10, 4);
149 assert_eq!(get_line_selection_range(&Some(sel), 10), Some((0, 4)));
151 }
152
153 #[test]
154 fn line_selection_middle_line_of_multiline() {
155 let sel = selection(5, 8, 10, 4);
156 assert_eq!(get_line_selection_range(&Some(sel), 7), Some((0, usize::MAX)));
158 }
159
160 #[test]
163 fn span_no_overlap_before_selection() {
164 let span = Span::styled("hello", Style::default());
165 let result = apply_selection_to_span(span.clone(), 0, 10, 15);
167 assert_eq!(result.len(), 1);
168 assert_eq!(result[0].content, "hello");
169 }
170
171 #[test]
172 fn span_no_overlap_after_selection() {
173 let span = Span::styled("hello", Style::default());
174 let result = apply_selection_to_span(span.clone(), 20, 10, 15);
176 assert_eq!(result.len(), 1);
177 assert_eq!(result[0].content, "hello");
178 }
179
180 #[test]
181 fn span_fully_inside_selection() {
182 let span = Span::styled("hello", Style::default());
183 let result = apply_selection_to_span(span, 10, 5, 20);
185 assert_eq!(result.len(), 1);
186 assert_eq!(result[0].content, "hello");
187 assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
188 }
189
190 #[test]
191 fn span_selection_at_start() {
192 let span = Span::styled("hello", Style::default());
193 let result = apply_selection_to_span(span, 0, 0, 2);
195 assert_eq!(result.len(), 2);
196 assert_eq!(result[0].content, "he");
197 assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
198 assert_eq!(result[1].content, "llo");
199 assert_eq!(result[1].style.bg, None);
200 }
201
202 #[test]
203 fn span_selection_at_end() {
204 let span = Span::styled("hello", Style::default());
205 let result = apply_selection_to_span(span, 0, 3, 10);
207 assert_eq!(result.len(), 2);
208 assert_eq!(result[0].content, "hel");
209 assert_eq!(result[0].style.bg, None);
210 assert_eq!(result[1].content, "lo");
211 assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
212 }
213
214 #[test]
215 fn span_selection_in_middle() {
216 let span = Span::styled("hello", Style::default());
217 let result = apply_selection_to_span(span, 0, 1, 4);
219 assert_eq!(result.len(), 3);
220 assert_eq!(result[0].content, "h");
221 assert_eq!(result[0].style.bg, None);
222 assert_eq!(result[1].content, "ell");
223 assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
224 assert_eq!(result[2].content, "o");
225 assert_eq!(result[2].style.bg, None);
226 }
227
228 #[test]
229 fn span_with_char_offset() {
230 let span = Span::styled("world", Style::default());
231 let result = apply_selection_to_span(span, 10, 12, 14);
233 assert_eq!(result.len(), 3);
234 assert_eq!(result[0].content, "wo");
235 assert_eq!(result[1].content, "rl");
236 assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
237 assert_eq!(result[2].content, "d");
238 }
239
240 #[test]
243 fn span_empty_string() {
244 let span = Span::styled("", Style::default());
245 let result = apply_selection_to_span(span, 0, 0, 10);
246 assert_eq!(result.len(), 1);
248 assert_eq!(result[0].content, "");
249 }
250
251 #[test]
252 fn span_unicode_content() {
253 let span = Span::styled("héllo wörld", Style::default());
257
258 let result = apply_selection_to_span(span.clone(), 0, 0, 11);
260 assert_eq!(result.len(), 1, "Entire string should be one selected span");
261 assert_eq!(result[0].content, "héllo wörld");
262 assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
263
264 let span2 = Span::styled("héllo wörld", Style::default());
266 let result2 = apply_selection_to_span(span2, 0, 0, 5);
267 assert_eq!(result2.len(), 2, "Should split into selected and unselected");
268 assert_eq!(result2[0].content, "héllo");
269 assert_eq!(result2[0].style.bg, Some(SELECTION_BG_COLOR));
270 assert_eq!(result2[1].content, " wörld");
271
272 let span3 = Span::styled("héllo wörld", Style::default());
274 let result3 = apply_selection_to_span(span3, 0, 6, 9); assert_eq!(result3.len(), 3);
276 assert_eq!(result3[0].content, "héllo ");
277 assert_eq!(result3[1].content, "wör");
278 assert_eq!(result3[1].style.bg, Some(SELECTION_BG_COLOR));
279 assert_eq!(result3[2].content, "ld");
280 }
281
282 #[test]
283 fn span_selection_exact_boundaries() {
284 let span = Span::styled("hello", Style::default());
285 let result = apply_selection_to_span(span, 5, 5, 10);
287 assert_eq!(result.len(), 1);
288 assert_eq!(result[0].content, "hello");
289 assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
290 }
291
292 #[test]
293 fn line_selection_normalizes_multiline_backwards() {
294 let sel = selection(10, 4, 5, 8);
296 assert_eq!(get_line_selection_range(&Some(sel.clone()), 7), Some((0, usize::MAX)));
298 assert_eq!(get_line_selection_range(&Some(sel.clone()), 5), Some((8, usize::MAX)));
300 assert_eq!(get_line_selection_range(&Some(sel), 10), Some((0, 4)));
302 }
303
304 #[test]
305 fn line_selection_at_boundary() {
306 let sel = selection(5, 0, 5, 0);
308 assert_eq!(get_line_selection_range(&Some(sel), 5), Some((0, 0)));
310 }
311
312 #[test]
313 fn span_preserves_original_style_fg() {
314 use ratatui::style::Color;
315 let base_style = Style::default().fg(Color::Red);
316 let span = Span::styled("hello", base_style);
317 let result = apply_selection_to_span(span, 0, 0, 5);
318 assert_eq!(result[0].style.fg, Some(Color::Red));
320 assert_eq!(result[0].style.bg, Some(SELECTION_BG_COLOR));
321 }
322
323 #[test]
324 fn span_single_char_selection() {
325 let span = Span::styled("hello", Style::default());
326 let result = apply_selection_to_span(span, 0, 2, 3);
327 assert_eq!(result.len(), 3);
328 assert_eq!(result[0].content, "he");
329 assert_eq!(result[1].content, "l");
330 assert_eq!(result[1].style.bg, Some(SELECTION_BG_COLOR));
331 assert_eq!(result[2].content, "lo");
332 }
333}