1use std::any::Any;
6use std::collections::HashMap;
7use std::sync::OnceLock;
8use std::sync::{Arc, Mutex};
9
10use crossterm::event::{KeyCode, KeyModifiers};
11use unicode_width::UnicodeWidthStr;
12
13use super::component::{Component, Focusable};
14use super::keybindings::Keybindings;
15use crate::ansi::CURSOR_MARKER;
16use crate::kill_ring::{KillRing, PushOptions};
17use crate::theme::Color;
18use crate::undo_stack::UndoStack;
19
20#[derive(Debug, Clone)]
22pub struct EditorStyle {
23 pub prompt: String,
24 pub placeholder: String,
25}
26
27impl Default for EditorStyle {
28 fn default() -> Self {
29 Self {
30 prompt: "> ".to_string(),
31 placeholder: String::new(),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct EditorOptions {
39 pub padding_x: usize,
41 pub autocomplete_max_visible: usize,
43 pub placeholder: Option<String>,
45 pub initial_text: Option<String>,
47}
48
49#[derive(Debug, Clone)]
51struct EditorState {
52 lines: Vec<String>,
54 cursor_row: usize,
56 cursor_col: usize,
58 selection_anchor: Option<(usize, usize)>,
60 #[allow(dead_code)]
62 scroll_offset: usize,
63 focused: bool,
65 history: Vec<String>,
67 history_index: usize,
69}
70
71impl Default for EditorState {
72 fn default() -> Self {
73 Self {
74 lines: vec![String::new()],
75 cursor_row: 0,
76 cursor_col: 0,
77 selection_anchor: None,
78 scroll_offset: 0,
79 focused: false,
80 history: Vec::new(),
81 history_index: 0,
82 }
83 }
84}
85
86fn snap_boundary(s: &str, idx: usize) -> usize {
99 let idx = idx.min(s.len());
100 if s.is_char_boundary(idx) {
107 return idx;
108 }
109 s.char_indices()
110 .take_while(|(b, _)| *b < idx)
111 .last()
112 .map(|(b, _)| b)
113 .unwrap_or(0)
114}
115
116#[derive(Debug, Clone)]
121struct PasteEntry {
122 content: String,
123}
124
125fn paste_marker_regex() -> &'static regex::Regex {
127 static RE: OnceLock<regex::Regex> = OnceLock::new();
128 RE.get_or_init(|| regex::Regex::new(r"\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]").unwrap())
129}
130
131fn paste_marker_len_ending_at(line: &str, end: usize) -> usize {
134 for m in paste_marker_regex().find_iter(line) {
135 if m.end() == end {
136 return m.as_str().len();
137 }
138 }
139 0
140}
141
142const PASTE_FOLD_LINE_THRESHOLD: usize = 10;
144const PASTE_FOLD_CHAR_THRESHOLD: usize = 1000;
145
146fn expand_paste_markers(document: &str, pastes: &HashMap<u32, PasteEntry>) -> String {
150 if pastes.is_empty() || !document.contains("[paste #") {
151 return document.to_string();
152 }
153 paste_marker_regex()
154 .replace_all(document, |caps: ®ex::Captures| {
155 let id: u32 = caps[1].parse().unwrap_or(0);
156 match pastes.get(&id) {
157 Some(entry) => entry.content.clone(),
158 None => caps[0].to_string(),
159 }
160 })
161 .into_owned()
162}
163
164fn decode_csi_u_ctrl(text: &str) -> String {
168 if !text.contains("\x1b[") {
169 return text.to_string();
170 }
171 static RE: OnceLock<regex::Regex> = OnceLock::new();
172 let re = RE.get_or_init(|| regex::Regex::new(r"\x1b\[(\d+);5u").unwrap());
173 re.replace_all(text, |caps: ®ex::Captures| {
174 let code: u32 = caps[1].parse().unwrap_or(0);
175 let lower = 0x61..=0x7a;
176 let upper = 0x41..=0x5a;
177 if lower.contains(&code) {
178 char::from_u32(code - 96)
179 .map(String::from)
180 .unwrap_or_default()
181 } else if upper.contains(&code) {
182 char::from_u32(code - 64)
183 .map(String::from)
184 .unwrap_or_default()
185 } else {
186 caps[0].to_string()
187 }
188 })
189 .into_owned()
190}
191
192fn wrap_line_ranges(line: &str, max_width: usize) -> Vec<(usize, usize)> {
196 if line.is_empty() {
197 return vec![(0, 0)];
198 }
199 let max_width = max_width.max(1);
200 let mut ranges = Vec::new();
201 let mut start = 0;
202 let mut width = 0;
203 for (idx, ch) in line.char_indices() {
204 let char_width = UnicodeWidthStr::width(ch.to_string().as_str());
205 if idx > start && width + char_width > max_width {
206 ranges.push((start, idx));
207 start = idx;
208 width = 0;
209 }
210 width += char_width;
211 }
212 ranges.push((start, line.len()));
213 ranges
214}
215
216fn insert_cursor_marker(text: &str, column: usize) -> String {
219 let mut out = String::with_capacity(text.len() + CURSOR_MARKER.len());
220 let mut visible = 0;
221 let mut chars = text.char_indices().peekable();
222 let mut inserted = false;
223 while let Some((idx, ch)) = chars.next() {
224 if ch == '\x1b' {
225 out.push(ch);
226 while let Some((_, esc)) = chars.next() {
227 out.push(esc);
228 if esc == '\x07' || ('@'..='~').contains(&esc) {
229 break;
230 }
231 }
232 continue;
233 }
234 if !inserted && visible >= column {
235 out.push_str(CURSOR_MARKER);
236 inserted = true;
237 }
238 out.push(ch);
239 visible += UnicodeWidthStr::width(ch.to_string().as_str());
240 let _ = idx;
241 }
242 if !inserted {
243 out.push_str(CURSOR_MARKER);
244 }
245 out
246}
247
248pub struct Editor {
250 state: Mutex<EditorState>,
251 options: EditorOptions,
252 #[allow(dead_code)]
253 style: EditorStyle,
254 #[allow(dead_code)]
255 keybindings: Arc<Keybindings>,
256 on_submit: Mutex<Option<Arc<dyn Fn(&str) + Send + Sync>>>,
257 on_change: Mutex<Option<Arc<dyn Fn(&str) + Send + Sync>>>,
258 undo_stack: Mutex<UndoStack<EditorSnapshot>>,
261 redo_stack: Mutex<Vec<EditorSnapshot>>,
263 kill_ring: Mutex<KillRing>,
265 last_action: Mutex<Option<&'static str>>,
267 jump_mode: Mutex<Option<i32>>,
271 border_color: Mutex<Option<Color>>,
275 working: Mutex<Option<WorkingState>>,
279 pastes: Mutex<HashMap<u32, PasteEntry>>,
282 paste_counter: Mutex<u32>,
284}
285
286#[derive(Debug, Clone)]
288pub struct WorkingState {
289 pub frame: usize,
291 pub started_at: std::time::Instant,
293 pub message: String,
295}
296
297#[derive(Clone)]
301struct EditorSnapshot {
302 lines: Vec<String>,
303 cursor_row: usize,
304 cursor_col: usize,
305 pastes: HashMap<u32, PasteEntry>,
306 paste_counter: u32,
307}
308
309impl Editor {
310 pub fn new(options: EditorOptions, style: EditorStyle, keybindings: Arc<Keybindings>) -> Self {
312 let mut state = EditorState::default();
313 if let Some(text) = &options.initial_text {
314 state.lines = text.split('\n').map(|s| s.to_string()).collect();
315 if state.lines.is_empty() {
316 state.lines.push(String::new());
317 }
318 }
319
320 Self {
321 state: Mutex::new(state),
322 options,
323 style,
324 keybindings,
325 on_submit: Mutex::new(None),
326 on_change: Mutex::new(None),
327 undo_stack: Mutex::new(UndoStack::with_max_size(200)),
328 redo_stack: Mutex::new(Vec::new()),
329 kill_ring: Mutex::new(KillRing::new()),
330 last_action: Mutex::new(None),
331 jump_mode: Mutex::new(None),
332 border_color: Mutex::new(None),
333 working: Mutex::new(None),
334 pastes: Mutex::new(HashMap::new()),
335 paste_counter: Mutex::new(0),
336 }
337 }
338
339 pub fn simple() -> Self {
341 Self::new(
342 EditorOptions::default(),
343 EditorStyle::default(),
344 Arc::new(Keybindings::new()),
345 )
346 }
347
348 pub fn set_text(&self, text: &str) {
350 if let Ok(mut state) = self.state.lock() {
351 state.lines = if text.is_empty() {
352 vec![String::new()]
353 } else {
354 text.split('\n').map(|s| s.to_string()).collect()
355 };
356 if state.lines.is_empty() {
357 state.lines.push(String::new());
358 }
359 state.cursor_row = 0;
360 state.cursor_col = 0;
361 }
362 self.reset_pastes();
365 self.notify_change();
367 }
368
369 pub fn get_text(&self) -> String {
375 self.state
376 .lock()
377 .map(|s| s.lines.join("\n"))
378 .unwrap_or_default()
379 }
380
381 pub fn get_expanded_text(&self) -> String {
386 let document = self.get_text();
387 let pastes = self.pastes.lock().map(|p| p.clone()).unwrap_or_default();
388 expand_paste_markers(&document, &pastes)
389 }
390
391 fn reset_pastes(&self) {
393 if let Ok(mut pastes) = self.pastes.lock() {
394 pastes.clear();
395 }
396 if let Ok(mut counter) = self.paste_counter.lock() {
397 *counter = 0;
398 }
399 }
400
401 fn paste_id_at_marker_start(&self, row: usize, start: usize) -> Option<u32> {
404 let state = self.state.lock().ok()?;
405 let line = state.lines.get(row)?;
406 paste_marker_regex()
407 .captures_iter(line)
408 .find(|caps| caps.get(0).unwrap().start() == start)
409 .and_then(|caps| caps[1].parse::<u32>().ok())
410 }
411
412 fn remove_paste(&self, id: u32) {
416 {
417 let mut pastes = self.pastes.lock().unwrap();
418 if pastes.remove(&id).is_none() {
419 return;
420 }
421 let higher: Vec<u32> = pastes.keys().copied().filter(|k| *k > id).collect();
422 for old in higher {
423 if let Some(entry) = pastes.remove(&old) {
424 pastes.insert(old - 1, entry);
425 }
426 }
427 }
428 {
429 let mut counter = self.paste_counter.lock().unwrap();
430 *counter = counter.saturating_sub(1);
431 }
432 if let Ok(mut state) = self.state.lock() {
435 for line in state.lines.iter_mut() {
436 *line = paste_marker_regex()
437 .replace_all(line, |caps: ®ex::Captures| {
438 let n: u32 = caps[1].parse().unwrap_or(0);
439 if n <= id {
440 caps[0].to_string()
441 } else {
442 let suffix = caps.get(2).map(|m| m.as_str()).unwrap_or("");
444 format!("[paste #{}{suffix}]", n - 1)
445 }
446 })
447 .into_owned();
448 }
449 }
450 }
451
452 pub fn handle_paste(&self, text: &str) {
460 let decoded = decode_csi_u_ctrl(text)
463 .replace("\r\n", "\n")
464 .replace('\r', "\n")
465 .replace('\t', " ");
466 let filtered: String = decoded
469 .chars()
470 .filter(|c| *c == '\n' || (*c as u32) >= 32)
471 .collect();
472 if filtered.is_empty() {
473 return;
474 }
475
476 let line_count = filtered.split('\n').count();
477 let char_count = filtered.chars().count();
478 if line_count > PASTE_FOLD_LINE_THRESHOLD || char_count > PASTE_FOLD_CHAR_THRESHOLD {
479 self.push_undo("insert");
480 if self.has_selection() {
481 self.delete_selection_no_undo();
482 }
483 let (id, by_lines, count) = {
484 let mut counter = self.paste_counter.lock().unwrap();
485 *counter += 1;
486 let id = *counter;
487 if line_count > PASTE_FOLD_LINE_THRESHOLD {
488 (id, true, line_count)
489 } else {
490 (id, false, char_count)
491 }
492 };
493 if let Ok(mut pastes) = self.pastes.lock() {
494 pastes.insert(id, PasteEntry { content: filtered });
495 }
496 let marker = if by_lines {
497 format!("[paste #{id} +{count} lines]")
498 } else {
499 format!("[paste #{id} {count} chars]")
500 };
501 self.insert_no_undo(&marker);
502 *self.last_action.lock().unwrap() = Some("insert");
503 } else {
504 self.insert(&filtered);
505 }
506 }
507
508 pub fn clear(&self) {
510 if let Ok(mut state) = self.state.lock() {
511 state.lines = vec![String::new()];
512 state.cursor_row = 0;
513 state.cursor_col = 0;
514 state.selection_anchor = None;
515 }
516 self.reset_pastes();
517 self.notify_change();
522 }
523
524 pub fn on_submit(&self, callback: Arc<dyn Fn(&str) + Send + Sync>) {
526 if let Ok(mut cb) = self.on_submit.lock() {
527 *cb = Some(callback);
528 }
529 }
530
531 pub fn on_change(&self, callback: Arc<dyn Fn(&str) + Send + Sync>) {
533 if let Ok(mut cb) = self.on_change.lock() {
534 *cb = Some(callback);
535 }
536 }
537
538 pub fn cursor_position(&self) -> (usize, usize) {
540 self.state
541 .lock()
542 .map(|s| (s.cursor_row, s.cursor_col))
543 .unwrap_or((0, 0))
544 }
545
546 pub fn set_cursor(&self, row: usize, col: usize) {
555 if let Ok(mut state) = self.state.lock() {
556 let row = row.min(state.lines.len().saturating_sub(1));
557 let col = snap_boundary(&state.lines[row], col);
558 state.cursor_row = row;
559 state.cursor_col = col;
560 state.selection_anchor = None;
561 }
562 }
563
564 fn snapshot_state(&self) -> EditorSnapshot {
567 let state = self.state.lock().unwrap();
568 EditorSnapshot {
569 lines: state.lines.clone(),
570 cursor_row: state.cursor_row,
571 cursor_col: state.cursor_col,
572 pastes: self.pastes.lock().map(|p| p.clone()).unwrap_or_default(),
573 paste_counter: self.paste_counter.lock().map(|c| *c).unwrap_or(0),
574 }
575 }
576
577 fn restore_state(&self, snap: &EditorSnapshot) {
579 if let Ok(mut state) = self.state.lock() {
580 state.lines = snap.lines.clone();
581 state.cursor_row = snap.cursor_row;
582 state.cursor_col = snap.cursor_col;
583 state.selection_anchor = None;
585 }
586 if let Ok(mut pastes) = self.pastes.lock() {
587 *pastes = snap.pastes.clone();
588 }
589 if let Ok(mut counter) = self.paste_counter.lock() {
590 *counter = snap.paste_counter;
591 }
592 self.notify_change();
593 }
594
595 fn push_undo(&self, action: &'static str) {
601 let coalesce = action == "insert" && *self.last_action.lock().unwrap() == Some("insert");
602 if !coalesce {
603 let snap = self.snapshot_state();
604 if let Ok(mut stack) = self.undo_stack.lock() {
605 stack.push(snap);
606 }
607 if let Ok(mut redo) = self.redo_stack.lock() {
608 redo.clear();
609 }
610 }
611 *self.last_action.lock().unwrap() = Some(action);
612 }
613
614 pub fn undo(&self) {
617 let popped = self.undo_stack.lock().unwrap().pop();
618 let Some(snap) = popped else { return };
619 self.redo_stack.lock().unwrap().push(self.snapshot_state());
621 self.restore_state(&snap);
622 *self.last_action.lock().unwrap() = None;
623 }
624
625 pub fn redo(&self) {
627 let popped = self.redo_stack.lock().unwrap().pop();
628 let Some(snap) = popped else { return };
629 self.undo_stack.lock().unwrap().push(self.snapshot_state());
630 self.restore_state(&snap);
631 *self.last_action.lock().unwrap() = None;
632 }
633
634 fn kill(&self, text: String, prepend: bool) {
637 if text.is_empty() {
638 return;
639 }
640 let accumulate = *self.last_action.lock().unwrap() == Some("kill");
641 if let Ok(mut ring) = self.kill_ring.lock() {
642 ring.push(
643 &text,
644 PushOptions {
645 prepend,
646 accumulate,
647 },
648 );
649 }
650 *self.last_action.lock().unwrap() = Some("kill");
651 }
652
653 pub fn yank(&self) {
656 let text = self.kill_ring.lock().unwrap().peek().map(str::to_string);
657 let Some(text) = text else { return };
658 self.push_undo("yank");
659 if self.has_selection() {
660 self.delete_selection_no_undo();
661 }
662 self.insert_no_undo(&text);
663 *self.last_action.lock().unwrap() = Some("yank");
664 }
665
666 pub fn yank_pop(&self) {
668 if self.kill_ring.lock().unwrap().len() <= 1 {
669 return;
670 }
671 self.kill_ring.lock().unwrap().rotate();
672 self.yank();
673 }
674
675 pub fn has_selection(&self) -> bool {
681 self.state.lock().unwrap().selection_anchor.is_some()
682 }
683
684 fn selection_ordered(&self) -> Option<(usize, usize, usize, usize)> {
687 let state = self.state.lock().unwrap();
688 let (ar, ac) = state.selection_anchor?;
689 let (br, bc) = (state.cursor_row, state.cursor_col);
690 Some(if (ar, ac) <= (br, bc) {
691 (ar, ac, br, bc)
692 } else {
693 (br, bc, ar, ac)
694 })
695 }
696
697 pub fn selected_text(&self) -> Option<String> {
699 let (sr, sc, er, ec) = self.selection_ordered()?;
700 let lines = self.state.lock().unwrap().lines.clone();
701 if sr == er {
702 Some(lines[sr][sc..ec].to_string())
703 } else {
704 let mut out = String::new();
705 out.push_str(&lines[sr][sc..]);
706 for r in sr + 1..er {
707 out.push('\n');
708 out.push_str(&lines[r]);
709 }
710 out.push('\n');
711 out.push_str(&lines[er][..ec]);
712 Some(out)
713 }
714 }
715
716 fn selection_span_on_row(&self, state: &EditorState, row: usize) -> Option<(usize, usize)> {
719 let (ar, ac) = state.selection_anchor?;
720 let (br, bc) = (state.cursor_row, state.cursor_col);
721 let (sr, sc, er, ec) = if (ar, ac) <= (br, bc) {
722 (ar, ac, br, bc)
723 } else {
724 (br, bc, ar, ac)
725 };
726 if row < sr || row > er {
727 return None;
728 }
729 if sr == er {
730 (sc != ec).then_some((sc, ec))
731 } else if row == sr {
732 (sc < state.lines[row].len()).then_some((sc, state.lines[row].len()))
733 } else if row == er {
734 (ec > 0).then_some((0, ec))
735 } else {
736 (!state.lines[row].is_empty()).then_some((0, state.lines[row].len()))
737 }
738 }
739
740 fn begin_selection(&self) {
742 let mut state = self.state.lock().unwrap();
743 if state.selection_anchor.is_none() {
744 state.selection_anchor = Some((state.cursor_row, state.cursor_col));
745 }
746 }
747
748 fn clear_selection(&self) {
750 self.state.lock().unwrap().selection_anchor = None;
751 }
752
753 fn delete_selection(&self) {
756 if !self.has_selection() {
757 return;
758 }
759 self.push_undo("edit");
760 self.delete_selection_no_undo();
761 }
762
763 fn delete_selection_no_undo(&self) {
766 let (sr, sc, er, ec) = match self.selection_ordered() {
767 Some(s) => s,
768 None => return,
769 };
770 let mut state = self.state.lock().unwrap();
771 if sr == er {
772 state.lines[sr].replace_range(sc..ec, "");
773 state.cursor_row = sr;
774 state.cursor_col = sc;
775 } else {
776 let mut joined = state.lines[sr][..sc].to_string();
777 joined.push_str(&state.lines[er][ec..]);
778 state.lines.drain(sr + 1..=er);
779 state.lines[sr] = joined;
780 state.cursor_row = sr;
781 state.cursor_col = sc;
782 }
783 state.selection_anchor = None;
784 drop(state);
785 self.notify_change();
786 }
787
788 pub fn copy_selection(&self) -> bool {
790 let Some(text) = self.selected_text() else {
791 return false;
792 };
793 if text.is_empty() {
794 return false;
795 }
796 if let Ok(mut ring) = self.kill_ring.lock() {
797 ring.push(&text, PushOptions::default());
798 }
799 true
800 }
801
802 pub fn cut_selection(&self) -> bool {
805 if !self.copy_selection() {
806 return false;
807 }
808 self.delete_selection();
809 true
810 }
811
812 pub fn insert(&self, text: &str) {
815 self.push_undo("insert");
816 if self.has_selection() {
817 self.delete_selection_no_undo();
818 }
819 self.insert_no_undo(text);
820 }
821
822 fn insert_no_undo(&self, text: &str) {
831 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
832 if let Ok(mut state) = self.state.lock() {
833 for ch in normalized.chars() {
834 if ch == '\n' {
835 let row = state.cursor_row;
837 let col = state.cursor_col;
838 let current_line = &mut state.lines[row];
839 let after = current_line.split_off(col);
840 state.lines.insert(row + 1, after);
841 state.cursor_row += 1;
842 state.cursor_col = 0;
843 } else {
844 let row = state.cursor_row;
845 let col = state.cursor_col;
846 state.lines[row].insert(col, ch);
847 state.cursor_col += ch.len_utf8();
853 }
854 }
855 }
856 self.notify_change();
857 }
858
859 fn backspace(&self) {
862 if self.has_selection() {
863 self.delete_selection();
864 return;
865 }
866 let deleted_marker = {
870 let state = self.state.lock().unwrap();
871 let row = state.cursor_row;
872 (state.cursor_col > 0)
873 .then(|| paste_marker_len_ending_at(&state.lines[row], state.cursor_col))
874 .filter(|len| *len > 0)
875 .map(|len| (row, state.cursor_col - len, len))
876 };
877 if let Some((row, start, len)) = deleted_marker {
878 self.push_undo("edit");
882 let id = self.paste_id_at_marker_start(row, start);
883 if let Ok(mut state) = self.state.lock() {
887 state.lines[row].replace_range(start..start + len, "");
888 state.cursor_col = start;
889 }
890 if let Some(id) = id {
891 self.remove_paste(id);
892 }
893 self.notify_change();
894 return;
895 }
896 self.push_undo("edit");
897 if let Ok(mut state) = self.state.lock() {
898 if state.cursor_col > 0 {
899 let row = state.cursor_row;
900 let prev_len = state.lines[row][..state.cursor_col]
906 .chars()
907 .last()
908 .map(|c| c.len_utf8())
909 .unwrap_or(0);
910 let col = state.cursor_col - prev_len;
911 state.lines[row].remove(col);
912 state.cursor_col = col;
913 } else if state.cursor_row > 0 {
914 let row = state.cursor_row;
916 let current = state.lines.remove(row);
917 state.cursor_row -= 1;
918 let prev_row = state.cursor_row;
919 state.cursor_col = state.lines[prev_row].len();
920 state.lines[prev_row].push_str(¤t);
921 }
922 }
923 self.notify_change();
924 }
925
926 fn delete(&self) {
929 if self.has_selection() {
930 self.delete_selection();
931 return;
932 }
933 self.push_undo("edit");
934 if let Ok(mut state) = self.state.lock() {
935 let row = state.cursor_row;
936 let line_len = state.lines[row].len();
937 if state.cursor_col < line_len {
938 let col = state.cursor_col;
939 state.lines[row].remove(col);
940 } else if row < state.lines.len() - 1 {
941 let next = state.lines.remove(row + 1);
943 state.lines[row].push_str(&next);
944 }
945 }
946 self.notify_change();
947 }
948
949 fn cursor_left(&self) {
951 if let Ok(mut state) = self.state.lock() {
952 if state.cursor_col > 0 {
953 let row = state.cursor_row;
954 let line = &state.lines[row];
955 let marker = paste_marker_len_ending_at(line, state.cursor_col);
957 if marker > 0 {
958 state.cursor_col -= marker;
959 return;
960 }
961 let prev_len = line[..state.cursor_col]
964 .chars()
965 .last()
966 .map(|c| c.len_utf8())
967 .unwrap_or(0);
968 state.cursor_col -= prev_len;
969 } else if state.cursor_row > 0 {
970 state.cursor_row -= 1;
971 state.cursor_col = state.lines[state.cursor_row].len();
972 }
973 }
974 }
975
976 fn cursor_right(&self) {
978 if let Ok(mut state) = self.state.lock() {
979 let row = state.cursor_row;
980 let line = &state.lines[row];
981 if state.cursor_col < line.len() {
982 for m in paste_marker_regex().find_iter(line) {
984 if m.start() == state.cursor_col {
985 state.cursor_col = m.end();
986 return;
987 }
988 }
989 let ch_len = line[state.cursor_col..]
991 .chars()
992 .next()
993 .map(|c| c.len_utf8())
994 .unwrap_or(0);
995 state.cursor_col += ch_len;
996 } else if row < state.lines.len() - 1 {
997 state.cursor_row += 1;
998 state.cursor_col = 0;
999 }
1000 }
1001 }
1002
1003 fn cursor_up(&self) {
1005 if let Ok(mut state) = self.state.lock() {
1006 if state.cursor_row > 0 {
1007 state.cursor_row -= 1;
1008 let new_row = state.cursor_row;
1009 let max = state.lines[new_row].len();
1010 state.cursor_col = snap_boundary(&state.lines[new_row], state.cursor_col.min(max));
1013 }
1014 }
1015 }
1016
1017 fn cursor_down(&self) {
1019 if let Ok(mut state) = self.state.lock() {
1020 if state.cursor_row < state.lines.len() - 1 {
1021 state.cursor_row += 1;
1022 let new_row = state.cursor_row;
1023 let max = state.lines[new_row].len();
1024 state.cursor_col = snap_boundary(&state.lines[new_row], state.cursor_col.min(max));
1025 }
1026 }
1027 }
1028
1029 fn cursor_home(&self) {
1031 if let Ok(mut state) = self.state.lock() {
1032 state.cursor_col = 0;
1033 }
1034 }
1035
1036 fn cursor_end(&self) {
1038 if let Ok(mut state) = self.state.lock() {
1039 let row = state.cursor_row;
1040 state.cursor_col = state.lines[row].len();
1041 }
1042 }
1043
1044 fn kill_word_backward(&self) {
1048 self.push_undo("kill");
1049 let killed = if let Ok(mut state) = self.state.lock() {
1050 let row = state.cursor_row;
1051 let col = state.cursor_col;
1052 let start = crate::word_navigation::find_word_backward(&state.lines[row], col);
1053 let dead: String = state.lines[row].drain(start..col).collect();
1054 state.cursor_col = start;
1055 dead
1056 } else {
1057 String::new()
1058 };
1059 self.kill(killed, true);
1060 self.notify_change();
1061 }
1062
1063 fn kill_word_forward(&self) {
1065 self.push_undo("kill");
1066 let killed = if let Ok(mut state) = self.state.lock() {
1067 let row = state.cursor_row;
1068 let col = state.cursor_col;
1069 let end = crate::word_navigation::find_word_end(&state.lines[row], col);
1072 let dead: String = state.lines[row].drain(col..end).collect();
1073 dead
1074 } else {
1075 String::new()
1076 };
1077 self.kill(killed, false);
1078 self.notify_change();
1079 }
1080
1081 fn cursor_word_left(&self) {
1083 if let Ok(mut state) = self.state.lock() {
1084 let row = state.cursor_row;
1085 state.cursor_col =
1086 crate::word_navigation::find_word_backward(&state.lines[row], state.cursor_col);
1087 }
1088 }
1089
1090 fn cursor_word_right(&self) {
1092 if let Ok(mut state) = self.state.lock() {
1093 let row = state.cursor_row;
1094 state.cursor_col =
1095 crate::word_navigation::find_word_forward(&state.lines[row], state.cursor_col);
1096 }
1097 }
1098
1099 fn page_scroll(&self, dir: i32) {
1104 const PAGE: usize = 10;
1105 if let Ok(mut state) = self.state.lock() {
1106 let target = if dir > 0 {
1107 (state.cursor_row + PAGE).min(state.lines.len().saturating_sub(1))
1108 } else {
1109 state.cursor_row.saturating_sub(PAGE)
1110 };
1111 state.cursor_row = target;
1112 let max = state.lines[target].len();
1113 state.cursor_col = snap_boundary(&state.lines[target], state.cursor_col.min(max));
1114 }
1115 }
1116
1117 fn jump_to_char(&self, target: char, dir: i32) {
1122 let forward = dir > 0;
1123 let mut state = self.state.lock().unwrap();
1124 let start_row = state.cursor_row;
1125 let mut row = start_row;
1126 let step: isize = if forward { 1 } else { -1 };
1127 loop {
1128 let line = &state.lines[row];
1129 let search_from = if row == start_row {
1130 if forward {
1131 (state.cursor_col + target.len_utf8()).min(line.len())
1132 } else {
1133 state.cursor_col.saturating_sub(target.len_utf8())
1134 }
1135 } else if forward {
1136 0
1137 } else {
1138 line.len()
1139 };
1140 let found = if forward {
1141 line[search_from..].find(target).map(|i| search_from + i)
1142 } else {
1143 line[..search_from].rfind(target)
1144 };
1145 if let Some(idx) = found {
1146 state.cursor_row = row;
1147 state.cursor_col = idx;
1148 return;
1149 }
1150 let next = row as isize + step;
1151 if next < 0 || next >= state.lines.len() as isize {
1152 return;
1153 }
1154 row = next as usize;
1155 }
1156 }
1157
1158 pub fn set_border_color(&self, color: Option<Color>) {
1161 if let Ok(mut guard) = self.border_color.lock() {
1162 *guard = color;
1163 }
1164 }
1165
1166 pub fn border_color(&self) -> Option<Color> {
1168 self.border_color.lock().ok().and_then(|guard| *guard)
1169 }
1170
1171 pub fn set_working(&self, working: Option<WorkingState>) {
1174 if let Ok(mut guard) = self.working.lock() {
1175 *guard = working;
1176 }
1177 }
1178
1179 pub fn working(&self) -> Option<WorkingState> {
1181 self.working.lock().ok().and_then(|guard| guard.clone())
1182 }
1183
1184 fn submit(&self) {
1186 let text = self.get_expanded_text();
1189 if let Ok(mut state) = self.state.lock() {
1190 if !text.is_empty() {
1191 state.history.push(text.clone());
1192 state.history_index = state.history.len();
1193 }
1194 }
1195 self.undo_stack.lock().unwrap().clear();
1197 self.redo_stack.lock().unwrap().clear();
1198 *self.last_action.lock().unwrap() = None;
1199 if let Ok(cb) = self.on_submit.lock() {
1200 if let Some(callback) = cb.as_ref() {
1201 callback(&text);
1202 }
1203 }
1204 self.clear();
1205 }
1206
1207 fn notify_change(&self) {
1209 if let Ok(cb) = self.on_change.lock() {
1210 if let Some(callback) = cb.as_ref() {
1211 let text = self.get_text();
1212 callback(&text);
1213 }
1214 }
1215 }
1216
1217 pub fn handle_key(&self, key: crossterm::event::KeyEvent) -> bool {
1219 match (key.modifiers, key.code) {
1220 (KeyModifiers::NONE, KeyCode::Left) => {
1223 self.clear_selection();
1224 self.cursor_left();
1225 }
1226 (KeyModifiers::NONE, KeyCode::Right) => {
1227 self.clear_selection();
1228 self.cursor_right();
1229 }
1230 (KeyModifiers::NONE, KeyCode::Up) => {
1231 self.clear_selection();
1232 self.cursor_up();
1233 }
1234 (KeyModifiers::NONE, KeyCode::Down) => {
1235 self.clear_selection();
1236 self.cursor_down();
1237 }
1238 (KeyModifiers::NONE, KeyCode::Home) => {
1239 self.clear_selection();
1240 self.cursor_home();
1241 }
1242 (KeyModifiers::NONE, KeyCode::End) => {
1243 self.clear_selection();
1244 self.cursor_end();
1245 }
1246 (KeyModifiers::SHIFT, KeyCode::Left) => {
1247 self.begin_selection();
1248 self.cursor_left();
1249 }
1250 (KeyModifiers::SHIFT, KeyCode::Right) => {
1251 self.begin_selection();
1252 self.cursor_right();
1253 }
1254 (KeyModifiers::SHIFT, KeyCode::Up) => {
1255 self.begin_selection();
1256 self.cursor_up();
1257 }
1258 (KeyModifiers::SHIFT, KeyCode::Down) => {
1259 self.begin_selection();
1260 self.cursor_down();
1261 }
1262 (KeyModifiers::SHIFT, KeyCode::Home) => {
1263 self.begin_selection();
1264 self.cursor_home();
1265 }
1266 (KeyModifiers::SHIFT, KeyCode::End) => {
1267 self.begin_selection();
1268 self.cursor_end();
1269 }
1270
1271 (KeyModifiers::NONE, KeyCode::Backspace) => self.backspace(),
1273 (KeyModifiers::NONE, KeyCode::Delete) => self.delete(),
1274 (KeyModifiers::NONE, KeyCode::Enter)
1275 | (KeyModifiers::NONE, KeyCode::Char('\n'))
1276 | (KeyModifiers::NONE, KeyCode::Char('\r')) => {
1277 self.submit();
1283 }
1284 (KeyModifiers::SHIFT, KeyCode::Enter)
1286 | (KeyModifiers::SHIFT, KeyCode::Char('\r'))
1287 | (KeyModifiers::SHIFT, KeyCode::Char('\n')) => self.insert("\n"),
1288
1289 (KeyModifiers::CONTROL, KeyCode::Char('a')) => self.cursor_home(),
1291 (KeyModifiers::CONTROL, KeyCode::Char('e')) => self.cursor_end(),
1292 (KeyModifiers::CONTROL, KeyCode::Char('k')) => {
1293 self.push_undo("kill");
1295 let killed = if let Ok(mut state) = self.state.lock() {
1296 let row = state.cursor_row;
1297 let col = state.cursor_col;
1298 let dead: String = state.lines[row].drain(col..).collect();
1299 state.lines[row].truncate(col);
1300 dead
1301 } else {
1302 String::new()
1303 };
1304 self.kill(killed, false);
1305 self.notify_change();
1306 }
1307 (KeyModifiers::CONTROL, KeyCode::Char('u')) => {
1308 self.push_undo("kill");
1311 let killed = if let Ok(mut state) = self.state.lock() {
1312 let row = state.cursor_row;
1313 let col = state.cursor_col;
1314 let before: String = state.lines[row].drain(..col).collect();
1315 state.cursor_col = 0;
1316 before
1317 } else {
1318 String::new()
1319 };
1320 self.kill(killed, true);
1321 self.notify_change();
1322 }
1323 (KeyModifiers::CONTROL, KeyCode::Char('-')) => self.undo(),
1326 (KeyModifiers::CONTROL, KeyCode::Char('r')) => self.redo(),
1327 (KeyModifiers::CONTROL, KeyCode::Char('x')) => {
1329 self.cut_selection();
1330 }
1331 (KeyModifiers::CONTROL, KeyCode::Char('c')) => {
1332 self.copy_selection();
1333 }
1334 (KeyModifiers::CONTROL, KeyCode::Char('v')) => {
1335 self.yank();
1336 }
1337 (KeyModifiers::CONTROL, KeyCode::Char('y')) => self.yank(),
1339 (KeyModifiers::ALT, KeyCode::Char('y')) => self.yank_pop(),
1340 (KeyModifiers::CONTROL, KeyCode::Char('w')) => self.kill_word_backward(),
1342 (KeyModifiers::ALT, KeyCode::Backspace) => self.kill_word_backward(),
1343 (KeyModifiers::ALT, KeyCode::Char('d')) | (KeyModifiers::ALT, KeyCode::Delete) => {
1344 self.kill_word_forward();
1345 }
1346 (KeyModifiers::ALT, KeyCode::Left) => self.cursor_word_left(),
1348 (KeyModifiers::ALT, KeyCode::Right) => self.cursor_word_right(),
1349 (KeyModifiers::CONTROL, KeyCode::Char('j')) => self.insert_no_undo(
1352 "
1353",
1354 ),
1355 (KeyModifiers::CONTROL, KeyCode::Char('d')) => self.delete(),
1356 (KeyModifiers::CONTROL, KeyCode::PageUp) => self.page_scroll(-1),
1359 (KeyModifiers::CONTROL, KeyCode::PageDown) => self.page_scroll(1),
1360 (_, KeyCode::Char(']')) if key.modifiers.contains(KeyModifiers::CONTROL) => {
1365 if key.modifiers.contains(KeyModifiers::ALT) {
1368 *self.jump_mode.lock().unwrap() = Some(-1);
1369 } else {
1370 *self.jump_mode.lock().unwrap() = Some(1);
1371 }
1372 }
1373
1374 (KeyModifiers::NONE | KeyModifiers::SHIFT, KeyCode::Char(c)) => {
1377 let jump = self.jump_mode.lock().unwrap().take();
1378 match jump {
1379 Some(dir) => self.jump_to_char(c, dir),
1380 None => self.insert(&c.to_string()),
1381 }
1382 }
1383
1384 _ => return false,
1385 }
1386 true
1387 }
1388}
1389
1390impl Component for Editor {
1391 fn render(&self, width: usize) -> Vec<String> {
1392 let state = self.state.lock().map(|s| s.clone()).unwrap_or_default();
1393 let mut content_lines = Vec::new();
1394
1395 let pad = " ".repeat(self.options.padding_x);
1396 let content_width = width.saturating_sub(self.options.padding_x).max(1);
1401 let active_width = content_width.saturating_sub(1).max(1);
1404 for (row_idx, line) in state.lines.iter().enumerate() {
1405 let ranges = wrap_line_ranges(
1406 line,
1407 if row_idx == state.cursor_row {
1408 active_width
1409 } else {
1410 content_width
1411 },
1412 );
1413 for (chunk_start, chunk_end) in ranges {
1414 let chunk = &line[chunk_start..chunk_end];
1415 let content =
1418 if chunk.is_empty() && row_idx == state.cursor_row && state.lines.len() == 1 {
1419 if let Some(placeholder) = &self.options.placeholder {
1420 format!("\x1b[2m{}\x1b[22m", placeholder)
1421 } else {
1422 String::new()
1423 }
1424 } else {
1425 chunk.to_string()
1426 };
1427
1428 let mut content = content;
1432 if let Some((sc, ec)) = self.selection_span_on_row(&state, row_idx) {
1433 let local_start = sc.max(chunk_start).min(chunk_end);
1434 let local_end = ec.max(chunk_start).min(chunk_end);
1435 if local_start < local_end {
1436 let before = &line[chunk_start..local_start];
1437 let sel = &line[local_start..local_end];
1438 let after = &line[local_end..chunk_end];
1439 content = format!("{before}\x1b[7m{sel}\x1b[27m{after}");
1440 }
1441 }
1442
1443 let visible_w = crate::ansi::visible_width(&content);
1444 let right_pad = " ".repeat(
1448 width
1449 .saturating_sub(self.options.padding_x)
1450 .saturating_sub(visible_w),
1451 );
1452 let right_pad_cursor = if state.focused && row_idx == state.cursor_row {
1453 right_pad
1454 .get(..right_pad.len().saturating_sub(1))
1455 .unwrap_or("")
1456 .to_string()
1457 } else {
1458 right_pad
1459 };
1460
1461 let mut rendered = format!("{pad}{content}{right_pad_cursor}");
1462
1463 if state.focused
1471 && row_idx == state.cursor_row
1472 && state.cursor_col >= chunk_start
1473 && state.cursor_col <= chunk_end
1474 {
1475 let cursor_col = UnicodeWidthStr::width(
1476 line[chunk_start..state.cursor_col.min(chunk_end)]
1477 .to_string()
1478 .as_str(),
1479 );
1480 rendered = insert_cursor_marker(&rendered, self.options.padding_x + cursor_col);
1481 }
1482
1483 content_lines.push(rendered);
1484 }
1485 }
1486
1487 if content_lines.is_empty() {
1490 content_lines.push(format!(
1491 "{pad}{}",
1492 " ".repeat(width.saturating_sub(self.options.padding_x))
1493 ));
1494 }
1495
1496 let border_color = self
1501 .border_color
1502 .lock()
1503 .ok()
1504 .and_then(|guard| *guard)
1505 .unwrap_or(crate::theme::theme().colors.border);
1506 let working_state = self.working.lock().ok().and_then(|mut guard| {
1514 guard.as_mut().map(|state| {
1515 let snapshot = state.clone();
1516 state.frame = state.frame.wrapping_add(1);
1517 snapshot
1518 })
1519 });
1520 let top_border = if let Some(ref working) = working_state {
1521 let frames = crate::loader::SPINNER_FRAMES;
1525 let spinner_char = frames[working.frame % frames.len()];
1526 let colors = crate::theme::theme().colors;
1527
1528 let elapsed = working.started_at.elapsed();
1529 let elapsed_str = if elapsed.as_secs() >= 60 {
1530 format!("{}m{}s", elapsed.as_secs() / 60, elapsed.as_secs() % 60)
1531 } else {
1532 format!("{}s", elapsed.as_secs())
1533 };
1534
1535 let plain = format!(" {} {} {} ", spinner_char, working.message, elapsed_str);
1536 let indicator_width = crate::ansi::visible_width(&plain);
1537
1538 if width >= indicator_width + 1 {
1539 let colored = format!(
1540 " {} {} {} ",
1541 colors.accent.fg(&spinner_char.to_string()),
1542 colors.muted.fg(&working.message),
1543 colors.muted.fg(&elapsed_str),
1544 );
1545 let right = border_color.fg(&"─".repeat(width - indicator_width - 1));
1546 format!("{}{}{}", border_color.fg("─"), colored, right)
1547 } else {
1548 border_color.fg(&"─".repeat(width))
1549 }
1550 } else {
1551 border_color.fg(&"─".repeat(width))
1552 };
1553
1554 let bottom_border = {
1555 let horizontal = "─".repeat(width);
1556 border_color.fg(&horizontal)
1557 };
1558
1559 let mut lines = Vec::with_capacity(content_lines.len() + 2);
1560 lines.push(top_border);
1561 lines.extend(content_lines);
1562 lines.push(bottom_border);
1563 lines
1564 }
1565
1566 fn invalidate(&self) {
1567 }
1569
1570 fn handle_input(&self, _data: &str) -> bool {
1571 false
1574 }
1575
1576 fn as_any(&self) -> &dyn Any {
1577 self
1578 }
1579}
1580
1581impl Focusable for Editor {
1582 fn set_focused(&self, focused: bool) {
1583 if let Ok(mut state) = self.state.lock() {
1584 state.focused = focused;
1585 }
1586 }
1587
1588 fn is_focused(&self) -> bool {
1589 self.state.lock().map(|s| s.focused).unwrap_or(false)
1590 }
1591}
1592
1593#[cfg(test)]
1594mod tests {
1595 use super::*;
1596
1597 #[test]
1598 fn working_spinner_advances_per_render_not_per_timer() {
1599 use crate::component::Component;
1600 let editor = Editor::simple();
1601 editor.set_working(Some(WorkingState {
1602 frame: 0,
1603 started_at: std::time::Instant::now(),
1604 message: "Working…".to_string(),
1605 }));
1606
1607 let first = crate::strip_ansi(&editor.render(60)[0]);
1609 assert!(first.contains("Working"), "top border: {first}");
1610
1611 let frame_after_first = editor.working().expect("working state").frame;
1614 assert_eq!(frame_after_first, 1, "render must advance the frame once");
1615 let _ = editor.render(60);
1616 assert_eq!(editor.working().expect("working state").frame, 2);
1617
1618 let a = crate::strip_ansi(&editor.render(60)[0]);
1620 let b = crate::strip_ansi(&editor.render(60)[0]);
1621 let spinner_of = |line: &str| line.chars().find(|c| "⠋⠘⠹⠸⠼⠴⠦⠧⠇⠏".contains(*c));
1622 assert_ne!(
1623 spinner_of(&a),
1624 spinner_of(&b),
1625 "consecutive renders must show different spinner frames: {a:?} vs {b:?}"
1626 );
1627
1628 editor.set_working(None);
1630 let idle = crate::strip_ansi(&editor.render(60)[0]);
1631 assert!(!idle.contains("Working"), "idle border: {idle}");
1632 }
1633
1634 #[test]
1635 fn working_spinner_uses_native_pi_frames_and_cadence() {
1636 assert_eq!(crate::loader::SPINNER_FRAMES.len(), 10);
1640 assert_eq!(
1641 crate::loader::SPINNER_FRAMES.iter().collect::<String>(),
1642 "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
1643 );
1644 assert_eq!(crate::loader::SPINNER_FRAME_MS, 80);
1646
1647 use crate::component::Component;
1649 let editor = Editor::simple();
1650 editor.set_working(Some(WorkingState {
1651 frame: 0,
1652 started_at: std::time::Instant::now(),
1653 message: "Working…".to_string(),
1654 }));
1655 let spinner_of = |line: &str| {
1656 let plain = crate::strip_ansi(line);
1657 crate::loader::SPINNER_FRAMES
1658 .iter()
1659 .copied()
1660 .find(|c| plain.contains(*c))
1661 };
1662 let mut seen = Vec::new();
1663 for _ in 0..10 {
1664 seen.push(spinner_of(&editor.render(60)[0]).expect("a spinner glyph"));
1665 }
1666 let expected: Vec<char> = crate::loader::SPINNER_FRAMES.to_vec();
1671 assert_eq!(seen, expected);
1672 assert_eq!(
1673 editor.working().expect("working").frame % crate::loader::SPINNER_FRAMES.len(),
1674 0,
1675 "ten paints must land back on the first frame"
1676 );
1677 }
1678
1679 #[test]
1680 fn idle_cadence_matches_native_pi() {
1681 use crate::component::Component;
1687 assert_eq!(crate::loader::SPINNER_FRAME_MS, 80);
1688 assert_eq!(
1689 crate::loader::SPINNER_FRAME_MS * crate::loader::SPINNER_FRAMES.len() as u64,
1690 800,
1691 "a full cycle should take 800ms, like upstream"
1692 );
1693
1694 let editor = Editor::simple();
1695 editor.set_working(Some(WorkingState {
1696 frame: 0,
1697 started_at: std::time::Instant::now(),
1698 message: "Working…".to_string(),
1699 }));
1700 let before = editor.working().expect("working").frame;
1702 let _ = editor.render(60);
1703 assert_eq!(editor.working().expect("working").frame, before + 1);
1704 }
1705
1706 #[test]
1707 fn explicit_border_color_overrides_the_theme_border() {
1708 use crate::component::Component;
1709 let editor = Editor::simple();
1710 editor.set_focused(true);
1711 editor.insert("echo hi");
1712
1713 let default_border = crate::theme::theme().colors.border.fg(&"─".repeat(20));
1714 assert!(editor.render(20).contains(&default_border));
1715 assert_eq!(editor.border_color(), None);
1716
1717 editor.set_border_color(Some(crate::theme::theme().colors.bash_mode));
1720 let bash_border = crate::theme::theme().colors.bash_mode.fg(&"─".repeat(20));
1721 assert_eq!(
1722 editor.border_color(),
1723 Some(crate::theme::theme().colors.bash_mode)
1724 );
1725 let rendered = editor.render(20);
1726 assert!(rendered.contains(&bash_border), "{rendered:?}");
1727 assert!(!rendered.contains(&default_border));
1728
1729 editor.set_border_color(None);
1732 assert!(editor.render(20).contains(&default_border));
1733 }
1734
1735 #[test]
1736 fn clear_and_set_text_notify_change_observers() {
1737 use std::sync::atomic::{AtomicUsize, Ordering};
1740 let calls = Arc::new(AtomicUsize::new(0));
1741 let calls_cb = calls.clone();
1742 let editor = Editor::simple();
1743 editor.on_change(Arc::new(move |_text: &str| {
1744 calls_cb.fetch_add(1, Ordering::SeqCst);
1745 }));
1746
1747 editor.set_text("!ls");
1748 assert_eq!(calls.load(Ordering::SeqCst), 1);
1749 editor.clear();
1750 assert_eq!(calls.load(Ordering::SeqCst), 2);
1751 }
1752
1753 #[test]
1754 fn snap_boundary_keeps_char_boundaries_including_end() {
1755 assert_eq!(snap_boundary("/mo", 3), 3);
1759 assert_eq!(snap_boundary("/model", 6), 6);
1760 assert_eq!(snap_boundary("hello", 5), 5);
1761 assert_eq!(snap_boundary("/model", 0), 0);
1763 assert_eq!(snap_boundary("/model", 2), 2);
1764 assert_eq!(snap_boundary("/mo", 99), 3);
1766 }
1767
1768 #[test]
1769 fn snap_boundary_mid_multibyte_snaps_down_to_char_start() {
1770 assert_eq!(snap_boundary("你", 1), 0);
1772 assert_eq!(snap_boundary("你", 2), 0);
1773 assert_eq!(snap_boundary("你", 3), 3);
1775 assert_eq!(snap_boundary("a你b", 5), 5);
1777 assert_eq!(snap_boundary("a你b", 2), 1);
1778 }
1779
1780 #[test]
1781 fn left_arrow_keeps_text_in_render() {
1782 use crate::component::Component;
1783 use crossterm::event::{KeyCode, KeyModifiers};
1784 let mk = |m, c| crossterm::event::KeyEvent::new(c, m);
1785 let editor = Editor::simple();
1786 editor.set_focused(true);
1787 editor.insert("hello world");
1788 editor.handle_key(mk(KeyModifiers::NONE, KeyCode::Left));
1789 let rendered = editor.render(40).join(
1790 "
1791",
1792 );
1793 assert!(
1794 rendered.contains("hello"),
1795 "text must survive Left, got: {rendered:?}"
1796 );
1797 }
1798 #[test]
1799 fn test_char_jump_and_page_scroll() {
1800 use crossterm::event::{KeyCode, KeyModifiers};
1801 let mk = |m, c| crossterm::event::KeyEvent::new(c, m);
1802 let editor = Editor::simple();
1803
1804 editor.insert("alpha xray xray");
1807 editor.handle_key(mk(KeyModifiers::CONTROL | KeyModifiers::ALT, KeyCode::End)); editor.handle_key(mk(
1809 KeyModifiers::CONTROL | KeyModifiers::ALT,
1810 KeyCode::Char(']'),
1811 ));
1812 editor.handle_key(mk(KeyModifiers::NONE, KeyCode::Char('x')));
1814 assert_eq!(editor.cursor_position(), (0, 11));
1815
1816 editor.set_text(
1818 "l0
1819l1
1820l2
1821l3
1822l4
1823l5
1824l6
1825l7
1826l8
1827l9
1828l10
1829l11
1830l12",
1831 );
1832 editor.set_cursor(0, 2);
1833 editor.handle_key(mk(KeyModifiers::CONTROL, KeyCode::PageDown));
1834 let (row, _) = editor.cursor_position();
1835 assert!(row >= 10, "page-down moves ~10 rows, got {row}");
1836 editor.handle_key(mk(KeyModifiers::CONTROL, KeyCode::PageUp));
1837 let (row2, _) = editor.cursor_position();
1838 assert!(row2 <= 2, "page-up returns near the start, got {row2}");
1839 }
1840 #[test]
1841 fn test_word_ops_and_new_keys() {
1842 use crossterm::event::{KeyCode, KeyModifiers};
1843 let mk = |m, c| crossterm::event::KeyEvent::new(c, m);
1844 let editor = Editor::simple();
1845 editor.insert("alpha beta gamma");
1846
1847 editor.handle_key(mk(KeyModifiers::CONTROL, KeyCode::End));
1849 editor.handle_key(mk(KeyModifiers::CONTROL, KeyCode::Char('w')));
1850 assert_eq!(editor.get_text(), "alpha beta ");
1851
1852 editor.handle_key(mk(KeyModifiers::ALT, KeyCode::Left));
1854 assert_eq!(editor.cursor_position(), (0, 6));
1855 editor.handle_key(mk(KeyModifiers::ALT, KeyCode::Char('d')));
1856 assert_eq!(editor.get_text(), "alpha ");
1857
1858 editor.set_text("a");
1860 editor.set_cursor(0, 1);
1861 editor.handle_key(mk(KeyModifiers::CONTROL, KeyCode::Char('j')));
1862 assert_eq!(
1863 editor.get_text(),
1864 "a
1865"
1866 );
1867
1868 editor.set_text("abc");
1870 editor.set_cursor(0, 1);
1871 editor.handle_key(mk(KeyModifiers::CONTROL, KeyCode::Char('d')));
1872 assert_eq!(editor.get_text(), "ac");
1873 }
1874 #[test]
1875 fn test_selection_cut_copy_paste() {
1876 let editor = Editor::simple();
1877 editor.insert("hello world");
1878 editor.handle_key(crossterm::event::KeyEvent::new(
1880 crossterm::event::KeyCode::Home,
1881 crossterm::event::KeyModifiers::NONE,
1882 ));
1883 let shift_right = || {
1884 editor.handle_key(crossterm::event::KeyEvent::new(
1885 crossterm::event::KeyCode::Right,
1886 crossterm::event::KeyModifiers::SHIFT,
1887 ))
1888 };
1889 for _ in 0..5 {
1890 shift_right();
1891 }
1892 assert!(editor.has_selection());
1893 assert_eq!(editor.selected_text().as_deref(), Some("hello"));
1894 editor.handle_key(crossterm::event::KeyEvent::new(
1896 crossterm::event::KeyCode::Char('c'),
1897 crossterm::event::KeyModifiers::CONTROL,
1898 ));
1899 assert_eq!(editor.get_text(), "hello world");
1900 editor.handle_key(crossterm::event::KeyEvent::new(
1901 crossterm::event::KeyCode::Right,
1902 crossterm::event::KeyModifiers::NONE,
1903 ));
1904 assert!(!editor.has_selection(), "plain movement clears selection");
1905 editor.handle_key(crossterm::event::KeyEvent::new(
1907 crossterm::event::KeyCode::End,
1908 crossterm::event::KeyModifiers::NONE,
1909 ));
1910 editor.handle_key(crossterm::event::KeyEvent::new(
1911 crossterm::event::KeyCode::Char('v'),
1912 crossterm::event::KeyModifiers::CONTROL,
1913 ));
1914 assert_eq!(editor.get_text(), "hello worldhello");
1915 }
1916
1917 #[test]
1918 fn test_selection_replace_and_cut() {
1919 let editor = Editor::simple();
1920 editor.insert("abcdef");
1921 editor.handle_key(crossterm::event::KeyEvent::new(
1923 crossterm::event::KeyCode::Home,
1924 crossterm::event::KeyModifiers::NONE,
1925 ));
1926 for _ in 0..4 {
1927 editor.handle_key(crossterm::event::KeyEvent::new(
1928 crossterm::event::KeyCode::Right,
1929 crossterm::event::KeyModifiers::SHIFT,
1930 ));
1931 }
1932 editor.handle_key(crossterm::event::KeyEvent::new(
1934 crossterm::event::KeyCode::Backspace,
1935 crossterm::event::KeyModifiers::NONE,
1936 ));
1937 assert_eq!(editor.get_text(), "ef", "backspace deletes the selection");
1938 editor.undo();
1940 assert_eq!(editor.get_text(), "abcdef");
1941 editor.handle_key(crossterm::event::KeyEvent::new(
1943 crossterm::event::KeyCode::Home,
1944 crossterm::event::KeyModifiers::NONE,
1945 ));
1946 for _ in 0..2 {
1947 editor.handle_key(crossterm::event::KeyEvent::new(
1948 crossterm::event::KeyCode::Right,
1949 crossterm::event::KeyModifiers::SHIFT,
1950 ));
1951 }
1952 editor.handle_key(crossterm::event::KeyEvent::new(
1953 crossterm::event::KeyCode::Char('x'),
1954 crossterm::event::KeyModifiers::CONTROL,
1955 ));
1956 assert_eq!(editor.get_text(), "cdef");
1957 editor.handle_key(crossterm::event::KeyEvent::new(
1959 crossterm::event::KeyCode::Right,
1960 crossterm::event::KeyModifiers::SHIFT,
1961 ));
1962 editor.insert("Z");
1963 assert_eq!(editor.get_text(), "Zdef", "typing replaces the selection");
1964 }
1965 #[test]
1966 fn test_undo_redo_roundtrip() {
1967 let editor = Editor::simple();
1968 editor.insert("hello");
1969 editor.insert(" world");
1970 assert_eq!(editor.get_text(), "hello world");
1972 editor.undo();
1973 assert_eq!(editor.get_text(), "", "coalesced inserts undo as one step");
1974 editor.redo();
1976 assert_eq!(editor.get_text(), "hello world");
1977 editor.undo();
1979 editor.insert("hi");
1980 editor.redo();
1981 assert_eq!(editor.get_text(), "hi", "new edit clears the redo stack");
1982 }
1983
1984 #[test]
1985 fn test_kill_ring_yank() {
1986 let editor = Editor::simple();
1987 editor.insert("alpha beta gamma");
1988 editor.set_cursor(0, 6); let key = |m, c| crossterm::event::KeyEvent::new(c, m);
1991 editor.handle_key(key(
1992 crossterm::event::KeyModifiers::CONTROL,
1993 crossterm::event::KeyCode::Char('k'),
1994 ));
1995 assert_eq!(editor.get_text(), "alpha ");
1996 editor.handle_key(key(
1998 crossterm::event::KeyModifiers::CONTROL,
1999 crossterm::event::KeyCode::Char('y'),
2000 ));
2001 assert_eq!(editor.get_text(), "alpha beta gamma");
2002 }
2003
2004 #[test]
2005 fn test_undo_after_kill() {
2006 let editor = Editor::simple();
2007 editor.insert("hello world");
2008 editor.set_cursor(0, 5);
2009 editor.handle_key(crossterm::event::KeyEvent::new(
2010 crossterm::event::KeyCode::Char('k'),
2011 crossterm::event::KeyModifiers::CONTROL,
2012 ));
2013 assert_eq!(editor.get_text(), "hello");
2014 editor.undo();
2015 assert_eq!(editor.get_text(), "hello world", "kill is undoable");
2016 }
2017
2018 #[test]
2019 fn shift_enter_encodings_insert_newline_without_submitting() {
2020 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2021 for code in [KeyCode::Enter, KeyCode::Char('\r'), KeyCode::Char('\n')] {
2022 let editor = Editor::simple();
2023 editor.on_submit(Arc::new(|_| panic!("Shift+Enter must not submit")));
2024 editor.insert("draft");
2025 editor.handle_key(KeyEvent::new(code, KeyModifiers::SHIFT));
2026 assert_eq!(editor.get_text(), "draft\n");
2027 }
2028 }
2029
2030 #[test]
2031 fn test_submit_clears_undo() {
2032 let editor = Editor::simple();
2033 editor.insert("committed");
2034 editor.handle_key(crossterm::event::KeyEvent::new(
2035 crossterm::event::KeyCode::Enter,
2036 crossterm::event::KeyModifiers::NONE,
2037 ));
2038 editor.undo();
2039 assert_eq!(
2040 editor.get_text(),
2041 "",
2042 "submit clears undo so committed text stays committed"
2043 );
2044 }
2045
2046 #[test]
2052 fn test_remote_lf_cr_enter_submits() {
2053 use std::sync::atomic::{AtomicUsize, Ordering};
2054 for code in [
2055 crossterm::event::KeyCode::Char('\n'),
2056 crossterm::event::KeyCode::Char('\r'),
2057 ] {
2058 let editor = Editor::simple();
2059 let submits = Arc::new(AtomicUsize::new(0));
2060 let submits_for_cb = submits.clone();
2061 editor.on_submit(Arc::new(move |_text: &str| {
2062 submits_for_cb.fetch_add(1, Ordering::SeqCst);
2063 }));
2064 editor.insert("hello");
2065 editor.handle_key(crossterm::event::KeyEvent::new(
2066 code,
2067 crossterm::event::KeyModifiers::NONE,
2068 ));
2069 assert_eq!(submits.load(Ordering::SeqCst), 1, "{code:?} must submit");
2070 assert_eq!(editor.get_text(), "", "editor cleared after submit");
2071 }
2072 }
2073 #[test]
2074 fn test_editor_text() {
2075 let editor = Editor::simple();
2076 editor.set_text("Hello");
2077 assert_eq!(editor.get_text(), "Hello");
2078 }
2079
2080 #[test]
2081 fn test_editor_insert() {
2082 let editor = Editor::simple();
2083 editor.insert("Hello");
2084 assert_eq!(editor.get_text(), "Hello");
2085 editor.insert(" World");
2086 assert_eq!(editor.get_text(), "Hello World");
2087 }
2088
2089 #[test]
2090 fn test_editor_clear() {
2091 let editor = Editor::simple();
2092 editor.set_text("Test");
2093 editor.clear();
2094 assert_eq!(editor.get_text(), "");
2095 }
2096
2097 #[test]
2098 fn test_editor_render_has_borders() {
2099 let editor = Editor::new(
2103 EditorOptions {
2104 padding_x: 1,
2105 ..Default::default()
2106 },
2107 EditorStyle::default(),
2108 Arc::new(Keybindings::new()),
2109 );
2110 editor.set_text("hi");
2111 let lines = editor.render(20);
2112 assert_eq!(lines.len(), 3, "expected [border, content, border]");
2113 let top = crate::ansi::strip_ansi(&lines[0]);
2115 let bottom = crate::ansi::strip_ansi(lines.last().unwrap());
2116 assert_eq!(
2117 top.chars().filter(|c| *c == '─').count(),
2118 20,
2119 "top border not full width: {top:?}"
2120 );
2121 assert_eq!(
2122 bottom.chars().filter(|c| *c == '─').count(),
2123 20,
2124 "bottom border not full width: {bottom:?}"
2125 );
2126 let content = crate::ansi::strip_ansi(&lines[1]);
2128 assert!(
2129 content.contains("hi"),
2130 "content line missing text: {content:?}"
2131 );
2132 assert!(
2133 !content.contains("> "),
2134 "content line should not have a prompt prefix: {content:?}"
2135 );
2136 }
2137
2138 #[test]
2139 fn test_editor_multibyte_no_panic() {
2140 let editor = Editor::simple();
2146 editor.set_focused(true);
2147 editor.insert("读");
2148 editor.insert("a");
2149 editor.insert("书");
2150 assert_eq!(editor.get_text(), "读a书");
2151 assert_eq!(editor.cursor_position(), (0, 7));
2153 let lines = editor.render(20);
2155 assert_eq!(
2156 lines.len(),
2157 3,
2158 "bordered box keeps [border, content, border]"
2159 );
2160
2161 editor.backspace(); assert_eq!(editor.get_text(), "读a");
2164 editor.cursor_left(); assert_eq!(editor.cursor_position(), (0, 3));
2166 editor.insert("b"); assert_eq!(editor.get_text(), "读ba");
2168 }
2169
2170 #[test]
2171 fn test_editor_wraps_long_draft_inside_width() {
2172 let editor = Editor::new(
2173 EditorOptions {
2174 padding_x: 1,
2175 ..Default::default()
2176 },
2177 EditorStyle::default(),
2178 Arc::new(Keybindings::new()),
2179 );
2180 editor.set_focused(true);
2181 editor.set_text("12345678901234567890");
2182 let lines = editor.render(10);
2183 assert_eq!(lines.len(), 5, "two wrapped content rows plus borders");
2184 for line in &lines {
2185 assert!(
2186 crate::ansi::visible_width(line) <= 10,
2187 "line escaped editor width: {line:?}"
2188 );
2189 }
2190 let rendered = crate::ansi::strip_ansi(&lines.join("\n"));
2191 assert!(rendered.contains("1234567"));
2192 assert!(rendered.contains("90123456"));
2193 }
2194
2195 #[test]
2196 fn test_editor_preserves_trailing_newline() {
2197 let editor = Editor::simple();
2198 editor.set_text("first\n");
2199 assert_eq!(editor.get_text(), "first\n");
2200 assert_eq!(editor.cursor_position(), (0, 0));
2201 }
2202
2203 #[test]
2204 fn test_autocomplete_cursor_origin() {
2205 use crate::autocomplete::AutocompleteManager;
2211
2212 let editor = Editor::simple();
2213 editor.set_text("读");
2214 editor.set_cursor(0, 3); let text = editor.get_text();
2216 let (_r, col) = editor.cursor_position();
2217 let cursor = col.min(text.len());
2218 let mgr = AutocompleteManager::new();
2219
2220 assert!(mgr.get_suggestions(&text, cursor).is_none());
2222 editor.set_text("/读");
2224 editor.set_cursor(0, 4);
2225 let text2 = editor.get_text();
2226 let mgr2 = AutocompleteManager::new();
2227 let _ = mgr2.get_suggestions(&text2, 9); }
2229
2230 #[test]
2231 fn insert_normalizes_crlf_and_lone_cr_to_newlines() {
2232 let editor = Editor::simple();
2236 editor.insert("line1\r\nline2\rline3");
2237 assert_eq!(editor.get_text(), "line1\nline2\nline3");
2238
2239 let (row, col) = editor.cursor_position();
2241 assert_eq!((row, col), (2, "line3".len()));
2242
2243 editor.insert("!");
2245 assert_eq!(editor.get_text(), "line1\nline2\nline3!");
2246 }
2247
2248 #[test]
2249 fn insert_of_a_lone_crlf_makes_exactly_one_newline() {
2250 let editor = Editor::simple();
2251 editor.insert("a\r\nb");
2252 assert_eq!(editor.get_text().matches('\n').count(), 1);
2253 assert_eq!(editor.get_text(), "a\nb");
2254 }
2255
2256 #[test]
2259 fn short_paste_is_inserted_verbatim() {
2260 let editor = Editor::simple();
2261 editor.handle_paste("hello\nworld\nthird");
2262 assert_eq!(editor.get_text(), "hello\nworld\nthird");
2263 assert_eq!(editor.get_expanded_text(), "hello\nworld\nthird");
2264 }
2265
2266 #[test]
2267 fn many_line_paste_folds_to_a_marker() {
2268 let editor = Editor::simple();
2269 let payload = (1..=12)
2270 .map(|i| format!("line {i}"))
2271 .collect::<Vec<_>>()
2272 .join("\n");
2273 editor.handle_paste(&payload);
2274
2275 assert_eq!(editor.get_text(), "[paste #1 +12 lines]");
2277 assert_eq!(editor.get_expanded_text(), payload);
2279 }
2280
2281 #[test]
2282 fn long_single_line_paste_folds_by_char_count() {
2283 let editor = Editor::simple();
2284 let payload = "x".repeat(2000);
2285 editor.handle_paste(&payload);
2286 assert_eq!(editor.get_text(), "[paste #1 2000 chars]");
2287 assert_eq!(editor.get_expanded_text(), payload);
2288 }
2289
2290 #[test]
2291 fn pasted_crlf_and_tabs_are_normalized_before_folding() {
2292 let editor = Editor::simple();
2293 let payload = (1..=12)
2294 .map(|i| format!("a\tb{i}"))
2295 .collect::<Vec<_>>()
2296 .join("\r\n");
2297 editor.handle_paste(&payload);
2298 let expanded = editor.get_expanded_text();
2299 assert!(!expanded.contains('\r'), "CR must be normalized away");
2300 assert!(!expanded.contains('\t'), "tab must expand to spaces");
2301 assert!(expanded.contains("a b1"), "tab becomes four spaces");
2302 assert_eq!(expanded.split('\n').count(), 12);
2303 }
2304
2305 #[test]
2306 fn two_folded_pastes_get_distinct_ids_and_both_expand() {
2307 let editor = Editor::simple();
2308 let first = "1\n".repeat(11);
2309 let second = "2\n".repeat(11);
2310 editor.handle_paste(&first);
2311 editor.handle_paste(&second);
2312 assert_eq!(
2313 editor.get_text(),
2314 "[paste #1 +12 lines][paste #2 +12 lines]"
2315 );
2316 assert_eq!(editor.get_expanded_text(), format!("{first}{second}"));
2317 }
2318
2319 #[test]
2320 fn backspace_over_a_folded_marker_drops_it_and_renumbers() {
2321 let editor = Editor::simple();
2322 let first = "a\n".repeat(11);
2323 let second = "b\n".repeat(11);
2324 editor.handle_paste(&first);
2325 editor.handle_paste(&second);
2326 assert_eq!(
2327 editor.get_text(),
2328 "[paste #1 +12 lines][paste #2 +12 lines]"
2329 );
2330
2331 let _ = editor.handle_key(crossterm::event::KeyEvent::new(
2334 KeyCode::Left,
2335 KeyModifiers::NONE,
2336 ));
2337 let _ = editor.handle_key(crossterm::event::KeyEvent::new(
2338 KeyCode::Backspace,
2339 KeyModifiers::NONE,
2340 ));
2341
2342 assert_eq!(editor.get_text(), "[paste #1 +12 lines]");
2344 assert_eq!(editor.get_expanded_text(), second);
2345 }
2346
2347 #[test]
2348 fn undo_restores_a_backspaced_folded_paste() {
2349 let editor = Editor::simple();
2350 let payload = "line\n".repeat(11);
2351 editor.handle_paste(&payload);
2352 assert_eq!(editor.get_text(), "[paste #1 +12 lines]");
2353
2354 let _ = editor.handle_key(crossterm::event::KeyEvent::new(
2355 KeyCode::Backspace,
2356 KeyModifiers::NONE,
2357 ));
2358 assert_eq!(editor.get_text(), "", "marker deleted");
2359
2360 editor.undo();
2362 assert_eq!(editor.get_text(), "[paste #1 +12 lines]");
2363 assert_eq!(editor.get_expanded_text(), payload);
2364 }
2365
2366 #[test]
2367 fn cursor_arrows_step_over_a_marker_atomically() {
2368 let editor = Editor::simple();
2369 let payload = "z\n".repeat(11);
2370 editor.handle_paste(&payload);
2371 let _ = editor.handle_key(crossterm::event::KeyEvent::new(
2373 KeyCode::Left,
2374 KeyModifiers::NONE,
2375 ));
2376 assert_eq!(
2377 editor.cursor_position(),
2378 (0, 0),
2379 "left jumps the whole marker"
2380 );
2381 let _ = editor.handle_key(crossterm::event::KeyEvent::new(
2383 KeyCode::Right,
2384 KeyModifiers::NONE,
2385 ));
2386 assert_eq!(
2387 editor.cursor_position(),
2388 (0, "[paste #1 +12 lines]".len()),
2389 "right jumps the whole marker"
2390 );
2391 }
2392
2393 #[test]
2394 fn clear_and_set_text_forget_folded_pastes() {
2395 let editor = Editor::simple();
2396 editor.handle_paste(&"m\n".repeat(11));
2397 editor.clear();
2398 assert_eq!(editor.get_text(), "");
2399 assert_eq!(editor.get_expanded_text(), "");
2400
2401 editor.handle_paste(&"n\n".repeat(11));
2402 editor.set_text("fresh");
2403 assert_eq!(editor.get_text(), "fresh");
2406 assert_eq!(editor.get_expanded_text(), "fresh");
2407 }
2408
2409 #[test]
2410 fn csi_u_ctrl_sequences_decode_then_drop_like_native_pi() {
2411 let editor = Editor::simple();
2415 editor.handle_paste(&"\x1b[104;5u".repeat(1200));
2416 assert_eq!(editor.get_text(), "");
2417 assert_eq!(editor.get_expanded_text(), "");
2418 }
2419}