fresh/view/ui/
text_edit.rs1use crate::primitives::word_navigation::{find_word_end_bytes, find_word_start_bytes};
12
13#[derive(Debug, Clone)]
15pub struct TextEdit {
16 pub lines: Vec<String>,
18 pub cursor_row: usize,
20 pub cursor_col: usize,
22 pub selection_anchor: Option<(usize, usize)>,
24 pub multiline: bool,
26}
27
28impl Default for TextEdit {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl TextEdit {
35 pub fn new() -> Self {
37 Self {
38 lines: vec![String::new()],
39 cursor_row: 0,
40 cursor_col: 0,
41 selection_anchor: None,
42 multiline: true,
43 }
44 }
45
46 pub fn single_line() -> Self {
48 Self {
49 lines: vec![String::new()],
50 cursor_row: 0,
51 cursor_col: 0,
52 selection_anchor: None,
53 multiline: false,
54 }
55 }
56
57 pub fn with_text(text: &str) -> Self {
59 let lines: Vec<String> = text.lines().map(String::from).collect();
60 let lines = if lines.is_empty() {
61 vec![String::new()]
62 } else {
63 lines
64 };
65 Self {
66 lines,
67 cursor_row: 0,
68 cursor_col: 0,
69 selection_anchor: None,
70 multiline: true,
71 }
72 }
73
74 pub fn single_line_with_text(text: &str) -> Self {
76 let first_line = text.lines().next().unwrap_or("").to_string();
77 Self {
78 lines: vec![first_line],
79 cursor_row: 0,
80 cursor_col: 0,
81 selection_anchor: None,
82 multiline: false,
83 }
84 }
85
86 pub fn value(&self) -> String {
88 self.lines.join("\n")
89 }
90
91 pub fn set_value(&mut self, text: &str) {
93 if self.multiline {
94 self.lines = text.lines().map(String::from).collect();
95 if self.lines.is_empty() {
96 self.lines.push(String::new());
97 }
98 } else {
99 self.lines = vec![text.lines().next().unwrap_or("").to_string()];
100 }
101 self.cursor_row = 0;
102 self.cursor_col = 0;
103 self.selection_anchor = None;
104 }
105
106 pub fn current_line(&self) -> &str {
108 self.lines
109 .get(self.cursor_row)
110 .map(|s| s.as_str())
111 .unwrap_or("")
112 }
113
114 pub fn line_count(&self) -> usize {
116 self.lines.len()
117 }
118
119 pub fn move_left(&mut self) {
125 self.clear_selection();
126 self.move_left_internal();
127 }
128
129 fn move_left_internal(&mut self) {
130 if self.cursor_col > 0 {
131 let line = &self.lines[self.cursor_row];
133 let mut new_col = self.cursor_col - 1;
134 while new_col > 0 && !line.is_char_boundary(new_col) {
135 new_col -= 1;
136 }
137 self.cursor_col = new_col;
138 } else if self.cursor_row > 0 && self.multiline {
139 self.cursor_row -= 1;
140 self.cursor_col = self.lines[self.cursor_row].len();
141 }
142 }
143
144 pub fn move_right(&mut self) {
146 self.clear_selection();
147 self.move_right_internal();
148 }
149
150 fn move_right_internal(&mut self) {
151 let line_len = self
152 .lines
153 .get(self.cursor_row)
154 .map(|l| l.len())
155 .unwrap_or(0);
156 if self.cursor_col < line_len {
157 let line = &self.lines[self.cursor_row];
159 let mut new_col = self.cursor_col + 1;
160 while new_col < line.len() && !line.is_char_boundary(new_col) {
161 new_col += 1;
162 }
163 self.cursor_col = new_col;
164 } else if self.cursor_row + 1 < self.lines.len() && self.multiline {
165 self.cursor_row += 1;
166 self.cursor_col = 0;
167 }
168 }
169
170 pub fn move_up(&mut self) {
172 self.clear_selection();
173 self.move_up_internal();
174 }
175
176 fn move_up_internal(&mut self) {
177 if self.cursor_row > 0 {
178 self.cursor_row -= 1;
179 let line_len = self.lines[self.cursor_row].len();
180 self.cursor_col = self.cursor_col.min(line_len);
181 }
182 }
183
184 pub fn move_down(&mut self) {
186 self.clear_selection();
187 self.move_down_internal();
188 }
189
190 fn move_down_internal(&mut self) {
191 if self.cursor_row + 1 < self.lines.len() {
192 self.cursor_row += 1;
193 let line_len = self.lines[self.cursor_row].len();
194 self.cursor_col = self.cursor_col.min(line_len);
195 }
196 }
197
198 pub fn move_home(&mut self) {
200 self.clear_selection();
201 self.cursor_col = 0;
202 }
203
204 pub fn move_end(&mut self) {
206 self.clear_selection();
207 self.cursor_col = self
208 .lines
209 .get(self.cursor_row)
210 .map(|l| l.len())
211 .unwrap_or(0);
212 }
213
214 pub fn move_word_left(&mut self) {
216 self.clear_selection();
217 self.move_word_left_internal();
218 }
219
220 fn move_word_left_internal(&mut self) {
221 let line = &self.lines[self.cursor_row];
222 if self.cursor_col > 0 {
223 let new_col = find_word_start_bytes(line.as_bytes(), self.cursor_col);
224 if new_col < self.cursor_col {
225 self.cursor_col = new_col;
226 return;
227 }
228 }
229 if self.cursor_row > 0 && self.multiline {
231 self.cursor_row -= 1;
232 self.cursor_col = self.lines[self.cursor_row].len();
233 }
234 }
235
236 pub fn move_word_right(&mut self) {
238 self.clear_selection();
239 self.move_word_right_internal();
240 }
241
242 fn move_word_right_internal(&mut self) {
243 let line = &self.lines[self.cursor_row];
244 if self.cursor_col < line.len() {
245 let new_col = find_word_end_bytes(line.as_bytes(), self.cursor_col);
246 if new_col > self.cursor_col {
247 self.cursor_col = new_col;
248 return;
249 }
250 }
251 if self.cursor_row + 1 < self.lines.len() && self.multiline {
253 self.cursor_row += 1;
254 self.cursor_col = 0;
255 }
256 }
257
258 pub fn has_selection(&self) -> bool {
264 if let Some((anchor_row, anchor_col)) = self.selection_anchor {
265 anchor_row != self.cursor_row || anchor_col != self.cursor_col
266 } else {
267 false
268 }
269 }
270
271 pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
274 let (anchor_row, anchor_col) = self.selection_anchor?;
275 if anchor_row == self.cursor_row && anchor_col == self.cursor_col {
276 return None;
277 }
278
279 let (start, end) = if anchor_row < self.cursor_row
280 || (anchor_row == self.cursor_row && anchor_col < self.cursor_col)
281 {
282 ((anchor_row, anchor_col), (self.cursor_row, self.cursor_col))
283 } else {
284 ((self.cursor_row, self.cursor_col), (anchor_row, anchor_col))
285 };
286 Some((start, end))
287 }
288
289 pub fn selected_text(&self) -> Option<String> {
291 let ((start_row, start_col), (end_row, end_col)) = self.selection_range()?;
292
293 if start_row == end_row {
294 let line = &self.lines[start_row];
295 let end_col = end_col.min(line.len());
296 let start_col = start_col.min(end_col);
297 Some(line[start_col..end_col].to_string())
298 } else {
299 let mut result = String::new();
300 let first_line = &self.lines[start_row];
302 result.push_str(&first_line[start_col.min(first_line.len())..]);
303 result.push('\n');
304 for row in (start_row + 1)..end_row {
306 result.push_str(&self.lines[row]);
307 result.push('\n');
308 }
309 let last_line = &self.lines[end_row];
311 result.push_str(&last_line[..end_col.min(last_line.len())]);
312 Some(result)
313 }
314 }
315
316 pub fn delete_selection(&mut self) -> Option<String> {
318 let ((start_row, start_col), (end_row, end_col)) = self.selection_range()?;
319 let deleted = self.selected_text()?;
320
321 if start_row == end_row {
322 let line = &mut self.lines[start_row];
323 let end_col = end_col.min(line.len());
324 let start_col = start_col.min(end_col);
325 line.drain(start_col..end_col);
326 } else {
327 let end_col = end_col.min(self.lines[end_row].len());
328 let after_end = self.lines[end_row][end_col..].to_string();
329 self.lines[start_row].truncate(start_col);
330 self.lines[start_row].push_str(&after_end);
331 for _ in (start_row + 1)..=end_row {
333 self.lines.remove(start_row + 1);
334 }
335 }
336
337 self.cursor_row = start_row;
338 self.cursor_col = start_col;
339 self.selection_anchor = None;
340 Some(deleted)
341 }
342
343 pub fn clear_selection(&mut self) {
345 self.selection_anchor = None;
346 }
347
348 fn ensure_anchor(&mut self) {
350 if self.selection_anchor.is_none() {
351 self.selection_anchor = Some((self.cursor_row, self.cursor_col));
352 }
353 }
354
355 pub fn move_left_selecting(&mut self) {
357 self.ensure_anchor();
358 self.move_left_internal();
359 }
360
361 pub fn move_right_selecting(&mut self) {
363 self.ensure_anchor();
364 self.move_right_internal();
365 }
366
367 pub fn move_up_selecting(&mut self) {
369 self.ensure_anchor();
370 self.move_up_internal();
371 }
372
373 pub fn move_down_selecting(&mut self) {
375 self.ensure_anchor();
376 self.move_down_internal();
377 }
378
379 pub fn move_home_selecting(&mut self) {
381 self.ensure_anchor();
382 self.cursor_col = 0;
383 }
384
385 pub fn move_end_selecting(&mut self) {
387 self.ensure_anchor();
388 self.cursor_col = self
389 .lines
390 .get(self.cursor_row)
391 .map(|l| l.len())
392 .unwrap_or(0);
393 }
394
395 pub fn move_word_left_selecting(&mut self) {
397 self.ensure_anchor();
398 self.move_word_left_internal();
399 }
400
401 pub fn move_word_right_selecting(&mut self) {
403 self.ensure_anchor();
404 self.move_word_right_internal();
405 }
406
407 pub fn select_all(&mut self) {
409 self.selection_anchor = Some((0, 0));
410 self.cursor_row = self.lines.len().saturating_sub(1);
411 self.cursor_col = self.lines.last().map(|l| l.len()).unwrap_or(0);
412 }
413
414 pub fn insert_char(&mut self, c: char) {
420 if self.has_selection() {
422 self.delete_selection();
423 }
424
425 if c == '\n' && self.multiline {
426 let current_line = &self.lines[self.cursor_row];
428 let col = self.cursor_col.min(current_line.len());
429 let (before, after) = current_line.split_at(col);
430 let before = before.to_string();
431 let after = after.to_string();
432 self.lines[self.cursor_row] = before;
433 self.lines.insert(self.cursor_row + 1, after);
434 self.cursor_row += 1;
435 self.cursor_col = 0;
436 } else if c != '\n' && self.cursor_row < self.lines.len() {
437 let line = &mut self.lines[self.cursor_row];
438 let col = self.cursor_col.min(line.len());
439 line.insert(col, c);
440 self.cursor_col = col + c.len_utf8();
441 }
442 }
444
445 pub fn insert_str(&mut self, text: &str) {
447 if self.has_selection() {
448 self.delete_selection();
449 }
450 for c in text.chars() {
451 if c == '\n' && !self.multiline {
453 continue;
454 }
455 self.insert_char(c);
456 }
457 }
458
459 pub fn backspace(&mut self) {
461 if self.has_selection() {
462 self.delete_selection();
463 return;
464 }
465
466 if self.cursor_col > 0 {
467 let line = &mut self.lines[self.cursor_row];
468 let mut del_start = self.cursor_col - 1;
470 while del_start > 0 && !line.is_char_boundary(del_start) {
471 del_start -= 1;
472 }
473 line.drain(del_start..self.cursor_col);
474 self.cursor_col = del_start;
475 } else if self.cursor_row > 0 && self.multiline {
476 let current_line = self.lines.remove(self.cursor_row);
478 self.cursor_row -= 1;
479 self.cursor_col = self.lines[self.cursor_row].len();
480 self.lines[self.cursor_row].push_str(¤t_line);
481 }
482 }
483
484 pub fn delete(&mut self) {
486 if self.has_selection() {
487 self.delete_selection();
488 return;
489 }
490
491 let line_len = self
492 .lines
493 .get(self.cursor_row)
494 .map(|l| l.len())
495 .unwrap_or(0);
496 if self.cursor_col < line_len {
497 let line = &mut self.lines[self.cursor_row];
498 let mut del_end = self.cursor_col + 1;
500 while del_end < line.len() && !line.is_char_boundary(del_end) {
501 del_end += 1;
502 }
503 line.drain(self.cursor_col..del_end);
504 } else if self.cursor_row + 1 < self.lines.len() && self.multiline {
505 let next_line = self.lines.remove(self.cursor_row + 1);
507 self.lines[self.cursor_row].push_str(&next_line);
508 }
509 }
510
511 pub fn delete_word_forward(&mut self) {
513 if self.has_selection() {
514 self.delete_selection();
515 return;
516 }
517
518 let line = &self.lines[self.cursor_row];
519 let word_end = find_word_end_bytes(line.as_bytes(), self.cursor_col);
520 if word_end > self.cursor_col {
521 let line = &mut self.lines[self.cursor_row];
522 line.drain(self.cursor_col..word_end);
523 } else if self.cursor_row + 1 < self.lines.len() && self.multiline {
524 let next_line = self.lines.remove(self.cursor_row + 1);
526 self.lines[self.cursor_row].push_str(&next_line);
527 }
528 }
529
530 pub fn delete_word_backward(&mut self) {
532 if self.has_selection() {
533 self.delete_selection();
534 return;
535 }
536
537 let line = &self.lines[self.cursor_row];
538 let word_start = find_word_start_bytes(line.as_bytes(), self.cursor_col);
539 if word_start < self.cursor_col {
540 let line = &mut self.lines[self.cursor_row];
541 line.drain(word_start..self.cursor_col);
542 self.cursor_col = word_start;
543 } else if self.cursor_row > 0 && self.multiline {
544 let current_line = self.lines.remove(self.cursor_row);
546 self.cursor_row -= 1;
547 self.cursor_col = self.lines[self.cursor_row].len();
548 self.lines[self.cursor_row].push_str(¤t_line);
549 }
550 }
551
552 pub fn delete_to_end(&mut self) {
554 if self.has_selection() {
555 self.delete_selection();
556 return;
557 }
558
559 if let Some(line) = self.lines.get_mut(self.cursor_row) {
560 line.truncate(self.cursor_col);
561 }
562 }
563
564 pub fn clear(&mut self) {
566 self.lines = vec![String::new()];
567 self.cursor_row = 0;
568 self.cursor_col = 0;
569 self.selection_anchor = None;
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_single_line_basic() {
579 let mut edit = TextEdit::single_line();
580 edit.insert_str("hello world");
581 assert_eq!(edit.value(), "hello world");
582 assert_eq!(edit.cursor_col, 11);
583 }
584
585 #[test]
586 fn test_single_line_ignores_newlines() {
587 let mut edit = TextEdit::single_line();
588 edit.insert_str("hello\nworld");
589 assert_eq!(edit.value(), "helloworld");
590 assert_eq!(edit.line_count(), 1);
591 }
592
593 #[test]
594 fn test_multiline_basic() {
595 let mut edit = TextEdit::new();
596 edit.insert_str("hello\nworld");
597 assert_eq!(edit.value(), "hello\nworld");
598 assert_eq!(edit.line_count(), 2);
599 assert_eq!(edit.cursor_row, 1);
600 assert_eq!(edit.cursor_col, 5);
601 }
602
603 #[test]
604 fn test_selection_single_line() {
605 let mut edit = TextEdit::single_line_with_text("hello world");
606 edit.cursor_col = 6; edit.move_right_selecting();
609 edit.move_right_selecting();
610 edit.move_right_selecting();
611 edit.move_right_selecting();
612 edit.move_right_selecting();
613
614 assert!(edit.has_selection());
615 assert_eq!(edit.selected_text(), Some("world".to_string()));
616 }
617
618 #[test]
619 fn test_selection_multiline() {
620 let mut edit = TextEdit::with_text("line1\nline2\nline3");
621 edit.cursor_row = 0;
622 edit.cursor_col = 3; edit.move_down_selecting();
626 edit.move_right_selecting();
627 edit.move_right_selecting();
628
629 assert!(edit.has_selection());
630 let selected = edit.selected_text().unwrap();
631 assert_eq!(selected, "e1\nline2");
632 }
633
634 #[test]
635 fn test_delete_selection() {
636 let mut edit = TextEdit::with_text("hello world");
637 edit.cursor_col = 0;
638
639 for _ in 0..6 {
641 edit.move_right_selecting();
642 }
643
644 let deleted = edit.delete_selection();
645 assert_eq!(deleted, Some("hello ".to_string()));
646 assert_eq!(edit.value(), "world");
647 assert_eq!(edit.cursor_col, 0);
648 }
649
650 #[test]
651 fn test_backspace_with_selection() {
652 let mut edit = TextEdit::with_text("hello world");
653 edit.select_all();
654 edit.backspace();
655 assert_eq!(edit.value(), "");
656 }
657
658 #[test]
659 fn test_insert_replaces_selection() {
660 let mut edit = TextEdit::with_text("hello world");
661 edit.select_all();
662 edit.insert_str("goodbye");
663 assert_eq!(edit.value(), "goodbye");
664 }
665
666 #[test]
667 fn test_word_navigation() {
668 let mut edit = TextEdit::single_line_with_text("one two three");
669 edit.cursor_col = 0;
670
671 edit.move_word_right();
672 assert_eq!(edit.cursor_col, 3); edit.move_word_right();
675 assert_eq!(edit.cursor_col, 7); edit.move_word_left();
678 assert_eq!(edit.cursor_col, 4); }
680}