1use crate::caps::{Capabilities, ProbeHints};
41use crate::input::{InputEvent, KeyCode, KeyEvent, Modifiers};
42use crate::surface::change::ChangeSequence;
43use crate::surface::{Change, Position};
44use crate::terminal::{new_terminal, Terminal};
45use crate::{bail, ensure, Result};
46
47mod actions;
48mod buffer;
49mod history;
50mod host;
51pub use actions::{Action, Movement, RepeatCount};
52pub use buffer::LineEditBuffer;
53pub use history::*;
54pub use host::*;
55
56pub struct LineEditor<'term> {
73 terminal: &'term mut dyn Terminal,
74 prompt: String,
75 line: LineEditBuffer,
76
77 history_pos: Option<usize>,
78 bottom_line: Option<String>,
79
80 completion: Option<CompletionState>,
81
82 move_to_editor_start: Option<Change>,
83 move_to_editor_end: Option<Change>,
84
85 state: EditorState,
86}
87
88#[derive(Clone, Eq, PartialEq, Debug)]
89enum EditorState {
90 Inactive,
91 Editing,
92 Cancelled,
93 Accepted,
94 Searching {
95 style: SearchStyle,
96 direction: SearchDirection,
97 matching_line: String,
98 cursor: usize,
99 },
100}
101
102struct CompletionState {
103 candidates: Vec<CompletionCandidate>,
104 index: usize,
105 original_line: String,
106 original_cursor: usize,
107}
108
109impl CompletionState {
110 fn next(&mut self) {
111 self.index += 1;
112 if self.index >= self.candidates.len() {
113 self.index = 0;
114 }
115 }
116
117 fn current(&self) -> (usize, String) {
118 let mut line = self.original_line.clone();
119 let candidate = &self.candidates[self.index];
120 line.replace_range(candidate.range.clone(), &candidate.text);
121
122 let range_len = candidate.range.end - candidate.range.start;
127 let new_cursor = self.original_cursor + candidate.text.len() - range_len;
128
129 (new_cursor, line)
130 }
131}
132
133impl<'term> LineEditor<'term> {
134 pub fn new(terminal: &'term mut dyn Terminal) -> Self {
153 Self {
154 terminal,
155 prompt: "> ".to_owned(),
156 line: LineEditBuffer::default(),
157 history_pos: None,
158 bottom_line: None,
159 completion: None,
160 move_to_editor_start: None,
161 move_to_editor_end: None,
162 state: EditorState::Inactive,
163 }
164 }
165
166 fn render(&mut self, host: &mut dyn LineEditorHost) -> Result<()> {
167 let screen_size = self.terminal.get_screen_size()?;
168
169 let mut changes = ChangeSequence::new(screen_size.rows, screen_size.cols);
170
171 changes.add(Change::ClearToEndOfScreen(Default::default()));
172 changes.add(Change::AllAttributes(Default::default()));
173 for ele in host.render_prompt(&self.prompt) {
174 changes.add(ele);
175 }
176 changes.add(Change::AllAttributes(Default::default()));
177
178 let (line_to_display, cursor) = match &self.state {
181 EditorState::Searching {
182 matching_line,
183 cursor,
184 ..
185 } => (matching_line.as_str(), *cursor),
186 _ => (self.line.get_line(), self.line.get_cursor()),
187 };
188
189 let cursor_position_after_printing_prompt = changes.current_cursor_position();
190
191 let (elements, cursor_x_pos) = host.highlight_line(line_to_display, cursor);
192
193 fn compute_cursor_after_printing_x_columns(
197 cursor_x: usize,
198 cursor_y: isize,
199 delta: usize,
200 screen_cols: usize,
201 ) -> (usize, isize) {
202 let y = (cursor_x + delta) / screen_cols;
203 let x = (cursor_x + delta) % screen_cols;
204
205 let row = cursor_y + y as isize;
206 let col = x.max(0) as usize;
207
208 (col, row)
209 }
210 let cursor_position = compute_cursor_after_printing_x_columns(
211 cursor_position_after_printing_prompt.0,
212 cursor_position_after_printing_prompt.1,
213 cursor_x_pos,
214 screen_size.cols,
215 );
216
217 for ele in elements {
218 changes.add(ele);
219 }
220
221 let cursor_after_line_render = changes.current_cursor_position();
222 if cursor_after_line_render.0 == screen_size.cols {
223 changes.add(" ");
228 }
229
230 if let EditorState::Editing = &self.state {
231 let preview_elements = host.render_preview(line_to_display);
232 if !preview_elements.is_empty() {
233 changes.add("\r\n");
235 changes.add(Change::AllAttributes(Default::default()));
237 for ele in preview_elements {
238 changes.add(ele);
239 }
240 }
241 }
242
243 if let EditorState::Searching {
244 style, direction, ..
245 } = &self.state
246 {
247 let label = match (style, direction) {
249 (SearchStyle::Substring, SearchDirection::Backwards) => "bck-i-search",
250 (SearchStyle::Substring, SearchDirection::Forwards) => "fwd-i-search",
251 };
252 changes.add(Change::AllAttributes(Default::default()));
254 changes.add(format!("\r\n{}: {}_", label, self.line.get_line()));
259 }
260
261 let render_height = changes.render_height();
271
272 changes.move_to(cursor_position);
273
274 let mut changes = changes.consume();
275 if let Some(start) = self.move_to_editor_start.take() {
276 changes.insert(0, start);
277 }
278 self.terminal.render(&changes)?;
279
280 self.move_to_editor_start.replace(Change::CursorPosition {
281 x: Position::Absolute(0),
282 y: Position::Relative(-1 * cursor_position.1),
283 });
284
285 self.move_to_editor_end.replace(Change::CursorPosition {
286 x: Position::Absolute(0),
287 y: Position::Relative(1 + render_height as isize - cursor_position.1),
288 });
289
290 Ok(())
291 }
292
293 pub fn set_prompt(&mut self, prompt: &str) {
294 self.prompt = prompt.to_owned();
295 }
296
297 pub fn read_line(&mut self, host: &mut dyn LineEditorHost) -> Result<Option<String>> {
302 self.read_line_with_optional_initial_value(host, None)
303 }
304
305 pub fn read_line_with_optional_initial_value(
306 &mut self,
307 host: &mut dyn LineEditorHost,
308 initial_value: Option<&str>,
309 ) -> Result<Option<String>> {
310 ensure!(
311 self.state == EditorState::Inactive,
312 "recursive call to read_line!"
313 );
314
315 self.move_to_editor_end.take();
318 self.move_to_editor_start.take();
319
320 self.terminal.set_raw_mode()?;
321 self.state = EditorState::Editing;
322 let res = self.read_line_impl(host, initial_value);
323 self.state = EditorState::Inactive;
324
325 if let Some(move_end) = self.move_to_editor_end.take() {
326 self.terminal
327 .render(&[move_end, Change::ClearToEndOfScreen(Default::default())])?;
328 }
329
330 self.terminal.flush()?;
331 self.terminal.set_cooked_mode()?;
332 res
333 }
334
335 fn resolve_action(
336 &mut self,
337 event: &InputEvent,
338 host: &mut dyn LineEditorHost,
339 ) -> Option<Action> {
340 if let Some(action) = host.resolve_action(event, self) {
341 return Some(action);
342 }
343
344 match event {
345 InputEvent::Key(KeyEvent {
346 key: KeyCode::Char('C'),
347 modifiers: Modifiers::CTRL,
348 }) => Some(Action::Cancel),
349
350 InputEvent::Key(KeyEvent {
351 key: KeyCode::Tab,
352 modifiers: Modifiers::NONE,
353 }) => Some(Action::Complete),
354
355 InputEvent::Key(KeyEvent {
356 key: KeyCode::Char('D'),
357 modifiers: Modifiers::CTRL,
358 }) => Some(Action::EndOfFile),
359
360 InputEvent::Key(KeyEvent {
361 key: KeyCode::Char('J'),
362 modifiers: Modifiers::CTRL,
363 })
364 | InputEvent::Key(KeyEvent {
365 key: KeyCode::Char('M'),
366 modifiers: Modifiers::CTRL,
367 })
368 | InputEvent::Key(KeyEvent {
369 key: KeyCode::Enter,
370 modifiers: Modifiers::NONE,
371 }) => Some(Action::AcceptLine),
372 InputEvent::Key(KeyEvent {
373 key: KeyCode::Char('H'),
374 modifiers: Modifiers::CTRL,
375 })
376 | InputEvent::Key(KeyEvent {
377 key: KeyCode::Backspace,
378 modifiers: Modifiers::NONE,
379 }) => Some(Action::Kill(Movement::BackwardChar(1))),
380 InputEvent::Key(KeyEvent {
381 key: KeyCode::Delete,
382 modifiers: Modifiers::NONE,
383 }) => Some(Action::KillAndMove(
384 Movement::ForwardChar(1),
385 Movement::None,
386 )),
387
388 InputEvent::Key(KeyEvent {
389 key: KeyCode::Char('P'),
390 modifiers: Modifiers::CTRL,
391 })
392 | InputEvent::Key(KeyEvent {
393 key: KeyCode::UpArrow,
394 modifiers: Modifiers::NONE,
395 })
396 | InputEvent::Key(KeyEvent {
397 key: KeyCode::ApplicationUpArrow,
398 modifiers: Modifiers::NONE,
399 }) => Some(Action::HistoryPrevious),
400
401 InputEvent::Key(KeyEvent {
402 key: KeyCode::Char('N'),
403 modifiers: Modifiers::CTRL,
404 })
405 | InputEvent::Key(KeyEvent {
406 key: KeyCode::DownArrow,
407 modifiers: Modifiers::NONE,
408 })
409 | InputEvent::Key(KeyEvent {
410 key: KeyCode::ApplicationDownArrow,
411 modifiers: Modifiers::NONE,
412 }) => Some(Action::HistoryNext),
413
414 InputEvent::Key(KeyEvent {
415 key: KeyCode::Char('B'),
416 modifiers: Modifiers::CTRL,
417 })
418 | InputEvent::Key(KeyEvent {
419 key: KeyCode::ApplicationLeftArrow,
420 modifiers: Modifiers::NONE,
421 })
422 | InputEvent::Key(KeyEvent {
423 key: KeyCode::LeftArrow,
424 modifiers: Modifiers::NONE,
425 }) => Some(Action::Move(Movement::BackwardChar(1))),
426
427 InputEvent::Key(KeyEvent {
428 key: KeyCode::Char('W'),
429 modifiers: Modifiers::CTRL,
430 }) => Some(Action::Kill(Movement::BackwardWord(1))),
431
432 InputEvent::Key(KeyEvent {
433 key: KeyCode::Char('b'),
434 modifiers: Modifiers::ALT,
435 })
436 | InputEvent::Key(KeyEvent {
437 key: KeyCode::LeftArrow,
438 modifiers: Modifiers::ALT,
439 })
440 | InputEvent::Key(KeyEvent {
441 key: KeyCode::ApplicationLeftArrow,
442 modifiers: Modifiers::ALT,
443 }) => Some(Action::Move(Movement::BackwardWord(1))),
444
445 InputEvent::Key(KeyEvent {
446 key: KeyCode::Char('f'),
447 modifiers: Modifiers::ALT,
448 })
449 | InputEvent::Key(KeyEvent {
450 key: KeyCode::RightArrow,
451 modifiers: Modifiers::ALT,
452 })
453 | InputEvent::Key(KeyEvent {
454 key: KeyCode::ApplicationRightArrow,
455 modifiers: Modifiers::ALT,
456 }) => Some(Action::Move(Movement::ForwardWord(1))),
457
458 InputEvent::Key(KeyEvent {
459 key: KeyCode::Char('A'),
460 modifiers: Modifiers::CTRL,
461 })
462 | InputEvent::Key(KeyEvent {
463 key: KeyCode::Home,
464 modifiers: Modifiers::NONE,
465 }) => Some(Action::Move(Movement::StartOfLine)),
466 InputEvent::Key(KeyEvent {
467 key: KeyCode::Char('E'),
468 modifiers: Modifiers::CTRL,
469 })
470 | InputEvent::Key(KeyEvent {
471 key: KeyCode::End,
472 modifiers: Modifiers::NONE,
473 }) => Some(Action::Move(Movement::EndOfLine)),
474 InputEvent::Key(KeyEvent {
475 key: KeyCode::Char('F'),
476 modifiers: Modifiers::CTRL,
477 })
478 | InputEvent::Key(KeyEvent {
479 key: KeyCode::RightArrow,
480 modifiers: Modifiers::NONE,
481 })
482 | InputEvent::Key(KeyEvent {
483 key: KeyCode::ApplicationRightArrow,
484 modifiers: Modifiers::NONE,
485 }) => Some(Action::Move(Movement::ForwardChar(1))),
486 InputEvent::Key(KeyEvent {
487 key: KeyCode::Char(c),
488 modifiers: Modifiers::SHIFT,
489 })
490 | InputEvent::Key(KeyEvent {
491 key: KeyCode::Char(c),
492 modifiers: Modifiers::NONE,
493 }) => Some(Action::InsertChar(1, *c)),
494 InputEvent::Paste(text) => Some(Action::InsertText(1, text.clone())),
495 InputEvent::Key(KeyEvent {
496 key: KeyCode::Char('L'),
497 modifiers: Modifiers::CTRL,
498 }) => Some(Action::Repaint),
499 InputEvent::Key(KeyEvent {
500 key: KeyCode::Char('K'),
501 modifiers: Modifiers::CTRL,
502 }) => Some(Action::Kill(Movement::EndOfLine)),
503
504 InputEvent::Key(KeyEvent {
505 key: KeyCode::Char('R'),
506 modifiers: Modifiers::CTRL,
507 }) => Some(Action::HistoryIncSearchBackwards),
508
509 InputEvent::Key(KeyEvent {
512 key: KeyCode::Char('S'),
513 modifiers: Modifiers::CTRL,
514 }) => Some(Action::HistoryIncSearchForwards),
515
516 _ => None,
517 }
518 }
519
520 fn kill_text(&mut self, kill_movement: Movement, move_movement: Movement) {
521 self.clear_completion();
522 self.line.kill_text(kill_movement, move_movement);
523 }
524
525 fn clear_completion(&mut self) {
526 self.completion = None;
527 }
528
529 fn cancel_search_state(&mut self) {
530 if let EditorState::Searching {
531 matching_line,
532 cursor,
533 ..
534 } = &self.state
535 {
536 self.line.set_line_and_cursor(matching_line, *cursor);
537 self.state = EditorState::Editing;
538 }
539 }
540
541 pub fn get_line_and_cursor(&mut self) -> (&str, usize) {
546 (self.line.get_line(), self.line.get_cursor())
547 }
548
549 pub fn set_line_and_cursor(&mut self, line: &str, cursor: usize) {
556 self.line.set_line_and_cursor(line, cursor);
557 }
558
559 fn reapply_search_pattern(&mut self, host: &mut dyn LineEditorHost) {
563 if let EditorState::Searching {
564 style,
565 direction,
566 matching_line,
567 cursor,
568 } = &self.state
569 {
570 self.history_pos.take();
572
573 let history_pos = match host.history().last() {
574 Some(p) => p,
575 None => {
576 return;
579 }
580 };
581
582 let last_matching_line;
583 let last_cursor;
584
585 if let Some(result) =
586 host.history()
587 .search(history_pos, *style, *direction, self.line.get_line())
588 {
589 self.history_pos.replace(result.idx);
590 last_matching_line = result.line.to_string();
591 last_cursor = result.cursor;
592 } else {
593 last_matching_line = matching_line.clone();
594 last_cursor = *cursor;
595 }
596
597 self.state = EditorState::Searching {
598 style: *style,
599 direction: *direction,
600 matching_line: last_matching_line,
601 cursor: last_cursor,
602 };
603 }
604 }
605
606 fn trigger_search(
607 &mut self,
608 style: SearchStyle,
609 direction: SearchDirection,
610 host: &mut dyn LineEditorHost,
611 ) {
612 self.clear_completion();
613
614 if let EditorState::Searching { .. } = &self.state {
615 } else {
617 self.line.clear();
620 self.history_pos.take();
621 }
622
623 let history_pos = match self.history_pos {
624 Some(p) => match direction.next(p) {
625 Some(p) => p,
626 None => return,
627 },
628 None => match host.history().last() {
629 Some(p) => p,
630 None => {
631 return;
634 }
635 },
636 };
637
638 let search_result =
639 host.history()
640 .search(history_pos, style, direction, self.line.get_line());
641
642 let last_matching_line;
643 let last_cursor;
644
645 if let Some(result) = search_result {
646 self.history_pos.replace(result.idx);
647 last_matching_line = result.line.to_string();
648 last_cursor = result.cursor;
649 } else if let EditorState::Searching {
650 matching_line,
651 cursor,
652 ..
653 } = &self.state
654 {
655 last_matching_line = matching_line.clone();
656 last_cursor = *cursor;
657 } else {
658 last_matching_line = String::new();
659 last_cursor = 0;
660 }
661
662 self.state = EditorState::Searching {
663 style,
664 direction,
665 matching_line: last_matching_line,
666 cursor: last_cursor,
667 };
668 }
669
670 pub fn apply_action(&mut self, host: &mut dyn LineEditorHost, action: Action) -> Result<()> {
674 let action = match (action, &self.state) {
677 (
678 Action::HistoryPrevious,
679 EditorState::Searching {
680 style: SearchStyle::Substring,
681 ..
682 },
683 ) => Action::HistoryIncSearchBackwards,
684 (
685 Action::HistoryNext,
686 EditorState::Searching {
687 style: SearchStyle::Substring,
688 ..
689 },
690 ) => Action::HistoryIncSearchForwards,
691 (action, _) => action,
692 };
693
694 match action {
695 Action::Cancel => self.state = EditorState::Cancelled,
696 Action::NoAction => {}
697 Action::AcceptLine => {
698 self.cancel_search_state();
702
703 self.state = EditorState::Accepted;
704 }
705 Action::EndOfFile => {
706 return Err(
707 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "End Of File").into(),
708 )
709 }
710 Action::Kill(movement) => {
711 self.kill_text(movement, movement);
712 self.reapply_search_pattern(host);
713 }
714 Action::KillAndMove(kill_movement, move_movement) => {
715 self.kill_text(kill_movement, move_movement);
716 self.reapply_search_pattern(host);
717 }
718
719 Action::Move(movement) => {
720 self.clear_completion();
721 self.cancel_search_state();
722 self.line.exec_movement(movement);
723 }
724
725 Action::InsertChar(rep, c) => {
726 self.clear_completion();
727 for _ in 0..rep {
728 self.line.insert_char(c);
729 }
730 self.reapply_search_pattern(host);
731 }
732 Action::InsertText(rep, text) => {
733 self.clear_completion();
734 for _ in 0..rep {
735 self.line.insert_text(&text);
736 }
737 self.reapply_search_pattern(host);
738 }
739 Action::Repaint => {
740 self.terminal
741 .render(&[Change::ClearScreen(Default::default())])?;
742 }
743 Action::HistoryPrevious => {
744 self.clear_completion();
745 self.cancel_search_state();
746
747 if let Some(cur_pos) = self.history_pos.as_ref() {
748 let prior_idx = cur_pos.saturating_sub(1);
749 if let Some(prior) = host.history().get(prior_idx) {
750 self.history_pos = Some(prior_idx);
751 self.line.set_line_and_cursor(&prior, prior.len());
752 }
753 } else if let Some(last) = host.history().last() {
754 self.bottom_line = Some(self.line.get_line().to_string());
755 self.history_pos = Some(last);
756 let line = host
757 .history()
758 .get(last)
759 .expect("History::last and History::get to be consistent");
760 self.line.set_line_and_cursor(&line, line.len())
761 }
762 }
763 Action::HistoryNext => {
764 self.clear_completion();
765 self.cancel_search_state();
766
767 if let Some(cur_pos) = self.history_pos.as_ref() {
768 let next_idx = cur_pos.saturating_add(1);
769 if let Some(next) = host.history().get(next_idx) {
770 self.history_pos = Some(next_idx);
771 self.line.set_line_and_cursor(&next, next.len());
772 } else if let Some(bottom) = self.bottom_line.take() {
773 self.line.set_line_and_cursor(&bottom, bottom.len());
774 } else {
775 self.line.clear();
776 }
777 }
778 }
779
780 Action::HistoryIncSearchBackwards => {
781 self.trigger_search(SearchStyle::Substring, SearchDirection::Backwards, host);
782 }
783 Action::HistoryIncSearchForwards => {
784 self.trigger_search(SearchStyle::Substring, SearchDirection::Forwards, host);
785 }
786
787 Action::Complete => {
788 self.cancel_search_state();
789
790 if self.completion.is_none() {
791 let candidates = host.complete(self.line.get_line(), self.line.get_cursor());
792 if !candidates.is_empty() {
793 let state = CompletionState {
794 candidates,
795 index: 0,
796 original_line: self.line.get_line().to_string(),
797 original_cursor: self.line.get_cursor(),
798 };
799
800 let (cursor, line) = state.current();
801 self.line.set_line_and_cursor(&line, cursor);
802
803 if state.candidates.len() > 1 {
807 self.completion = Some(state);
808 }
809 }
810 } else if let Some(state) = self.completion.as_mut() {
811 state.next();
812 let (cursor, line) = state.current();
813 self.line.set_line_and_cursor(&line, cursor);
814 }
815 }
816 }
817
818 Ok(())
819 }
820
821 fn read_line_impl(
822 &mut self,
823 host: &mut dyn LineEditorHost,
824 initial_value: Option<&str>,
825 ) -> Result<Option<String>> {
826 self.line.clear();
827 if let Some(value) = initial_value {
828 self.line.set_line_and_cursor(value, value.len());
829 }
830 self.history_pos = None;
831 self.bottom_line = None;
832 self.clear_completion();
833
834 self.render(host)?;
835 while let Some(event) = self.terminal.poll_input(None)? {
836 if let Some(action) = self.resolve_action(&event, host) {
837 self.apply_action(host, action)?;
838 self.render(host)?;
841 match self.state {
842 EditorState::Searching { .. } | EditorState::Editing => {}
843 EditorState::Cancelled => return Ok(None),
844 EditorState::Accepted => return Ok(Some(self.line.get_line().to_string())),
845 EditorState::Inactive => bail!("editor is inactive during read line!?"),
846 }
847 } else {
848 self.render(host)?;
849 }
850 }
851 Ok(Some(self.line.get_line().to_string()))
852 }
853}
854
855pub fn line_editor_terminal() -> Result<impl Terminal> {
858 let hints = ProbeHints::new_from_env().mouse_reporting(Some(false));
859 let caps = Capabilities::new_with_hints(hints)?;
860 new_terminal(caps)
861}