1use crate::primitives::word_navigation::{find_word_end_bytes, find_word_start_bytes};
12
13fn flat_offset(lines: &[String], row: usize, col: usize) -> usize {
17 let mut offset = 0;
18 for (i, line) in lines.iter().enumerate() {
19 if i == row {
20 return offset + col.min(line.len());
21 }
22 offset += line.len() + 1;
23 }
24 offset.saturating_sub(1)
25}
26
27#[derive(Debug, Clone)]
29pub struct TextEdit {
30 pub lines: Vec<String>,
32 pub cursor_row: usize,
34 pub cursor_col: usize,
36 pub selection_anchor: Option<(usize, usize)>,
38 pub multiline: bool,
40}
41
42impl Default for TextEdit {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl TextEdit {
49 pub fn new() -> Self {
51 Self {
52 lines: vec![String::new()],
53 cursor_row: 0,
54 cursor_col: 0,
55 selection_anchor: None,
56 multiline: true,
57 }
58 }
59
60 pub fn single_line() -> Self {
62 Self {
63 lines: vec![String::new()],
64 cursor_row: 0,
65 cursor_col: 0,
66 selection_anchor: None,
67 multiline: false,
68 }
69 }
70
71 pub fn with_text(text: &str) -> Self {
73 let lines: Vec<String> = text.lines().map(String::from).collect();
74 let lines = if lines.is_empty() {
75 vec![String::new()]
76 } else {
77 lines
78 };
79 Self {
80 lines,
81 cursor_row: 0,
82 cursor_col: 0,
83 selection_anchor: None,
84 multiline: true,
85 }
86 }
87
88 pub fn single_line_with_text(text: &str) -> Self {
90 let first_line = text.lines().next().unwrap_or("").to_string();
91 Self {
92 lines: vec![first_line],
93 cursor_row: 0,
94 cursor_col: 0,
95 selection_anchor: None,
96 multiline: false,
97 }
98 }
99
100 pub fn value(&self) -> String {
102 self.lines.join("\n")
103 }
104
105 pub fn set_value(&mut self, text: &str) {
107 if self.multiline {
108 self.lines = text.lines().map(String::from).collect();
109 if self.lines.is_empty() {
110 self.lines.push(String::new());
111 }
112 } else {
113 self.lines = vec![text.lines().next().unwrap_or("").to_string()];
114 }
115 self.cursor_row = 0;
116 self.cursor_col = 0;
117 self.selection_anchor = None;
118 }
119
120 pub fn current_line(&self) -> &str {
122 self.lines
123 .get(self.cursor_row)
124 .map(|s| s.as_str())
125 .unwrap_or("")
126 }
127
128 pub fn line_count(&self) -> usize {
130 self.lines.len()
131 }
132
133 pub fn flat_cursor_byte(&self) -> usize {
137 let mut offset = 0;
138 for (i, line) in self.lines.iter().enumerate() {
139 if i == self.cursor_row {
140 return offset + self.cursor_col.min(line.len());
141 }
142 offset += line.len() + 1; }
144 self.value().len()
146 }
147
148 pub fn set_cursor_from_flat(&mut self, byte: usize) {
153 self.clear_selection();
154 let total = self.value().len();
155 let mut remaining = byte.min(total);
156 for (i, line) in self.lines.iter().enumerate() {
157 if remaining <= line.len() {
158 let mut col = remaining;
159 while col > 0 && !line.is_char_boundary(col) {
160 col -= 1;
161 }
162 self.cursor_row = i;
163 self.cursor_col = col;
164 return;
165 }
166 remaining -= line.len() + 1; }
168 self.cursor_row = self.lines.len().saturating_sub(1);
169 self.cursor_col = self.lines.last().map(|l| l.len()).unwrap_or(0);
170 }
171
172 pub fn selection_flat_range(&self) -> Option<(usize, usize)> {
177 let ((sr, sc), (er, ec)) = self.selection_range()?;
178 Some((
179 flat_offset(&self.lines, sr, sc),
180 flat_offset(&self.lines, er, ec),
181 ))
182 }
183
184 pub fn move_left(&mut self) {
190 self.clear_selection();
191 self.move_left_internal();
192 }
193
194 fn move_left_internal(&mut self) {
195 if self.cursor_col > 0 {
196 let line = &self.lines[self.cursor_row];
198 let mut new_col = self.cursor_col - 1;
199 while new_col > 0 && !line.is_char_boundary(new_col) {
200 new_col -= 1;
201 }
202 self.cursor_col = new_col;
203 } else if self.cursor_row > 0 && self.multiline {
204 self.cursor_row -= 1;
205 self.cursor_col = self.lines[self.cursor_row].len();
206 }
207 }
208
209 pub fn move_right(&mut self) {
211 self.clear_selection();
212 self.move_right_internal();
213 }
214
215 fn move_right_internal(&mut self) {
216 let line_len = self
217 .lines
218 .get(self.cursor_row)
219 .map(|l| l.len())
220 .unwrap_or(0);
221 if self.cursor_col < line_len {
222 let line = &self.lines[self.cursor_row];
224 let mut new_col = self.cursor_col + 1;
225 while new_col < line.len() && !line.is_char_boundary(new_col) {
226 new_col += 1;
227 }
228 self.cursor_col = new_col;
229 } else if self.cursor_row + 1 < self.lines.len() && self.multiline {
230 self.cursor_row += 1;
231 self.cursor_col = 0;
232 }
233 }
234
235 pub fn move_up(&mut self) {
237 self.clear_selection();
238 self.move_up_internal();
239 }
240
241 fn move_up_internal(&mut self) {
242 if self.cursor_row > 0 {
243 self.cursor_row -= 1;
244 let line_len = self.lines[self.cursor_row].len();
245 self.cursor_col = self.cursor_col.min(line_len);
246 }
247 }
248
249 pub fn move_down(&mut self) {
251 self.clear_selection();
252 self.move_down_internal();
253 }
254
255 fn move_down_internal(&mut self) {
256 if self.cursor_row + 1 < self.lines.len() {
257 self.cursor_row += 1;
258 let line_len = self.lines[self.cursor_row].len();
259 self.cursor_col = self.cursor_col.min(line_len);
260 }
261 }
262
263 pub fn move_home(&mut self) {
265 self.clear_selection();
266 self.cursor_col = 0;
267 }
268
269 pub fn move_end(&mut self) {
271 self.clear_selection();
272 self.cursor_col = self
273 .lines
274 .get(self.cursor_row)
275 .map(|l| l.len())
276 .unwrap_or(0);
277 }
278
279 pub fn move_word_left(&mut self) {
281 self.clear_selection();
282 self.move_word_left_internal();
283 }
284
285 fn move_word_left_internal(&mut self) {
286 let line = &self.lines[self.cursor_row];
287 if self.cursor_col > 0 {
288 let new_col = find_word_start_bytes(line.as_bytes(), self.cursor_col);
289 if new_col < self.cursor_col {
290 self.cursor_col = new_col;
291 return;
292 }
293 }
294 if self.cursor_row > 0 && self.multiline {
296 self.cursor_row -= 1;
297 self.cursor_col = self.lines[self.cursor_row].len();
298 }
299 }
300
301 pub fn move_word_right(&mut self) {
303 self.clear_selection();
304 self.move_word_right_internal();
305 }
306
307 fn move_word_right_internal(&mut self) {
308 let line = &self.lines[self.cursor_row];
309 if self.cursor_col < line.len() {
310 let new_col = find_word_end_bytes(line.as_bytes(), self.cursor_col);
311 if new_col > self.cursor_col {
312 self.cursor_col = new_col;
313 return;
314 }
315 }
316 if self.cursor_row + 1 < self.lines.len() && self.multiline {
318 self.cursor_row += 1;
319 self.cursor_col = 0;
320 }
321 }
322
323 pub fn has_selection(&self) -> bool {
329 if let Some((anchor_row, anchor_col)) = self.selection_anchor {
330 anchor_row != self.cursor_row || anchor_col != self.cursor_col
331 } else {
332 false
333 }
334 }
335
336 pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
339 let (anchor_row, anchor_col) = self.selection_anchor?;
340 if anchor_row == self.cursor_row && anchor_col == self.cursor_col {
341 return None;
342 }
343
344 let (start, end) = if anchor_row < self.cursor_row
345 || (anchor_row == self.cursor_row && anchor_col < self.cursor_col)
346 {
347 ((anchor_row, anchor_col), (self.cursor_row, self.cursor_col))
348 } else {
349 ((self.cursor_row, self.cursor_col), (anchor_row, anchor_col))
350 };
351 Some((start, end))
352 }
353
354 pub fn selected_text(&self) -> Option<String> {
356 let ((start_row, start_col), (end_row, end_col)) = self.selection_range()?;
357
358 if start_row == end_row {
359 let line = &self.lines[start_row];
360 let end_col = end_col.min(line.len());
361 let start_col = start_col.min(end_col);
362 Some(line[start_col..end_col].to_string())
363 } else {
364 let mut result = String::new();
365 let first_line = &self.lines[start_row];
367 result.push_str(&first_line[start_col.min(first_line.len())..]);
368 result.push('\n');
369 for row in (start_row + 1)..end_row {
371 result.push_str(&self.lines[row]);
372 result.push('\n');
373 }
374 let last_line = &self.lines[end_row];
376 result.push_str(&last_line[..end_col.min(last_line.len())]);
377 Some(result)
378 }
379 }
380
381 pub fn delete_selection(&mut self) -> Option<String> {
383 let ((start_row, start_col), (end_row, end_col)) = self.selection_range()?;
384 let deleted = self.selected_text()?;
385
386 if start_row == end_row {
387 let line = &mut self.lines[start_row];
388 let end_col = end_col.min(line.len());
389 let start_col = start_col.min(end_col);
390 line.drain(start_col..end_col);
391 } else {
392 let end_col = end_col.min(self.lines[end_row].len());
393 let after_end = self.lines[end_row][end_col..].to_string();
394 self.lines[start_row].truncate(start_col);
395 self.lines[start_row].push_str(&after_end);
396 for _ in (start_row + 1)..=end_row {
398 self.lines.remove(start_row + 1);
399 }
400 }
401
402 self.cursor_row = start_row;
403 self.cursor_col = start_col;
404 self.selection_anchor = None;
405 Some(deleted)
406 }
407
408 pub fn clear_selection(&mut self) {
410 self.selection_anchor = None;
411 }
412
413 fn ensure_anchor(&mut self) {
415 if self.selection_anchor.is_none() {
416 self.selection_anchor = Some((self.cursor_row, self.cursor_col));
417 }
418 }
419
420 pub fn move_left_selecting(&mut self) {
422 self.ensure_anchor();
423 self.move_left_internal();
424 }
425
426 pub fn move_right_selecting(&mut self) {
428 self.ensure_anchor();
429 self.move_right_internal();
430 }
431
432 pub fn move_up_selecting(&mut self) {
434 self.ensure_anchor();
435 self.move_up_internal();
436 }
437
438 pub fn move_down_selecting(&mut self) {
440 self.ensure_anchor();
441 self.move_down_internal();
442 }
443
444 pub fn move_home_selecting(&mut self) {
446 self.ensure_anchor();
447 self.cursor_col = 0;
448 }
449
450 pub fn move_end_selecting(&mut self) {
452 self.ensure_anchor();
453 self.cursor_col = self
454 .lines
455 .get(self.cursor_row)
456 .map(|l| l.len())
457 .unwrap_or(0);
458 }
459
460 pub fn move_word_left_selecting(&mut self) {
462 self.ensure_anchor();
463 self.move_word_left_internal();
464 }
465
466 pub fn move_word_right_selecting(&mut self) {
468 self.ensure_anchor();
469 self.move_word_right_internal();
470 }
471
472 pub fn select_all(&mut self) {
474 self.selection_anchor = Some((0, 0));
475 self.cursor_row = self.lines.len().saturating_sub(1);
476 self.cursor_col = self.lines.last().map(|l| l.len()).unwrap_or(0);
477 }
478
479 pub fn insert_char(&mut self, c: char) {
485 if self.has_selection() {
487 self.delete_selection();
488 }
489
490 if c == '\n' && self.multiline {
491 let current_line = &self.lines[self.cursor_row];
493 let col = self.cursor_col.min(current_line.len());
494 let (before, after) = current_line.split_at(col);
495 let before = before.to_string();
496 let after = after.to_string();
497 self.lines[self.cursor_row] = before;
498 self.lines.insert(self.cursor_row + 1, after);
499 self.cursor_row += 1;
500 self.cursor_col = 0;
501 } else if c != '\n' && self.cursor_row < self.lines.len() {
502 let line = &mut self.lines[self.cursor_row];
503 let col = self.cursor_col.min(line.len());
504 line.insert(col, c);
505 self.cursor_col = col + c.len_utf8();
506 }
507 }
509
510 pub fn insert_str(&mut self, text: &str) {
520 if self.has_selection() {
521 self.delete_selection();
522 }
523 for c in text.chars() {
524 if c == '\n' && !self.multiline {
525 self.insert_char(' ');
526 continue;
527 }
528 self.insert_char(c);
529 }
530 }
531
532 pub fn backspace(&mut self) {
534 if self.has_selection() {
535 self.delete_selection();
536 return;
537 }
538
539 if self.cursor_col > 0 {
540 let line = &mut self.lines[self.cursor_row];
541 let mut del_start = self.cursor_col - 1;
543 while del_start > 0 && !line.is_char_boundary(del_start) {
544 del_start -= 1;
545 }
546 line.drain(del_start..self.cursor_col);
547 self.cursor_col = del_start;
548 } else if self.cursor_row > 0 && self.multiline {
549 let current_line = self.lines.remove(self.cursor_row);
551 self.cursor_row -= 1;
552 self.cursor_col = self.lines[self.cursor_row].len();
553 self.lines[self.cursor_row].push_str(¤t_line);
554 }
555 }
556
557 pub fn delete(&mut self) {
559 if self.has_selection() {
560 self.delete_selection();
561 return;
562 }
563
564 let line_len = self
565 .lines
566 .get(self.cursor_row)
567 .map(|l| l.len())
568 .unwrap_or(0);
569 if self.cursor_col < line_len {
570 let line = &mut self.lines[self.cursor_row];
571 let mut del_end = self.cursor_col + 1;
573 while del_end < line.len() && !line.is_char_boundary(del_end) {
574 del_end += 1;
575 }
576 line.drain(self.cursor_col..del_end);
577 } else if self.cursor_row + 1 < self.lines.len() && self.multiline {
578 let next_line = self.lines.remove(self.cursor_row + 1);
580 self.lines[self.cursor_row].push_str(&next_line);
581 }
582 }
583
584 pub fn delete_word_forward(&mut self) {
586 if self.has_selection() {
587 self.delete_selection();
588 return;
589 }
590
591 let line = &self.lines[self.cursor_row];
592 let word_end = find_word_end_bytes(line.as_bytes(), self.cursor_col);
593 if word_end > self.cursor_col {
594 let line = &mut self.lines[self.cursor_row];
595 line.drain(self.cursor_col..word_end);
596 } else if self.cursor_row + 1 < self.lines.len() && self.multiline {
597 let next_line = self.lines.remove(self.cursor_row + 1);
599 self.lines[self.cursor_row].push_str(&next_line);
600 }
601 }
602
603 pub fn delete_word_backward(&mut self) {
605 if self.has_selection() {
606 self.delete_selection();
607 return;
608 }
609
610 let line = &self.lines[self.cursor_row];
611 let word_start = find_word_start_bytes(line.as_bytes(), self.cursor_col);
612 if word_start < self.cursor_col {
613 let line = &mut self.lines[self.cursor_row];
614 line.drain(word_start..self.cursor_col);
615 self.cursor_col = word_start;
616 } else if self.cursor_row > 0 && self.multiline {
617 let current_line = self.lines.remove(self.cursor_row);
619 self.cursor_row -= 1;
620 self.cursor_col = self.lines[self.cursor_row].len();
621 self.lines[self.cursor_row].push_str(¤t_line);
622 }
623 }
624
625 pub fn delete_to_end(&mut self) {
627 if self.has_selection() {
628 self.delete_selection();
629 return;
630 }
631
632 if let Some(line) = self.lines.get_mut(self.cursor_row) {
633 line.truncate(self.cursor_col);
634 }
635 }
636
637 pub fn clear(&mut self) {
639 self.lines = vec![String::new()];
640 self.cursor_row = 0;
641 self.cursor_col = 0;
642 self.selection_anchor = None;
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn test_single_line_basic() {
652 let mut edit = TextEdit::single_line();
653 edit.insert_str("hello world");
654 assert_eq!(edit.value(), "hello world");
655 assert_eq!(edit.cursor_col, 11);
656 }
657
658 #[test]
659 fn test_single_line_flattens_newlines_to_spaces() {
660 let mut edit = TextEdit::single_line();
664 edit.insert_str("hello\nworld");
665 assert_eq!(edit.value(), "hello world");
666 assert_eq!(edit.line_count(), 1);
667 }
668
669 #[test]
670 fn test_multiline_basic() {
671 let mut edit = TextEdit::new();
672 edit.insert_str("hello\nworld");
673 assert_eq!(edit.value(), "hello\nworld");
674 assert_eq!(edit.line_count(), 2);
675 assert_eq!(edit.cursor_row, 1);
676 assert_eq!(edit.cursor_col, 5);
677 }
678
679 #[test]
680 fn test_selection_single_line() {
681 let mut edit = TextEdit::single_line_with_text("hello world");
682 edit.cursor_col = 6; edit.move_right_selecting();
685 edit.move_right_selecting();
686 edit.move_right_selecting();
687 edit.move_right_selecting();
688 edit.move_right_selecting();
689
690 assert!(edit.has_selection());
691 assert_eq!(edit.selected_text(), Some("world".to_string()));
692 }
693
694 #[test]
695 fn test_selection_multiline() {
696 let mut edit = TextEdit::with_text("line1\nline2\nline3");
697 edit.cursor_row = 0;
698 edit.cursor_col = 3; edit.move_down_selecting();
702 edit.move_right_selecting();
703 edit.move_right_selecting();
704
705 assert!(edit.has_selection());
706 let selected = edit.selected_text().unwrap();
707 assert_eq!(selected, "e1\nline2");
708 }
709
710 #[test]
711 fn test_delete_selection() {
712 let mut edit = TextEdit::with_text("hello world");
713 edit.cursor_col = 0;
714
715 for _ in 0..6 {
717 edit.move_right_selecting();
718 }
719
720 let deleted = edit.delete_selection();
721 assert_eq!(deleted, Some("hello ".to_string()));
722 assert_eq!(edit.value(), "world");
723 assert_eq!(edit.cursor_col, 0);
724 }
725
726 #[test]
727 fn test_backspace_with_selection() {
728 let mut edit = TextEdit::with_text("hello world");
729 edit.select_all();
730 edit.backspace();
731 assert_eq!(edit.value(), "");
732 }
733
734 #[test]
735 fn test_insert_replaces_selection() {
736 let mut edit = TextEdit::with_text("hello world");
737 edit.select_all();
738 edit.insert_str("goodbye");
739 assert_eq!(edit.value(), "goodbye");
740 }
741
742 #[test]
743 fn test_flat_cursor_byte_round_trips_multiline() {
744 let mut edit = TextEdit::with_text("ab\ncde\nf");
745 edit.cursor_row = 1;
747 edit.cursor_col = 2;
748 let flat = edit.flat_cursor_byte();
749 assert_eq!(flat, 5);
751 let mut edit2 = TextEdit::with_text("ab\ncde\nf");
753 edit2.set_cursor_from_flat(5);
754 assert_eq!((edit2.cursor_row, edit2.cursor_col), (1, 2));
755 }
756
757 #[test]
758 fn test_set_cursor_from_flat_clamps_past_end() {
759 let mut edit = TextEdit::with_text("abc\nde");
760 edit.set_cursor_from_flat(999);
761 assert_eq!((edit.cursor_row, edit.cursor_col), (1, 2));
762 }
763
764 #[test]
765 fn test_set_cursor_from_flat_snaps_to_char_boundary() {
766 let mut edit = TextEdit::single_line_with_text("é");
768 edit.set_cursor_from_flat(1);
769 assert_eq!(edit.cursor_col, 0);
770 }
771
772 #[test]
773 fn test_selection_flat_range_spans_newline() {
774 let mut edit = TextEdit::with_text("ab\ncd");
775 edit.cursor_row = 0;
776 edit.cursor_col = 1;
777 edit.move_right_selecting(); edit.move_right_selecting(); edit.move_right_selecting(); assert_eq!(edit.selection_flat_range(), Some((1, 4)));
782 }
783
784 #[test]
785 fn test_word_navigation() {
786 let mut edit = TextEdit::single_line_with_text("one two three");
787 edit.cursor_col = 0;
788
789 edit.move_word_right();
790 assert_eq!(edit.cursor_col, 3); edit.move_word_right();
793 assert_eq!(edit.cursor_col, 7); edit.move_word_left();
796 assert_eq!(edit.cursor_col, 4); }
798}