diff --git a/examples/completions.rs b/examples/completions.rs index 95f4975..47f3c1b 100644 --- a/examples/completions.rs +++ b/examples/completions.rs @@ -5,8 +5,8 @@ // [Enter] to select the chosen alternative use reedline::{ - default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, Emacs, KeyCode, - KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, + default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs, + KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, }; use std::io; @@ -19,8 +19,21 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) { ReedlineEvent::MenuNext, ]), ); + keybindings.add_binding( + KeyModifiers::ALT, + KeyCode::Enter, + ReedlineEvent::Edit(vec![EditCommand::InsertNewline]), + ); } + fn main() -> io::Result<()> { + // Number of columns + let columns: u16 = 4; + // Column width + let col_width: Option = None; + // Column padding + let col_padding: usize = 2; + let commands = vec![ "test".into(), "hello world".into(), @@ -28,8 +41,15 @@ fn main() -> io::Result<()> { "this is the reedline crate".into(), ]; let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2)); + // Use the interactive menu to select options from the completer - let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); + let columnar_menu = ColumnarMenu::default() + .with_name("completion_menu") + .with_columns(columns) + .with_column_width(col_width) + .with_column_padding(col_padding); + + let completion_menu = Box::new(columnar_menu); let mut keybindings = default_emacs_keybindings(); add_menu_keybindings(&mut keybindings); diff --git a/examples/ide_completions.rs b/examples/ide_completions.rs index d0b608c..4231c8d 100644 --- a/examples/ide_completions.rs +++ b/examples/ide_completions.rs @@ -5,8 +5,9 @@ // [Enter] to select the chosen alternative use reedline::{ - default_emacs_keybindings, DefaultCompleter, DefaultPrompt, Emacs, IdeMenu, KeyCode, - KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, + default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand, + Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, + Signal, }; use std::io; @@ -19,8 +20,45 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) { ReedlineEvent::MenuNext, ]), ); + keybindings.add_binding( + KeyModifiers::ALT, + KeyCode::Enter, + ReedlineEvent::Edit(vec![EditCommand::InsertNewline]), + ); } + 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::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(), "hello world".into(), @@ -28,8 +66,26 @@ fn main() -> io::Result<()> { "this is the reedline crate".into(), ]; let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2)); + // Use the interactive menu to select options from the completer - let completion_menu = Box::new(IdeMenu::default().with_name("completion_menu")); + 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); diff --git a/src/completion/base.rs b/src/completion/base.rs index fdf7369..53c467e 100644 --- a/src/completion/base.rs +++ b/src/completion/base.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + /// A span of source code, with positions in bytes #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)] pub struct Span { @@ -31,6 +33,22 @@ pub trait Completer: Send { /// span to replace and the contents of that replacement fn complete(&mut self, line: &str, pos: usize) -> Vec; + /// same as [`Completer::complete`] but it will return a vector of ranges of the strings + /// the suggestions are based on + fn complete_with_base_ranges( + &mut self, + line: &str, + pos: usize, + ) -> (Vec, Vec>) { + let mut ranges = vec![]; + let suggestions = self.complete(line, pos); + for suggestion in &suggestions { + ranges.push(suggestion.span.start..suggestion.span.end); + } + ranges.dedup(); + (suggestions, ranges) + } + /// action that will return a partial section of available completions /// this command comes handy when trying to avoid to pull all the data at once /// from the completer diff --git a/src/completion/default.rs b/src/completion/default.rs index 4a3dd87..b1d617c 100644 --- a/src/completion/default.rs +++ b/src/completion/default.rs @@ -356,10 +356,10 @@ impl CompletionNode { #[cfg(test)] mod tests { + use super::*; + use pretty_assertions::assert_eq; #[test] fn default_completer_with_non_ansi() { - use super::*; - let mut completions = DefaultCompleter::default(); completions.insert( ["nushell", "null", "number"] @@ -395,4 +395,51 @@ mod tests { ] ); } + + #[test] + fn default_completer_with_start_strings() { + let mut completions = DefaultCompleter::default(); + completions.insert( + ["this is the reedline crate", "test"] + .iter() + .map(|s| s.to_string()) + .collect(), + ); + + let buffer = "this is t"; + + let (suggestions, ranges) = completions.complete_with_base_ranges(buffer, 9); + assert_eq!( + suggestions, + [ + Suggestion { + value: "test".into(), + description: None, + extra: None, + span: Span { start: 8, end: 9 }, + append_whitespace: false, + }, + Suggestion { + value: "this is the reedline crate".into(), + description: None, + extra: None, + span: Span { start: 8, end: 9 }, + append_whitespace: false, + }, + Suggestion { + value: "this is the reedline crate".into(), + description: None, + extra: None, + span: Span { start: 0, end: 9 }, + append_whitespace: false, + }, + ] + ); + + assert_eq!(ranges, [8..9, 0..9]); + assert_eq!( + ["t", "this is t"], + [&buffer[ranges[0].clone()], &buffer[ranges[1].clone()]] + ); + } } diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 0b2d7bc..67c46e0 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -728,10 +728,6 @@ impl Menu for ColumnarMenu { .collect() } } - - fn set_cursor_pos(&mut self, _pos: (u16, u16)) { - // The columnar menu does not need the cursor position - } } #[cfg(test)] diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 12ef2ae..6d0c636 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -49,11 +49,11 @@ impl Default for BorderSymbols { /// the initial declaration of the menu and are always kept as reference for the /// changeable [`IdeMenuDetails`] values. struct DefaultIdeMenuDetails { - /// Minimum width of the completion box, including the border + /// Min width of the completion box, including the border pub min_completion_width: u16, - /// max width of the completion box, including the border + /// Max width of the completion box, including the border pub max_completion_width: u16, - /// max height of the completion box, including the border + /// Max height of the completion box, including the border /// this will be capped by the lines available in the terminal pub max_completion_height: u16, /// Padding to the left and right of the suggestions @@ -75,6 +75,14 @@ struct DefaultIdeMenuDetails { pub max_description_height: u16, /// Offset from the suggestion box to the description box pub description_offset: u16, + /// 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 + /// ``` + pub correct_cursor_pos: bool, } impl Default for DefaultIdeMenuDetails { @@ -91,6 +99,7 @@ impl Default for DefaultIdeMenuDetails { max_description_width: 50, max_description_height: 10, description_offset: 1, + correct_cursor_pos: false, } } } @@ -114,6 +123,9 @@ struct IdeMenuDetails { pub space_right: u16, /// Corrected description offset, based on the available space pub description_offset: u16, + /// The ranges of the strings, the suggestions are based on (ranges in [`Editor::get_buffer`]) + /// This is required to adjust the suggestion boxes position, when `correct_cursor_pos` in [`DefaultIdeMenuDetails`] is true + pub base_strings: Vec, } /// Menu to present suggestions like similar to Ide completion menus @@ -306,6 +318,13 @@ impl IdeMenu { self.default_details.description_offset = description_offset; self } + + /// Menu builder with new correct cursor pos + #[must_use] + pub fn with_correct_cursor_pos(mut self, correct_cursor_pos: bool) -> Self { + self.default_details.correct_cursor_pos = correct_cursor_pos; + self + } } // Menu functionality @@ -662,16 +681,16 @@ impl Menu for IdeMenu { /// Update menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - self.values = if self.only_buffer_difference { + let (values, base_ranges) = if self.only_buffer_difference { if let Some(old_string) = &self.input { let (start, input) = string_difference(editor.get_buffer(), old_string); if !input.is_empty() { - completer.complete(input, start + input.len()) + completer.complete_with_base_ranges(input, start + input.len()) } else { - completer.complete("", editor.insertion_point()) + completer.complete_with_base_ranges("", editor.insertion_point()) } } else { - completer.complete("", editor.insertion_point()) + completer.complete_with_base_ranges("", editor.insertion_point()) } } else { // If there is a new line character in the line buffer, the completer @@ -680,12 +699,18 @@ impl Menu for IdeMenu { // Also, by replacing the new line character with a space, the insert // position is maintain in the line buffer. let trimmed_buffer = editor.get_buffer().replace("\r\n", " ").replace('\n', " "); - completer.complete( + completer.complete_with_base_ranges( &trimmed_buffer[..editor.insertion_point()], editor.insertion_point(), ) }; + self.values = values; + self.working_details.base_strings = base_ranges + .iter() + .map(|range| editor.get_buffer()[range.clone()].to_string()) + .collect::>(); + self.reset_position(); } @@ -739,7 +764,18 @@ impl Menu for IdeMenu { }); let terminal_width = painter.screen_width(); - let cursor_pos = self.working_details.cursor_col; + let mut cursor_pos = self.working_details.cursor_col; + + if self.default_details.correct_cursor_pos { + let base_string = self + .working_details + .base_strings + .iter() + .min_by_key(|s| s.len()) + .cloned() + .unwrap_or_default(); + cursor_pos = cursor_pos.saturating_sub(base_string.width() as u16); + } let border_width = if self.default_details.border.is_some() { 2 diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index 81f12c2..d90e367 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -670,10 +670,6 @@ impl Menu for ListMenu { fn min_rows(&self) -> u16 { self.max_lines + 1 } - - fn set_cursor_pos(&mut self, _pos: (u16, u16)) { - // The list menu does not need the cursor position - } } fn number_of_lines(entry: &str, max_lines: usize, terminal_columns: u16) -> u16 { diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 52691a9..b9c1d98 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -123,7 +123,9 @@ pub trait Menu: Send { /// Gets cached values from menu that will be displayed fn get_values(&self) -> &[Suggestion]; /// Sets the position of the cursor (currently only required by the IDE menu) - fn set_cursor_pos(&mut self, pos: (u16, u16)); + fn set_cursor_pos(&mut self, _pos: (u16, u16)) { + // empty implementation to make it optional + } } /// Allowed menus in Reedline