1use crate::text_buffer::TextBuffer;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct CursorPosition {
14 pub line: usize,
16 pub col: usize,
18}
19
20impl CursorPosition {
21 pub fn new(line: usize, col: usize) -> Self {
23 Self { line, col }
24 }
25
26 pub fn beginning() -> Self {
28 Self { line: 0, col: 0 }
29 }
30}
31
32impl PartialOrd for CursorPosition {
33 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
34 Some(self.cmp(other))
35 }
36}
37
38impl Ord for CursorPosition {
39 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
40 self.line.cmp(&other.line).then(self.col.cmp(&other.col))
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct Selection {
51 pub anchor: CursorPosition,
53 pub head: CursorPosition,
55}
56
57impl Selection {
58 pub fn new(anchor: CursorPosition, head: CursorPosition) -> Self {
60 Self { anchor, head }
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.anchor == self.head
66 }
67
68 pub fn ordered(&self) -> (CursorPosition, CursorPosition) {
70 if self.anchor <= self.head {
71 (self.anchor, self.head)
72 } else {
73 (self.head, self.anchor)
74 }
75 }
76
77 pub fn contains(&self, pos: CursorPosition) -> bool {
79 let (start, end) = self.ordered();
80 pos >= start && pos < end
81 }
82
83 pub fn line_range(&self) -> (usize, usize) {
85 let (start, end) = self.ordered();
86 (start.line, end.line)
87 }
88}
89
90#[derive(Clone, Debug)]
96pub struct CursorState {
97 pub position: CursorPosition,
99 pub selection: Option<Selection>,
101 pub preferred_col: Option<usize>,
103}
104
105impl CursorState {
106 pub fn new(line: usize, col: usize) -> Self {
108 Self {
109 position: CursorPosition::new(line, col),
110 selection: None,
111 preferred_col: None,
112 }
113 }
114
115 pub fn move_left(&mut self, buffer: &TextBuffer) {
117 self.clear_selection();
118 if self.position.col > 0 {
119 self.position.col -= 1;
120 } else if self.position.line > 0 {
121 self.position.line -= 1;
122 self.position.col = buffer.line_len(self.position.line).unwrap_or(0);
123 }
124 self.preferred_col = None;
125 }
126
127 pub fn move_right(&mut self, buffer: &TextBuffer) {
129 self.clear_selection();
130 let line_len = buffer.line_len(self.position.line).unwrap_or(0);
131 if self.position.col < line_len {
132 self.position.col += 1;
133 } else if self.position.line + 1 < buffer.line_count() {
134 self.position.line += 1;
135 self.position.col = 0;
136 }
137 self.preferred_col = None;
138 }
139
140 pub fn move_up(&mut self, buffer: &TextBuffer) {
142 self.clear_selection();
143 if self.position.line > 0 {
144 let target_col = self.preferred_col.unwrap_or(self.position.col);
145 self.preferred_col = Some(target_col);
146 self.position.line -= 1;
147 let line_len = buffer.line_len(self.position.line).unwrap_or(0);
148 self.position.col = target_col.min(line_len);
149 }
150 }
151
152 pub fn move_down(&mut self, buffer: &TextBuffer) {
154 self.clear_selection();
155 if self.position.line + 1 < buffer.line_count() {
156 let target_col = self.preferred_col.unwrap_or(self.position.col);
157 self.preferred_col = Some(target_col);
158 self.position.line += 1;
159 let line_len = buffer.line_len(self.position.line).unwrap_or(0);
160 self.position.col = target_col.min(line_len);
161 }
162 }
163
164 pub fn move_to_line_start(&mut self) {
166 self.clear_selection();
167 self.position.col = 0;
168 self.preferred_col = None;
169 }
170
171 pub fn move_to_line_end(&mut self, buffer: &TextBuffer) {
173 self.clear_selection();
174 self.position.col = buffer.line_len(self.position.line).unwrap_or(0);
175 self.preferred_col = None;
176 }
177
178 pub fn move_to_buffer_start(&mut self) {
180 self.clear_selection();
181 self.position = CursorPosition::beginning();
182 self.preferred_col = None;
183 }
184
185 pub fn move_to_buffer_end(&mut self, buffer: &TextBuffer) {
187 self.clear_selection();
188 let last_line = buffer.line_count().saturating_sub(1);
189 self.position.line = last_line;
190 self.position.col = buffer.line_len(last_line).unwrap_or(0);
191 self.preferred_col = None;
192 }
193
194 pub fn start_selection(&mut self) {
196 self.selection = Some(Selection::new(self.position, self.position));
197 }
198
199 pub fn extend_selection(&mut self) {
203 match &mut self.selection {
204 Some(sel) => sel.head = self.position,
205 None => self.start_selection(),
206 }
207 }
208
209 pub fn clear_selection(&mut self) {
211 self.selection = None;
212 }
213
214 pub fn selected_text(&self, buffer: &TextBuffer) -> Option<String> {
218 let sel = self.selection.as_ref()?;
219 if sel.is_empty() {
220 return None;
221 }
222 let (start, end) = sel.ordered();
223
224 let mut result = String::new();
225 for line_idx in start.line..=end.line {
226 if let Some(line_text) = buffer.line(line_idx) {
227 let line_start = if line_idx == start.line { start.col } else { 0 };
228 let line_end = if line_idx == end.line {
229 end.col.min(line_text.chars().count())
230 } else {
231 line_text.chars().count()
232 };
233
234 let chars: String = line_text
235 .chars()
236 .skip(line_start)
237 .take(line_end.saturating_sub(line_start))
238 .collect();
239 result.push_str(&chars);
240
241 if line_idx < end.line {
243 result.push('\n');
244 }
245 }
246 }
247
248 if result.is_empty() {
249 None
250 } else {
251 Some(result)
252 }
253 }
254}
255
256#[cfg(test)]
257#[allow(clippy::unwrap_used)]
258mod tests {
259 use super::*;
260
261 fn buf(text: &str) -> TextBuffer {
262 TextBuffer::from_text(text)
263 }
264
265 #[test]
268 fn cursor_position_new() {
269 let p = CursorPosition::new(3, 5);
270 assert!(p.line == 3);
271 assert!(p.col == 5);
272 }
273
274 #[test]
275 fn cursor_position_beginning() {
276 let p = CursorPosition::beginning();
277 assert!(p.line == 0);
278 assert!(p.col == 0);
279 }
280
281 #[test]
282 fn cursor_position_ordering() {
283 let a = CursorPosition::new(0, 5);
284 let b = CursorPosition::new(1, 0);
285 let c = CursorPosition::new(0, 10);
286 assert!(a < b);
287 assert!(a < c);
288 assert!(b > c);
289 }
290
291 #[test]
294 fn selection_empty() {
295 let p = CursorPosition::new(0, 0);
296 let sel = Selection::new(p, p);
297 assert!(sel.is_empty());
298 }
299
300 #[test]
301 fn selection_ordered_forward() {
302 let a = CursorPosition::new(0, 0);
303 let b = CursorPosition::new(1, 5);
304 let sel = Selection::new(a, b);
305 let (start, end) = sel.ordered();
306 assert!(start == a);
307 assert!(end == b);
308 }
309
310 #[test]
311 fn selection_ordered_backward() {
312 let a = CursorPosition::new(1, 5);
313 let b = CursorPosition::new(0, 0);
314 let sel = Selection::new(a, b);
315 let (start, end) = sel.ordered();
316 assert!(start == b);
317 assert!(end == a);
318 }
319
320 #[test]
321 fn selection_contains() {
322 let sel = Selection::new(CursorPosition::new(0, 2), CursorPosition::new(0, 8));
323 assert!(sel.contains(CursorPosition::new(0, 5)));
324 assert!(!sel.contains(CursorPosition::new(0, 8))); assert!(!sel.contains(CursorPosition::new(0, 1)));
326 }
327
328 #[test]
329 fn selection_line_range() {
330 let sel = Selection::new(CursorPosition::new(2, 0), CursorPosition::new(5, 3));
331 assert!(sel.line_range() == (2, 5));
332 }
333
334 #[test]
337 fn move_left_within_line() {
338 let b = buf("hello");
339 let mut c = CursorState::new(0, 3);
340 c.move_left(&b);
341 assert!(c.position.col == 2);
342 }
343
344 #[test]
345 fn move_left_wraps_to_prev_line() {
346 let b = buf("hello\nworld");
347 let mut c = CursorState::new(1, 0);
348 c.move_left(&b);
349 assert!(c.position.line == 0);
350 assert!(c.position.col == 5);
351 }
352
353 #[test]
354 fn move_left_at_beginning_stays() {
355 let b = buf("hello");
356 let mut c = CursorState::new(0, 0);
357 c.move_left(&b);
358 assert!(c.position == CursorPosition::beginning());
359 }
360
361 #[test]
362 fn move_right_within_line() {
363 let b = buf("hello");
364 let mut c = CursorState::new(0, 2);
365 c.move_right(&b);
366 assert!(c.position.col == 3);
367 }
368
369 #[test]
370 fn move_right_wraps_to_next_line() {
371 let b = buf("hello\nworld");
372 let mut c = CursorState::new(0, 5);
373 c.move_right(&b);
374 assert!(c.position.line == 1);
375 assert!(c.position.col == 0);
376 }
377
378 #[test]
379 fn move_right_at_end_stays() {
380 let b = buf("hello");
381 let mut c = CursorState::new(0, 5);
382 c.move_right(&b);
383 assert!(c.position.line == 0);
384 assert!(c.position.col == 5);
385 }
386
387 #[test]
388 fn move_up_preserves_preferred_col() {
389 let b = buf("long line here\nhi\nanother long line");
390 let mut c = CursorState::new(0, 10);
391 c.move_down(&b); assert!(c.position.col == 2);
393 c.move_down(&b); assert!(c.position.col == 10);
395 }
396
397 #[test]
398 fn move_up_at_top_stays() {
399 let b = buf("hello");
400 let mut c = CursorState::new(0, 3);
401 c.move_up(&b);
402 assert!(c.position.line == 0);
403 }
404
405 #[test]
406 fn move_down_at_bottom_stays() {
407 let b = buf("hello");
408 let mut c = CursorState::new(0, 3);
409 c.move_down(&b);
410 assert!(c.position.line == 0);
411 }
412
413 #[test]
414 fn move_to_line_start_and_end() {
415 let b = buf("hello");
416 let mut c = CursorState::new(0, 3);
417 c.move_to_line_start();
418 assert!(c.position.col == 0);
419 c.move_to_line_end(&b);
420 assert!(c.position.col == 5);
421 }
422
423 #[test]
424 fn move_to_buffer_start_and_end() {
425 let b = buf("hello\nworld\nfoo");
426 let mut c = CursorState::new(1, 3);
427 c.move_to_buffer_start();
428 assert!(c.position == CursorPosition::beginning());
429 c.move_to_buffer_end(&b);
430 assert!(c.position.line == 2);
431 assert!(c.position.col == 3);
432 }
433
434 #[test]
437 fn start_and_extend_selection() {
438 let mut c = CursorState::new(0, 5);
439 c.start_selection();
440 assert!(c.selection.is_some());
441 c.position.col = 10;
442 c.extend_selection();
443 match &c.selection {
444 Some(sel) => {
445 assert!(sel.anchor.col == 5);
446 assert!(sel.head.col == 10);
447 }
448 None => unreachable!("expected selection"),
449 }
450 }
451
452 #[test]
453 fn clear_selection() {
454 let mut c = CursorState::new(0, 0);
455 c.start_selection();
456 assert!(c.selection.is_some());
457 c.clear_selection();
458 assert!(c.selection.is_none());
459 }
460
461 #[test]
462 fn selected_text_single_line() {
463 let b = buf("hello world");
464 let mut c = CursorState::new(0, 0);
465 c.selection = Some(Selection::new(
466 CursorPosition::new(0, 6),
467 CursorPosition::new(0, 11),
468 ));
469 match c.selected_text(&b) {
470 Some(ref s) if s == "world" => {}
471 other => unreachable!("expected 'world', got {other:?}"),
472 }
473 }
474
475 #[test]
476 fn selected_text_multi_line() {
477 let b = buf("hello\nworld\nfoo");
478 let mut c = CursorState::new(0, 0);
479 c.selection = Some(Selection::new(
480 CursorPosition::new(0, 3),
481 CursorPosition::new(1, 3),
482 ));
483 match c.selected_text(&b) {
484 Some(ref s) if s == "lo\nwor" => {}
485 other => unreachable!("expected 'lo\\nwor', got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn selected_text_empty_selection_returns_none() {
491 let b = buf("hello");
492 let mut c = CursorState::new(0, 3);
493 c.selection = Some(Selection::new(
494 CursorPosition::new(0, 3),
495 CursorPosition::new(0, 3),
496 ));
497 assert!(c.selected_text(&b).is_none());
498 }
499
500 #[test]
501 fn movement_clears_selection() {
502 let b = buf("hello");
503 let mut c = CursorState::new(0, 3);
504 c.start_selection();
505 c.move_right(&b);
506 assert!(c.selection.is_none());
507 }
508}