rustyline/
keymap.rs

1//! Bindings from keys to command for Emacs and Vi modes
2use log::debug;
3
4use super::Result;
5use crate::highlight::CmdKind;
6use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M};
7use crate::tty::{self, RawReader, Term, Terminal};
8use crate::{Config, EditMode};
9#[cfg(feature = "custom-bindings")]
10use crate::{Event, EventContext, EventHandler};
11
12/// The number of times one command should be repeated.
13pub type RepeatCount = u16;
14
15/// Commands
16#[derive(Debug, Clone, Eq, PartialEq)]
17#[non_exhaustive]
18pub enum Cmd {
19    /// abort
20    Abort, // Miscellaneous Command
21    /// accept-line
22    ///
23    /// See also `AcceptOrInsertLine`
24    AcceptLine,
25    /// beginning-of-history
26    BeginningOfHistory,
27    /// capitalize-word
28    CapitalizeWord,
29    /// clear-screen
30    ClearScreen,
31    /// Paste from the clipboard
32    #[cfg(windows)]
33    PasteFromClipboard,
34    /// complete
35    Complete,
36    /// complete-backward
37    CompleteBackward,
38    /// complete-hint
39    CompleteHint,
40    /// Dedent current line
41    Dedent(Movement),
42    /// downcase-word
43    DowncaseWord,
44    /// vi-eof-maybe
45    EndOfFile,
46    /// end-of-history
47    EndOfHistory,
48    /// forward-search-history (incremental search)
49    ForwardSearchHistory,
50    /// history-search-backward (common prefix search)
51    HistorySearchBackward,
52    /// history-search-forward (common prefix search)
53    HistorySearchForward,
54    /// Indent current line
55    Indent(Movement),
56    /// Insert text
57    Insert(RepeatCount, String),
58    /// Interrupt signal (Ctrl-C)
59    Interrupt,
60    /// backward-delete-char, backward-kill-line, backward-kill-word
61    /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout,
62    /// vi-delete, vi-delete-to, vi-rubout
63    Kill(Movement),
64    /// backward-char, backward-word, beginning-of-line, end-of-line,
65    /// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word,
66    /// vi-prev-word
67    Move(Movement),
68    /// next-history
69    NextHistory,
70    /// No action
71    Noop,
72    /// repaint
73    Repaint,
74    /// vi-replace
75    Overwrite(char),
76    /// previous-history
77    PreviousHistory,
78    /// quoted-insert
79    QuotedInsert,
80    /// vi-change-char
81    ReplaceChar(RepeatCount, char),
82    /// vi-change-to, vi-substitute
83    Replace(Movement, Option<String>),
84    /// reverse-search-history (incremental search)
85    ReverseSearchHistory,
86    /// self-insert
87    SelfInsert(RepeatCount, char),
88    /// Suspend signal (Ctrl-Z on unix platform)
89    Suspend,
90    /// transpose-chars
91    TransposeChars,
92    /// transpose-words
93    TransposeWords(RepeatCount),
94    /// undo
95    Undo(RepeatCount),
96    /// Unsupported / unexpected
97    Unknown,
98    /// upcase-word
99    UpcaseWord,
100    /// vi-yank-to
101    ViYankTo(Movement),
102    /// yank, vi-put
103    Yank(RepeatCount, Anchor),
104    /// yank-pop
105    YankPop,
106    /// moves cursor to the line above or switches to prev history entry if
107    /// the cursor is already on the first line
108    LineUpOrPreviousHistory(RepeatCount),
109    /// moves cursor to the line below or switches to next history entry if
110    /// the cursor is already on the last line
111    LineDownOrNextHistory(RepeatCount),
112    /// Inserts a newline
113    Newline,
114    /// Either accepts or inserts a newline
115    ///
116    /// Always inserts newline if input is non-valid. Can also insert newline
117    /// if cursor is in the middle of the text
118    ///
119    /// If you support multi-line input:
120    /// * Use `accept_in_the_middle: true` for mostly single-line cases, for
121    ///   example command-line.
122    /// * Use `accept_in_the_middle: false` for mostly multi-line cases, for
123    ///   example SQL or JSON input.
124    AcceptOrInsertLine {
125        /// Whether this commands accepts input if the cursor not at the end
126        /// of the current input
127        accept_in_the_middle: bool,
128    },
129}
130
131impl Cmd {
132    /// Tells if current command should reset kill ring.
133    #[must_use]
134    pub const fn should_reset_kill_ring(&self) -> bool {
135        match *self {
136            Self::Kill(Movement::BackwardChar(_) | Movement::ForwardChar(_)) => true,
137            Self::ClearScreen
138            | Self::Kill(_)
139            | Self::Replace(..)
140            | Self::Noop
141            | Self::Suspend
142            | Self::Yank(..)
143            | Self::YankPop => false,
144            _ => true,
145        }
146    }
147
148    const fn is_repeatable_change(&self) -> bool {
149        matches!(
150            *self,
151            Self::Dedent(..)
152                | Self::Indent(..)
153                | Self::Insert(..)
154                | Self::Kill(_)
155                | Self::ReplaceChar(..)
156                | Self::Replace(..)
157                | Self::SelfInsert(..)
158                | Self::ViYankTo(_)
159                | Self::Yank(..) // Cmd::TransposeChars | TODO Validate
160        )
161    }
162
163    const fn is_repeatable(&self) -> bool {
164        match *self {
165            Self::Move(_) => true,
166            _ => self.is_repeatable_change(),
167        }
168    }
169
170    // Replay this command with a possible different `RepeatCount`.
171    fn redo(&self, new: Option<RepeatCount>, wrt: &dyn Refresher) -> Self {
172        match *self {
173            Self::Dedent(ref mvt) => Self::Dedent(mvt.redo(new)),
174            Self::Indent(ref mvt) => Self::Indent(mvt.redo(new)),
175            Self::Insert(previous, ref text) => {
176                Self::Insert(repeat_count(previous, new), text.clone())
177            }
178            Self::Kill(ref mvt) => Self::Kill(mvt.redo(new)),
179            Self::Move(ref mvt) => Self::Move(mvt.redo(new)),
180            Self::ReplaceChar(previous, c) => Self::ReplaceChar(repeat_count(previous, new), c),
181            Self::Replace(ref mvt, ref text) => {
182                if text.is_none() {
183                    let last_insert = wrt.last_insert();
184                    if let Movement::ForwardChar(0) = mvt {
185                        Self::Replace(
186                            Movement::ForwardChar(
187                                RepeatCount::try_from(last_insert.as_ref().map_or(0, String::len))
188                                    .unwrap(),
189                            ),
190                            last_insert,
191                        )
192                    } else {
193                        Self::Replace(mvt.redo(new), last_insert)
194                    }
195                } else {
196                    Self::Replace(mvt.redo(new), text.clone())
197                }
198            }
199            Self::SelfInsert(previous, c) => {
200                // consecutive char inserts are repeatable not only the last one...
201                if let Some(text) = wrt.last_insert() {
202                    Self::Insert(repeat_count(previous, new), text)
203                } else {
204                    Self::SelfInsert(repeat_count(previous, new), c)
205                }
206            }
207            // Cmd::TransposeChars => Cmd::TransposeChars,
208            Self::ViYankTo(ref mvt) => Self::ViYankTo(mvt.redo(new)),
209            Self::Yank(previous, anchor) => Self::Yank(repeat_count(previous, new), anchor),
210            _ => unreachable!(),
211        }
212    }
213}
214
215const fn repeat_count(previous: RepeatCount, new: Option<RepeatCount>) -> RepeatCount {
216    match new {
217        Some(n) => n,
218        None => previous,
219    }
220}
221
222/// Different word definitions
223#[derive(Debug, Clone, Eq, PartialEq, Copy)]
224pub enum Word {
225    /// non-blanks characters
226    Big,
227    /// alphanumeric characters
228    Emacs,
229    /// alphanumeric (and '_') characters
230    Vi,
231}
232
233/// Where to move with respect to word boundary
234#[derive(Debug, Clone, Eq, PartialEq, Copy)]
235pub enum At {
236    /// Start of word.
237    Start,
238    /// Before end of word.
239    BeforeEnd,
240    /// After end of word.
241    AfterEnd,
242}
243
244/// Where to paste (relative to cursor position)
245#[derive(Debug, Clone, Eq, PartialEq, Copy)]
246pub enum Anchor {
247    /// After cursor
248    After,
249    /// Before cursor
250    Before,
251}
252
253/// character search
254#[derive(Debug, Clone, Eq, PartialEq, Copy)]
255pub enum CharSearch {
256    /// Forward search
257    Forward(char),
258    /// Forward search until
259    ForwardBefore(char),
260    /// Backward search
261    Backward(char),
262    /// Backward search until
263    BackwardAfter(char),
264}
265
266impl CharSearch {
267    const fn opposite(self) -> Self {
268        match self {
269            Self::Forward(c) => Self::Backward(c),
270            Self::ForwardBefore(c) => Self::BackwardAfter(c),
271            Self::Backward(c) => Self::Forward(c),
272            Self::BackwardAfter(c) => Self::ForwardBefore(c),
273        }
274    }
275}
276
277/// Where to move
278#[derive(Debug, Clone, Eq, PartialEq)]
279#[non_exhaustive]
280pub enum Movement {
281    /// Whole current line (not really a movement but a range)
282    WholeLine,
283    /// beginning-of-line
284    BeginningOfLine,
285    /// end-of-line
286    EndOfLine,
287    /// backward-word, vi-prev-word
288    BackwardWord(RepeatCount, Word), // Backward until start of word
289    /// forward-word, vi-end-word, vi-next-word
290    ForwardWord(RepeatCount, At, Word), // Forward until start/end of word
291    /// character-search, character-search-backward, vi-char-search
292    ViCharSearch(RepeatCount, CharSearch),
293    /// vi-first-print
294    ViFirstPrint,
295    /// backward-char
296    BackwardChar(RepeatCount),
297    /// forward-char
298    ForwardChar(RepeatCount),
299    /// move to the same column on the previous line
300    LineUp(RepeatCount),
301    /// move to the same column on the next line
302    LineDown(RepeatCount),
303    /// Whole user input (not really a movement but a range)
304    WholeBuffer,
305    /// beginning-of-buffer
306    BeginningOfBuffer,
307    /// end-of-buffer
308    EndOfBuffer,
309}
310
311impl Movement {
312    // Replay this movement with a possible different `RepeatCount`.
313    const fn redo(&self, new: Option<RepeatCount>) -> Self {
314        match *self {
315            Self::WholeLine => Self::WholeLine,
316            Self::BeginningOfLine => Self::BeginningOfLine,
317            Self::ViFirstPrint => Self::ViFirstPrint,
318            Self::EndOfLine => Self::EndOfLine,
319            Self::BackwardWord(previous, word) => {
320                Self::BackwardWord(repeat_count(previous, new), word)
321            }
322            Self::ForwardWord(previous, at, word) => {
323                Self::ForwardWord(repeat_count(previous, new), at, word)
324            }
325            Self::ViCharSearch(previous, char_search) => {
326                Self::ViCharSearch(repeat_count(previous, new), char_search)
327            }
328            Self::BackwardChar(previous) => Self::BackwardChar(repeat_count(previous, new)),
329            Self::ForwardChar(previous) => Self::ForwardChar(repeat_count(previous, new)),
330            Self::LineUp(previous) => Self::LineUp(repeat_count(previous, new)),
331            Self::LineDown(previous) => Self::LineDown(repeat_count(previous, new)),
332            Self::WholeBuffer => Self::WholeBuffer,
333            Self::BeginningOfBuffer => Self::BeginningOfBuffer,
334            Self::EndOfBuffer => Self::EndOfBuffer,
335        }
336    }
337}
338
339/// Vi input modes
340#[derive(Clone, Copy, Eq, PartialEq)]
341pub enum InputMode {
342    /// Vi Command/Alternate
343    Command,
344    /// Insert/Input mode
345    Insert,
346    /// Overwrite mode
347    Replace,
348}
349
350/// Transform key(s) to commands based on current input mode
351pub struct InputState<'b> {
352    pub(crate) mode: EditMode,
353    #[cfg_attr(not(feature = "custom-bindings"), expect(dead_code))]
354    custom_bindings: &'b Bindings,
355    pub(crate) input_mode: InputMode, // vi only ?
356    // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7
357    num_args: i16,
358    last_cmd: Cmd,                        // vi only
359    last_char_search: Option<CharSearch>, // vi only
360}
361
362/// Provide indirect mutation to user input.
363pub trait Invoke {
364    /// currently edited line
365    fn input(&self) -> &str;
366    // TODO
367    //fn invoke(&mut self, cmd: Cmd) -> Result<?>;
368}
369
370impl Invoke for &str {
371    fn input(&self) -> &str {
372        self
373    }
374}
375
376pub trait Refresher {
377    /// Rewrite the currently edited line accordingly to the buffer content,
378    /// cursor position, and number of columns of the terminal.
379    fn refresh_line(&mut self) -> Result<()>;
380    /// Same as [`refresh_line`] with a specific message instead of hint
381    fn refresh_line_with_msg(&mut self, msg: Option<&str>, kind: CmdKind) -> Result<()>;
382    /// Same as `refresh_line` but with a dynamic prompt.
383    fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>;
384    /// Vi only, switch to insert mode.
385    fn doing_insert(&mut self);
386    /// Vi only, switch to command mode.
387    fn done_inserting(&mut self);
388    /// Vi only, last text inserted.
389    fn last_insert(&self) -> Option<String>;
390    /// Returns `true` if the cursor is currently at the end of the line.
391    fn is_cursor_at_end(&self) -> bool;
392    /// Returns `true` if there is a hint displayed.
393    fn has_hint(&self) -> bool;
394    /// Returns the hint text that is shown after the current cursor position.
395    #[cfg_attr(not(feature = "custom-bindings"), expect(dead_code))]
396    fn hint_text(&self) -> Option<&str>;
397    /// currently edited line
398    fn line(&self) -> &str;
399    /// Current cursor position (byte position)
400    #[cfg_attr(not(feature = "custom-bindings"), expect(dead_code))]
401    fn pos(&self) -> usize;
402    /// Display `msg` above currently edited line.
403    fn external_print(&mut self, msg: String) -> Result<()>;
404}
405
406impl<'b> InputState<'b> {
407    pub fn new(config: &Config, custom_bindings: &'b Bindings) -> Self {
408        Self {
409            mode: config.edit_mode(),
410            custom_bindings,
411            input_mode: InputMode::Insert,
412            num_args: 0,
413            last_cmd: Cmd::Noop,
414            last_char_search: None,
415        }
416    }
417
418    pub fn is_emacs_mode(&self) -> bool {
419        self.mode == EditMode::Emacs
420    }
421
422    /// Parse user input into one command
423    /// `single_esc_abort` is used in emacs mode on unix platform when a single
424    /// esc key is expected to abort current action.
425    pub fn next_cmd(
426        &mut self,
427        rdr: &mut <Terminal as Term>::Reader,
428        wrt: &mut dyn Refresher,
429        single_esc_abort: bool,
430        ignore_external_print: bool,
431    ) -> Result<Cmd> {
432        let single_esc_abort = self.single_esc_abort(single_esc_abort);
433        let key;
434        if ignore_external_print {
435            key = rdr.next_key(single_esc_abort)?;
436        } else {
437            loop {
438                let event = rdr.wait_for_input(single_esc_abort)?;
439                match event {
440                    tty::Event::KeyPress(k) => {
441                        key = k;
442                        break;
443                    }
444                    tty::Event::ExternalPrint(msg) => {
445                        wrt.external_print(msg)?;
446                    }
447                    #[cfg(target_os = "macos")]
448                    _ => {}
449                }
450            }
451        }
452        match self.mode {
453            EditMode::Emacs => self.emacs(rdr, wrt, key),
454            EditMode::Vi if self.input_mode != InputMode::Command => self.vi_insert(rdr, wrt, key),
455            EditMode::Vi => self.vi_command(rdr, wrt, key),
456        }
457    }
458
459    fn single_esc_abort(&self, single_esc_abort: bool) -> bool {
460        match self.mode {
461            EditMode::Emacs => single_esc_abort,
462            EditMode::Vi => false,
463        }
464    }
465
466    /// Terminal peculiar binding
467    fn term_binding<R: RawReader>(rdr: &R, wrt: &dyn Refresher, key: &KeyEvent) -> Option<Cmd> {
468        let cmd = rdr.find_binding(key);
469        if cmd == Some(Cmd::EndOfFile) && !wrt.line().is_empty() {
470            None // ReadlineError::Eof only if line is empty
471        } else {
472            cmd
473        }
474    }
475
476    fn emacs_digit_argument<R: RawReader>(
477        &mut self,
478        rdr: &mut R,
479        wrt: &mut dyn Refresher,
480        digit: char,
481    ) -> Result<KeyEvent> {
482        #[expect(clippy::cast_possible_truncation)]
483        match digit {
484            '0'..='9' => {
485                self.num_args = digit.to_digit(10).unwrap() as i16;
486            }
487            '-' => {
488                self.num_args = -1;
489            }
490            _ => unreachable!(),
491        }
492        loop {
493            wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?;
494            let key = rdr.next_key(true)?;
495            #[expect(clippy::cast_possible_truncation)]
496            match key {
497                E(K::Char(digit @ '0'..='9'), m) if m == M::NONE || m == M::ALT => {
498                    if self.num_args == -1 {
499                        self.num_args *= digit.to_digit(10).unwrap() as i16;
500                    } else if self.num_args.abs() < 1000 {
501                        // shouldn't ever need more than 4 digits
502                        self.num_args = self
503                            .num_args
504                            .saturating_mul(10)
505                            .saturating_add(digit.to_digit(10).unwrap() as i16);
506                    }
507                }
508                E(K::Char('-'), m) if m == M::NONE || m == M::ALT => {}
509                _ => {
510                    wrt.refresh_line()?;
511                    return Ok(key);
512                }
513            };
514        }
515    }
516
517    fn emacs<R: RawReader>(
518        &mut self,
519        rdr: &mut R,
520        wrt: &mut dyn Refresher,
521        mut key: KeyEvent,
522    ) -> Result<Cmd> {
523        if let E(K::Char(digit @ '-'), M::ALT) = key {
524            key = self.emacs_digit_argument(rdr, wrt, digit)?;
525        } else if let E(K::Char(digit @ '0'..='9'), M::ALT) = key {
526            key = self.emacs_digit_argument(rdr, wrt, digit)?;
527        }
528        let (n, positive) = self.emacs_num_args(); // consume them in all cases
529
530        let mut evt = key.into();
531        if let Some(cmd) = self.custom_binding(wrt, &evt, n, positive) {
532            return Ok(if cmd.is_repeatable() {
533                cmd.redo(Some(n), wrt)
534            } else {
535                cmd
536            });
537        } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) {
538            return Ok(cmd);
539        }
540        let cmd = match key {
541            E(K::Char(c), M::NONE) => {
542                if positive {
543                    Cmd::SelfInsert(n, c)
544                } else {
545                    Cmd::Unknown
546                }
547            }
548            E(K::Char('A'), M::CTRL) => Cmd::Move(Movement::BeginningOfLine),
549            E(K::Char('B'), M::CTRL) => Cmd::Move(if positive {
550                Movement::BackwardChar(n)
551            } else {
552                Movement::ForwardChar(n)
553            }),
554            E(K::Char('E'), M::CTRL) => Cmd::Move(Movement::EndOfLine),
555            E(K::Char('F'), M::CTRL) => Cmd::Move(if positive {
556                Movement::ForwardChar(n)
557            } else {
558                Movement::BackwardChar(n)
559            }),
560            E(K::Char('G'), M::CTRL | M::CTRL_ALT) | E::ESC => Cmd::Abort,
561            E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(if positive {
562                Movement::BackwardChar(n)
563            } else {
564                Movement::ForwardChar(n)
565            }),
566            E(K::BackTab, M::NONE) => Cmd::CompleteBackward,
567            E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
568                if positive {
569                    Cmd::Complete
570                } else {
571                    Cmd::CompleteBackward
572                }
573            }
574            // Don't complete hints when the cursor is not at the end of a line
575            E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint,
576            E(K::Char('K'), M::CTRL) => Cmd::Kill(if positive {
577                Movement::EndOfLine
578            } else {
579                Movement::BeginningOfLine
580            }),
581            E(K::Char('L'), M::CTRL) => Cmd::ClearScreen,
582            E(K::Char('N'), M::CTRL) => Cmd::NextHistory,
583            E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory,
584            E(K::Char('X'), M::CTRL) => {
585                if let Some(cmd) = self.custom_seq_binding(rdr, wrt, &mut evt, n, positive)? {
586                    cmd
587                } else {
588                    let snd_key = match evt {
589                        // we may have already read the second key in custom_seq_binding
590                        #[allow(clippy::out_of_bounds_indexing)]
591                        Event::KeySeq(ref key_seq) if key_seq.len() > 1 => key_seq[1],
592                        _ => rdr.next_key(true)?,
593                    };
594                    match snd_key {
595                        E(K::Char('G'), M::CTRL) | E::ESC => Cmd::Abort,
596                        E(K::Char('U'), M::CTRL) => Cmd::Undo(n),
597                        E(K::Backspace, M::NONE) => Cmd::Kill(if positive {
598                            Movement::BeginningOfLine
599                        } else {
600                            Movement::EndOfLine
601                        }),
602                        _ => Cmd::Unknown,
603                    }
604                }
605            }
606            // character-search, character-search-backward
607            E(K::Char(']'), m @ (M::CTRL | M::CTRL_ALT)) => {
608                let ch = rdr.next_key(false)?;
609                match ch {
610                    E(K::Char(ch), M::NONE) => Cmd::Move(Movement::ViCharSearch(
611                        n,
612                        if positive {
613                            if m.contains(M::ALT) {
614                                CharSearch::Backward(ch)
615                            } else {
616                                CharSearch::ForwardBefore(ch)
617                            }
618                        } else if m.contains(M::ALT) {
619                            CharSearch::ForwardBefore(ch)
620                        } else {
621                            CharSearch::Backward(ch)
622                        },
623                    )),
624                    _ => Cmd::Unknown,
625                }
626            }
627            E(K::Backspace, M::ALT) => Cmd::Kill(if positive {
628                Movement::BackwardWord(n, Word::Emacs)
629            } else {
630                Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
631            }),
632            E(K::Char('<'), M::ALT) => Cmd::BeginningOfHistory,
633            E(K::Char('>'), M::ALT) => Cmd::EndOfHistory,
634            E(K::Char('B' | 'b') | K::Left, M::ALT) | E(K::Left, M::CTRL) => {
635                Cmd::Move(if positive {
636                    Movement::BackwardWord(n, Word::Emacs)
637                } else {
638                    Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
639                })
640            }
641            E(K::Char('C' | 'c'), M::ALT) => Cmd::CapitalizeWord,
642            E(K::Char('D' | 'd'), M::ALT) => Cmd::Kill(if positive {
643                Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
644            } else {
645                Movement::BackwardWord(n, Word::Emacs)
646            }),
647            E(K::Char('F' | 'f') | K::Right, M::ALT) | E(K::Right, M::CTRL) => {
648                Cmd::Move(if positive {
649                    Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
650                } else {
651                    Movement::BackwardWord(n, Word::Emacs)
652                })
653            }
654            E(K::Char('L' | 'l'), M::ALT) => Cmd::DowncaseWord,
655            E(K::Char('T' | 't'), M::ALT) => Cmd::TransposeWords(n),
656            // TODO ESC-R (r): Undo all changes made to this line.
657            E(K::Char('U' | 'u'), M::ALT) => Cmd::UpcaseWord,
658            E(K::Char('Y' | 'y'), M::ALT) => Cmd::YankPop,
659            _ => self.common(rdr, wrt, evt, key, n, positive)?,
660        };
661        debug!(target: "rustyline", "Emacs command: {cmd:?}");
662        Ok(cmd)
663    }
664
665    #[expect(clippy::cast_possible_truncation)]
666    fn vi_arg_digit<R: RawReader>(
667        &mut self,
668        rdr: &mut R,
669        wrt: &mut dyn Refresher,
670        digit: char,
671    ) -> Result<KeyEvent> {
672        self.num_args = digit.to_digit(10).unwrap() as i16;
673        loop {
674            wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?;
675            let key = rdr.next_key(false)?;
676            if let E(K::Char(digit @ '0'..='9'), M::NONE) = key {
677                if self.num_args.abs() < 1000 {
678                    // shouldn't ever need more than 4 digits
679                    self.num_args = self
680                        .num_args
681                        .saturating_mul(10)
682                        .saturating_add(digit.to_digit(10).unwrap() as i16);
683                }
684            } else {
685                wrt.refresh_line()?;
686                return Ok(key);
687            };
688        }
689    }
690
691    fn vi_command<R: RawReader>(
692        &mut self,
693        rdr: &mut R,
694        wrt: &mut dyn Refresher,
695        mut key: KeyEvent,
696    ) -> Result<Cmd> {
697        if let E(K::Char(digit @ '1'..='9'), M::NONE) = key {
698            key = self.vi_arg_digit(rdr, wrt, digit)?;
699        }
700        let no_num_args = self.num_args == 0;
701        let n = self.vi_num_args(); // consume them in all cases
702        let evt = key.into();
703        if let Some(cmd) = self.custom_binding(wrt, &evt, n, true) {
704            return Ok(if cmd.is_repeatable() {
705                if no_num_args {
706                    cmd.redo(None, wrt)
707                } else {
708                    cmd.redo(Some(n), wrt)
709                }
710            } else {
711                cmd
712            });
713        } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) {
714            return Ok(cmd);
715        }
716        let cmd = match key {
717            E(K::Char('$') | K::End, M::NONE) => Cmd::Move(Movement::EndOfLine),
718            E(K::Char('.'), M::NONE) => {
719                // vi-redo (repeat last command)
720                if !self.last_cmd.is_repeatable() {
721                    Cmd::Noop
722                } else if no_num_args {
723                    self.last_cmd.redo(None, wrt)
724                } else {
725                    self.last_cmd.redo(Some(n), wrt)
726                }
727            }
728            // TODO E(K::Char('%'), M::NONE) => Cmd::???, Move to the corresponding opening/closing
729            // bracket
730            E(K::Char('0'), M::NONE) => Cmd::Move(Movement::BeginningOfLine),
731            E(K::Char('^'), M::NONE) => Cmd::Move(Movement::ViFirstPrint),
732            E(K::Char('a'), M::NONE) => {
733                // vi-append-mode
734                self.input_mode = InputMode::Insert;
735                wrt.doing_insert();
736                Cmd::Move(Movement::ForwardChar(n))
737            }
738            E(K::Char('A'), M::NONE) => {
739                // vi-append-eol
740                self.input_mode = InputMode::Insert;
741                wrt.doing_insert();
742                Cmd::Move(Movement::EndOfLine)
743            }
744            E(K::Char('b'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), /* vi-prev-word */
745            E(K::Char('B'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Big)),
746            E(K::Char('c'), M::NONE) => {
747                self.input_mode = InputMode::Insert;
748                match self.vi_cmd_motion(rdr, wrt, key, n)? {
749                    Some(mvt) => Cmd::Replace(mvt, None),
750                    None => Cmd::Unknown,
751                }
752            }
753            E(K::Char('C'), M::NONE) => {
754                self.input_mode = InputMode::Insert;
755                Cmd::Replace(Movement::EndOfLine, None)
756            }
757            E(K::Char('d'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
758                Some(mvt) => Cmd::Kill(mvt),
759                None => Cmd::Unknown,
760            },
761            E(K::Char('D'), M::NONE) | E(K::Char('K'), M::CTRL) => Cmd::Kill(Movement::EndOfLine),
762            E(K::Char('e'), M::NONE) => {
763                Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi))
764            }
765            E(K::Char('E'), M::NONE) => {
766                Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big))
767            }
768            E(K::Char('i'), M::NONE) => {
769                // vi-insertion-mode
770                self.input_mode = InputMode::Insert;
771                wrt.doing_insert();
772                Cmd::Noop
773            }
774            E(K::Char('I'), M::NONE) => {
775                // vi-insert-beg
776                self.input_mode = InputMode::Insert;
777                wrt.doing_insert();
778                Cmd::Move(Movement::BeginningOfLine)
779            }
780            E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
781                // vi-char-search
782                let cs = self.vi_char_search(rdr, c)?;
783                match cs {
784                    Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)),
785                    None => Cmd::Unknown,
786                }
787            }
788            E(K::Char(';'), M::NONE) => match self.last_char_search {
789                Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)),
790                None => Cmd::Noop,
791            },
792            E(K::Char(','), M::NONE) => match self.last_char_search {
793                Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())),
794                None => Cmd::Noop,
795            },
796            // TODO E(K::Char('G'), M::NONE) => Cmd::???, Move to the history line n
797            E(K::Char('p'), M::NONE) => Cmd::Yank(n, Anchor::After), // vi-put
798            E(K::Char('P'), M::NONE) => Cmd::Yank(n, Anchor::Before), // vi-put
799            E(K::Char('r'), M::NONE) => {
800                // vi-replace-char:
801                let ch = rdr.next_key(false)?;
802                match ch {
803                    E(K::Char(c), M::NONE) => Cmd::ReplaceChar(n, c),
804                    E::ESC => Cmd::Noop,
805                    _ => Cmd::Unknown,
806                }
807            }
808            E(K::Char('R'), M::NONE) => {
809                //  vi-replace-mode (overwrite-mode)
810                self.input_mode = InputMode::Replace;
811                Cmd::Replace(Movement::ForwardChar(0), None)
812            }
813            E(K::Char('s'), M::NONE) => {
814                // vi-substitute-char:
815                self.input_mode = InputMode::Insert;
816                Cmd::Replace(Movement::ForwardChar(n), None)
817            }
818            E(K::Char('S'), M::NONE) => {
819                // vi-substitute-line:
820                self.input_mode = InputMode::Insert;
821                Cmd::Replace(Movement::WholeLine, None)
822            }
823            E(K::Char('u'), M::NONE) => Cmd::Undo(n),
824            // E(K::Char('U'), M::NONE) => Cmd::???, // revert-line
825            E(K::Char('w'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), /* vi-next-word */
826            E(K::Char('W'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), /* vi-next-word */
827            // TODO move backward if eol
828            E(K::Char('x'), M::NONE) => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete
829            E(K::Char('X'), M::NONE) => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout
830            E(K::Char('y'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
831                Some(mvt) => Cmd::ViYankTo(mvt),
832                None => Cmd::Unknown,
833            },
834            // E(K::Char('Y'), M::NONE) => Cmd::???, // vi-yank-to
835            E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => {
836                Cmd::Move(Movement::BackwardChar(n))
837            }
838            E(K::Char('G'), M::CTRL) => Cmd::Abort,
839            E(K::Char('l' | ' '), M::NONE) => Cmd::Move(Movement::ForwardChar(n)),
840            E(K::Char('L'), M::CTRL) => Cmd::ClearScreen,
841            E(K::Char('+' | 'j'), M::NONE) => Cmd::LineDownOrNextHistory(n),
842            // TODO: move to the start of the line.
843            E(K::Char('N'), M::CTRL) => Cmd::NextHistory,
844            E(K::Char('-' | 'k'), M::NONE) => Cmd::LineUpOrPreviousHistory(n),
845            // TODO: move to the start of the line.
846            E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory,
847            E(K::Char('R'), M::CTRL) => {
848                self.input_mode = InputMode::Insert; // TODO Validate
849                Cmd::ReverseSearchHistory
850            }
851            E(K::Char('S'), M::CTRL) => {
852                self.input_mode = InputMode::Insert; // TODO Validate
853                Cmd::ForwardSearchHistory
854            }
855            E(K::Char('<'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
856                Some(mvt) => Cmd::Dedent(mvt),
857                None => Cmd::Unknown,
858            },
859            E(K::Char('>'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
860                Some(mvt) => Cmd::Indent(mvt),
861                None => Cmd::Unknown,
862            },
863            E::ESC => Cmd::Noop,
864            _ => self.common(rdr, wrt, evt, key, n, true)?,
865        };
866        debug!(target: "rustyline", "Vi command: {cmd:?}");
867        if cmd.is_repeatable_change() {
868            self.last_cmd = cmd.clone();
869        }
870        Ok(cmd)
871    }
872
873    fn vi_insert<R: RawReader>(
874        &mut self,
875        rdr: &mut R,
876        wrt: &mut dyn Refresher,
877        key: KeyEvent,
878    ) -> Result<Cmd> {
879        let evt = key.into();
880        if let Some(cmd) = self.custom_binding(wrt, &evt, 0, true) {
881            return Ok(if cmd.is_repeatable() {
882                cmd.redo(None, wrt)
883            } else {
884                cmd
885            });
886        } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) {
887            return Ok(cmd);
888        }
889        let cmd = match key {
890            E(K::Char(c), M::NONE) => {
891                if self.input_mode == InputMode::Replace {
892                    Cmd::Overwrite(c)
893                } else {
894                    Cmd::SelfInsert(1, c)
895                }
896            }
897            E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(Movement::BackwardChar(1)),
898            E(K::BackTab, M::NONE) => Cmd::CompleteBackward,
899            E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => Cmd::Complete,
900            // Don't complete hints when the cursor is not at the end of a line
901            E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint,
902            E(K::Char(k), M::ALT) => {
903                debug!(target: "rustyline", "Vi fast command mode: {k}");
904                self.input_mode = InputMode::Command;
905                wrt.done_inserting();
906
907                self.vi_command(rdr, wrt, E(K::Char(k), M::NONE))?
908            }
909            E::ESC => {
910                // vi-movement-mode/vi-command-mode
911                self.input_mode = InputMode::Command;
912                wrt.done_inserting();
913                Cmd::Move(Movement::BackwardChar(1))
914            }
915            _ => self.common(rdr, wrt, evt, key, 1, true)?,
916        };
917        debug!(target: "rustyline", "Vi insert: {cmd:?}");
918        if cmd.is_repeatable_change() {
919            if let (Cmd::Replace(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) {
920                // replacing...
921            } else if let (Cmd::SelfInsert(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) {
922                // inserting...
923            } else {
924                self.last_cmd = cmd.clone();
925            }
926        }
927        Ok(cmd)
928    }
929
930    fn vi_cmd_motion<R: RawReader>(
931        &mut self,
932        rdr: &mut R,
933        wrt: &mut dyn Refresher,
934        key: KeyEvent,
935        n: RepeatCount,
936    ) -> Result<Option<Movement>> {
937        let mut mvt = rdr.next_key(false)?;
938        if mvt == key {
939            return Ok(Some(Movement::WholeLine));
940        }
941        let mut n = n;
942        if let E(K::Char(digit @ '1'..='9'), M::NONE) = mvt {
943            // vi-arg-digit
944            mvt = self.vi_arg_digit(rdr, wrt, digit)?;
945            n = self.vi_num_args().saturating_mul(n);
946        }
947        Ok(match mvt {
948            E(K::Char('$'), M::NONE) => Some(Movement::EndOfLine),
949            E(K::Char('0'), M::NONE) => Some(Movement::BeginningOfLine),
950            E(K::Char('^'), M::NONE) => Some(Movement::ViFirstPrint),
951            E(K::Char('b'), M::NONE) => Some(Movement::BackwardWord(n, Word::Vi)),
952            E(K::Char('B'), M::NONE) => Some(Movement::BackwardWord(n, Word::Big)),
953            E(K::Char('e'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)),
954            E(K::Char('E'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)),
955            E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
956                let cs = self.vi_char_search(rdr, c)?;
957                cs.map(|cs| Movement::ViCharSearch(n, cs))
958            }
959            E(K::Char(';'), M::NONE) => self
960                .last_char_search
961                .map(|cs| Movement::ViCharSearch(n, cs)),
962            E(K::Char(','), M::NONE) => self
963                .last_char_search
964                .map(|cs| Movement::ViCharSearch(n, cs.opposite())),
965            E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => {
966                Some(Movement::BackwardChar(n))
967            }
968            E(K::Char('l' | ' '), M::NONE) => Some(Movement::ForwardChar(n)),
969            E(K::Char('j' | '+'), M::NONE) => Some(Movement::LineDown(n)),
970            E(K::Char('k' | '-'), M::NONE) => Some(Movement::LineUp(n)),
971            E(K::Char('w'), M::NONE) => {
972                // 'cw' is 'ce'
973                if key == E(K::Char('c'), M::NONE) {
974                    Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi))
975                } else {
976                    Some(Movement::ForwardWord(n, At::Start, Word::Vi))
977                }
978            }
979            E(K::Char('W'), M::NONE) => {
980                // 'cW' is 'cE'
981                if key == E(K::Char('c'), M::NONE) {
982                    Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big))
983                } else {
984                    Some(Movement::ForwardWord(n, At::Start, Word::Big))
985                }
986            }
987            _ => None,
988        })
989    }
990
991    fn vi_char_search<R: RawReader>(
992        &mut self,
993        rdr: &mut R,
994        cmd: char,
995    ) -> Result<Option<CharSearch>> {
996        let ch = rdr.next_key(false)?;
997        Ok(match ch {
998            E(K::Char(ch), M::NONE) => {
999                let cs = match cmd {
1000                    'f' => CharSearch::Forward(ch),
1001                    't' => CharSearch::ForwardBefore(ch),
1002                    'F' => CharSearch::Backward(ch),
1003                    'T' => CharSearch::BackwardAfter(ch),
1004                    _ => unreachable!(),
1005                };
1006                self.last_char_search = Some(cs);
1007                Some(cs)
1008            }
1009            _ => None,
1010        })
1011    }
1012
1013    fn common<R: RawReader>(
1014        &mut self,
1015        rdr: &mut R,
1016        wrt: &dyn Refresher,
1017        mut evt: Event,
1018        key: KeyEvent,
1019        n: RepeatCount,
1020        positive: bool,
1021    ) -> Result<Cmd> {
1022        Ok(match key {
1023            E(K::Home, M::NONE) => Cmd::Move(Movement::BeginningOfLine),
1024            E(K::Left, M::NONE) => Cmd::Move(if positive {
1025                Movement::BackwardChar(n)
1026            } else {
1027                Movement::ForwardChar(n)
1028            }),
1029            #[cfg(any(windows, test))]
1030            E(K::Char('C'), M::CTRL) => Cmd::Interrupt,
1031            E(K::Char('D'), M::CTRL) => {
1032                if self.is_emacs_mode() && !wrt.line().is_empty() {
1033                    Cmd::Kill(if positive {
1034                        Movement::ForwardChar(n)
1035                    } else {
1036                        Movement::BackwardChar(n)
1037                    })
1038                } else if cfg!(windows) || cfg!(test) || !wrt.line().is_empty() {
1039                    Cmd::EndOfFile
1040                } else {
1041                    Cmd::Unknown
1042                }
1043            }
1044            E(K::Delete, M::NONE) => Cmd::Kill(if positive {
1045                Movement::ForwardChar(n)
1046            } else {
1047                Movement::BackwardChar(n)
1048            }),
1049            E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine),
1050            E(K::Right, M::NONE) => Cmd::Move(if positive {
1051                Movement::ForwardChar(n)
1052            } else {
1053                Movement::BackwardChar(n)
1054            }),
1055            E(K::Char('J' | 'M'), M::CTRL) | E::ENTER => Cmd::AcceptOrInsertLine {
1056                accept_in_the_middle: true,
1057            },
1058            E(K::Down, M::NONE) => Cmd::LineDownOrNextHistory(1),
1059            E(K::Up, M::NONE) => Cmd::LineUpOrPreviousHistory(1),
1060            E(K::Char('R'), M::CTRL) => Cmd::ReverseSearchHistory,
1061            // most terminals override Ctrl+S to suspend execution
1062            E(K::Char('S'), M::CTRL) => Cmd::ForwardSearchHistory,
1063            E(K::Char('T'), M::CTRL) => Cmd::TransposeChars,
1064            E(K::Char('U'), M::CTRL) => Cmd::Kill(if positive {
1065                Movement::BeginningOfLine
1066            } else {
1067                Movement::EndOfLine
1068            }),
1069            // most terminals override Ctrl+Q to resume execution
1070            E(K::Char('Q'), M::CTRL) => Cmd::QuotedInsert,
1071            #[cfg(not(windows))]
1072            E(K::Char('V'), M::CTRL) => Cmd::QuotedInsert,
1073            #[cfg(windows)]
1074            E(K::Char('V'), M::CTRL) => Cmd::PasteFromClipboard,
1075            E(K::Char('W'), M::CTRL) => Cmd::Kill(if positive {
1076                Movement::BackwardWord(n, Word::Big)
1077            } else {
1078                Movement::ForwardWord(n, At::AfterEnd, Word::Big)
1079            }),
1080            E(K::Char('Y'), M::CTRL) => {
1081                if positive {
1082                    Cmd::Yank(n, Anchor::Before)
1083                } else {
1084                    Cmd::Unknown // TODO Validate
1085                }
1086            }
1087            E(K::Char('_'), M::CTRL) => Cmd::Undo(n),
1088            E(K::UnknownEscSeq, M::NONE) => Cmd::Noop,
1089            E(K::BracketedPasteStart, M::NONE) => {
1090                let paste = rdr.read_pasted_text()?;
1091                Cmd::Insert(1, paste)
1092            }
1093            _ => self
1094                .custom_seq_binding(rdr, wrt, &mut evt, n, positive)?
1095                .unwrap_or(Cmd::Unknown),
1096        })
1097    }
1098
1099    fn num_args(&mut self) -> i16 {
1100        let num_args = match self.num_args {
1101            0 => 1,
1102            _ => self.num_args,
1103        };
1104        self.num_args = 0;
1105        num_args
1106    }
1107
1108    #[expect(clippy::cast_sign_loss)]
1109    fn emacs_num_args(&mut self) -> (RepeatCount, bool) {
1110        let num_args = self.num_args();
1111        if num_args < 0 {
1112            if let (n, false) = num_args.overflowing_abs() {
1113                (n as RepeatCount, false)
1114            } else {
1115                (RepeatCount::MAX, false)
1116            }
1117        } else {
1118            (num_args as RepeatCount, true)
1119        }
1120    }
1121
1122    fn vi_num_args(&mut self) -> RepeatCount {
1123        let num_args = self.num_args();
1124        if num_args < 0 {
1125            unreachable!()
1126        } else {
1127            num_args.unsigned_abs() as RepeatCount
1128        }
1129    }
1130}
1131
1132#[cfg(feature = "custom-bindings")]
1133impl InputState<'_> {
1134    /// Application customized binding
1135    fn custom_binding(
1136        &self,
1137        wrt: &dyn Refresher,
1138        evt: &Event,
1139        n: RepeatCount,
1140        positive: bool,
1141    ) -> Option<Cmd> {
1142        let bindings = self.custom_bindings;
1143        let handler = bindings.get(evt).or_else(|| bindings.get(&Event::Any));
1144        if let Some(handler) = handler {
1145            match handler {
1146                EventHandler::Simple(cmd) => Some(cmd.clone()),
1147                EventHandler::Conditional(handler) => {
1148                    let ctx = EventContext::new(self, wrt);
1149                    handler.handle(evt, n, positive, &ctx)
1150                }
1151            }
1152        } else {
1153            None
1154        }
1155    }
1156
1157    fn custom_seq_binding<R: RawReader>(
1158        &self,
1159        rdr: &mut R,
1160        wrt: &dyn Refresher,
1161        evt: &mut Event,
1162        n: RepeatCount,
1163        positive: bool,
1164    ) -> Result<Option<Cmd>> {
1165        while let Some(subtrie) = self.custom_bindings.get_raw_descendant(evt) {
1166            let snd_key = rdr.next_key(true)?;
1167            if let Event::KeySeq(ref mut key_seq) = evt {
1168                key_seq.push(snd_key);
1169            } else {
1170                break;
1171            }
1172            let handler = subtrie.get(evt).unwrap();
1173            if let Some(handler) = handler {
1174                let cmd = match handler {
1175                    EventHandler::Simple(cmd) => Some(cmd.clone()),
1176                    EventHandler::Conditional(handler) => {
1177                        let ctx = EventContext::new(self, wrt);
1178                        handler.handle(evt, n, positive, &ctx)
1179                    }
1180                };
1181                if cmd.is_some() {
1182                    return Ok(cmd);
1183                }
1184            }
1185        }
1186        Ok(None)
1187    }
1188}
1189
1190#[cfg(not(feature = "custom-bindings"))]
1191impl<'b> InputState<'b> {
1192    fn custom_binding(&self, _: &dyn Refresher, _: &Event, _: RepeatCount, _: bool) -> Option<Cmd> {
1193        None
1194    }
1195
1196    fn custom_seq_binding<R: RawReader>(
1197        &self,
1198        _: &mut R,
1199        _: &dyn Refresher,
1200        _: &mut Event,
1201        _: RepeatCount,
1202        _: bool,
1203    ) -> Result<Option<Cmd>> {
1204        Ok(None)
1205    }
1206}
1207
1208cfg_if::cfg_if! {
1209    if #[cfg(feature = "custom-bindings")] {
1210pub type Bindings = radix_trie::Trie<Event, EventHandler>;
1211    } else {
1212enum Event {
1213   KeySeq([KeyEvent; 1]),
1214}
1215impl From<KeyEvent> for Event {
1216    fn from(k: KeyEvent) -> Self {
1217        Self::KeySeq([k])
1218    }
1219}
1220pub struct Bindings {}
1221impl Bindings {
1222    pub fn new() -> Self {
1223        Self {}
1224    }
1225}
1226    }
1227}