Skip to main content

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    pub fn is_vi_cmd_mode(&self) -> bool {
423        self.input_mode == InputMode::Command && self.mode == EditMode::Vi
424    }
425
426    /// Parse user input into one command
427    /// `single_esc_abort` is used in emacs mode on unix platform when a single
428    /// esc key is expected to abort current action.
429    pub fn next_cmd(
430        &mut self,
431        rdr: &mut <Terminal as Term>::Reader,
432        wrt: &mut dyn Refresher,
433        single_esc_abort: bool,
434        ignore_external_print: bool,
435    ) -> Result<Cmd> {
436        let single_esc_abort = self.single_esc_abort(single_esc_abort);
437        let key;
438        if ignore_external_print {
439            key = rdr.next_key(single_esc_abort)?;
440        } else {
441            loop {
442                let event = rdr.wait_for_input(single_esc_abort)?;
443                match event {
444                    tty::Event::KeyPress(k) => {
445                        key = k;
446                        break;
447                    }
448                    tty::Event::ExternalPrint(msg) => {
449                        wrt.external_print(msg)?;
450                    }
451                    #[cfg(target_os = "macos")]
452                    _ => {}
453                }
454            }
455        }
456        match self.mode {
457            EditMode::Emacs => self.emacs(rdr, wrt, key),
458            EditMode::Vi if self.input_mode != InputMode::Command => self.vi_insert(rdr, wrt, key),
459            EditMode::Vi => self.vi_command(rdr, wrt, key),
460        }
461    }
462
463    fn single_esc_abort(&self, single_esc_abort: bool) -> bool {
464        match self.mode {
465            EditMode::Emacs => single_esc_abort,
466            EditMode::Vi => false,
467        }
468    }
469
470    /// Terminal peculiar binding
471    fn term_binding<R: RawReader>(rdr: &R, wrt: &dyn Refresher, key: &KeyEvent) -> Option<Cmd> {
472        let cmd = rdr.find_binding(key);
473        if cmd == Some(Cmd::EndOfFile) && !wrt.line().is_empty() {
474            None // ReadlineError::Eof only if line is empty
475        } else {
476            cmd
477        }
478    }
479
480    fn emacs_digit_argument<R: RawReader>(
481        &mut self,
482        rdr: &mut R,
483        wrt: &mut dyn Refresher,
484        digit: char,
485    ) -> Result<KeyEvent> {
486        #[expect(clippy::cast_possible_truncation)]
487        match digit {
488            '0'..='9' => {
489                self.num_args = digit.to_digit(10).unwrap() as i16;
490            }
491            '-' => {
492                self.num_args = -1;
493            }
494            _ => unreachable!(),
495        }
496        loop {
497            wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?;
498            let key = rdr.next_key(true)?;
499            #[expect(clippy::cast_possible_truncation)]
500            match key {
501                E(K::Char(digit @ '0'..='9'), m) if m == M::NONE || m == M::ALT => {
502                    if self.num_args == -1 {
503                        self.num_args *= digit.to_digit(10).unwrap() as i16;
504                    } else if self.num_args.abs() < 1000 {
505                        // shouldn't ever need more than 4 digits
506                        self.num_args = self
507                            .num_args
508                            .saturating_mul(10)
509                            .saturating_add(digit.to_digit(10).unwrap() as i16);
510                    }
511                }
512                E(K::Char('-'), m) if m == M::NONE || m == M::ALT => {}
513                _ => {
514                    wrt.refresh_line()?;
515                    return Ok(key);
516                }
517            };
518        }
519    }
520
521    fn emacs<R: RawReader>(
522        &mut self,
523        rdr: &mut R,
524        wrt: &mut dyn Refresher,
525        mut key: KeyEvent,
526    ) -> Result<Cmd> {
527        if let E(K::Char(digit @ '-'), M::ALT) = key {
528            key = self.emacs_digit_argument(rdr, wrt, digit)?;
529        } else if let E(K::Char(digit @ '0'..='9'), M::ALT) = key {
530            key = self.emacs_digit_argument(rdr, wrt, digit)?;
531        }
532        let (n, positive) = self.emacs_num_args(); // consume them in all cases
533
534        let mut evt = key.into();
535        if let Some(cmd) = self.custom_binding(wrt, &evt, n, positive) {
536            return Ok(if cmd.is_repeatable() {
537                cmd.redo(Some(n), wrt)
538            } else {
539                cmd
540            });
541        } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) {
542            return Ok(cmd);
543        }
544        let cmd = match key {
545            E(K::Char(c), M::NONE) => {
546                if positive {
547                    Cmd::SelfInsert(n, c)
548                } else {
549                    Cmd::Unknown
550                }
551            }
552            E(K::Char('A'), M::CTRL) => Cmd::Move(Movement::BeginningOfLine),
553            E(K::Char('B'), M::CTRL) => Cmd::Move(if positive {
554                Movement::BackwardChar(n)
555            } else {
556                Movement::ForwardChar(n)
557            }),
558            E(K::Char('E'), M::CTRL) => Cmd::Move(Movement::EndOfLine),
559            E(K::Char('F'), M::CTRL) => Cmd::Move(if positive {
560                Movement::ForwardChar(n)
561            } else {
562                Movement::BackwardChar(n)
563            }),
564            E(K::Char('G'), M::CTRL | M::CTRL_ALT) | E::ESC => Cmd::Abort,
565            E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(if positive {
566                Movement::BackwardChar(n)
567            } else {
568                Movement::ForwardChar(n)
569            }),
570            E(K::BackTab, M::NONE) => Cmd::CompleteBackward,
571            E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
572                if positive {
573                    Cmd::Complete
574                } else {
575                    Cmd::CompleteBackward
576                }
577            }
578            // Don't complete hints when the cursor is not at the end of a line
579            E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint,
580            E(K::Char('K'), M::CTRL) => Cmd::Kill(if positive {
581                Movement::EndOfLine
582            } else {
583                Movement::BeginningOfLine
584            }),
585            E(K::Char('L'), M::CTRL) => Cmd::ClearScreen,
586            E(K::Char('N'), M::CTRL) => Cmd::NextHistory,
587            E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory,
588            E(K::Char('X'), M::CTRL) => {
589                if let Some(cmd) = self.custom_seq_binding(rdr, wrt, &mut evt, n, positive)? {
590                    cmd
591                } else {
592                    let snd_key = match evt {
593                        // we may have already read the second key in custom_seq_binding
594                        #[allow(clippy::out_of_bounds_indexing)]
595                        Event::KeySeq(ref key_seq) if key_seq.len() > 1 => key_seq[1],
596                        _ => rdr.next_key(true)?,
597                    };
598                    match snd_key {
599                        E(K::Char('G'), M::CTRL) | E::ESC => Cmd::Abort,
600                        E(K::Char('U'), M::CTRL) => Cmd::Undo(n),
601                        E(K::Backspace, M::NONE) => Cmd::Kill(if positive {
602                            Movement::BeginningOfLine
603                        } else {
604                            Movement::EndOfLine
605                        }),
606                        _ => Cmd::Unknown,
607                    }
608                }
609            }
610            // character-search, character-search-backward
611            E(K::Char(']'), m @ (M::CTRL | M::CTRL_ALT)) => {
612                let ch = rdr.next_key(false)?;
613                match ch {
614                    E(K::Char(ch), M::NONE) => Cmd::Move(Movement::ViCharSearch(
615                        n,
616                        if positive {
617                            if m.contains(M::ALT) {
618                                CharSearch::Backward(ch)
619                            } else {
620                                CharSearch::ForwardBefore(ch)
621                            }
622                        } else if m.contains(M::ALT) {
623                            CharSearch::ForwardBefore(ch)
624                        } else {
625                            CharSearch::Backward(ch)
626                        },
627                    )),
628                    _ => Cmd::Unknown,
629                }
630            }
631            E(K::Backspace, M::ALT) => Cmd::Kill(if positive {
632                Movement::BackwardWord(n, Word::Emacs)
633            } else {
634                Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
635            }),
636            E(K::Char('<'), M::ALT) => Cmd::BeginningOfHistory,
637            E(K::Char('>'), M::ALT) => Cmd::EndOfHistory,
638            E(K::Char('B' | 'b') | K::Left, M::ALT) | E(K::Left, M::CTRL) => {
639                Cmd::Move(if positive {
640                    Movement::BackwardWord(n, Word::Emacs)
641                } else {
642                    Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
643                })
644            }
645            E(K::Char('C' | 'c'), M::ALT) => Cmd::CapitalizeWord,
646            E(K::Char('D' | 'd'), M::ALT) => Cmd::Kill(if positive {
647                Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
648            } else {
649                Movement::BackwardWord(n, Word::Emacs)
650            }),
651            E(K::Char('F' | 'f') | K::Right, M::ALT) | E(K::Right, M::CTRL) => {
652                Cmd::Move(if positive {
653                    Movement::ForwardWord(n, At::AfterEnd, Word::Emacs)
654                } else {
655                    Movement::BackwardWord(n, Word::Emacs)
656                })
657            }
658            E(K::Char('L' | 'l'), M::ALT) => Cmd::DowncaseWord,
659            E(K::Char('T' | 't'), M::ALT) => Cmd::TransposeWords(n),
660            // TODO ESC-R (r): Undo all changes made to this line.
661            E(K::Char('U' | 'u'), M::ALT) => Cmd::UpcaseWord,
662            E(K::Char('Y' | 'y'), M::ALT) => Cmd::YankPop,
663            _ => self.common(rdr, wrt, evt, key, n, positive)?,
664        };
665        debug!(target: "rustyline", "Emacs command: {cmd:?}");
666        Ok(cmd)
667    }
668
669    #[expect(clippy::cast_possible_truncation)]
670    fn vi_arg_digit<R: RawReader>(
671        &mut self,
672        rdr: &mut R,
673        wrt: &mut dyn Refresher,
674        digit: char,
675    ) -> Result<KeyEvent> {
676        self.num_args = digit.to_digit(10).unwrap() as i16;
677        loop {
678            wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args))?;
679            let key = rdr.next_key(false)?;
680            if let E(K::Char(digit @ '0'..='9'), M::NONE) = key {
681                if self.num_args.abs() < 1000 {
682                    // shouldn't ever need more than 4 digits
683                    self.num_args = self
684                        .num_args
685                        .saturating_mul(10)
686                        .saturating_add(digit.to_digit(10).unwrap() as i16);
687                }
688            } else {
689                wrt.refresh_line()?;
690                return Ok(key);
691            }
692        }
693    }
694
695    fn vi_command<R: RawReader>(
696        &mut self,
697        rdr: &mut R,
698        wrt: &mut dyn Refresher,
699        mut key: KeyEvent,
700    ) -> Result<Cmd> {
701        if let E(K::Char(digit @ '1'..='9'), M::NONE) = key {
702            key = self.vi_arg_digit(rdr, wrt, digit)?;
703        }
704        let no_num_args = self.num_args == 0;
705        let n = self.vi_num_args(); // consume them in all cases
706        let evt = key.into();
707        if let Some(cmd) = self.custom_binding(wrt, &evt, n, true) {
708            return Ok(if cmd.is_repeatable() {
709                if no_num_args {
710                    cmd.redo(None, wrt)
711                } else {
712                    cmd.redo(Some(n), wrt)
713                }
714            } else {
715                cmd
716            });
717        } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) {
718            return Ok(cmd);
719        }
720        let cmd = match key {
721            E(K::Char('$') | K::End, M::NONE) => Cmd::Move(Movement::EndOfLine),
722            E(K::Char('.'), M::NONE) => {
723                // vi-redo (repeat last command)
724                if !self.last_cmd.is_repeatable() {
725                    Cmd::Noop
726                } else if no_num_args {
727                    self.last_cmd.redo(None, wrt)
728                } else {
729                    self.last_cmd.redo(Some(n), wrt)
730                }
731            }
732            // TODO E(K::Char('%'), M::NONE) => Cmd::???, Move to the corresponding opening/closing
733            // bracket
734            E(K::Char('0'), M::NONE) => Cmd::Move(Movement::BeginningOfLine),
735            E(K::Char('^'), M::NONE) => Cmd::Move(Movement::ViFirstPrint),
736            E(K::Char('a'), M::NONE) => {
737                // vi-append-mode
738                self.input_mode = InputMode::Insert;
739                wrt.doing_insert();
740                Cmd::Move(Movement::ForwardChar(n))
741            }
742            E(K::Char('A'), M::NONE) => {
743                // vi-append-eol
744                self.input_mode = InputMode::Insert;
745                wrt.doing_insert();
746                Cmd::Move(Movement::EndOfLine)
747            }
748            E(K::Char('b'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), /* vi-prev-word */
749            E(K::Char('B'), M::NONE) => Cmd::Move(Movement::BackwardWord(n, Word::Big)),
750            E(K::Char('c'), M::NONE) => {
751                self.input_mode = InputMode::Insert;
752                match self.vi_cmd_motion(rdr, wrt, key, n)? {
753                    Some(mvt) => Cmd::Replace(mvt, None),
754                    None => Cmd::Unknown,
755                }
756            }
757            E(K::Char('C'), M::NONE) => {
758                self.input_mode = InputMode::Insert;
759                Cmd::Replace(Movement::EndOfLine, None)
760            }
761            E(K::Char('d'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
762                Some(mvt) => Cmd::Kill(mvt),
763                None => Cmd::Unknown,
764            },
765            E(K::Char('D'), M::NONE) | E(K::Char('K'), M::CTRL) => Cmd::Kill(Movement::EndOfLine),
766            E(K::Char('e'), M::NONE) => {
767                Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi))
768            }
769            E(K::Char('E'), M::NONE) => {
770                Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big))
771            }
772            E(K::Char('i'), M::NONE) => {
773                // vi-insertion-mode
774                self.input_mode = InputMode::Insert;
775                wrt.doing_insert();
776                Cmd::Noop
777            }
778            E(K::Char('I'), M::NONE) => {
779                // vi-insert-beg
780                self.input_mode = InputMode::Insert;
781                wrt.doing_insert();
782                Cmd::Move(Movement::BeginningOfLine)
783            }
784            E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
785                // vi-char-search
786                let cs = self.vi_char_search(rdr, c)?;
787                match cs {
788                    Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)),
789                    None => Cmd::Unknown,
790                }
791            }
792            E(K::Char(';'), M::NONE) => match self.last_char_search {
793                Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)),
794                None => Cmd::Noop,
795            },
796            E(K::Char(','), M::NONE) => match self.last_char_search {
797                Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())),
798                None => Cmd::Noop,
799            },
800            // TODO E(K::Char('G'), M::NONE) => Cmd::???, Move to the history line n
801            E(K::Char('p'), M::NONE) => Cmd::Yank(n, Anchor::After), // vi-put
802            E(K::Char('P'), M::NONE) => Cmd::Yank(n, Anchor::Before), // vi-put
803            E(K::Char('r'), M::NONE) => {
804                // vi-replace-char:
805                let ch = rdr.next_key(false)?;
806                match ch {
807                    E(K::Char(c), M::NONE) => Cmd::ReplaceChar(n, c),
808                    E::ESC => Cmd::Noop,
809                    _ => Cmd::Unknown,
810                }
811            }
812            E(K::Char('R'), M::NONE) => {
813                //  vi-replace-mode (overwrite-mode)
814                self.input_mode = InputMode::Replace;
815                Cmd::Replace(Movement::ForwardChar(0), None)
816            }
817            E(K::Char('s'), M::NONE) => {
818                // vi-substitute-char:
819                self.input_mode = InputMode::Insert;
820                Cmd::Replace(Movement::ForwardChar(n), None)
821            }
822            E(K::Char('S'), M::NONE) => {
823                // vi-substitute-line:
824                self.input_mode = InputMode::Insert;
825                Cmd::Replace(Movement::WholeLine, None)
826            }
827            E(K::Char('u'), M::NONE) => Cmd::Undo(n),
828            // E(K::Char('U'), M::NONE) => Cmd::???, // revert-line
829            E(K::Char('w'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), /* vi-next-word */
830            E(K::Char('W'), M::NONE) => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), /* vi-next-word */
831            // TODO move backward if eol
832            E(K::Char('x'), M::NONE) => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete
833            E(K::Char('X'), M::NONE) => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout
834            E(K::Char('y'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
835                Some(mvt) => Cmd::ViYankTo(mvt),
836                None => Cmd::Unknown,
837            },
838            // E(K::Char('Y'), M::NONE) => Cmd::???, // vi-yank-to
839            E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => {
840                Cmd::Move(Movement::BackwardChar(n))
841            }
842            E(K::Char('G'), M::CTRL) => Cmd::Abort,
843            E(K::Char('l' | ' '), M::NONE) => Cmd::Move(Movement::ForwardChar(n)),
844            E(K::Char('L'), M::CTRL) => Cmd::ClearScreen,
845            E(K::Char('+' | 'j'), M::NONE) => Cmd::LineDownOrNextHistory(n),
846            // TODO: move to the start of the line.
847            E(K::Char('N'), M::CTRL) => Cmd::NextHistory,
848            E(K::Char('-' | 'k'), M::NONE) => Cmd::LineUpOrPreviousHistory(n),
849            // TODO: move to the start of the line.
850            E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory,
851            E(K::Char('R'), M::CTRL) => {
852                self.input_mode = InputMode::Insert; // TODO Validate
853                Cmd::ReverseSearchHistory
854            }
855            E(K::Char('S'), M::CTRL) => {
856                self.input_mode = InputMode::Insert; // TODO Validate
857                Cmd::ForwardSearchHistory
858            }
859            E(K::Char('<'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
860                Some(mvt) => Cmd::Dedent(mvt),
861                None => Cmd::Unknown,
862            },
863            E(K::Char('>'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
864                Some(mvt) => Cmd::Indent(mvt),
865                None => Cmd::Unknown,
866            },
867            E::ESC => Cmd::Noop,
868            _ => self.common(rdr, wrt, evt, key, n, true)?,
869        };
870        debug!(target: "rustyline", "Vi command: {cmd:?}");
871        if cmd.is_repeatable_change() {
872            self.last_cmd = cmd.clone();
873        }
874        Ok(cmd)
875    }
876
877    fn vi_insert<R: RawReader>(
878        &mut self,
879        rdr: &mut R,
880        wrt: &mut dyn Refresher,
881        key: KeyEvent,
882    ) -> Result<Cmd> {
883        let evt = key.into();
884        if let Some(cmd) = self.custom_binding(wrt, &evt, 0, true) {
885            return Ok(if cmd.is_repeatable() {
886                cmd.redo(None, wrt)
887            } else {
888                cmd
889            });
890        } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) {
891            return Ok(cmd);
892        }
893        let cmd = match key {
894            E(K::Char(c), M::NONE) => {
895                if self.input_mode == InputMode::Replace {
896                    Cmd::Overwrite(c)
897                } else {
898                    Cmd::SelfInsert(1, c)
899                }
900            }
901            E(K::Char('H'), M::CTRL) | E::BACKSPACE => Cmd::Kill(Movement::BackwardChar(1)),
902            E(K::BackTab, M::NONE) => Cmd::CompleteBackward,
903            E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => Cmd::Complete,
904            // Don't complete hints when the cursor is not at the end of a line
905            E(K::Right, M::NONE) if wrt.has_hint() && wrt.is_cursor_at_end() => Cmd::CompleteHint,
906            E(K::Char(k), M::ALT) => {
907                debug!(target: "rustyline", "Vi fast command mode: {k}");
908                self.input_mode = InputMode::Command;
909                wrt.done_inserting();
910
911                self.vi_command(rdr, wrt, E(K::Char(k), M::NONE))?
912            }
913            E::ESC => {
914                // vi-movement-mode/vi-command-mode
915                self.input_mode = InputMode::Command;
916                wrt.done_inserting();
917                Cmd::Move(Movement::BackwardChar(1))
918            }
919            _ => self.common(rdr, wrt, evt, key, 1, true)?,
920        };
921        debug!(target: "rustyline", "Vi insert: {cmd:?}");
922        if cmd.is_repeatable_change() {
923            if let (Cmd::Replace(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) {
924                // replacing...
925            } else if let (Cmd::SelfInsert(..), Cmd::SelfInsert(..)) = (&self.last_cmd, &cmd) {
926                // inserting...
927            } else {
928                self.last_cmd = cmd.clone();
929            }
930        }
931        Ok(cmd)
932    }
933
934    fn vi_cmd_motion<R: RawReader>(
935        &mut self,
936        rdr: &mut R,
937        wrt: &mut dyn Refresher,
938        key: KeyEvent,
939        n: RepeatCount,
940    ) -> Result<Option<Movement>> {
941        let mut mvt = rdr.next_key(false)?;
942        if mvt == key {
943            return Ok(Some(Movement::WholeLine));
944        }
945        let mut n = n;
946        if let E(K::Char(digit @ '1'..='9'), M::NONE) = mvt {
947            // vi-arg-digit
948            mvt = self.vi_arg_digit(rdr, wrt, digit)?;
949            n = self.vi_num_args().saturating_mul(n);
950        }
951        Ok(match mvt {
952            E(K::Char('$'), M::NONE) => Some(Movement::EndOfLine),
953            E(K::Char('0'), M::NONE) => Some(Movement::BeginningOfLine),
954            E(K::Char('^'), M::NONE) => Some(Movement::ViFirstPrint),
955            E(K::Char('b'), M::NONE) => Some(Movement::BackwardWord(n, Word::Vi)),
956            E(K::Char('B'), M::NONE) => Some(Movement::BackwardWord(n, Word::Big)),
957            E(K::Char('e'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)),
958            E(K::Char('E'), M::NONE) => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)),
959            E(K::Char(c), M::NONE) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
960                let cs = self.vi_char_search(rdr, c)?;
961                cs.map(|cs| Movement::ViCharSearch(n, cs))
962            }
963            E(K::Char(';'), M::NONE) => self
964                .last_char_search
965                .map(|cs| Movement::ViCharSearch(n, cs)),
966            E(K::Char(','), M::NONE) => self
967                .last_char_search
968                .map(|cs| Movement::ViCharSearch(n, cs.opposite())),
969            E(K::Char('h'), M::NONE) | E(K::Char('H'), M::CTRL) | E::BACKSPACE => {
970                Some(Movement::BackwardChar(n))
971            }
972            E(K::Char('l' | ' '), M::NONE) => Some(Movement::ForwardChar(n)),
973            E(K::Char('j' | '+'), M::NONE) => Some(Movement::LineDown(n)),
974            E(K::Char('k' | '-'), M::NONE) => Some(Movement::LineUp(n)),
975            E(K::Char('w'), M::NONE) => {
976                // 'cw' is 'ce'
977                if key == E(K::Char('c'), M::NONE) {
978                    Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi))
979                } else {
980                    Some(Movement::ForwardWord(n, At::Start, Word::Vi))
981                }
982            }
983            E(K::Char('W'), M::NONE) => {
984                // 'cW' is 'cE'
985                if key == E(K::Char('c'), M::NONE) {
986                    Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big))
987                } else {
988                    Some(Movement::ForwardWord(n, At::Start, Word::Big))
989                }
990            }
991            _ => None,
992        })
993    }
994
995    fn vi_char_search<R: RawReader>(
996        &mut self,
997        rdr: &mut R,
998        cmd: char,
999    ) -> Result<Option<CharSearch>> {
1000        let ch = rdr.next_key(false)?;
1001        Ok(match ch {
1002            E(K::Char(ch), M::NONE) => {
1003                let cs = match cmd {
1004                    'f' => CharSearch::Forward(ch),
1005                    't' => CharSearch::ForwardBefore(ch),
1006                    'F' => CharSearch::Backward(ch),
1007                    'T' => CharSearch::BackwardAfter(ch),
1008                    _ => unreachable!(),
1009                };
1010                self.last_char_search = Some(cs);
1011                Some(cs)
1012            }
1013            _ => None,
1014        })
1015    }
1016
1017    fn common<R: RawReader>(
1018        &mut self,
1019        rdr: &mut R,
1020        wrt: &dyn Refresher,
1021        mut evt: Event,
1022        key: KeyEvent,
1023        n: RepeatCount,
1024        positive: bool,
1025    ) -> Result<Cmd> {
1026        Ok(match key {
1027            E(K::Home, M::NONE) => Cmd::Move(Movement::BeginningOfLine),
1028            E(K::Left, M::NONE) => Cmd::Move(if positive {
1029                Movement::BackwardChar(n)
1030            } else {
1031                Movement::ForwardChar(n)
1032            }),
1033            #[cfg(any(windows, test))]
1034            E(K::Char('C'), M::CTRL) => Cmd::Interrupt,
1035            E(K::Char('D'), M::CTRL) => {
1036                if self.is_emacs_mode() && !wrt.line().is_empty() {
1037                    Cmd::Kill(if positive {
1038                        Movement::ForwardChar(n)
1039                    } else {
1040                        Movement::BackwardChar(n)
1041                    })
1042                } else if cfg!(windows) || cfg!(test) || !wrt.line().is_empty() {
1043                    Cmd::EndOfFile
1044                } else {
1045                    Cmd::Unknown
1046                }
1047            }
1048            E(K::Delete, M::NONE) => Cmd::Kill(if positive {
1049                Movement::ForwardChar(n)
1050            } else {
1051                Movement::BackwardChar(n)
1052            }),
1053            E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine),
1054            E(K::Right, M::NONE) => Cmd::Move(if positive {
1055                Movement::ForwardChar(n)
1056            } else {
1057                Movement::BackwardChar(n)
1058            }),
1059            E(K::Char('J' | 'M'), M::CTRL) | E::ENTER => Cmd::AcceptOrInsertLine {
1060                accept_in_the_middle: true,
1061            },
1062            E(K::Down, M::NONE) => Cmd::LineDownOrNextHistory(1),
1063            E(K::Up, M::NONE) => Cmd::LineUpOrPreviousHistory(1),
1064            E(K::Char('R'), M::CTRL) => Cmd::ReverseSearchHistory,
1065            // most terminals override Ctrl+S to suspend execution
1066            E(K::Char('S'), M::CTRL) => Cmd::ForwardSearchHistory,
1067            E(K::Char('T'), M::CTRL) => Cmd::TransposeChars,
1068            E(K::Char('U'), M::CTRL) => Cmd::Kill(if positive {
1069                Movement::BeginningOfLine
1070            } else {
1071                Movement::EndOfLine
1072            }),
1073            // most terminals override Ctrl+Q to resume execution
1074            E(K::Char('Q'), M::CTRL) => Cmd::QuotedInsert,
1075            #[cfg(not(windows))]
1076            E(K::Char('V'), M::CTRL) => Cmd::QuotedInsert,
1077            #[cfg(windows)]
1078            E(K::Char('V'), M::CTRL) => Cmd::PasteFromClipboard,
1079            E(K::Char('W'), M::CTRL) => Cmd::Kill(if positive {
1080                Movement::BackwardWord(n, Word::Big)
1081            } else {
1082                Movement::ForwardWord(n, At::AfterEnd, Word::Big)
1083            }),
1084            E(K::Char('Y'), M::CTRL) => {
1085                if positive {
1086                    Cmd::Yank(n, Anchor::Before)
1087                } else {
1088                    Cmd::Unknown // TODO Validate
1089                }
1090            }
1091            E(K::Char('_'), M::CTRL) => Cmd::Undo(n),
1092            E(K::UnknownEscSeq, M::NONE) => Cmd::Noop,
1093            E(K::BracketedPasteStart, M::NONE) => {
1094                let paste = rdr.read_pasted_text()?;
1095                Cmd::Insert(1, paste)
1096            }
1097            _ => self
1098                .custom_seq_binding(rdr, wrt, &mut evt, n, positive)?
1099                .unwrap_or(Cmd::Unknown),
1100        })
1101    }
1102
1103    fn num_args(&mut self) -> i16 {
1104        let num_args = match self.num_args {
1105            0 => 1,
1106            _ => self.num_args,
1107        };
1108        self.num_args = 0;
1109        num_args
1110    }
1111
1112    #[expect(clippy::cast_sign_loss)]
1113    fn emacs_num_args(&mut self) -> (RepeatCount, bool) {
1114        let num_args = self.num_args();
1115        if num_args < 0 {
1116            if let (n, false) = num_args.overflowing_abs() {
1117                (n as RepeatCount, false)
1118            } else {
1119                (RepeatCount::MAX, false)
1120            }
1121        } else {
1122            (num_args as RepeatCount, true)
1123        }
1124    }
1125
1126    fn vi_num_args(&mut self) -> RepeatCount {
1127        let num_args = self.num_args();
1128        if num_args < 0 {
1129            unreachable!()
1130        } else {
1131            num_args.unsigned_abs() as RepeatCount
1132        }
1133    }
1134}
1135
1136#[cfg(feature = "custom-bindings")]
1137impl InputState<'_> {
1138    /// Application customized binding
1139    fn custom_binding(
1140        &self,
1141        wrt: &dyn Refresher,
1142        evt: &Event,
1143        n: RepeatCount,
1144        positive: bool,
1145    ) -> Option<Cmd> {
1146        let bindings = self.custom_bindings;
1147        let handler = bindings.get(evt).or_else(|| bindings.get(&Event::Any));
1148        if let Some(handler) = handler {
1149            match handler {
1150                EventHandler::Simple(cmd) => Some(cmd.clone()),
1151                EventHandler::Conditional(handler) => {
1152                    let ctx = EventContext::new(self, wrt);
1153                    handler.handle(evt, n, positive, &ctx)
1154                }
1155            }
1156        } else {
1157            None
1158        }
1159    }
1160
1161    fn custom_seq_binding<R: RawReader>(
1162        &self,
1163        rdr: &mut R,
1164        wrt: &dyn Refresher,
1165        evt: &mut Event,
1166        n: RepeatCount,
1167        positive: bool,
1168    ) -> Result<Option<Cmd>> {
1169        while let Some(subtrie) = self.custom_bindings.get_raw_descendant(evt) {
1170            let snd_key = rdr.next_key(true)?;
1171            if let Event::KeySeq(ref mut key_seq) = evt {
1172                key_seq.push(snd_key);
1173            } else {
1174                break;
1175            }
1176            let handler = subtrie.get(evt).unwrap_or_default();
1177            if let Some(handler) = handler {
1178                let cmd = match handler {
1179                    EventHandler::Simple(cmd) => Some(cmd.clone()),
1180                    EventHandler::Conditional(handler) => {
1181                        let ctx = EventContext::new(self, wrt);
1182                        handler.handle(evt, n, positive, &ctx)
1183                    }
1184                };
1185                if cmd.is_some() {
1186                    return Ok(cmd);
1187                }
1188            }
1189        }
1190        Ok(None)
1191    }
1192}
1193
1194#[cfg(not(feature = "custom-bindings"))]
1195impl<'b> InputState<'b> {
1196    fn custom_binding(&self, _: &dyn Refresher, _: &Event, _: RepeatCount, _: bool) -> Option<Cmd> {
1197        None
1198    }
1199
1200    fn custom_seq_binding<R: RawReader>(
1201        &self,
1202        _: &mut R,
1203        _: &dyn Refresher,
1204        _: &mut Event,
1205        _: RepeatCount,
1206        _: bool,
1207    ) -> Result<Option<Cmd>> {
1208        Ok(None)
1209    }
1210}
1211
1212cfg_if::cfg_if! {
1213    if #[cfg(feature = "custom-bindings")] {
1214pub type Bindings = radix_trie::Trie<Event, EventHandler>;
1215    } else {
1216enum Event {
1217   KeySeq([KeyEvent; 1]),
1218}
1219impl From<KeyEvent> for Event {
1220    fn from(k: KeyEvent) -> Self {
1221        Self::KeySeq([k])
1222    }
1223}
1224pub struct Bindings {}
1225impl Bindings {
1226    pub fn new() -> Self {
1227        Self {}
1228    }
1229}
1230    }
1231}