From a4b0d654cc3c608460c02539a29a3dde952ad8f5 Mon Sep 17 00:00:00 2001 From: Emanuil Chizhov Date: Mon, 2 Dec 2024 16:55:46 +0200 Subject: [PATCH 1/6] Add modal menus and change deactivate logic --- examples/ide_completions_with_modal.rs | 137 +++++++++++++++++++++++++ src/engine.rs | 29 +++++- src/enums.rs | 4 + src/menu/columnar_menu.rs | 14 ++- src/menu/description_menu.rs | 16 ++- src/menu/ide_menu.rs | 14 ++- src/menu/list_menu.rs | 16 +-- src/menu/mod.rs | 2 +- 8 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 examples/ide_completions_with_modal.rs diff --git a/examples/ide_completions_with_modal.rs b/examples/ide_completions_with_modal.rs new file mode 100644 index 00000000..907416f2 --- /dev/null +++ b/examples/ide_completions_with_modal.rs @@ -0,0 +1,137 @@ +// Create a reedline object with tab completions support and modal modes +// cargo run --example completions +// +// "t" [Tab] will allow you to select the completions "test" and "this is the reedline crate" +// [Enter] to select the chosen alternative + +use reedline::{ + default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand, + Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, + ReedlineMenu, Signal, +}; +use std::io; + +fn add_menu_keybindings(keybindings: &mut Keybindings) { + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + keybindings.add_binding( + KeyModifiers::ALT, + KeyCode::Enter, + ReedlineEvent::Edit(vec![EditCommand::InsertNewline]), + ); + keybindings.add_binding( + KeyModifiers::ALT, + KeyCode::Char('f'), + ReedlineEvent::ForceDeactivate, + ); +} + +fn main() -> io::Result<()> { + // Min width of the completion box, including the border + let min_completion_width: u16 = 0; + // Max width of the completion box, including the border + let max_completion_width: u16 = 50; + // Max height of the completion box, including the border + let max_completion_height: u16 = u16::MAX; + // Padding inside of the completion box (on the left and right side) + let padding: u16 = 0; + // Whether to draw the default border around the completion box + let border: bool = false; + // Offset of the cursor from the top left corner of the completion box + // By default the top left corner is below the cursor + let cursor_offset: i16 = 0; + // How the description should be aligned + let description_mode: DescriptionMode = DescriptionMode::PreferRight; + // Min width of the description box, including the border + let min_description_width: u16 = 0; + // Max width of the description box, including the border + let max_description_width: u16 = 50; + // Distance between the completion and the description box + let description_offset: u16 = 1; + // If true, the cursor pos will be corrected, so the suggestions match up with the typed text + // ```text + // C:\> str + // str join + // str trim + // str split + // ``` + // If a border is being used + let correct_cursor_pos: bool = false; + + let commands = vec![ + "test".into(), + "clear".into(), + "exit".into(), + "history 1".into(), + "history 2".into(), + "logout".into(), + "login".into(), + "hello world".into(), + "hello world reedline".into(), + "hello world something".into(), + "hello world another".into(), + "hello world 1".into(), + "hello world 2".into(), + "hello another very large option for hello word that will force one column".into(), + "this is the reedline crate".into(), + "abaaabas".into(), + "abaaacas".into(), + "ababac".into(), + "abacaxyc".into(), + "abadarabc".into(), + ]; + + let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2)); + + // Use the interactive menu to select options from the completer + let mut ide_menu = IdeMenu::default() + .with_name("completion_menu") + .with_min_completion_width(min_completion_width) + .with_max_completion_width(max_completion_width) + .with_max_completion_height(max_completion_height) + .with_padding(padding) + .with_cursor_offset(cursor_offset) + .with_description_mode(description_mode) + .with_min_description_width(min_description_width) + .with_max_description_width(max_description_width) + .with_description_offset(description_offset) + .with_correct_cursor_pos(correct_cursor_pos); + + if border { + ide_menu = ide_menu.with_default_border(); + } + + let completion_menu = Box::new(ide_menu); + + let mut keybindings = default_emacs_keybindings(); + add_menu_keybindings(&mut keybindings); + + let edit_mode = Box::new(Emacs::new(keybindings)); + + let mut line_editor = Reedline::create() + .with_completer(completer) + .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) + .with_edit_mode(edit_mode) + .with_modal_mode(true); + + let prompt = DefaultPrompt::default(); + + loop { + let sig = line_editor.read_line(&prompt)?; + match sig { + Signal::Success(buffer) => { + println!("We processed: {buffer}"); + } + Signal::CtrlD | Signal::CtrlC => { + println!("\nAborted!"); + break Ok(()); + } + } + } +} diff --git a/src/engine.rs b/src/engine.rs index 7c78a923..cdae86e5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -113,6 +113,8 @@ pub struct Reedline { history_cursor_on_excluded: bool, input_mode: InputMode, + modal_mode: bool, + // State of the painter after a `ReedlineEvent::ExecuteHostCommand` was requested, used after // execution to decide if we can re-use the previous prompt or paint a new one. suspended_state: Option, @@ -219,6 +221,7 @@ impl Reedline { history_excluded_item: None, history_cursor_on_excluded: false, input_mode: InputMode::Regular, + modal_mode: false, suspended_state: None, painter, transient_prompt: None, @@ -537,6 +540,12 @@ impl Reedline { self } + /// A builder which configures the modal mode for your instance of the Reedline engine + pub fn with_modal_mode(mut self, modal: bool) -> Self { + self.modal_mode = modal; + self + } + /// A builder that enables reedline changing the cursor shape based on the current edit mode. /// The current implementation sets the cursor shape when drawing the prompt. /// Do not use this if the cursor shape is set elsewhere, e.g. in the terminal settings or by ansi escape sequences. @@ -858,6 +867,7 @@ impl Reedline { self.painter.clear_scrollback()?; Ok(EventStatus::Handled) } + ReedlineEvent::ForceDeactivate => Ok(EventStatus::Handled), // Not sure what to do here ReedlineEvent::Enter | ReedlineEvent::HistoryHintComplete | ReedlineEvent::Submit @@ -1093,13 +1103,24 @@ impl Reedline { self.painter.clear_scrollback()?; Ok(EventStatus::Handled) } + ReedlineEvent::ForceDeactivate => { + for menu in self.menus.iter_mut() { + if menu.is_active() { + menu.replace_in_buffer(&mut self.editor); + menu.menu_event(MenuEvent::Deactivate(false)); + + return Ok(EventStatus::Handled); + } + } + Ok(EventStatus::Handled) + } ReedlineEvent::Enter | ReedlineEvent::Submit | ReedlineEvent::SubmitOrNewline if self.menus.iter().any(|menu| menu.is_active()) => { for menu in self.menus.iter_mut() { if menu.is_active() { menu.replace_in_buffer(&mut self.editor); - menu.menu_event(MenuEvent::Deactivate); + menu.menu_event(MenuEvent::Deactivate(self.modal_mode)); return Ok(EventStatus::Handled); } @@ -1161,7 +1182,7 @@ impl Reedline { Some(&EditCommand::Backspace) | Some(&EditCommand::BackspaceWord) | Some(&EditCommand::MoveToLineStart { select: false }) => { - menu.menu_event(MenuEvent::Deactivate) + menu.menu_event(MenuEvent::Deactivate(self.modal_mode)) } _ => { menu.menu_event(MenuEvent::Edit(self.quick_completions)); @@ -1189,7 +1210,7 @@ impl Reedline { } } if self.editor.line_buffer().get_buffer().is_empty() { - menu.menu_event(MenuEvent::Deactivate); + menu.menu_event(MenuEvent::Deactivate(self.modal_mode)); } else { menu.menu_event(MenuEvent::Edit(self.quick_completions)); } @@ -1279,7 +1300,7 @@ impl Reedline { fn deactivate_menus(&mut self) { self.menus .iter_mut() - .for_each(|menu| menu.menu_event(MenuEvent::Deactivate)); + .for_each(|menu| menu.menu_event(MenuEvent::Deactivate(self.modal_mode))); } fn previous_history(&mut self) { diff --git a/src/enums.rs b/src/enums.rs index cde42b1a..c4fc8601 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -569,6 +569,9 @@ pub enum ReedlineEvent { /// Submit at the end of the *complete* text, otherwise newline SubmitOrNewline, + /// Force deactivate - used for modal menus + ForceDeactivate, + /// Esc event Esc, @@ -659,6 +662,7 @@ impl Display for ReedlineEvent { ReedlineEvent::Enter => write!(f, "Enter"), ReedlineEvent::Submit => write!(f, "Submit"), ReedlineEvent::SubmitOrNewline => write!(f, "SubmitOrNewline"), + ReedlineEvent::ForceDeactivate => write!(f, "ForceDeactivate"), ReedlineEvent::Esc => write!(f, "Esc"), ReedlineEvent::Mouse => write!(f, "Mouse"), ReedlineEvent::Resize(_, _) => write!(f, "Resize "), diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 4de25ee5..7c6b4a91 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -475,9 +475,11 @@ impl Menu for ColumnarMenu { fn menu_event(&mut self, event: MenuEvent) { match &event { MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + self.input = None; + } } _ => {} } @@ -530,7 +532,11 @@ impl Menu for ColumnarMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + } + } MenuEvent::Edit(updated) => { self.reset_position(); diff --git a/src/menu/description_menu.rs b/src/menu/description_menu.rs index d9687206..b5908104 100644 --- a/src/menu/description_menu.rs +++ b/src/menu/description_menu.rs @@ -429,10 +429,12 @@ impl Menu for DescriptionMenu { fn menu_event(&mut self, event: MenuEvent) { match &event { MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; - self.values = Vec::new(); + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + self.input = None; + self.values = Vec::new(); + } } _ => {} }; @@ -468,7 +470,11 @@ impl Menu for DescriptionMenu { self.input = Some(editor.get_buffer().to_string()); self.update_values(editor, completer); } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + } + } MenuEvent::Edit(_) => { self.reset_position(); self.update_values(editor, completer); diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 2c11fa32..1d4a49a9 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -617,9 +617,11 @@ impl Menu for IdeMenu { fn menu_event(&mut self, event: MenuEvent) { match &event { MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + self.input = None; + } } _ => {} } @@ -671,7 +673,11 @@ impl Menu for IdeMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + } + } MenuEvent::Edit(updated) => { self.reset_position(); diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index 3ed16f6b..a79deaf5 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -334,9 +334,11 @@ impl Menu for ListMenu { fn menu_event(&mut self, event: MenuEvent) { match &event { MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + self.input = None; + } } _ => {} } @@ -435,9 +437,11 @@ impl Menu for ListMenu { full: false, }); } - MenuEvent::Deactivate => { - self.active = false; - self.input = None; + MenuEvent::Deactivate(modal_mode) => { + if !modal_mode { + self.active = false; + self.input = None; + } } MenuEvent::Edit(_) => { self.update_values(editor, completer); diff --git a/src/menu/mod.rs b/src/menu/mod.rs index a08ccbc0..f0c46417 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -49,7 +49,7 @@ pub enum MenuEvent { /// have already being updated. This is true when the option `quick_completions` is true Activate(bool), /// Deactivation event - Deactivate, + Deactivate(bool), /// Line buffer edit event. When the bool is true it means that the values /// have already being updated. This is true when the option `quick_completions` is true Edit(bool), From a9435c79a8d9a443acea81b23447dde5e61e1aad Mon Sep 17 00:00:00 2001 From: Emanuil Chizhov Date: Mon, 2 Dec 2024 16:58:55 +0200 Subject: [PATCH 2/6] Add double keybind for instant enter --- examples/ide_completions_with_modal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ide_completions_with_modal.rs b/examples/ide_completions_with_modal.rs index 907416f2..05550ae3 100644 --- a/examples/ide_completions_with_modal.rs +++ b/examples/ide_completions_with_modal.rs @@ -28,7 +28,7 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) { keybindings.add_binding( KeyModifiers::ALT, KeyCode::Char('f'), - ReedlineEvent::ForceDeactivate, + ReedlineEvent::Multiple(vec![ReedlineEvent::ForceDeactivate, ReedlineEvent::Enter]), ); } From 06ac4547576eb93402bcd3b942e8a60bfbeecc1e Mon Sep 17 00:00:00 2001 From: Emanuil Chizhov Date: Mon, 2 Dec 2024 21:34:17 +0200 Subject: [PATCH 3/6] Turn modal mode on by default for testing --- src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine.rs b/src/engine.rs index cdae86e5..0ff3c436 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -221,7 +221,7 @@ impl Reedline { history_excluded_item: None, history_cursor_on_excluded: false, input_mode: InputMode::Regular, - modal_mode: false, + modal_mode: true, suspended_state: None, painter, transient_prompt: None, From 6a9229a9f51d67b4acb8dd299b8c0766804ebe4e Mon Sep 17 00:00:00 2001 From: Emanuil Chizhov Date: Mon, 2 Dec 2024 22:01:05 +0200 Subject: [PATCH 4/6] Potential fix for live build --- src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine.rs b/src/engine.rs index 0ff3c436..bfbdc37e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1182,7 +1182,7 @@ impl Reedline { Some(&EditCommand::Backspace) | Some(&EditCommand::BackspaceWord) | Some(&EditCommand::MoveToLineStart { select: false }) => { - menu.menu_event(MenuEvent::Deactivate(self.modal_mode)) + menu.menu_event(MenuEvent::Deactivate(false)) } _ => { menu.menu_event(MenuEvent::Edit(self.quick_completions)); From a23024c74d3488d68f013c8e80c2357977c536a4 Mon Sep 17 00:00:00 2001 From: Emanuil Chizhov Date: Tue, 3 Dec 2024 20:30:46 +0200 Subject: [PATCH 5/6] Potential fix failed --- src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine.rs b/src/engine.rs index bfbdc37e..0ff3c436 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1182,7 +1182,7 @@ impl Reedline { Some(&EditCommand::Backspace) | Some(&EditCommand::BackspaceWord) | Some(&EditCommand::MoveToLineStart { select: false }) => { - menu.menu_event(MenuEvent::Deactivate(false)) + menu.menu_event(MenuEvent::Deactivate(self.modal_mode)) } _ => { menu.menu_event(MenuEvent::Edit(self.quick_completions)); From cdb9b294086143bdcbe08c3417eea16537b64700 Mon Sep 17 00:00:00 2001 From: Emanuil Chizhov Date: Tue, 3 Dec 2024 20:31:11 +0200 Subject: [PATCH 6/6] Disable default modal mode --- src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine.rs b/src/engine.rs index 0ff3c436..cdae86e5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -221,7 +221,7 @@ impl Reedline { history_excluded_item: None, history_cursor_on_excluded: false, input_mode: InputMode::Regular, - modal_mode: true, + modal_mode: false, suspended_state: None, painter, transient_prompt: None,