From f98df65427a0c7114217524ae2a6d27c218510cd Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:55:49 +0100 Subject: [PATCH 01/23] get correct cursor pos when menu indicator contains newline --- src/painting/prompt_lines.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/painting/prompt_lines.rs b/src/painting/prompt_lines.rs index 2df8964b..91727d76 100644 --- a/src/painting/prompt_lines.rs +++ b/src/painting/prompt_lines.rs @@ -92,8 +92,12 @@ impl<'prompt> PromptLines<'prompt> { /// The height is relative to the prompt pub(crate) fn cursor_pos(&self, terminal_columns: u16) -> (u16, u16) { // If we have a multiline prompt (e.g starship), we expect the cursor to be on the last line - let prompt_str = self.prompt_str_left.lines().last().unwrap_or_default(); - let prompt_width = line_width(&format!("{}{}", prompt_str, self.prompt_indicator)); + let prompt_width = line_width( + format!("{}{}", self.prompt_str_left, self.prompt_indicator) + .lines() + .last() + .unwrap_or_default(), + ); let buffer_width = line_width(&self.before_cursor); let total_width = prompt_width + buffer_width; From b53ebc84468799ea9af2fc30f6359086717c4736 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:54:40 +0100 Subject: [PATCH 02/23] add tests --- src/painting/prompt_lines.rs | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/painting/prompt_lines.rs b/src/painting/prompt_lines.rs index 91727d76..4088c83b 100644 --- a/src/painting/prompt_lines.rs +++ b/src/painting/prompt_lines.rs @@ -158,3 +158,76 @@ impl<'prompt> PromptLines<'prompt> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + #[case( + "~/path/", + "❯ ", + "", + 100, + (9, 0) + )] + #[case( + "~/longer/path/\n", + "❯ ", + "test", + 100, + (6, 0) + )] + #[case( + "~/longer/path/", + "\n❯ ", + "test", + 100, + (6, 0) + )] + #[case( + "~/longer/path/\n", + "\n❯ ", + "test", + 100, + (6, 0) + )] + #[case( + "~/path/", + "❯ ", + "very long input that does not fit in a single line", + 40, + (19, 1) + )] + #[case( + "~/path/\n", + "\n❯\n ", + "very long input that does not fit in a single line", + 10, + (1, 5) + )] + + fn test_cursor_pos( + #[case] prompt_str_left: &str, + #[case] prompt_indicator: &str, + #[case] before_cursor: &str, + #[case] terminal_columns: u16, + #[case] expected: (u16, u16), + ) { + let prompt_lines = PromptLines { + prompt_str_left: Cow::Borrowed(prompt_str_left), + prompt_str_right: Cow::Borrowed(""), + prompt_indicator: Cow::Borrowed(prompt_indicator), + before_cursor: Cow::Borrowed(before_cursor), + after_cursor: Cow::Borrowed(""), + hint: Cow::Borrowed(""), + right_prompt_on_last_line: false, + }; + + let pos = prompt_lines.cursor_pos(terminal_columns); + + assert_eq!(pos, expected); + } +} From 425bb238d12b24eeb920c314a061a731cac0e207 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:10:26 +0100 Subject: [PATCH 03/23] fix cursor pos in multiline prompt --- src/painting/prompt_lines.rs | 42 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/painting/prompt_lines.rs b/src/painting/prompt_lines.rs index 4088c83b..06082868 100644 --- a/src/painting/prompt_lines.rs +++ b/src/painting/prompt_lines.rs @@ -92,20 +92,28 @@ impl<'prompt> PromptLines<'prompt> { /// The height is relative to the prompt pub(crate) fn cursor_pos(&self, terminal_columns: u16) -> (u16, u16) { // If we have a multiline prompt (e.g starship), we expect the cursor to be on the last line - let prompt_width = line_width( - format!("{}{}", self.prompt_str_left, self.prompt_indicator) - .lines() - .last() - .unwrap_or_default(), - ); - let buffer_width = line_width(&self.before_cursor); + let prompt_str = format!("{}{}", self.prompt_str_left, self.prompt_indicator); + // The Cursor position will be relative to this + let last_prompt_str = prompt_str.lines().last().unwrap_or_default(); - let total_width = prompt_width + buffer_width; + let is_multiline = self.before_cursor.contains('\n'); + let buffer_width = line_width(self.before_cursor.lines().last().unwrap_or_default()); + + let total_width = if is_multiline { + // The buffer already contains the multiline prompt + buffer_width + } else { + buffer_width + line_width(last_prompt_str) + }; + + let buffer_width_prompt = format!("{}{}", last_prompt_str, self.before_cursor); + + let cursor_y = (estimate_required_lines(&buffer_width_prompt, terminal_columns) as u16) + .saturating_sub(1); // 0 based let cursor_x = (total_width % terminal_columns as usize) as u16; - let cursor_y = (total_width / terminal_columns as usize) as u16; - (cursor_x, cursor_y) + (cursor_x, cursor_y as u16) } /// Total lines that the prompt uses considering that it may wrap the screen @@ -208,6 +216,20 @@ mod tests { 10, (1, 5) )] + #[case( + "~/path/", + "❯ ", + "this is a text that contains newlines\n::: and a multiline prompt", + 40, + (26, 2) + )] + #[case( + "~/path/", + "❯ ", + "this is a text that contains newlines\n::: and very loooooooooooooooong text that wraps", + 40, + (8, 3) + )] fn test_cursor_pos( #[case] prompt_str_left: &str, From da5b239e7acdfc5ba30e631e9ea8306c560b0846 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:46:33 +0100 Subject: [PATCH 04/23] make description mode enum public --- src/lib.rs | 2 +- src/menu/mod.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 893207ab..71e5a49f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,7 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, IdeMenu, ListMenu, Menu, MenuEvent, MenuTextStyle, ReedlineMenu, + menu_functions, ColumnarMenu, IdeMenu, DescriptionMode, ListMenu, Menu, MenuEvent, MenuTextStyle, ReedlineMenu, }; mod terminal_extensions; diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 65a56470..fa4a93b3 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -8,6 +8,7 @@ use crate::History; use crate::{completion::history::HistoryCompleter, painting::Painter, Completer, Suggestion}; pub use columnar_menu::ColumnarMenu; pub use ide_menu::IdeMenu; +pub use ide_menu::DescriptionMode; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style}; From 1f612dc313841b268227348df0a92488fb63c837 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:47:53 +0100 Subject: [PATCH 05/23] add doc comment --- src/lib.rs | 3 ++- src/menu/ide_menu.rs | 1 + src/menu/mod.rs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 71e5a49f..53bdca9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,8 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, IdeMenu, DescriptionMode, ListMenu, Menu, MenuEvent, MenuTextStyle, ReedlineMenu, + menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuEvent, + MenuTextStyle, ReedlineMenu, }; mod terminal_extensions; diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index c931833b..2b1ea477 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -11,6 +11,7 @@ use nu_ansi_term::{ansi::RESET, Style}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; +/// The direction of the description box pub enum DescriptionMode { /// Description is always shown on the left Left, diff --git a/src/menu/mod.rs b/src/menu/mod.rs index fa4a93b3..52691a90 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -7,8 +7,8 @@ use crate::core_editor::Editor; use crate::History; use crate::{completion::history::HistoryCompleter, painting::Painter, Completer, Suggestion}; pub use columnar_menu::ColumnarMenu; -pub use ide_menu::IdeMenu; pub use ide_menu::DescriptionMode; +pub use ide_menu::IdeMenu; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style}; From 070d600545737b53bc9f48e7b9e93cf9fa8fc171 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:11:17 +0100 Subject: [PATCH 06/23] respect windows newline in update_values --- src/menu/columnar_menu.rs | 2 +- src/menu/ide_menu.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index b30ce360..0b2d7bca 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -543,7 +543,7 @@ impl Menu for ColumnarMenu { // editing a multiline buffer. // 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('\n', " "); + let trimmed_buffer = editor.get_buffer().replace("\r\n", " ").replace('\n', " "); completer.complete( &trimmed_buffer[..editor.insertion_point()], editor.insertion_point(), diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 2b1ea477..12ef2aee 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -679,7 +679,7 @@ impl Menu for IdeMenu { // editing a multiline buffer. // 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('\n', " "); + let trimmed_buffer = editor.get_buffer().replace("\r\n", " ").replace('\n', " "); completer.complete( &trimmed_buffer[..editor.insertion_point()], editor.insertion_point(), From 75bdae3f60efd04cc21197bf8d13823a05f6b8b2 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:12:51 +0100 Subject: [PATCH 07/23] Revert "respect windows newline in update_values" This reverts commit 070d600545737b53bc9f48e7b9e93cf9fa8fc171. --- src/menu/columnar_menu.rs | 2 +- src/menu/ide_menu.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 0b2d7bca..b30ce360 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -543,7 +543,7 @@ impl Menu for ColumnarMenu { // editing a multiline buffer. // 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', " "); + let trimmed_buffer = editor.get_buffer().replace('\n', " "); completer.complete( &trimmed_buffer[..editor.insertion_point()], editor.insertion_point(), diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 12ef2aee..2b1ea477 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -679,7 +679,7 @@ impl Menu for IdeMenu { // editing a multiline buffer. // 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', " "); + let trimmed_buffer = editor.get_buffer().replace('\n', " "); completer.complete( &trimmed_buffer[..editor.insertion_point()], editor.insertion_point(), From 36245bc3693bc7c39f0e2e61aeb768772e255fe9 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:34:23 +0100 Subject: [PATCH 08/23] add complete_with_base_ranges to Completer --- src/completion/base.rs | 18 ++++++++++++++ src/completion/default.rs | 51 +++++++++++++++++++++++++++++++++++++-- src/menu/columnar_menu.rs | 4 --- src/menu/ide_menu.rs | 41 ++++++++++++++++++++++++++----- src/menu/list_menu.rs | 4 --- src/menu/mod.rs | 4 ++- 6 files changed, 105 insertions(+), 17 deletions(-) diff --git a/src/completion/base.rs b/src/completion/base.rs index fdf73696..53c467eb 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 4a3dd871..b1d617c6 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 0b2d7bca..67c46e02 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 12ef2aee..6aafc084 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -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 @@ -662,16 +674,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 +692,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 +757,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 81f12c29..d90e3672 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 52691a90..b9c1d98d 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 From 5f65d771873b3a15b862604746106bd2bfec4a84 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:56:07 +0100 Subject: [PATCH 09/23] add builder for correct_cursor_pos --- src/menu/ide_menu.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 6aafc084..6f547c6a 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -318,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 From ce4bf9aeee86d9fb3fe7bacb3c3e738ad002ea53 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:13:19 +0100 Subject: [PATCH 10/23] add config options to completion examples --- examples/completions.rs | 26 ++++++++++++++-- examples/ide_completions.rs | 62 +++++++++++++++++++++++++++++++++++-- src/menu/ide_menu.rs | 6 ++-- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/examples/completions.rs b/examples/completions.rs index 95f4975e..47f3c1b1 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 d0b608c0..4231c8dd 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/menu/ide_menu.rs b/src/menu/ide_menu.rs index 6f547c6a..6d0c6369 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 From a5fbf9ebb8a6d5923014a79ea27bf0286e28daf2 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:06:00 +0100 Subject: [PATCH 11/23] add style to ide menu --- src/menu/ide_menu.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index c2ef4b64..ac39cf2f 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -559,8 +559,9 @@ impl IdeMenu { if use_ansi_coloring { if index == self.index() { format!( - "{}{}{}{}{}{}{}", + "{}{}{}{}{}{}{}{}", vertical_border, + suggestion.style.unwrap_or(self.color.text_style).reverse().prefix(), self.color.selected_text_style.prefix(), " ".repeat(padding), string, @@ -572,7 +573,7 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}", vertical_border, - self.color.text_style.prefix(), + suggestion.style.unwrap_or(self.color.text_style).prefix(), " ".repeat(padding), string, " ".repeat(padding_right), From 3f0dd62d2ae93e3c94de26119555c9c591b1a9bf Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:08:31 +0100 Subject: [PATCH 12/23] run fmt --- src/menu/ide_menu.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index ac39cf2f..7ff1d29a 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -561,7 +561,11 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}{}", vertical_border, - suggestion.style.unwrap_or(self.color.text_style).reverse().prefix(), + suggestion + .style + .unwrap_or(self.color.text_style) + .reverse() + .prefix(), self.color.selected_text_style.prefix(), " ".repeat(padding), string, From 62726f29bea0067e5860cc2a0cbace460d59517c Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:05:12 +0100 Subject: [PATCH 13/23] start menu refactor --- examples/completions.rs | 2 +- examples/demo.rs | 4 +- examples/ide_completions.rs | 4 +- examples/transient_prompt.rs | 2 +- src/lib.rs | 2 +- src/menu/columnar_menu.rs | 189 +++++++++-------------------- src/menu/ide_menu.rs | 223 +++++++++++------------------------ src/menu/list_menu.rs | 171 ++++++--------------------- src/menu/mod.rs | 146 ++++++++++++++++++++++- 9 files changed, 312 insertions(+), 431 deletions(-) diff --git a/examples/completions.rs b/examples/completions.rs index 47f3c1b1..bb80142d 100644 --- a/examples/completions.rs +++ b/examples/completions.rs @@ -6,7 +6,7 @@ use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs, - KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, + KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, }; use std::io; diff --git a/examples/demo.rs b/examples/demo.rs index 2e7a402e..74d95b66 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -9,8 +9,8 @@ use { reedline::{ default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultPrompt, DefaultValidator, - EditCommand, EditMode, Emacs, ExampleHighlighter, Keybindings, ListMenu, Reedline, - ReedlineEvent, ReedlineMenu, Signal, Vi, + EditCommand, EditMode, Emacs, ExampleHighlighter, Keybindings, ListMenu, MenuBuilder, + Reedline, ReedlineEvent, ReedlineMenu, Signal, Vi, }, }; diff --git a/examples/ide_completions.rs b/examples/ide_completions.rs index 2b9487b5..f6e69b7b 100644 --- a/examples/ide_completions.rs +++ b/examples/ide_completions.rs @@ -6,8 +6,8 @@ use reedline::{ default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand, - Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, - Signal, + Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, + ReedlineMenu, Signal, }; use std::io; diff --git a/examples/transient_prompt.rs b/examples/transient_prompt.rs index f971955b..d3aaa390 100644 --- a/examples/transient_prompt.rs +++ b/examples/transient_prompt.rs @@ -8,7 +8,7 @@ use nu_ansi_term::{Color, Style}; use reedline::SqliteBackedHistory; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultPrompt, Emacs, - ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, Prompt, PromptEditMode, + ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, ReedlineMenu, Signal, ValidationResult, Validator, }; diff --git a/src/lib.rs b/src/lib.rs index 53bdca9b..663acf4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,7 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuEvent, + menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuBuilder, MenuEvent, MenuTextStyle, ReedlineMenu, }; diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index c32d1f71..20b7a83e 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -1,9 +1,9 @@ -use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; +use super::{menu_functions::find_common_string, Menu, MenuBuilder, MenuCommon, MenuEvent}; use crate::{ core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, Suggestion, UndoBehavior, }; -use nu_ansi_term::{ansi::RESET, Style}; +use nu_ansi_term::ansi::RESET; /// Default values used as reference for the menu. These values are set during /// the initial declaration of the menu and are always kept as reference for the @@ -40,12 +40,8 @@ struct ColumnDetails { /// Menu to present suggestions in a columnar fashion /// It presents a description of the suggestion if available pub struct ColumnarMenu { - /// Menu name - name: String, - /// Columnar menu active status - active: bool, - /// Menu coloring - color: MenuTextStyle, + /// Common menu values + common: MenuCommon, /// Default column details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultColumnDetails, @@ -60,70 +56,27 @@ pub struct ColumnarMenu { col_pos: u16, /// row position in the menu. Starts from 0 row_pos: u16, - /// Menu marker when active - marker: String, - /// Event sent to the menu - event: Option, - /// Longest suggestion found in the values - longest_suggestion: usize, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for ColumnarMenu { fn default() -> Self { Self { - name: "columnar_menu".to_string(), - active: false, - color: MenuTextStyle::default(), + common: MenuCommon::new("columnar_menu"), default_details: DefaultColumnDetails::default(), min_rows: 3, working_details: ColumnDetails::default(), values: Vec::new(), col_pos: 0, row_pos: 0, - marker: "| ".to_string(), - event: None, - longest_suggestion: 0, - input: None, - only_buffer_difference: false, } } } -// Menu configuration functions -impl ColumnarMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self - } +/// Menu builder +impl MenuBuilder for ColumnarMenu {} +/// Menu specific builder +impl ColumnarMenu { /// Menu builder with new columns value #[must_use] pub fn with_columns(mut self, columns: u16) -> Self { @@ -144,20 +97,6 @@ impl ColumnarMenu { self.default_details.col_padding = col_padding; self } - - /// Menu builder with marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } } // Menu functionality @@ -268,11 +207,6 @@ impl ColumnarMenu { index as usize } - /// Get selected value from the menu - fn get_value(&self) -> Option { - self.get_values().get(self.index()).cloned() - } - /// Calculates how many rows the Menu will use fn get_rows(&self) -> u16 { let values = self.get_values().len() as u16; @@ -306,7 +240,7 @@ impl ColumnarMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), msg, RESET ) @@ -341,20 +275,21 @@ impl ColumnarMenu { if use_ansi_coloring { if index == self.index() { if let Some(description) = &suggestion.description { - let left_text_size = self.longest_suggestion + self.default_details.col_padding; + let left_text_size = + self.common.longest_suggestion + self.default_details.col_padding; let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{}{:max$}{}{}{}{}{}{}", suggestion .style - .unwrap_or(self.color.text_style) + .unwrap_or(self.common.color.text_style) .reverse() .prefix(), - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), &suggestion.value, RESET, - self.color.description_style.reverse().prefix(), - self.color.selected_text_style.prefix(), + self.common.color.description_style.reverse().prefix(), + self.common.color.selected_text_style.prefix(), description .chars() .take(right_text_size) @@ -369,10 +304,10 @@ impl ColumnarMenu { "{}{}{}{}{:>empty$}{}", suggestion .style - .unwrap_or(self.color.text_style) + .unwrap_or(self.common.color.text_style) .reverse() .prefix(), - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), &suggestion.value, RESET, "", @@ -381,14 +316,18 @@ impl ColumnarMenu { ) } } else if let Some(description) = &suggestion.description { - let left_text_size = self.longest_suggestion + self.default_details.col_padding; + let left_text_size = + self.common.longest_suggestion + self.default_details.col_padding; let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{:max$}{}{}{}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), description .chars() .take(right_text_size) @@ -401,10 +340,13 @@ impl ColumnarMenu { } else { format!( "{}{}{}{}{:>empty$}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), "", RESET, self.end_of_line(column), @@ -426,7 +368,7 @@ impl ColumnarMenu { .collect::() .replace('\n', " "), self.end_of_line(column), - max = self.longest_suggestion + max = self.common.longest_suggestion + self .default_details .col_padding @@ -453,21 +395,6 @@ impl ColumnarMenu { } impl Menu for ColumnarMenu { - /// Menu name - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() - } - - /// Deactivates context menu - fn is_active(&self) -> bool { - self.active - } - /// The columnar menu can to quick complete if there is only one element fn can_quick_complete(&self) -> bool { true @@ -523,24 +450,10 @@ impl Menu for ColumnarMenu { } } - /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent) { - match &event { - MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; - } - _ => {} - } - - self.event = Some(event); - } - /// Updates menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - self.values = if self.only_buffer_difference { - if let Some(old_string) = &self.input { + self.values = if self.common.only_buffer_difference { + if let Some(old_string) = &self.common.input { let (start, input) = string_difference(editor.get_buffer(), old_string); if !input.is_empty() { completer.complete(input, start + input.len()) @@ -574,7 +487,7 @@ impl Menu for ColumnarMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.event.take() { + if let Some(event) = self.common.event.take() { // The working value for the menu are updated first before executing any of the // menu events // @@ -589,13 +502,14 @@ impl Menu for ColumnarMenu { self.working_details.columns = 1; self.working_details.col_width = painter.screen_width() as usize; - self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { - prev - } else { - suggestion.value.len() - } - }); + self.common.longest_suggestion = + self.get_values().iter().fold(0, |prev, suggestion| { + if prev >= suggestion.value.len() { + prev + } else { + suggestion.value.len() + } + }); } else { let max_width = self.get_values().iter().fold(0, |acc, suggestion| { let str_len = suggestion.value.len() + self.default_details.col_padding; @@ -635,10 +549,10 @@ impl Menu for ColumnarMenu { match event { MenuEvent::Activate(updated) => { - self.active = true; + self.common.active = true; self.reset_position(); - self.input = if self.only_buffer_difference { + self.common.input = if self.common.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -648,7 +562,7 @@ impl Menu for ColumnarMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate => self.common.active = false, MenuEvent::Edit(updated) => { self.reset_position(); @@ -699,11 +613,6 @@ impl Menu for ColumnarMenu { self.get_rows().min(self.min_rows) } - /// Gets values from filler that will be displayed in the menu - fn get_values(&self) -> &[Suggestion] { - &self.values - } - fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { self.get_rows() } @@ -741,6 +650,14 @@ impl Menu for ColumnarMenu { .collect() } } + + fn common(&self) -> &MenuCommon { + &self.common + } + + fn common_mut(&mut self) -> &mut MenuCommon { + &mut self.common + } } #[cfg(test)] diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 7ff1d29a..270d5f0e 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -1,4 +1,4 @@ -use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; +use super::{menu_functions::find_common_string, Menu, MenuBuilder, MenuCommon, MenuEvent}; use crate::{ core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, Suggestion, UndoBehavior, @@ -7,7 +7,7 @@ use itertools::{ EitherOrBoth::{Both, Left, Right}, Itertools, }; -use nu_ansi_term::{ansi::RESET, Style}; +use nu_ansi_term::ansi::RESET; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -130,118 +130,66 @@ struct IdeMenuDetails { /// Menu to present suggestions like similar to Ide completion menus pub struct IdeMenu { - /// Menu name - name: String, - /// Ide menu active status - active: bool, - /// Menu coloring - color: MenuTextStyle, + /// Common menu values + common: MenuCommon, /// Default ide menu details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultIdeMenuDetails, /// Working ide menu details keep changing based on the collected values working_details: IdeMenuDetails, - /// Menu cached values - values: Vec, - /// Selected value. Starts at 0 - selected: u16, - /// Menu marker when active - marker: String, - /// Event sent to the menu - event: Option, - /// Longest suggestion found in the values - longest_suggestion: usize, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for IdeMenu { fn default() -> Self { Self { - name: "ide_completion_menu".to_string(), - active: false, - color: MenuTextStyle::default(), + common: MenuCommon::new("ide_completion_menu"), default_details: DefaultIdeMenuDetails::default(), working_details: IdeMenuDetails::default(), - values: Vec::new(), - selected: 0, - marker: "| ".to_string(), - event: None, - longest_suggestion: 0, - input: None, - only_buffer_difference: false, } } } -impl IdeMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } +/// Menu builder +impl MenuBuilder for IdeMenu {} - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self - } - - /// Menu builder with new value for min completion width value +/// Menu specific builder +impl IdeMenu { + /// Menu builder with new value for min completion width #[must_use] pub fn with_min_completion_width(mut self, width: u16) -> Self { self.default_details.min_completion_width = width; self } - /// Menu builder with new value for max completion width value + /// Menu builder with new value for max completion width #[must_use] pub fn with_max_completion_width(mut self, width: u16) -> Self { self.default_details.max_completion_width = width; self } - /// Menu builder with new value for max completion height value + /// Menu builder with new value for max completion height #[must_use] pub fn with_max_completion_height(mut self, height: u16) -> Self { self.default_details.max_completion_height = height; self } - /// Menu builder with new value for padding value + /// Menu builder with new value for padding #[must_use] pub fn with_padding(mut self, padding: u16) -> Self { self.default_details.padding = padding; self } - /// Menu builder with the default border value + /// Menu builder with the default border #[must_use] pub fn with_default_border(mut self) -> Self { self.default_details.border = Some(BorderSymbols::default()); self } - /// Menu builder with new value for border value + /// Menu builder with new value for border #[must_use] pub fn with_border( mut self, @@ -263,27 +211,13 @@ impl IdeMenu { self } - /// Menu builder with new value for cursor offset value + /// Menu builder with new value for cursor offset #[must_use] pub fn with_cursor_offset(mut self, cursor_offset: i16) -> Self { self.default_details.cursor_offset = cursor_offset; self } - /// Menu builder with marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } - /// Menu builder with new description mode #[must_use] pub fn with_description_mode(mut self, description_mode: DescriptionMode) -> Self { @@ -330,29 +264,21 @@ impl IdeMenu { // Menu functionality impl IdeMenu { fn move_next(&mut self) { - if self.selected < (self.values.len() as u16).saturating_sub(1) { - self.selected += 1; + if self.common.selected < (self.common.values.len() as u16).saturating_sub(1) { + self.common.selected += 1; } else { - self.selected = 0; + self.common.selected = 0; } } fn move_previous(&mut self) { - if self.selected > 0 { - self.selected -= 1; + if self.common.selected > 0 { + self.common.selected -= 1; } else { - self.selected = self.values.len().saturating_sub(1) as u16; + self.common.selected = self.common.values.len().saturating_sub(1) as u16; } } - fn index(&self) -> usize { - self.selected as usize - } - - fn get_value(&self) -> Option { - self.values.get(self.index()).cloned() - } - /// Calculates how many rows the Menu will try to use (if available) fn get_rows(&self) -> u16 { let mut values = self.get_values().len() as u16; @@ -391,7 +317,7 @@ impl IdeMenu { } fn reset_position(&mut self) { - self.selected = 0; + self.common.selected = 0; } fn no_records_msg(&self, use_ansi_coloring: bool) -> String { @@ -399,7 +325,7 @@ impl IdeMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), msg, RESET ) @@ -457,7 +383,7 @@ impl IdeMenu { *line = format!( "{}{}{}{}{}{}", border.vertical, - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), line, padding, RESET, @@ -486,7 +412,7 @@ impl IdeMenu { if use_ansi_coloring { *line = format!( "{}{}{}{}", - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), line, padding, RESET @@ -563,10 +489,10 @@ impl IdeMenu { vertical_border, suggestion .style - .unwrap_or(self.color.text_style) + .unwrap_or(self.common.color.text_style) .reverse() .prefix(), - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -577,7 +503,10 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}", vertical_border, - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -602,21 +531,6 @@ impl IdeMenu { } impl Menu for IdeMenu { - /// Menu name - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() - } - - /// Deactivates context menu - fn is_active(&self) -> bool { - self.active - } - /// The ide menu can to quick complete if there is only one element fn can_quick_complete(&self) -> bool { true @@ -673,21 +587,21 @@ impl Menu for IdeMenu { /// Selects what type of event happened with the menu fn menu_event(&mut self, event: MenuEvent) { match &event { - MenuEvent::Activate(_) => self.active = true, + MenuEvent::Activate(_) => self.common.active = true, MenuEvent::Deactivate => { - self.active = false; - self.input = None; + self.common.active = false; + self.common.input = None; } _ => {} } - self.event = Some(event); + self.common.event = Some(event); } /// Update menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - let (values, base_ranges) = if self.only_buffer_difference { - if let Some(old_string) = &self.input { + let (values, base_ranges) = if self.common.only_buffer_difference { + if let Some(old_string) = &self.common.input { let (start, input) = string_difference(editor.get_buffer(), old_string); if !input.is_empty() { completer.complete_with_base_ranges(input, start + input.len()) @@ -710,7 +624,7 @@ impl Menu for IdeMenu { ) }; - self.values = values; + self.common.values = values; self.working_details.base_strings = base_ranges .iter() .map(|range| editor.get_buffer()[range.clone()].to_string()) @@ -727,14 +641,14 @@ impl Menu for IdeMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.event.take() { + if let Some(event) = self.common.event.take() { // The working value for the menu are updated first before executing any of the match event { MenuEvent::Activate(updated) => { - self.active = true; + self.common.active = true; self.reset_position(); - self.input = if self.only_buffer_difference { + self.common.input = if self.common.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -744,7 +658,7 @@ impl Menu for IdeMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate => self.common.active = false, MenuEvent::Edit(updated) => { self.reset_position(); @@ -760,13 +674,14 @@ impl Menu for IdeMenu { | MenuEvent::NextPage => {} } - self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { - prev - } else { - suggestion.value.len() - } - }); + self.common.longest_suggestion = + self.get_values().iter().fold(0, |prev, suggestion| { + if prev >= suggestion.value.len() { + prev + } else { + suggestion.value.len() + } + }); let terminal_width = painter.screen_width(); let mut cursor_pos = self.working_details.cursor_col; @@ -808,7 +723,7 @@ impl Menu for IdeMenu { 0 }; - let completion_width = ((self.longest_suggestion.min(u16::MAX as usize) as u16) + let completion_width = ((self.common.longest_suggestion.min(u16::MAX as usize) as u16) + 2 * self.default_details.padding + border_width) .min(self.default_details.max_completion_width) @@ -914,10 +829,6 @@ impl Menu for IdeMenu { self.get_rows() } - fn get_values(&self) -> &[Suggestion] { - &self.values - } - fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { self.get_rows() } @@ -935,20 +846,22 @@ impl Menu for IdeMenu { let available_lines = available_lines.min(self.default_details.max_completion_height); // The skip values represent the number of lines that should be skipped // while printing the menu - let skip_values = if self.selected >= available_lines.saturating_sub(border_width) { - let skip_lines = self - .selected - .saturating_sub(available_lines.saturating_sub(border_width)) - + 1; - skip_lines as usize - } else { - 0 - }; + let skip_values = + if self.common.selected >= available_lines.saturating_sub(border_width) { + let skip_lines = self + .common + .selected + .saturating_sub(available_lines.saturating_sub(border_width)) + + 1; + skip_lines as usize + } else { + 0 + }; let available_values = available_lines.saturating_sub(border_width) as usize; let max_padding = self.working_details.completion_width.saturating_sub( - self.longest_suggestion.min(u16::MAX as usize) as u16 + border_width, + self.common.longest_suggestion.min(u16::MAX as usize) as u16 + border_width, ) / 2; let corrected_padding = self.default_details.padding.min(max_padding) as usize; @@ -1086,6 +999,14 @@ impl Menu for IdeMenu { fn set_cursor_pos(&mut self, pos: (u16, u16)) { self.working_details.cursor_col = pos.0; } + + fn common(&self) -> &MenuCommon { + &self.common + } + + fn common_mut(&mut self) -> &mut MenuCommon { + &mut self.common + } } /// Split the input into strings that are at most `max_length` (in columns, not in chars) long diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index d90e3672..1af1e006 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -1,14 +1,14 @@ use { super::{ menu_functions::{parse_selection_char, string_difference}, - Menu, MenuEvent, MenuTextStyle, + Menu, MenuBuilder, MenuCommon, MenuEvent, }, crate::{ core_editor::Editor, painting::{estimate_single_line_wraps, Painter}, Completer, Suggestion, UndoBehavior, }, - nu_ansi_term::{ansi::RESET, Style}, + nu_ansi_term::ansi::RESET, std::{fmt::Write, iter::Sum}, unicode_width::UnicodeWidthStr, }; @@ -41,22 +41,10 @@ impl<'a> Sum<&'a Page> for Page { /// Struct to store the menu style /// Context menu definition pub struct ListMenu { - /// Menu name - name: String, - /// Menu coloring - color: MenuTextStyle, + /// Common menu values + common: MenuCommon, /// Number of records pulled until page is full page_size: usize, - /// Menu marker displayed when the menu is active - marker: String, - /// Menu active status - active: bool, - /// Cached values collected when querying the completer. - /// When collecting chronological values, the menu only caches at least - /// page_size records. - /// When performing a query to the completer, the cached values will - /// be the result from such query - values: Vec, /// row position in the menu. Starts from 0 row_position: u16, /// Max size of the suggestions when querying without a search buffer @@ -69,67 +57,28 @@ pub struct ListMenu { pages: Vec, /// Page index page: usize, - /// Event sent to the menu - event: Option, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for ListMenu { fn default() -> Self { Self { - name: "search_menu".to_string(), - color: MenuTextStyle::default(), + common: MenuCommon::new("search_menu").with_marker("? "), page_size: 10, - active: false, - values: Vec::new(), row_position: 0, page: 0, query_size: None, - marker: "? ".to_string(), max_lines: 5, multiline_marker: ":::".to_string(), pages: Vec::new(), - event: None, - input: None, - only_buffer_difference: true, } } } -// Menu configuration functions -impl ListMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for description style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self - } +/// Menu builder +impl MenuBuilder for ListMenu {} +/// Menu specific builder +impl ListMenu { /// Menu builder with new page size #[must_use] pub fn with_page_size(mut self, page_size: usize) -> Self { @@ -137,23 +86,6 @@ impl ListMenu { self } - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } -} - -// Menu functionality -impl ListMenu { - /// Menu builder with menu marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - /// Menu builder with max entry lines #[must_use] pub fn with_max_entry_lines(mut self, max_lines: u16) -> Self { @@ -177,7 +109,7 @@ impl ListMenu { } fn total_values(&self) -> usize { - self.query_size.unwrap_or(self.values.len()) + self.query_size.unwrap_or(self.common.values.len()) } fn values_until_current_page(&self) -> usize { @@ -196,11 +128,6 @@ impl ListMenu { self.row_position as usize } - /// Get selected value from the menu - fn get_value(&self) -> Option { - self.get_values().get(self.index()).cloned() - } - /// Reset menu position fn reset_position(&mut self) { self.page = 0; @@ -246,7 +173,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), msg, RESET ) @@ -257,7 +184,7 @@ impl ListMenu { fn banner_message(&self, page: &Page, use_ansi_coloring: bool) -> String { let values_until = self.values_until_current_page().saturating_sub(1); - let value_before = if self.values.is_empty() || self.page == 0 { + let value_before = if self.common.values.is_empty() || self.page == 0 { 0 } else { let page_size = self.pages.get(self.page).map(|page| page.size).unwrap_or(0); @@ -277,7 +204,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), status_bar, RESET, ) @@ -294,9 +221,9 @@ impl ListMenu { /// Text style for menu fn text_style(&self, index: usize) -> String { if index == self.index() { - self.color.selected_text_style.prefix().to_string() + self.common.color.selected_text_style.prefix().to_string() } else { - self.color.text_style.prefix().to_string() + self.common.color.text_style.prefix().to_string() } } @@ -313,7 +240,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}({}) {}", - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), desc, RESET ) @@ -348,20 +275,6 @@ impl ListMenu { } impl Menu for ListMenu { - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() - } - - /// Deactivates context menu - fn is_active(&self) -> bool { - self.active - } - /// There is no use for quick complete for the menu fn can_quick_complete(&self) -> bool { false @@ -378,25 +291,11 @@ impl Menu for ListMenu { false } - /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent) { - match &event { - MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; - } - _ => {} - } - - self.event = Some(event); - } - /// Collecting the value from the completer to be shown in the menu fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { let line_buffer = editor.line_buffer(); - let (pos, input) = if self.only_buffer_difference { - match &self.input { + let (pos, input) = if self.common.only_buffer_difference { + match &self.common.input { Some(old_string) => { let (start, input) = string_difference(line_buffer.get_buffer(), old_string); if input.is_empty() { @@ -419,11 +318,11 @@ impl Menu for ListMenu { // If there are no row selector and the menu has an Edit event, this clears // the position together with the pages vector - if matches!(self.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() { + if matches!(self.common.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() { self.reset_position(); } - self.values = if parsed.remainder.is_empty() { + self.common.values = if parsed.remainder.is_empty() { self.query_size = Some(completer.total_completions(parsed.remainder, pos)); let skip = self.pages.iter().take(self.page).sum::().size; @@ -445,13 +344,13 @@ impl Menu for ListMenu { if self.query_size.is_some() { // When there is a size value it means that only a chunk of the // chronological data from the database was collected - &self.values + &self.common.values } else { // If no record then it means that the values hold the result // from the query to the database. This slice can be used to get the // data that will be shown in the menu - if self.values.is_empty() { - return &self.values; + if self.common.values.is_empty() { + return &self.common.values; } let start = self.pages.iter().take(self.page).sum::().size; @@ -463,7 +362,7 @@ impl Menu for ListMenu { }; let end = end.min(self.total_values()); - &self.values[start..end] + &self.common.values[start..end] } } @@ -498,12 +397,12 @@ impl Menu for ListMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.event.clone() { + if let Some(event) = self.common.event.clone() { match event { MenuEvent::Activate(_) => { self.reset_position(); - self.input = if self.only_buffer_difference { + self.common.input = if self.common.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -517,8 +416,8 @@ impl Menu for ListMenu { }); } MenuEvent::Deactivate => { - self.active = false; - self.input = None; + self.common.active = false; + self.common.input = None; } MenuEvent::Edit(_) => { self.update_values(editor, completer); @@ -532,7 +431,7 @@ impl Menu for ListMenu { if let Some(page) = self.pages.get(self.page) { if new_pos >= page.size as u16 { - self.event = Some(MenuEvent::NextPage); + self.common.event = Some(MenuEvent::NextPage); self.update_working_details(editor, completer, painter); } else { self.row_position = new_pos; @@ -553,7 +452,7 @@ impl Menu for ListMenu { self.row_position = page.size.saturating_sub(1) as u16; } - self.event = Some(MenuEvent::PreviousPage); + self.common.event = Some(MenuEvent::PreviousPage); self.update_working_details(editor, completer, painter); } } @@ -591,7 +490,7 @@ impl Menu for ListMenu { } } - self.event = None; + self.common.event = None; } } @@ -670,6 +569,14 @@ impl Menu for ListMenu { fn min_rows(&self) -> u16 { self.max_lines + 1 } + + fn common(&self) -> &MenuCommon { + &self.common + } + + fn common_mut(&mut self) -> &mut MenuCommon { + &mut self.common + } } 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 b9c1d98d..bc586682 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -12,6 +12,54 @@ pub use ide_menu::IdeMenu; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style}; +/// Common menu values +pub struct MenuCommon { + /// Menu name + name: String, + /// Ide menu active status + active: bool, + /// Menu coloring + color: MenuTextStyle, + /// Menu cached values + values: Vec, + /// Selected value. Starts at 0 + selected: u16, + /// Menu marker when active + marker: String, + /// Event sent to the menu + event: Option, + /// Longest suggestion found in the values + longest_suggestion: usize, + /// String collected after the menu is activated + input: Option, + /// Calls the completer using only the line buffer difference difference + /// after the menu was activated + only_buffer_difference: bool, +} + +impl MenuCommon { + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + active: false, + color: MenuTextStyle::default(), + values: Vec::new(), + selected: 0, + marker: "| ".to_string(), + event: None, + longest_suggestion: 0, + input: None, + only_buffer_difference: false, + } + } + + #[must_use] + pub fn with_marker(mut self, marker: &str) -> Self { + self.marker = marker.to_string(); + self + } +} + /// Struct to store the menu style pub struct MenuTextStyle { /// Text style for selected text in a menu @@ -63,17 +111,40 @@ pub enum MenuEvent { /// Trait that defines how a menu will be printed by the painter pub trait Menu: Send { + /// Returns the common menu values + fn common(&self) -> &MenuCommon; + + /// Returns the common menu values as mutable + fn common_mut(&mut self) -> &mut MenuCommon; + /// Menu name - fn name(&self) -> &str; + fn name(&self) -> &str { + &self.common().name + } /// Menu indicator - fn indicator(&self) -> &str; + fn indicator(&self) -> &str { + &self.common().marker + } /// Checks if the menu is active - fn is_active(&self) -> bool; + fn is_active(&self) -> bool { + self.common().active + } /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent); + fn menu_event(&mut self, event: MenuEvent) { + match &event { + MenuEvent::Activate(_) => self.common_mut().active = true, + MenuEvent::Deactivate => { + self.common_mut().active = false; + self.common_mut().input = None; + } + _ => {} + } + + self.common_mut().event = Some(event); + } /// A menu may not be allowed to quick complete because it needs to stay /// active even with one element @@ -120,14 +191,71 @@ pub trait Menu: Send { /// Minimum rows that should be displayed by the menu fn min_rows(&self) -> u16; + /// Gets the index of the selected value + fn index(&self) -> usize { + self.common().selected as usize + } + + /// Gets the selected value + fn get_value(&self) -> Option { + self.common().values.get(self.index()).cloned() + } + /// Gets cached values from menu that will be displayed - fn get_values(&self) -> &[Suggestion]; + fn get_values(&self) -> &[Suggestion] { + &self.common().values + } /// Sets the position of the cursor (currently only required by the IDE menu) fn set_cursor_pos(&mut self, _pos: (u16, u16)) { // empty implementation to make it optional } } +/// Common builder for menus +pub trait MenuBuilder: Sized + Menu { + /// Menu builder with new name + #[must_use] + fn with_name(mut self, name: &str) -> Self { + self.common_mut().name = name.into(); + self + } + + /// Menu builder with new value for text style + #[must_use] + fn with_text_style(mut self, text_style: Style) -> Self { + self.common_mut().color.text_style = text_style; + self + } + + /// Menu builder with new value for selected text style + #[must_use] + fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { + self.common_mut().color.selected_text_style = selected_text_style; + self + } + + /// Menu builder with new value for description style + #[must_use] + fn with_description_style(mut self, description_style: Style) -> Self { + self.common_mut().color.description_style = description_style; + self + } + + /// Menu builder with new value for marker + #[must_use] + fn with_marker(mut self, marker: &str) -> Self { + self.common_mut().marker = marker.to_string(); + self + } + + /// Menu builder with new only buffer difference + #[must_use] + fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.common_mut().only_buffer_difference = only_buffer_difference; + self + } +} + /// Allowed menus in Reedline pub enum ReedlineMenu { /// Menu that uses Reedline's completer to update its values @@ -229,6 +357,14 @@ impl ReedlineMenu { } impl Menu for ReedlineMenu { + fn common(&self) -> &MenuCommon { + self.as_ref().common() + } + + fn common_mut(&mut self) -> &mut MenuCommon { + self.as_mut().common_mut() + } + fn name(&self) -> &str { self.as_ref().name() } From 65c32418524b111b46dfbbb202ac66761b67b583 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:06:33 +0100 Subject: [PATCH 14/23] Revert "start menu refactor" This reverts commit 62726f29bea0067e5860cc2a0cbace460d59517c. --- examples/completions.rs | 2 +- examples/demo.rs | 4 +- examples/ide_completions.rs | 4 +- examples/transient_prompt.rs | 2 +- src/lib.rs | 2 +- src/menu/columnar_menu.rs | 189 ++++++++++++++++++++--------- src/menu/ide_menu.rs | 223 ++++++++++++++++++++++++----------- src/menu/list_menu.rs | 171 +++++++++++++++++++++------ src/menu/mod.rs | 146 +---------------------- 9 files changed, 431 insertions(+), 312 deletions(-) diff --git a/examples/completions.rs b/examples/completions.rs index bb80142d..47f3c1b1 100644 --- a/examples/completions.rs +++ b/examples/completions.rs @@ -6,7 +6,7 @@ use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs, - KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, + KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, }; use std::io; diff --git a/examples/demo.rs b/examples/demo.rs index 74d95b66..2e7a402e 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -9,8 +9,8 @@ use { reedline::{ default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultPrompt, DefaultValidator, - EditCommand, EditMode, Emacs, ExampleHighlighter, Keybindings, ListMenu, MenuBuilder, - Reedline, ReedlineEvent, ReedlineMenu, Signal, Vi, + EditCommand, EditMode, Emacs, ExampleHighlighter, Keybindings, ListMenu, Reedline, + ReedlineEvent, ReedlineMenu, Signal, Vi, }, }; diff --git a/examples/ide_completions.rs b/examples/ide_completions.rs index f6e69b7b..2b9487b5 100644 --- a/examples/ide_completions.rs +++ b/examples/ide_completions.rs @@ -6,8 +6,8 @@ use reedline::{ default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand, - Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, - ReedlineMenu, Signal, + Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, + Signal, }; use std::io; diff --git a/examples/transient_prompt.rs b/examples/transient_prompt.rs index d3aaa390..f971955b 100644 --- a/examples/transient_prompt.rs +++ b/examples/transient_prompt.rs @@ -8,7 +8,7 @@ use nu_ansi_term::{Color, Style}; use reedline::SqliteBackedHistory; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultPrompt, Emacs, - ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Prompt, PromptEditMode, + ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, ReedlineMenu, Signal, ValidationResult, Validator, }; diff --git a/src/lib.rs b/src/lib.rs index 663acf4f..53bdca9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,7 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuBuilder, MenuEvent, + menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuEvent, MenuTextStyle, ReedlineMenu, }; diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 20b7a83e..c32d1f71 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -1,9 +1,9 @@ -use super::{menu_functions::find_common_string, Menu, MenuBuilder, MenuCommon, MenuEvent}; +use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; use crate::{ core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, Suggestion, UndoBehavior, }; -use nu_ansi_term::ansi::RESET; +use nu_ansi_term::{ansi::RESET, Style}; /// Default values used as reference for the menu. These values are set during /// the initial declaration of the menu and are always kept as reference for the @@ -40,8 +40,12 @@ struct ColumnDetails { /// Menu to present suggestions in a columnar fashion /// It presents a description of the suggestion if available pub struct ColumnarMenu { - /// Common menu values - common: MenuCommon, + /// Menu name + name: String, + /// Columnar menu active status + active: bool, + /// Menu coloring + color: MenuTextStyle, /// Default column details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultColumnDetails, @@ -56,27 +60,70 @@ pub struct ColumnarMenu { col_pos: u16, /// row position in the menu. Starts from 0 row_pos: u16, + /// Menu marker when active + marker: String, + /// Event sent to the menu + event: Option, + /// Longest suggestion found in the values + longest_suggestion: usize, + /// String collected after the menu is activated + input: Option, + /// Calls the completer using only the line buffer difference difference + /// after the menu was activated + only_buffer_difference: bool, } impl Default for ColumnarMenu { fn default() -> Self { Self { - common: MenuCommon::new("columnar_menu"), + name: "columnar_menu".to_string(), + active: false, + color: MenuTextStyle::default(), default_details: DefaultColumnDetails::default(), min_rows: 3, working_details: ColumnDetails::default(), values: Vec::new(), col_pos: 0, row_pos: 0, + marker: "| ".to_string(), + event: None, + longest_suggestion: 0, + input: None, + only_buffer_difference: false, } } } -/// Menu builder -impl MenuBuilder for ColumnarMenu {} - -/// Menu specific builder +// Menu configuration functions impl ColumnarMenu { + /// Menu builder with new name + #[must_use] + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.into(); + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_text_style(mut self, text_style: Style) -> Self { + self.color.text_style = text_style; + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { + self.color.selected_text_style = selected_text_style; + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { + self.color.description_style = description_text_style; + self + } + /// Menu builder with new columns value #[must_use] pub fn with_columns(mut self, columns: u16) -> Self { @@ -97,6 +144,20 @@ impl ColumnarMenu { self.default_details.col_padding = col_padding; self } + + /// Menu builder with marker + #[must_use] + pub fn with_marker(mut self, marker: String) -> Self { + self.marker = marker; + self + } + + /// Menu builder with new only buffer difference + #[must_use] + pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.only_buffer_difference = only_buffer_difference; + self + } } // Menu functionality @@ -207,6 +268,11 @@ impl ColumnarMenu { index as usize } + /// Get selected value from the menu + fn get_value(&self) -> Option { + self.get_values().get(self.index()).cloned() + } + /// Calculates how many rows the Menu will use fn get_rows(&self) -> u16 { let values = self.get_values().len() as u16; @@ -240,7 +306,7 @@ impl ColumnarMenu { if use_ansi_coloring { format!( "{}{}{}", - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), msg, RESET ) @@ -275,21 +341,20 @@ impl ColumnarMenu { if use_ansi_coloring { if index == self.index() { if let Some(description) = &suggestion.description { - let left_text_size = - self.common.longest_suggestion + self.default_details.col_padding; + let left_text_size = self.longest_suggestion + self.default_details.col_padding; let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{}{:max$}{}{}{}{}{}{}", suggestion .style - .unwrap_or(self.common.color.text_style) + .unwrap_or(self.color.text_style) .reverse() .prefix(), - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), &suggestion.value, RESET, - self.common.color.description_style.reverse().prefix(), - self.common.color.selected_text_style.prefix(), + self.color.description_style.reverse().prefix(), + self.color.selected_text_style.prefix(), description .chars() .take(right_text_size) @@ -304,10 +369,10 @@ impl ColumnarMenu { "{}{}{}{}{:>empty$}{}", suggestion .style - .unwrap_or(self.common.color.text_style) + .unwrap_or(self.color.text_style) .reverse() .prefix(), - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), &suggestion.value, RESET, "", @@ -316,18 +381,14 @@ impl ColumnarMenu { ) } } else if let Some(description) = &suggestion.description { - let left_text_size = - self.common.longest_suggestion + self.default_details.col_padding; + let left_text_size = self.longest_suggestion + self.default_details.col_padding; let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{:max$}{}{}{}{}{}", - suggestion - .style - .unwrap_or(self.common.color.text_style) - .prefix(), + suggestion.style.unwrap_or(self.color.text_style).prefix(), &suggestion.value, RESET, - self.common.color.description_style.prefix(), + self.color.description_style.prefix(), description .chars() .take(right_text_size) @@ -340,13 +401,10 @@ impl ColumnarMenu { } else { format!( "{}{}{}{}{:>empty$}{}{}", - suggestion - .style - .unwrap_or(self.common.color.text_style) - .prefix(), + suggestion.style.unwrap_or(self.color.text_style).prefix(), &suggestion.value, RESET, - self.common.color.description_style.prefix(), + self.color.description_style.prefix(), "", RESET, self.end_of_line(column), @@ -368,7 +426,7 @@ impl ColumnarMenu { .collect::() .replace('\n', " "), self.end_of_line(column), - max = self.common.longest_suggestion + max = self.longest_suggestion + self .default_details .col_padding @@ -395,6 +453,21 @@ impl ColumnarMenu { } impl Menu for ColumnarMenu { + /// Menu name + fn name(&self) -> &str { + self.name.as_str() + } + + /// Menu indicator + fn indicator(&self) -> &str { + self.marker.as_str() + } + + /// Deactivates context menu + fn is_active(&self) -> bool { + self.active + } + /// The columnar menu can to quick complete if there is only one element fn can_quick_complete(&self) -> bool { true @@ -450,10 +523,24 @@ impl Menu for ColumnarMenu { } } + /// Selects what type of event happened with the menu + fn menu_event(&mut self, event: MenuEvent) { + match &event { + MenuEvent::Activate(_) => self.active = true, + MenuEvent::Deactivate => { + self.active = false; + self.input = None; + } + _ => {} + } + + self.event = Some(event); + } + /// Updates menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - self.values = if self.common.only_buffer_difference { - if let Some(old_string) = &self.common.input { + self.values = 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()) @@ -487,7 +574,7 @@ impl Menu for ColumnarMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.common.event.take() { + if let Some(event) = self.event.take() { // The working value for the menu are updated first before executing any of the // menu events // @@ -502,14 +589,13 @@ impl Menu for ColumnarMenu { self.working_details.columns = 1; self.working_details.col_width = painter.screen_width() as usize; - self.common.longest_suggestion = - self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { - prev - } else { - suggestion.value.len() - } - }); + self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { + if prev >= suggestion.value.len() { + prev + } else { + suggestion.value.len() + } + }); } else { let max_width = self.get_values().iter().fold(0, |acc, suggestion| { let str_len = suggestion.value.len() + self.default_details.col_padding; @@ -549,10 +635,10 @@ impl Menu for ColumnarMenu { match event { MenuEvent::Activate(updated) => { - self.common.active = true; + self.active = true; self.reset_position(); - self.common.input = if self.common.only_buffer_difference { + self.input = if self.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -562,7 +648,7 @@ impl Menu for ColumnarMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.common.active = false, + MenuEvent::Deactivate => self.active = false, MenuEvent::Edit(updated) => { self.reset_position(); @@ -613,6 +699,11 @@ impl Menu for ColumnarMenu { self.get_rows().min(self.min_rows) } + /// Gets values from filler that will be displayed in the menu + fn get_values(&self) -> &[Suggestion] { + &self.values + } + fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { self.get_rows() } @@ -650,14 +741,6 @@ impl Menu for ColumnarMenu { .collect() } } - - fn common(&self) -> &MenuCommon { - &self.common - } - - fn common_mut(&mut self) -> &mut MenuCommon { - &mut self.common - } } #[cfg(test)] diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 270d5f0e..7ff1d29a 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -1,4 +1,4 @@ -use super::{menu_functions::find_common_string, Menu, MenuBuilder, MenuCommon, MenuEvent}; +use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; use crate::{ core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, Suggestion, UndoBehavior, @@ -7,7 +7,7 @@ use itertools::{ EitherOrBoth::{Both, Left, Right}, Itertools, }; -use nu_ansi_term::ansi::RESET; +use nu_ansi_term::{ansi::RESET, Style}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -130,66 +130,118 @@ struct IdeMenuDetails { /// Menu to present suggestions like similar to Ide completion menus pub struct IdeMenu { - /// Common menu values - common: MenuCommon, + /// Menu name + name: String, + /// Ide menu active status + active: bool, + /// Menu coloring + color: MenuTextStyle, /// Default ide menu details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultIdeMenuDetails, /// Working ide menu details keep changing based on the collected values working_details: IdeMenuDetails, + /// Menu cached values + values: Vec, + /// Selected value. Starts at 0 + selected: u16, + /// Menu marker when active + marker: String, + /// Event sent to the menu + event: Option, + /// Longest suggestion found in the values + longest_suggestion: usize, + /// String collected after the menu is activated + input: Option, + /// Calls the completer using only the line buffer difference difference + /// after the menu was activated + only_buffer_difference: bool, } impl Default for IdeMenu { fn default() -> Self { Self { - common: MenuCommon::new("ide_completion_menu"), + name: "ide_completion_menu".to_string(), + active: false, + color: MenuTextStyle::default(), default_details: DefaultIdeMenuDetails::default(), working_details: IdeMenuDetails::default(), + values: Vec::new(), + selected: 0, + marker: "| ".to_string(), + event: None, + longest_suggestion: 0, + input: None, + only_buffer_difference: false, } } } -/// Menu builder -impl MenuBuilder for IdeMenu {} - -/// Menu specific builder impl IdeMenu { - /// Menu builder with new value for min completion width + /// Menu builder with new name + #[must_use] + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.into(); + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_text_style(mut self, text_style: Style) -> Self { + self.color.text_style = text_style; + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { + self.color.selected_text_style = selected_text_style; + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { + self.color.description_style = description_text_style; + self + } + + /// Menu builder with new value for min completion width value #[must_use] pub fn with_min_completion_width(mut self, width: u16) -> Self { self.default_details.min_completion_width = width; self } - /// Menu builder with new value for max completion width + /// Menu builder with new value for max completion width value #[must_use] pub fn with_max_completion_width(mut self, width: u16) -> Self { self.default_details.max_completion_width = width; self } - /// Menu builder with new value for max completion height + /// Menu builder with new value for max completion height value #[must_use] pub fn with_max_completion_height(mut self, height: u16) -> Self { self.default_details.max_completion_height = height; self } - /// Menu builder with new value for padding + /// Menu builder with new value for padding value #[must_use] pub fn with_padding(mut self, padding: u16) -> Self { self.default_details.padding = padding; self } - /// Menu builder with the default border + /// Menu builder with the default border value #[must_use] pub fn with_default_border(mut self) -> Self { self.default_details.border = Some(BorderSymbols::default()); self } - /// Menu builder with new value for border + /// Menu builder with new value for border value #[must_use] pub fn with_border( mut self, @@ -211,13 +263,27 @@ impl IdeMenu { self } - /// Menu builder with new value for cursor offset + /// Menu builder with new value for cursor offset value #[must_use] pub fn with_cursor_offset(mut self, cursor_offset: i16) -> Self { self.default_details.cursor_offset = cursor_offset; self } + /// Menu builder with marker + #[must_use] + pub fn with_marker(mut self, marker: String) -> Self { + self.marker = marker; + self + } + + /// Menu builder with new only buffer difference + #[must_use] + pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.only_buffer_difference = only_buffer_difference; + self + } + /// Menu builder with new description mode #[must_use] pub fn with_description_mode(mut self, description_mode: DescriptionMode) -> Self { @@ -264,21 +330,29 @@ impl IdeMenu { // Menu functionality impl IdeMenu { fn move_next(&mut self) { - if self.common.selected < (self.common.values.len() as u16).saturating_sub(1) { - self.common.selected += 1; + if self.selected < (self.values.len() as u16).saturating_sub(1) { + self.selected += 1; } else { - self.common.selected = 0; + self.selected = 0; } } fn move_previous(&mut self) { - if self.common.selected > 0 { - self.common.selected -= 1; + if self.selected > 0 { + self.selected -= 1; } else { - self.common.selected = self.common.values.len().saturating_sub(1) as u16; + self.selected = self.values.len().saturating_sub(1) as u16; } } + fn index(&self) -> usize { + self.selected as usize + } + + fn get_value(&self) -> Option { + self.values.get(self.index()).cloned() + } + /// Calculates how many rows the Menu will try to use (if available) fn get_rows(&self) -> u16 { let mut values = self.get_values().len() as u16; @@ -317,7 +391,7 @@ impl IdeMenu { } fn reset_position(&mut self) { - self.common.selected = 0; + self.selected = 0; } fn no_records_msg(&self, use_ansi_coloring: bool) -> String { @@ -325,7 +399,7 @@ impl IdeMenu { if use_ansi_coloring { format!( "{}{}{}", - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), msg, RESET ) @@ -383,7 +457,7 @@ impl IdeMenu { *line = format!( "{}{}{}{}{}{}", border.vertical, - self.common.color.description_style.prefix(), + self.color.description_style.prefix(), line, padding, RESET, @@ -412,7 +486,7 @@ impl IdeMenu { if use_ansi_coloring { *line = format!( "{}{}{}{}", - self.common.color.description_style.prefix(), + self.color.description_style.prefix(), line, padding, RESET @@ -489,10 +563,10 @@ impl IdeMenu { vertical_border, suggestion .style - .unwrap_or(self.common.color.text_style) + .unwrap_or(self.color.text_style) .reverse() .prefix(), - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -503,10 +577,7 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}", vertical_border, - suggestion - .style - .unwrap_or(self.common.color.text_style) - .prefix(), + suggestion.style.unwrap_or(self.color.text_style).prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -531,6 +602,21 @@ impl IdeMenu { } impl Menu for IdeMenu { + /// Menu name + fn name(&self) -> &str { + self.name.as_str() + } + + /// Menu indicator + fn indicator(&self) -> &str { + self.marker.as_str() + } + + /// Deactivates context menu + fn is_active(&self) -> bool { + self.active + } + /// The ide menu can to quick complete if there is only one element fn can_quick_complete(&self) -> bool { true @@ -587,21 +673,21 @@ impl Menu for IdeMenu { /// Selects what type of event happened with the menu fn menu_event(&mut self, event: MenuEvent) { match &event { - MenuEvent::Activate(_) => self.common.active = true, + MenuEvent::Activate(_) => self.active = true, MenuEvent::Deactivate => { - self.common.active = false; - self.common.input = None; + self.active = false; + self.input = None; } _ => {} } - self.common.event = Some(event); + self.event = Some(event); } /// Update menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - let (values, base_ranges) = if self.common.only_buffer_difference { - if let Some(old_string) = &self.common.input { + 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_with_base_ranges(input, start + input.len()) @@ -624,7 +710,7 @@ impl Menu for IdeMenu { ) }; - self.common.values = values; + self.values = values; self.working_details.base_strings = base_ranges .iter() .map(|range| editor.get_buffer()[range.clone()].to_string()) @@ -641,14 +727,14 @@ impl Menu for IdeMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.common.event.take() { + if let Some(event) = self.event.take() { // The working value for the menu are updated first before executing any of the match event { MenuEvent::Activate(updated) => { - self.common.active = true; + self.active = true; self.reset_position(); - self.common.input = if self.common.only_buffer_difference { + self.input = if self.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -658,7 +744,7 @@ impl Menu for IdeMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.common.active = false, + MenuEvent::Deactivate => self.active = false, MenuEvent::Edit(updated) => { self.reset_position(); @@ -674,14 +760,13 @@ impl Menu for IdeMenu { | MenuEvent::NextPage => {} } - self.common.longest_suggestion = - self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { - prev - } else { - suggestion.value.len() - } - }); + self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { + if prev >= suggestion.value.len() { + prev + } else { + suggestion.value.len() + } + }); let terminal_width = painter.screen_width(); let mut cursor_pos = self.working_details.cursor_col; @@ -723,7 +808,7 @@ impl Menu for IdeMenu { 0 }; - let completion_width = ((self.common.longest_suggestion.min(u16::MAX as usize) as u16) + let completion_width = ((self.longest_suggestion.min(u16::MAX as usize) as u16) + 2 * self.default_details.padding + border_width) .min(self.default_details.max_completion_width) @@ -829,6 +914,10 @@ impl Menu for IdeMenu { self.get_rows() } + fn get_values(&self) -> &[Suggestion] { + &self.values + } + fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { self.get_rows() } @@ -846,22 +935,20 @@ impl Menu for IdeMenu { let available_lines = available_lines.min(self.default_details.max_completion_height); // The skip values represent the number of lines that should be skipped // while printing the menu - let skip_values = - if self.common.selected >= available_lines.saturating_sub(border_width) { - let skip_lines = self - .common - .selected - .saturating_sub(available_lines.saturating_sub(border_width)) - + 1; - skip_lines as usize - } else { - 0 - }; + let skip_values = if self.selected >= available_lines.saturating_sub(border_width) { + let skip_lines = self + .selected + .saturating_sub(available_lines.saturating_sub(border_width)) + + 1; + skip_lines as usize + } else { + 0 + }; let available_values = available_lines.saturating_sub(border_width) as usize; let max_padding = self.working_details.completion_width.saturating_sub( - self.common.longest_suggestion.min(u16::MAX as usize) as u16 + border_width, + self.longest_suggestion.min(u16::MAX as usize) as u16 + border_width, ) / 2; let corrected_padding = self.default_details.padding.min(max_padding) as usize; @@ -999,14 +1086,6 @@ impl Menu for IdeMenu { fn set_cursor_pos(&mut self, pos: (u16, u16)) { self.working_details.cursor_col = pos.0; } - - fn common(&self) -> &MenuCommon { - &self.common - } - - fn common_mut(&mut self) -> &mut MenuCommon { - &mut self.common - } } /// Split the input into strings that are at most `max_length` (in columns, not in chars) long diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index 1af1e006..d90e3672 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -1,14 +1,14 @@ use { super::{ menu_functions::{parse_selection_char, string_difference}, - Menu, MenuBuilder, MenuCommon, MenuEvent, + Menu, MenuEvent, MenuTextStyle, }, crate::{ core_editor::Editor, painting::{estimate_single_line_wraps, Painter}, Completer, Suggestion, UndoBehavior, }, - nu_ansi_term::ansi::RESET, + nu_ansi_term::{ansi::RESET, Style}, std::{fmt::Write, iter::Sum}, unicode_width::UnicodeWidthStr, }; @@ -41,10 +41,22 @@ impl<'a> Sum<&'a Page> for Page { /// Struct to store the menu style /// Context menu definition pub struct ListMenu { - /// Common menu values - common: MenuCommon, + /// Menu name + name: String, + /// Menu coloring + color: MenuTextStyle, /// Number of records pulled until page is full page_size: usize, + /// Menu marker displayed when the menu is active + marker: String, + /// Menu active status + active: bool, + /// Cached values collected when querying the completer. + /// When collecting chronological values, the menu only caches at least + /// page_size records. + /// When performing a query to the completer, the cached values will + /// be the result from such query + values: Vec, /// row position in the menu. Starts from 0 row_position: u16, /// Max size of the suggestions when querying without a search buffer @@ -57,28 +69,67 @@ pub struct ListMenu { pages: Vec, /// Page index page: usize, + /// Event sent to the menu + event: Option, + /// String collected after the menu is activated + input: Option, + /// Calls the completer using only the line buffer difference difference + /// after the menu was activated + only_buffer_difference: bool, } impl Default for ListMenu { fn default() -> Self { Self { - common: MenuCommon::new("search_menu").with_marker("? "), + name: "search_menu".to_string(), + color: MenuTextStyle::default(), page_size: 10, + active: false, + values: Vec::new(), row_position: 0, page: 0, query_size: None, + marker: "? ".to_string(), max_lines: 5, multiline_marker: ":::".to_string(), pages: Vec::new(), + event: None, + input: None, + only_buffer_difference: true, } } } -/// Menu builder -impl MenuBuilder for ListMenu {} - -/// Menu specific builder +// Menu configuration functions impl ListMenu { + /// Menu builder with new name + #[must_use] + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.into(); + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_text_style(mut self, text_style: Style) -> Self { + self.color.text_style = text_style; + self + } + + /// Menu builder with new value for text style + #[must_use] + pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { + self.color.selected_text_style = selected_text_style; + self + } + + /// Menu builder with new value for description style + #[must_use] + pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { + self.color.description_style = description_text_style; + self + } + /// Menu builder with new page size #[must_use] pub fn with_page_size(mut self, page_size: usize) -> Self { @@ -86,6 +137,23 @@ impl ListMenu { self } + /// Menu builder with new only buffer difference + #[must_use] + pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.only_buffer_difference = only_buffer_difference; + self + } +} + +// Menu functionality +impl ListMenu { + /// Menu builder with menu marker + #[must_use] + pub fn with_marker(mut self, marker: String) -> Self { + self.marker = marker; + self + } + /// Menu builder with max entry lines #[must_use] pub fn with_max_entry_lines(mut self, max_lines: u16) -> Self { @@ -109,7 +177,7 @@ impl ListMenu { } fn total_values(&self) -> usize { - self.query_size.unwrap_or(self.common.values.len()) + self.query_size.unwrap_or(self.values.len()) } fn values_until_current_page(&self) -> usize { @@ -128,6 +196,11 @@ impl ListMenu { self.row_position as usize } + /// Get selected value from the menu + fn get_value(&self) -> Option { + self.get_values().get(self.index()).cloned() + } + /// Reset menu position fn reset_position(&mut self) { self.page = 0; @@ -173,7 +246,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), msg, RESET ) @@ -184,7 +257,7 @@ impl ListMenu { fn banner_message(&self, page: &Page, use_ansi_coloring: bool) -> String { let values_until = self.values_until_current_page().saturating_sub(1); - let value_before = if self.common.values.is_empty() || self.page == 0 { + let value_before = if self.values.is_empty() || self.page == 0 { 0 } else { let page_size = self.pages.get(self.page).map(|page| page.size).unwrap_or(0); @@ -204,7 +277,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.common.color.selected_text_style.prefix(), + self.color.selected_text_style.prefix(), status_bar, RESET, ) @@ -221,9 +294,9 @@ impl ListMenu { /// Text style for menu fn text_style(&self, index: usize) -> String { if index == self.index() { - self.common.color.selected_text_style.prefix().to_string() + self.color.selected_text_style.prefix().to_string() } else { - self.common.color.text_style.prefix().to_string() + self.color.text_style.prefix().to_string() } } @@ -240,7 +313,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}({}) {}", - self.common.color.description_style.prefix(), + self.color.description_style.prefix(), desc, RESET ) @@ -275,6 +348,20 @@ impl ListMenu { } impl Menu for ListMenu { + fn name(&self) -> &str { + self.name.as_str() + } + + /// Menu indicator + fn indicator(&self) -> &str { + self.marker.as_str() + } + + /// Deactivates context menu + fn is_active(&self) -> bool { + self.active + } + /// There is no use for quick complete for the menu fn can_quick_complete(&self) -> bool { false @@ -291,11 +378,25 @@ impl Menu for ListMenu { false } + /// Selects what type of event happened with the menu + fn menu_event(&mut self, event: MenuEvent) { + match &event { + MenuEvent::Activate(_) => self.active = true, + MenuEvent::Deactivate => { + self.active = false; + self.input = None; + } + _ => {} + } + + self.event = Some(event); + } + /// Collecting the value from the completer to be shown in the menu fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { let line_buffer = editor.line_buffer(); - let (pos, input) = if self.common.only_buffer_difference { - match &self.common.input { + let (pos, input) = if self.only_buffer_difference { + match &self.input { Some(old_string) => { let (start, input) = string_difference(line_buffer.get_buffer(), old_string); if input.is_empty() { @@ -318,11 +419,11 @@ impl Menu for ListMenu { // If there are no row selector and the menu has an Edit event, this clears // the position together with the pages vector - if matches!(self.common.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() { + if matches!(self.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() { self.reset_position(); } - self.common.values = if parsed.remainder.is_empty() { + self.values = if parsed.remainder.is_empty() { self.query_size = Some(completer.total_completions(parsed.remainder, pos)); let skip = self.pages.iter().take(self.page).sum::().size; @@ -344,13 +445,13 @@ impl Menu for ListMenu { if self.query_size.is_some() { // When there is a size value it means that only a chunk of the // chronological data from the database was collected - &self.common.values + &self.values } else { // If no record then it means that the values hold the result // from the query to the database. This slice can be used to get the // data that will be shown in the menu - if self.common.values.is_empty() { - return &self.common.values; + if self.values.is_empty() { + return &self.values; } let start = self.pages.iter().take(self.page).sum::().size; @@ -362,7 +463,7 @@ impl Menu for ListMenu { }; let end = end.min(self.total_values()); - &self.common.values[start..end] + &self.values[start..end] } } @@ -397,12 +498,12 @@ impl Menu for ListMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.common.event.clone() { + if let Some(event) = self.event.clone() { match event { MenuEvent::Activate(_) => { self.reset_position(); - self.common.input = if self.common.only_buffer_difference { + self.input = if self.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -416,8 +517,8 @@ impl Menu for ListMenu { }); } MenuEvent::Deactivate => { - self.common.active = false; - self.common.input = None; + self.active = false; + self.input = None; } MenuEvent::Edit(_) => { self.update_values(editor, completer); @@ -431,7 +532,7 @@ impl Menu for ListMenu { if let Some(page) = self.pages.get(self.page) { if new_pos >= page.size as u16 { - self.common.event = Some(MenuEvent::NextPage); + self.event = Some(MenuEvent::NextPage); self.update_working_details(editor, completer, painter); } else { self.row_position = new_pos; @@ -452,7 +553,7 @@ impl Menu for ListMenu { self.row_position = page.size.saturating_sub(1) as u16; } - self.common.event = Some(MenuEvent::PreviousPage); + self.event = Some(MenuEvent::PreviousPage); self.update_working_details(editor, completer, painter); } } @@ -490,7 +591,7 @@ impl Menu for ListMenu { } } - self.common.event = None; + self.event = None; } } @@ -569,14 +670,6 @@ impl Menu for ListMenu { fn min_rows(&self) -> u16 { self.max_lines + 1 } - - fn common(&self) -> &MenuCommon { - &self.common - } - - fn common_mut(&mut self) -> &mut MenuCommon { - &mut self.common - } } 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 bc586682..b9c1d98d 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -12,54 +12,6 @@ pub use ide_menu::IdeMenu; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style}; -/// Common menu values -pub struct MenuCommon { - /// Menu name - name: String, - /// Ide menu active status - active: bool, - /// Menu coloring - color: MenuTextStyle, - /// Menu cached values - values: Vec, - /// Selected value. Starts at 0 - selected: u16, - /// Menu marker when active - marker: String, - /// Event sent to the menu - event: Option, - /// Longest suggestion found in the values - longest_suggestion: usize, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, -} - -impl MenuCommon { - pub fn new(name: &str) -> Self { - Self { - name: name.into(), - active: false, - color: MenuTextStyle::default(), - values: Vec::new(), - selected: 0, - marker: "| ".to_string(), - event: None, - longest_suggestion: 0, - input: None, - only_buffer_difference: false, - } - } - - #[must_use] - pub fn with_marker(mut self, marker: &str) -> Self { - self.marker = marker.to_string(); - self - } -} - /// Struct to store the menu style pub struct MenuTextStyle { /// Text style for selected text in a menu @@ -111,40 +63,17 @@ pub enum MenuEvent { /// Trait that defines how a menu will be printed by the painter pub trait Menu: Send { - /// Returns the common menu values - fn common(&self) -> &MenuCommon; - - /// Returns the common menu values as mutable - fn common_mut(&mut self) -> &mut MenuCommon; - /// Menu name - fn name(&self) -> &str { - &self.common().name - } + fn name(&self) -> &str; /// Menu indicator - fn indicator(&self) -> &str { - &self.common().marker - } + fn indicator(&self) -> &str; /// Checks if the menu is active - fn is_active(&self) -> bool { - self.common().active - } + fn is_active(&self) -> bool; /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent) { - match &event { - MenuEvent::Activate(_) => self.common_mut().active = true, - MenuEvent::Deactivate => { - self.common_mut().active = false; - self.common_mut().input = None; - } - _ => {} - } - - self.common_mut().event = Some(event); - } + fn menu_event(&mut self, event: MenuEvent); /// A menu may not be allowed to quick complete because it needs to stay /// active even with one element @@ -191,71 +120,14 @@ pub trait Menu: Send { /// Minimum rows that should be displayed by the menu fn min_rows(&self) -> u16; - /// Gets the index of the selected value - fn index(&self) -> usize { - self.common().selected as usize - } - - /// Gets the selected value - fn get_value(&self) -> Option { - self.common().values.get(self.index()).cloned() - } - /// Gets cached values from menu that will be displayed - fn get_values(&self) -> &[Suggestion] { - &self.common().values - } + 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)) { // empty implementation to make it optional } } -/// Common builder for menus -pub trait MenuBuilder: Sized + Menu { - /// Menu builder with new name - #[must_use] - fn with_name(mut self, name: &str) -> Self { - self.common_mut().name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - fn with_text_style(mut self, text_style: Style) -> Self { - self.common_mut().color.text_style = text_style; - self - } - - /// Menu builder with new value for selected text style - #[must_use] - fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.common_mut().color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for description style - #[must_use] - fn with_description_style(mut self, description_style: Style) -> Self { - self.common_mut().color.description_style = description_style; - self - } - - /// Menu builder with new value for marker - #[must_use] - fn with_marker(mut self, marker: &str) -> Self { - self.common_mut().marker = marker.to_string(); - self - } - - /// Menu builder with new only buffer difference - #[must_use] - fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.common_mut().only_buffer_difference = only_buffer_difference; - self - } -} - /// Allowed menus in Reedline pub enum ReedlineMenu { /// Menu that uses Reedline's completer to update its values @@ -357,14 +229,6 @@ impl ReedlineMenu { } impl Menu for ReedlineMenu { - fn common(&self) -> &MenuCommon { - self.as_ref().common() - } - - fn common_mut(&mut self) -> &mut MenuCommon { - self.as_mut().common_mut() - } - fn name(&self) -> &str { self.as_ref().name() } From e369e09652de836bd667a1b57dd0118fafb24e39 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:46:39 +0100 Subject: [PATCH 15/23] start menu refactor --- examples/completions.rs | 2 +- examples/demo.rs | 2 +- examples/ide_completions.rs | 4 +- examples/transient_prompt.rs | 2 +- src/lib.rs | 6 +- src/menu/columnar_menu.rs | 193 ++++++++++++----------------------- src/menu/ide_menu.rs | 184 ++++++++++----------------------- src/menu/list_menu.rs | 165 ++++++++---------------------- src/menu/mod.rs | 138 ++++++++++++++++++++++++- 9 files changed, 300 insertions(+), 396 deletions(-) diff --git a/examples/completions.rs b/examples/completions.rs index 47f3c1b1..bb80142d 100644 --- a/examples/completions.rs +++ b/examples/completions.rs @@ -6,7 +6,7 @@ use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs, - KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, + KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, }; use std::io; diff --git a/examples/demo.rs b/examples/demo.rs index f1b6d576..ad1f808a 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -14,9 +14,9 @@ use { }, }; -use reedline::CursorConfig; #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))] use reedline::FileBackedHistory; +use reedline::{CursorConfig, MenuBuilder}; fn main() -> reedline::Result<()> { println!("Ctrl-D to quit"); diff --git a/examples/ide_completions.rs b/examples/ide_completions.rs index 2b9487b5..f6e69b7b 100644 --- a/examples/ide_completions.rs +++ b/examples/ide_completions.rs @@ -6,8 +6,8 @@ use reedline::{ default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand, - Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, - Signal, + Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, + ReedlineMenu, Signal, }; use std::io; diff --git a/examples/transient_prompt.rs b/examples/transient_prompt.rs index f971955b..d3aaa390 100644 --- a/examples/transient_prompt.rs +++ b/examples/transient_prompt.rs @@ -8,7 +8,7 @@ use nu_ansi_term::{Color, Style}; use reedline::SqliteBackedHistory; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultPrompt, Emacs, - ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, Prompt, PromptEditMode, + ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, ReedlineMenu, Signal, ValidationResult, Validator, }; diff --git a/src/lib.rs b/src/lib.rs index 53bdca9b..7abfe010 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,7 @@ //! ```rust //! // Create a reedline object with tab completions support //! -//! use reedline::{default_emacs_keybindings, ColumnarMenu, DefaultCompleter, Emacs, KeyCode, KeyModifiers, Reedline, ReedlineEvent, ReedlineMenu}; +//! use reedline::{default_emacs_keybindings, ColumnarMenu, DefaultCompleter, Emacs, KeyCode, KeyModifiers, Reedline, ReedlineEvent, ReedlineMenu, MenuBuilder}; //! //! let commands = vec![ //! "test".into(), @@ -277,8 +277,8 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuEvent, - MenuTextStyle, ReedlineMenu, + menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuBuilder, + MenuCommon, MenuEvent, MenuTextStyle, ReedlineMenu, }; mod terminal_extensions; diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 649fd694..9a3587e5 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -1,9 +1,9 @@ -use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; +use super::{menu_functions::find_common_string, Menu, MenuBuilder, MenuCommon, MenuEvent}; use crate::{ core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, Suggestion, UndoBehavior, }; -use nu_ansi_term::{ansi::RESET, Style}; +use nu_ansi_term::ansi::RESET; /// Default values used as reference for the menu. These values are set during /// the initial declaration of the menu and are always kept as reference for the @@ -40,12 +40,8 @@ struct ColumnDetails { /// Menu to present suggestions in a columnar fashion /// It presents a description of the suggestion if available pub struct ColumnarMenu { - /// Menu name - name: String, - /// Columnar menu active status - active: bool, - /// Menu coloring - color: MenuTextStyle, + /// Common menu values + common: MenuCommon, /// Default column details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultColumnDetails, @@ -54,76 +50,30 @@ pub struct ColumnarMenu { min_rows: u16, /// Working column details keep changing based on the collected values working_details: ColumnDetails, - /// Menu cached values - values: Vec, /// column position of the cursor. Starts from 0 col_pos: u16, /// row position in the menu. Starts from 0 row_pos: u16, - /// Menu marker when active - marker: String, - /// Event sent to the menu - event: Option, - /// Longest suggestion found in the values - longest_suggestion: usize, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for ColumnarMenu { fn default() -> Self { Self { - name: "columnar_menu".to_string(), - active: false, - color: MenuTextStyle::default(), + common: MenuCommon::default().with_name("columnar_menu"), default_details: DefaultColumnDetails::default(), min_rows: 3, working_details: ColumnDetails::default(), - values: Vec::new(), col_pos: 0, row_pos: 0, - marker: "| ".to_string(), - event: None, - longest_suggestion: 0, - input: None, - only_buffer_difference: false, } } } -// Menu configuration functions -impl ColumnarMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self - } +// Menu common configuration functions +impl MenuBuilder for ColumnarMenu {} +// Menu specific configuration functions +impl ColumnarMenu { /// Menu builder with new columns value #[must_use] pub fn with_columns(mut self, columns: u16) -> Self { @@ -144,20 +94,6 @@ impl ColumnarMenu { self.default_details.col_padding = col_padding; self } - - /// Menu builder with marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } } // Menu functionality @@ -218,7 +154,7 @@ impl ColumnarMenu { } else { let new_row = self.get_rows().saturating_sub(1); let index = new_row * self.get_cols() + self.col_pos; - if index >= self.values.len() as u16 { + if index >= self.common.values.len() as u16 { new_row.saturating_sub(1) } else { new_row @@ -233,7 +169,7 @@ impl ColumnarMenu { 0 } else { let index = new_row * self.get_cols() + self.col_pos; - if index >= self.values.len() as u16 { + if index >= self.common.values.len() as u16 { 0 } else { new_row @@ -245,7 +181,7 @@ impl ColumnarMenu { fn move_left(&mut self) { self.col_pos = if let Some(row) = self.col_pos.checked_sub(1) { row - } else if self.index() + 1 == self.values.len() { + } else if self.index() + 1 == self.common.values.len() { 0 } else { self.get_cols().saturating_sub(1) @@ -255,7 +191,8 @@ impl ColumnarMenu { /// Move menu cursor element fn move_right(&mut self) { let new_col = self.col_pos + 1; - self.col_pos = if new_col >= self.get_cols() || self.index() + 2 > self.values.len() { + self.col_pos = if new_col >= self.get_cols() || self.index() + 2 > self.common.values.len() + { 0 } else { new_col @@ -306,7 +243,7 @@ impl ColumnarMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), msg, RESET ) @@ -341,16 +278,20 @@ impl ColumnarMenu { if use_ansi_coloring { if index == self.index() { if let Some(description) = &suggestion.description { - let left_text_size = self.longest_suggestion + self.default_details.col_padding; + let left_text_size = + self.common.longest_suggestion + self.default_details.col_padding; let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{}{:max$}{}{}{}{}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), - self.color.selected_text_style.prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), + self.common.color.selected_text_style.prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), - self.color.selected_text_style.prefix(), + self.common.color.description_style.prefix(), + self.common.color.selected_text_style.prefix(), description .chars() .take(right_text_size) @@ -363,8 +304,11 @@ impl ColumnarMenu { } else { format!( "{}{}{}{}{:>empty$}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), - self.color.selected_text_style.prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), + self.common.color.selected_text_style.prefix(), &suggestion.value, RESET, "", @@ -373,14 +317,18 @@ impl ColumnarMenu { ) } } else if let Some(description) = &suggestion.description { - let left_text_size = self.longest_suggestion + self.default_details.col_padding; + let left_text_size = + self.common.longest_suggestion + self.default_details.col_padding; let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{:max$}{}{}{}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), description .chars() .take(right_text_size) @@ -393,10 +341,13 @@ impl ColumnarMenu { } else { format!( "{}{}{}{}{:>empty$}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), "", RESET, self.end_of_line(column), @@ -418,7 +369,7 @@ impl ColumnarMenu { .collect::() .replace('\n', " "), self.end_of_line(column), - max = self.longest_suggestion + max = self.common.longest_suggestion + self .default_details .col_padding @@ -445,19 +396,14 @@ impl ColumnarMenu { } impl Menu for ColumnarMenu { - /// Menu name - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() + /// Get MenuCommon + fn common(&self) -> &MenuCommon { + &self.common } - /// Deactivates context menu - fn is_active(&self) -> bool { - self.active + /// Get mutable MenuCommon + fn common_mut(&mut self) -> &mut MenuCommon { + &mut self.common } /// The columnar menu can to quick complete if there is only one element @@ -515,24 +461,10 @@ impl Menu for ColumnarMenu { } } - /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent) { - match &event { - MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; - } - _ => {} - } - - self.event = Some(event); - } - /// Updates menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - self.values = if self.only_buffer_difference { - if let Some(old_string) = &self.input { + self.common.values = if self.common.only_buffer_difference { + if let Some(old_string) = &self.common.input { let (start, input) = string_difference(editor.get_buffer(), old_string); if !input.is_empty() { completer.complete(input, start + input.len()) @@ -566,7 +498,7 @@ impl Menu for ColumnarMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.event.take() { + if let Some(event) = self.common.event.take() { // The working value for the menu are updated first before executing any of the // menu events // @@ -581,13 +513,14 @@ impl Menu for ColumnarMenu { self.working_details.columns = 1; self.working_details.col_width = painter.screen_width() as usize; - self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { - prev - } else { - suggestion.value.len() - } - }); + self.common.longest_suggestion = + self.get_values().iter().fold(0, |prev, suggestion| { + if prev >= suggestion.value.len() { + prev + } else { + suggestion.value.len() + } + }); } else { let max_width = self.get_values().iter().fold(0, |acc, suggestion| { let str_len = suggestion.value.len() + self.default_details.col_padding; @@ -627,10 +560,10 @@ impl Menu for ColumnarMenu { match event { MenuEvent::Activate(updated) => { - self.active = true; + self.common.active = true; self.reset_position(); - self.input = if self.only_buffer_difference { + self.common.input = if self.common.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -640,7 +573,7 @@ impl Menu for ColumnarMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate => self.common.active = false, MenuEvent::Edit(updated) => { self.reset_position(); @@ -693,7 +626,7 @@ impl Menu for ColumnarMenu { /// Gets values from filler that will be displayed in the menu fn get_values(&self) -> &[Suggestion] { - &self.values + &self.common.values } fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 7ff1d29a..b3f21534 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -1,4 +1,4 @@ -use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; +use super::{menu_functions::find_common_string, Menu, MenuBuilder, MenuCommon, MenuEvent}; use crate::{ core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, Suggestion, UndoBehavior, @@ -7,7 +7,7 @@ use itertools::{ EitherOrBoth::{Both, Left, Right}, Itertools, }; -use nu_ansi_term::{ansi::RESET, Style}; +use nu_ansi_term::ansi::RESET; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -130,118 +130,69 @@ struct IdeMenuDetails { /// Menu to present suggestions like similar to Ide completion menus pub struct IdeMenu { - /// Menu name - name: String, - /// Ide menu active status - active: bool, - /// Menu coloring - color: MenuTextStyle, + /// Common menu values + common: MenuCommon, /// Default ide menu details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultIdeMenuDetails, /// Working ide menu details keep changing based on the collected values working_details: IdeMenuDetails, - /// Menu cached values - values: Vec, - /// Selected value. Starts at 0 + /// Selected value selected: u16, - /// Menu marker when active - marker: String, - /// Event sent to the menu - event: Option, - /// Longest suggestion found in the values - longest_suggestion: usize, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for IdeMenu { fn default() -> Self { Self { - name: "ide_completion_menu".to_string(), - active: false, - color: MenuTextStyle::default(), + common: MenuCommon::default().with_name("ide_completion_menu"), default_details: DefaultIdeMenuDetails::default(), working_details: IdeMenuDetails::default(), - values: Vec::new(), selected: 0, - marker: "| ".to_string(), - event: None, - longest_suggestion: 0, - input: None, - only_buffer_difference: false, } } } -impl IdeMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self - } +// Menu common configuration functions +impl MenuBuilder for IdeMenu {} - /// Menu builder with new value for min completion width value +// Menu specific configuration functions +impl IdeMenu { + /// Menu builder with new value for min completion width #[must_use] pub fn with_min_completion_width(mut self, width: u16) -> Self { self.default_details.min_completion_width = width; self } - /// Menu builder with new value for max completion width value + /// Menu builder with new value for max completion width #[must_use] pub fn with_max_completion_width(mut self, width: u16) -> Self { self.default_details.max_completion_width = width; self } - /// Menu builder with new value for max completion height value + /// Menu builder with new value for max completion height #[must_use] pub fn with_max_completion_height(mut self, height: u16) -> Self { self.default_details.max_completion_height = height; self } - /// Menu builder with new value for padding value + /// Menu builder with new value for padding #[must_use] pub fn with_padding(mut self, padding: u16) -> Self { self.default_details.padding = padding; self } - /// Menu builder with the default border value + /// Menu builder with the default border #[must_use] pub fn with_default_border(mut self) -> Self { self.default_details.border = Some(BorderSymbols::default()); self } - /// Menu builder with new value for border value + /// Menu builder with new value for border #[must_use] pub fn with_border( mut self, @@ -263,27 +214,13 @@ impl IdeMenu { self } - /// Menu builder with new value for cursor offset value + /// Menu builder with new value for cursor offset #[must_use] pub fn with_cursor_offset(mut self, cursor_offset: i16) -> Self { self.default_details.cursor_offset = cursor_offset; self } - /// Menu builder with marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } - /// Menu builder with new description mode #[must_use] pub fn with_description_mode(mut self, description_mode: DescriptionMode) -> Self { @@ -330,7 +267,7 @@ impl IdeMenu { // Menu functionality impl IdeMenu { fn move_next(&mut self) { - if self.selected < (self.values.len() as u16).saturating_sub(1) { + if self.selected < (self.common.values.len() as u16).saturating_sub(1) { self.selected += 1; } else { self.selected = 0; @@ -341,7 +278,7 @@ impl IdeMenu { if self.selected > 0 { self.selected -= 1; } else { - self.selected = self.values.len().saturating_sub(1) as u16; + self.selected = self.common.values.len().saturating_sub(1) as u16; } } @@ -350,7 +287,7 @@ impl IdeMenu { } fn get_value(&self) -> Option { - self.values.get(self.index()).cloned() + self.common.values.get(self.index()).cloned() } /// Calculates how many rows the Menu will try to use (if available) @@ -399,7 +336,7 @@ impl IdeMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), msg, RESET ) @@ -457,7 +394,7 @@ impl IdeMenu { *line = format!( "{}{}{}{}{}{}", border.vertical, - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), line, padding, RESET, @@ -486,7 +423,7 @@ impl IdeMenu { if use_ansi_coloring { *line = format!( "{}{}{}{}", - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), line, padding, RESET @@ -563,10 +500,10 @@ impl IdeMenu { vertical_border, suggestion .style - .unwrap_or(self.color.text_style) + .unwrap_or(self.common.color.text_style) .reverse() .prefix(), - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -577,7 +514,10 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}", vertical_border, - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -602,19 +542,14 @@ impl IdeMenu { } impl Menu for IdeMenu { - /// Menu name - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() + /// Get MenuCommon + fn common(&self) -> &MenuCommon { + &self.common } - /// Deactivates context menu - fn is_active(&self) -> bool { - self.active + /// Get mutable MenuCommon + fn common_mut(&mut self) -> &mut MenuCommon { + &mut self.common } /// The ide menu can to quick complete if there is only one element @@ -670,24 +605,10 @@ impl Menu for IdeMenu { } } - /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent) { - match &event { - MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; - } - _ => {} - } - - self.event = Some(event); - } - /// Update menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - let (values, base_ranges) = if self.only_buffer_difference { - if let Some(old_string) = &self.input { + let (values, base_ranges) = if self.common.only_buffer_difference { + if let Some(old_string) = &self.common.input { let (start, input) = string_difference(editor.get_buffer(), old_string); if !input.is_empty() { completer.complete_with_base_ranges(input, start + input.len()) @@ -710,7 +631,7 @@ impl Menu for IdeMenu { ) }; - self.values = values; + self.common.values = values; self.working_details.base_strings = base_ranges .iter() .map(|range| editor.get_buffer()[range.clone()].to_string()) @@ -727,14 +648,14 @@ impl Menu for IdeMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.event.take() { + if let Some(event) = self.common.event.take() { // The working value for the menu are updated first before executing any of the match event { MenuEvent::Activate(updated) => { - self.active = true; + self.common.active = true; self.reset_position(); - self.input = if self.only_buffer_difference { + self.common.input = if self.common.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -744,7 +665,7 @@ impl Menu for IdeMenu { self.update_values(editor, completer); } } - MenuEvent::Deactivate => self.active = false, + MenuEvent::Deactivate => self.common.active = false, MenuEvent::Edit(updated) => { self.reset_position(); @@ -760,13 +681,14 @@ impl Menu for IdeMenu { | MenuEvent::NextPage => {} } - self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| { - if prev >= suggestion.value.len() { - prev - } else { - suggestion.value.len() - } - }); + self.common.longest_suggestion = + self.get_values().iter().fold(0, |prev, suggestion| { + if prev >= suggestion.value.len() { + prev + } else { + suggestion.value.len() + } + }); let terminal_width = painter.screen_width(); let mut cursor_pos = self.working_details.cursor_col; @@ -808,7 +730,7 @@ impl Menu for IdeMenu { 0 }; - let completion_width = ((self.longest_suggestion.min(u16::MAX as usize) as u16) + let completion_width = ((self.common.longest_suggestion.min(u16::MAX as usize) as u16) + 2 * self.default_details.padding + border_width) .min(self.default_details.max_completion_width) @@ -915,7 +837,7 @@ impl Menu for IdeMenu { } fn get_values(&self) -> &[Suggestion] { - &self.values + &self.common.values } fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { @@ -948,7 +870,7 @@ impl Menu for IdeMenu { let available_values = available_lines.saturating_sub(border_width) as usize; let max_padding = self.working_details.completion_width.saturating_sub( - self.longest_suggestion.min(u16::MAX as usize) as u16 + border_width, + self.common.longest_suggestion.min(u16::MAX as usize) as u16 + border_width, ) / 2; let corrected_padding = self.default_details.padding.min(max_padding) as usize; diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index d90e3672..65d36c28 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -1,14 +1,14 @@ use { super::{ menu_functions::{parse_selection_char, string_difference}, - Menu, MenuEvent, MenuTextStyle, + Menu, MenuBuilder, MenuCommon, MenuEvent, }, crate::{ core_editor::Editor, painting::{estimate_single_line_wraps, Painter}, Completer, Suggestion, UndoBehavior, }, - nu_ansi_term::{ansi::RESET, Style}, + nu_ansi_term::ansi::RESET, std::{fmt::Write, iter::Sum}, unicode_width::UnicodeWidthStr, }; @@ -41,22 +41,10 @@ impl<'a> Sum<&'a Page> for Page { /// Struct to store the menu style /// Context menu definition pub struct ListMenu { - /// Menu name - name: String, - /// Menu coloring - color: MenuTextStyle, + /// Common menu values + common: MenuCommon, /// Number of records pulled until page is full page_size: usize, - /// Menu marker displayed when the menu is active - marker: String, - /// Menu active status - active: bool, - /// Cached values collected when querying the completer. - /// When collecting chronological values, the menu only caches at least - /// page_size records. - /// When performing a query to the completer, the cached values will - /// be the result from such query - values: Vec, /// row position in the menu. Starts from 0 row_position: u16, /// Max size of the suggestions when querying without a search buffer @@ -69,67 +57,30 @@ pub struct ListMenu { pages: Vec, /// Page index page: usize, - /// Event sent to the menu - event: Option, - /// String collected after the menu is activated - input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for ListMenu { fn default() -> Self { Self { - name: "search_menu".to_string(), - color: MenuTextStyle::default(), + common: MenuCommon::default() + .with_name("search_menu") + .with_marker("? "), page_size: 10, - active: false, - values: Vec::new(), row_position: 0, page: 0, query_size: None, - marker: "? ".to_string(), max_lines: 5, multiline_marker: ":::".to_string(), pages: Vec::new(), - event: None, - input: None, - only_buffer_difference: true, } } } -// Menu configuration functions -impl ListMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for description style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self - } +// Menu common configuration functions +impl MenuBuilder for ListMenu {} +// Menu specific configuration functions +impl ListMenu { /// Menu builder with new page size #[must_use] pub fn with_page_size(mut self, page_size: usize) -> Self { @@ -137,30 +88,16 @@ impl ListMenu { self } - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } -} - -// Menu functionality -impl ListMenu { - /// Menu builder with menu marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - /// Menu builder with max entry lines #[must_use] pub fn with_max_entry_lines(mut self, max_lines: u16) -> Self { self.max_lines = max_lines; self } +} +// Menu functionality +impl ListMenu { fn update_row_pos(&mut self, new_pos: Option) { if let (Some(row), Some(page)) = (new_pos, self.pages.get(self.page)) { let values_before_page = self.pages.iter().take(self.page).sum::().size; @@ -177,7 +114,7 @@ impl ListMenu { } fn total_values(&self) -> usize { - self.query_size.unwrap_or(self.values.len()) + self.query_size.unwrap_or(self.common.values.len()) } fn values_until_current_page(&self) -> usize { @@ -246,7 +183,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), msg, RESET ) @@ -257,7 +194,7 @@ impl ListMenu { fn banner_message(&self, page: &Page, use_ansi_coloring: bool) -> String { let values_until = self.values_until_current_page().saturating_sub(1); - let value_before = if self.values.is_empty() || self.page == 0 { + let value_before = if self.common.values.is_empty() || self.page == 0 { 0 } else { let page_size = self.pages.get(self.page).map(|page| page.size).unwrap_or(0); @@ -277,7 +214,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.common.color.selected_text_style.prefix(), status_bar, RESET, ) @@ -294,9 +231,9 @@ impl ListMenu { /// Text style for menu fn text_style(&self, index: usize) -> String { if index == self.index() { - self.color.selected_text_style.prefix().to_string() + self.common.color.selected_text_style.prefix().to_string() } else { - self.color.text_style.prefix().to_string() + self.common.color.text_style.prefix().to_string() } } @@ -313,7 +250,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}({}) {}", - self.color.description_style.prefix(), + self.common.color.description_style.prefix(), desc, RESET ) @@ -348,18 +285,14 @@ impl ListMenu { } impl Menu for ListMenu { - fn name(&self) -> &str { - self.name.as_str() + /// Get MenuCommon + fn common(&self) -> &MenuCommon { + &self.common } - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() - } - - /// Deactivates context menu - fn is_active(&self) -> bool { - self.active + /// Get mutable MenuCommon + fn common_mut(&mut self) -> &mut MenuCommon { + &mut self.common } /// There is no use for quick complete for the menu @@ -378,25 +311,11 @@ impl Menu for ListMenu { false } - /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent) { - match &event { - MenuEvent::Activate(_) => self.active = true, - MenuEvent::Deactivate => { - self.active = false; - self.input = None; - } - _ => {} - } - - self.event = Some(event); - } - /// Collecting the value from the completer to be shown in the menu fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { let line_buffer = editor.line_buffer(); - let (pos, input) = if self.only_buffer_difference { - match &self.input { + let (pos, input) = if self.common.only_buffer_difference { + match &self.common.input { Some(old_string) => { let (start, input) = string_difference(line_buffer.get_buffer(), old_string); if input.is_empty() { @@ -419,11 +338,11 @@ impl Menu for ListMenu { // If there are no row selector and the menu has an Edit event, this clears // the position together with the pages vector - if matches!(self.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() { + if matches!(self.common.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() { self.reset_position(); } - self.values = if parsed.remainder.is_empty() { + self.common.values = if parsed.remainder.is_empty() { self.query_size = Some(completer.total_completions(parsed.remainder, pos)); let skip = self.pages.iter().take(self.page).sum::().size; @@ -445,13 +364,13 @@ impl Menu for ListMenu { if self.query_size.is_some() { // When there is a size value it means that only a chunk of the // chronological data from the database was collected - &self.values + &self.common.values } else { // If no record then it means that the values hold the result // from the query to the database. This slice can be used to get the // data that will be shown in the menu - if self.values.is_empty() { - return &self.values; + if self.common.values.is_empty() { + return &self.common.values; } let start = self.pages.iter().take(self.page).sum::().size; @@ -463,7 +382,7 @@ impl Menu for ListMenu { }; let end = end.min(self.total_values()); - &self.values[start..end] + &self.common.values[start..end] } } @@ -498,12 +417,12 @@ impl Menu for ListMenu { completer: &mut dyn Completer, painter: &Painter, ) { - if let Some(event) = self.event.clone() { + if let Some(event) = self.common.event.clone() { match event { MenuEvent::Activate(_) => { self.reset_position(); - self.input = if self.only_buffer_difference { + self.common.input = if self.common.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -517,8 +436,8 @@ impl Menu for ListMenu { }); } MenuEvent::Deactivate => { - self.active = false; - self.input = None; + self.common.active = false; + self.common.input = None; } MenuEvent::Edit(_) => { self.update_values(editor, completer); @@ -532,7 +451,7 @@ impl Menu for ListMenu { if let Some(page) = self.pages.get(self.page) { if new_pos >= page.size as u16 { - self.event = Some(MenuEvent::NextPage); + self.common.event = Some(MenuEvent::NextPage); self.update_working_details(editor, completer, painter); } else { self.row_position = new_pos; @@ -553,7 +472,7 @@ impl Menu for ListMenu { self.row_position = page.size.saturating_sub(1) as u16; } - self.event = Some(MenuEvent::PreviousPage); + self.common.event = Some(MenuEvent::PreviousPage); self.update_working_details(editor, completer, painter); } } @@ -591,7 +510,7 @@ impl Menu for ListMenu { } } - self.event = None; + self.common.event = None; } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index b9c1d98d..46a03db8 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -12,6 +12,61 @@ pub use ide_menu::IdeMenu; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style}; +/// Common menu values +pub struct MenuCommon { + /// Menu name + name: String, + /// Columnar menu active status + active: bool, + /// Menu coloring + color: MenuTextStyle, + /// Menu cached values + values: Vec, + /// Menu marker when active + marker: String, + /// Event sent to the menu + event: Option, + /// Longest suggestion found in the values + longest_suggestion: usize, + /// String collected after the menu is activated + input: Option, + /// Calls the completer using only the line buffer difference difference + /// after the menu was activated + only_buffer_difference: bool, +} + +impl Default for MenuCommon { + fn default() -> Self { + Self { + name: "menu".to_string(), + active: false, + color: MenuTextStyle::default(), + values: Vec::new(), + marker: "| ".to_string(), + event: None, + longest_suggestion: 0, + input: None, + only_buffer_difference: false, + } + } +} + +impl MenuCommon { + /// MenuCommon builder with value for name + #[must_use] + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self + } + + /// MenuCommon builder with value for marker + #[must_use] + pub fn with_marker(mut self, marker: &str) -> Self { + self.marker = marker.to_string(); + self + } +} + /// Struct to store the menu style pub struct MenuTextStyle { /// Text style for selected text in a menu @@ -63,17 +118,39 @@ pub enum MenuEvent { /// Trait that defines how a menu will be printed by the painter pub trait Menu: Send { + /// Get MenuCommon + fn common(&self) -> &MenuCommon; + /// Get mutable MenuCommon + fn common_mut(&mut self) -> &mut MenuCommon; + /// Menu name - fn name(&self) -> &str; + fn name(&self) -> &str { + &self.common().name + } /// Menu indicator - fn indicator(&self) -> &str; + fn indicator(&self) -> &str { + &self.common().marker + } /// Checks if the menu is active - fn is_active(&self) -> bool; + fn is_active(&self) -> bool { + self.common().active + } /// Selects what type of event happened with the menu - fn menu_event(&mut self, event: MenuEvent); + fn menu_event(&mut self, event: MenuEvent) { + match &event { + MenuEvent::Activate(_) => self.common_mut().active = true, + MenuEvent::Deactivate => { + self.common_mut().active = false; + self.common_mut().input = None; + } + _ => {} + } + + self.common_mut().event = Some(event); + } /// A menu may not be allowed to quick complete because it needs to stay /// active even with one element @@ -128,6 +205,51 @@ pub trait Menu: Send { } } +/// Common menu builder +pub trait MenuBuilder: Menu + Sized { + /// Menu builder with new name + #[must_use] + fn with_name(mut self, name: &str) -> Self { + self.common_mut().name = name.to_string(); + self + } + + /// Menu builder with new value for text style + #[must_use] + fn with_text_style(mut self, text_style: Style) -> Self { + self.common_mut().color.text_style = text_style; + self + } + + /// Menu builder with new value for selected text style + #[must_use] + fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { + self.common_mut().color.selected_text_style = selected_text_style; + self + } + + /// Menu builder with new value for description style + #[must_use] + fn with_description_style(mut self, description_style: Style) -> Self { + self.common_mut().color.description_style = description_style; + self + } + + /// Menu builder with new value for marker + #[must_use] + fn with_marker(mut self, marker: &str) -> Self { + self.common_mut().marker = marker.to_string(); + self + } + + /// Menu builder with new only buffer difference + #[must_use] + fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.common_mut().only_buffer_difference = only_buffer_difference; + self + } +} + /// Allowed menus in Reedline pub enum ReedlineMenu { /// Menu that uses Reedline's completer to update its values @@ -229,6 +351,14 @@ impl ReedlineMenu { } impl Menu for ReedlineMenu { + fn common(&self) -> &MenuCommon { + self.as_ref().common() + } + + fn common_mut(&mut self) -> &mut MenuCommon { + self.as_mut().common_mut() + } + fn name(&self) -> &str { self.as_ref().name() } From 824b41acf85b7ce81b7efe47e78a8b66039ccb65 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:40:40 +0100 Subject: [PATCH 16/23] fix ci --- src/menu/ide_menu.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 40e552fb..6a15b8f5 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -498,8 +498,11 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}{}", vertical_border, - suggestion.style.unwrap_or(self.color.text_style).prefix(), - self.color.selected_text_style.prefix(), + suggestion + .style + .unwrap_or(self.common.color.text_style) + .prefix(), + self.common.color.selected_text_style.prefix(), " ".repeat(padding), string, " ".repeat(padding_right), From 7082ad8d929f4ed4593d26734e6f924abf8e22be Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:55:21 +0100 Subject: [PATCH 17/23] use MenuSettings struct --- examples/completions.rs | 2 +- examples/demo.rs | 2 +- examples/ide_completions.rs | 4 +- examples/transient_prompt.rs | 2 +- src/completion/default.rs | 13 +- src/lib.rs | 6 +- src/menu/columnar_menu.rs | 209 +++-------- src/menu/description_menu.rs | 668 +++++++++++++++++++++++++++++++++++ src/menu/ide_menu.rs | 210 +++-------- src/menu/list_menu.rs | 157 ++------ src/menu/menu_functions.rs | 149 +++++++- src/menu/mod.rs | 119 ++++++- 12 files changed, 1097 insertions(+), 444 deletions(-) create mode 100644 src/menu/description_menu.rs diff --git a/examples/completions.rs b/examples/completions.rs index 47f3c1b1..bb80142d 100644 --- a/examples/completions.rs +++ b/examples/completions.rs @@ -6,7 +6,7 @@ use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs, - KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal, + KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, }; use std::io; diff --git a/examples/demo.rs b/examples/demo.rs index f1b6d576..ad1f808a 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -14,9 +14,9 @@ use { }, }; -use reedline::CursorConfig; #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))] use reedline::FileBackedHistory; +use reedline::{CursorConfig, MenuBuilder}; fn main() -> reedline::Result<()> { println!("Ctrl-D to quit"); diff --git a/examples/ide_completions.rs b/examples/ide_completions.rs index 2b9487b5..f6e69b7b 100644 --- a/examples/ide_completions.rs +++ b/examples/ide_completions.rs @@ -6,8 +6,8 @@ use reedline::{ default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand, - Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, - Signal, + Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent, + ReedlineMenu, Signal, }; use std::io; diff --git a/examples/transient_prompt.rs b/examples/transient_prompt.rs index f971955b..d3aaa390 100644 --- a/examples/transient_prompt.rs +++ b/examples/transient_prompt.rs @@ -8,7 +8,7 @@ use nu_ansi_term::{Color, Style}; use reedline::SqliteBackedHistory; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultPrompt, Emacs, - ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, Prompt, PromptEditMode, + ExampleHighlighter, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, ReedlineMenu, Signal, ValidationResult, Validator, }; diff --git a/src/completion/default.rs b/src/completion/default.rs index c8823c24..42f4d8a8 100644 --- a/src/completion/default.rs +++ b/src/completion/default.rs @@ -61,11 +61,11 @@ impl Completer for DefaultCompleter { /// ]); /// /// assert_eq!( - /// completions.complete("to the bat",10), + /// completions.complete("to the\r\nbat",11), /// vec![ - /// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 7, end: 10 }, append_whitespace: false}, - /// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 7, end: 10 }, append_whitespace: false}, - /// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 7, end: 10 }, append_whitespace: false}, + /// Suggestion {value: "batcave".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false}, + /// Suggestion {value: "batman".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false}, + /// Suggestion {value: "batmobile".into(), description: None, style: None, extra: None, span: Span { start: 8, end: 11 }, append_whitespace: false}, /// ]); /// ``` fn complete(&mut self, line: &str, pos: usize) -> Vec { @@ -75,6 +75,10 @@ impl Completer for DefaultCompleter { // `only_buffer_difference` is false let line = if line.len() > pos { &line[..pos] } else { line }; if !line.is_empty() { + // When editing a multiline buffer, there can be new line characters in it. + // Also, by replacing the new line character with a space, the insert + // position is maintain in the line buffer. + let line = line.replace("\r\n", " ").replace('\n', " "); let mut split = line.split(' ').rev(); let mut span_line: String = String::new(); for _ in 0..split.clone().count() { @@ -119,6 +123,7 @@ impl Completer for DefaultCompleter { completions } } + impl DefaultCompleter { /// Construct the default completer with a list of commands/keywords to highlight pub fn new(external_commands: Vec) -> Self { diff --git a/src/lib.rs b/src/lib.rs index 53bdca9b..4d14261b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,7 @@ //! ```rust //! // Create a reedline object with tab completions support //! -//! use reedline::{default_emacs_keybindings, ColumnarMenu, DefaultCompleter, Emacs, KeyCode, KeyModifiers, Reedline, ReedlineEvent, ReedlineMenu}; +//! use reedline::{default_emacs_keybindings, ColumnarMenu, DefaultCompleter, Emacs, KeyCode, KeyModifiers, Reedline, ReedlineEvent, ReedlineMenu, MenuBuilder}; //! //! let commands = vec![ //! "test".into(), @@ -277,8 +277,8 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, DescriptionMode, IdeMenu, ListMenu, Menu, MenuEvent, - MenuTextStyle, ReedlineMenu, + menu_functions, ColumnarMenu, DescriptionMenu, DescriptionMode, IdeMenu, ListMenu, Menu, + MenuBuilder, MenuEvent, MenuTextStyle, ReedlineMenu, }; mod terminal_extensions; diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 649fd694..6378b456 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -1,9 +1,11 @@ -use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; +use super::{Menu, MenuBuilder, MenuEvent, MenuSettings}; use crate::{ - core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, - Suggestion, UndoBehavior, + core_editor::Editor, + menu_functions::{can_partially_complete, completer_input, replace_in_buffer}, + painting::Painter, + Completer, Suggestion, }; -use nu_ansi_term::{ansi::RESET, Style}; +use nu_ansi_term::ansi::RESET; /// Default values used as reference for the menu. These values are set during /// the initial declaration of the menu and are always kept as reference for the @@ -40,12 +42,10 @@ struct ColumnDetails { /// Menu to present suggestions in a columnar fashion /// It presents a description of the suggestion if available pub struct ColumnarMenu { - /// Menu name - name: String, + /// Menu settings + settings: MenuSettings, /// Columnar menu active status active: bool, - /// Menu coloring - color: MenuTextStyle, /// Default column details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultColumnDetails, @@ -60,70 +60,41 @@ pub struct ColumnarMenu { col_pos: u16, /// row position in the menu. Starts from 0 row_pos: u16, - /// Menu marker when active - marker: String, /// Event sent to the menu event: Option, /// Longest suggestion found in the values longest_suggestion: usize, /// String collected after the menu is activated input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for ColumnarMenu { fn default() -> Self { Self { - name: "columnar_menu".to_string(), + settings: MenuSettings::default(), active: false, - color: MenuTextStyle::default(), default_details: DefaultColumnDetails::default(), min_rows: 3, working_details: ColumnDetails::default(), values: Vec::new(), col_pos: 0, row_pos: 0, - marker: "| ".to_string(), event: None, longest_suggestion: 0, input: None, - only_buffer_difference: false, } } } // Menu configuration functions -impl ColumnarMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self +impl MenuBuilder for ColumnarMenu { + fn settings_mut(&mut self) -> &mut MenuSettings { + &mut self.settings } +} +// Menu specific configuration functions +impl ColumnarMenu { /// Menu builder with new columns value #[must_use] pub fn with_columns(mut self, columns: u16) -> Self { @@ -144,20 +115,6 @@ impl ColumnarMenu { self.default_details.col_padding = col_padding; self } - - /// Menu builder with marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } } // Menu functionality @@ -306,7 +263,7 @@ impl ColumnarMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.settings.color.selected_text_style.prefix(), msg, RESET ) @@ -345,12 +302,15 @@ impl ColumnarMenu { let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{}{:max$}{}{}{}{}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), - self.color.selected_text_style.prefix(), + suggestion + .style + .unwrap_or(self.settings.color.text_style) + .prefix(), + self.settings.color.selected_text_style.prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), - self.color.selected_text_style.prefix(), + self.settings.color.description_style.prefix(), + self.settings.color.selected_text_style.prefix(), description .chars() .take(right_text_size) @@ -363,8 +323,11 @@ impl ColumnarMenu { } else { format!( "{}{}{}{}{:>empty$}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), - self.color.selected_text_style.prefix(), + suggestion + .style + .unwrap_or(self.settings.color.text_style) + .prefix(), + self.settings.color.selected_text_style.prefix(), &suggestion.value, RESET, "", @@ -377,10 +340,13 @@ impl ColumnarMenu { let right_text_size = self.get_width().saturating_sub(left_text_size); format!( "{}{:max$}{}{}{}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.settings.color.text_style) + .prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), + self.settings.color.description_style.prefix(), description .chars() .take(right_text_size) @@ -393,10 +359,13 @@ impl ColumnarMenu { } else { format!( "{}{}{}{}{:>empty$}{}{}", - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.settings.color.text_style) + .prefix(), &suggestion.value, RESET, - self.color.description_style.prefix(), + self.settings.color.description_style.prefix(), "", RESET, self.end_of_line(column), @@ -445,14 +414,9 @@ impl ColumnarMenu { } impl Menu for ColumnarMenu { - /// Menu name - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() + /// Menu settings + fn settings(&self) -> &MenuSettings { + &self.settings } /// Deactivates context menu @@ -479,37 +443,12 @@ impl Menu for ColumnarMenu { self.update_values(editor, completer); } - let values = self.get_values(); - if let (Some(Suggestion { value, span, .. }), Some(index)) = find_common_string(values) { - let index = index.min(value.len()); - let matching = &value[0..index]; - - // make sure that the partial completion does not overwrite user entered input - let extends_input = matching.starts_with(&editor.get_buffer()[span.start..span.end]); - - if !matching.is_empty() && extends_input { - let mut line_buffer = editor.line_buffer().clone(); - line_buffer.replace_range(span.start..span.end, matching); - - let offset = if matching.len() < (span.end - span.start) { - line_buffer - .insertion_point() - .saturating_sub((span.end - span.start) - matching.len()) - } else { - line_buffer.insertion_point() + matching.len() - (span.end - span.start) - }; - - line_buffer.set_insertion_point(offset); - editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); - - // The values need to be updated because the spans need to be - // recalculated for accurate replacement in the string - self.update_values(editor, completer); + if can_partially_complete(self.get_values(), editor) { + // The values need to be updated because the spans need to be + // recalculated for accurate replacement in the string + self.update_values(editor, completer); - true - } else { - false - } + true } else { false } @@ -531,29 +470,13 @@ impl Menu for ColumnarMenu { /// Updates menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - self.values = 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()) - } else { - completer.complete("", editor.insertion_point()) - } - } else { - completer.complete("", editor.insertion_point()) - } - } else { - // If there is a new line character in the line buffer, the completer - // doesn't calculate the suggested values correctly. This happens when - // editing a multiline buffer. - // 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( - &trimmed_buffer[..editor.insertion_point()], - editor.insertion_point(), - ) - }; + let (input, pos) = completer_input( + editor.get_buffer(), + editor.insertion_point(), + self.input.as_deref(), + self.settings.only_buffer_difference, + ); + self.values = completer.complete(&input, pos); self.reset_position(); } @@ -630,7 +553,7 @@ impl Menu for ColumnarMenu { self.active = true; self.reset_position(); - self.input = if self.only_buffer_difference { + self.input = if self.settings.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -663,27 +586,7 @@ impl Menu for ColumnarMenu { /// The buffer gets replaced in the Span location fn replace_in_buffer(&self, editor: &mut Editor) { - if let Some(Suggestion { - mut value, - span, - append_whitespace, - .. - }) = self.get_value() - { - let start = span.start.min(editor.line_buffer().len()); - let end = span.end.min(editor.line_buffer().len()); - if append_whitespace { - value.push(' '); - } - let mut line_buffer = editor.line_buffer().clone(); - line_buffer.replace_range(start..end, &value); - - let mut offset = line_buffer.insertion_point(); - offset = offset.saturating_add(value.len()); - offset = offset.saturating_sub(end.saturating_sub(start)); - line_buffer.set_insertion_point(offset); - editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); - } + replace_in_buffer(self.get_value(), editor); } /// Minimum rows that should be displayed by the menu @@ -737,7 +640,7 @@ impl Menu for ColumnarMenu { #[cfg(test)] mod tests { - use crate::Span; + use crate::{Span, UndoBehavior}; use super::*; diff --git a/src/menu/description_menu.rs b/src/menu/description_menu.rs new file mode 100644 index 00000000..9954b4b8 --- /dev/null +++ b/src/menu/description_menu.rs @@ -0,0 +1,668 @@ +use { + super::MenuSettings, + crate::{ + menu_functions::{completer_input, replace_in_buffer}, + Completer, Editor, Menu, MenuBuilder, MenuEvent, Painter, Suggestion, + }, + nu_ansi_term::ansi::RESET, +}; + +/// Default values used as reference for the menu. These values are set during +/// the initial declaration of the menu and are always kept as reference for the +/// changeable [`WorkingDetails`] +struct DefaultMenuDetails { + /// Number of columns that the menu will have + pub columns: u16, + /// Column width + pub col_width: Option, + /// Column padding + pub col_padding: usize, + /// Number of rows for commands + pub selection_rows: u16, + /// Number of rows allowed to display the description + pub description_rows: usize, +} + +impl Default for DefaultMenuDetails { + fn default() -> Self { + Self { + columns: 4, + col_width: None, + col_padding: 2, + selection_rows: 4, + description_rows: 10, + } + } +} + +/// Represents the actual column conditions of the menu. These conditions change +/// since they need to accommodate possible different line sizes for the column values +#[derive(Default)] +struct WorkingDetails { + /// Number of columns that the menu will have + pub columns: u16, + /// Column width + pub col_width: usize, + /// Number of rows for description + pub description_rows: usize, +} + +/// Completion menu definition +pub struct DescriptionMenu { + /// Menu settings + settings: MenuSettings, + /// Menu status + active: bool, + /// Default column details that are set when creating the menu + /// These values are the reference for the working details + default_details: DefaultMenuDetails, + /// Number of minimum rows that are displayed when + /// the required lines is larger than the available lines + min_rows: u16, + /// Working column details keep changing based on the collected values + working_details: WorkingDetails, + /// Menu cached values + values: Vec, + /// column position of the cursor. Starts from 0 + col_pos: u16, + /// row position in the menu. Starts from 0 + row_pos: u16, + /// Event sent to the menu + event: Option, + /// String collected after the menu is activated + input: Option, + /// Examples to select + examples: Vec, + /// Example index + example_index: Option, + /// Examples may not be shown if there is not enough space in the screen + show_examples: bool, + /// Skipped description rows + skipped_rows: usize, +} + +impl Default for DescriptionMenu { + fn default() -> Self { + Self { + settings: MenuSettings::default() + .with_name("description_menu") + .with_marker("? ") + .with_only_buffer_difference(true), + active: false, + default_details: DefaultMenuDetails::default(), + min_rows: 3, + working_details: WorkingDetails::default(), + values: Vec::new(), + col_pos: 0, + row_pos: 0, + event: None, + input: None, + examples: Vec::new(), + example_index: None, + show_examples: true, + skipped_rows: 0, + } + } +} + +// Menu configuration functions +impl MenuBuilder for DescriptionMenu { + fn settings_mut(&mut self) -> &mut MenuSettings { + &mut self.settings + } +} + +// Menu specific configuration functions +impl DescriptionMenu { + /// Menu builder with new columns value + pub fn with_columns(mut self, columns: u16) -> Self { + self.default_details.columns = columns; + self + } + + /// Menu builder with new column width value + pub fn with_column_width(mut self, col_width: Option) -> Self { + self.default_details.col_width = col_width; + self + } + + /// Menu builder with new column width value + pub fn with_column_padding(mut self, col_padding: usize) -> Self { + self.default_details.col_padding = col_padding; + self + } + + /// Menu builder with new selection rows value + pub fn with_selection_rows(mut self, selection_rows: u16) -> Self { + self.default_details.selection_rows = selection_rows; + self + } + + /// Menu builder with new description rows value + pub fn with_description_rows(mut self, description_rows: usize) -> Self { + self.default_details.description_rows = description_rows; + self + } +} + +// Menu functionality +impl DescriptionMenu { + /// Move menu cursor to the next element + fn move_next(&mut self) { + let mut new_col = self.col_pos + 1; + let mut new_row = self.row_pos; + + if new_col >= self.get_cols() { + new_row += 1; + new_col = 0; + } + + if new_row >= self.get_rows() { + new_row = 0; + new_col = 0; + } + + let position = new_row * self.get_cols() + new_col; + if position >= self.get_values().len() as u16 { + self.reset_position(); + } else { + self.col_pos = new_col; + self.row_pos = new_row; + } + } + + /// Move menu cursor to the previous element + fn move_previous(&mut self) { + let new_col = self.col_pos.checked_sub(1); + + let (new_col, new_row) = match new_col { + Some(col) => (col, self.row_pos), + None => match self.row_pos.checked_sub(1) { + Some(row) => (self.get_cols().saturating_sub(1), row), + None => ( + self.get_cols().saturating_sub(1), + self.get_rows().saturating_sub(1), + ), + }, + }; + + let position = new_row * self.get_cols() + new_col; + if position >= self.get_values().len() as u16 { + self.col_pos = (self.get_values().len() as u16 % self.get_cols()).saturating_sub(1); + self.row_pos = self.get_rows().saturating_sub(1); + } else { + self.col_pos = new_col; + self.row_pos = new_row; + } + } + + /// Menu index based on column and row position + fn index(&self) -> usize { + let index = self.row_pos * self.get_cols() + self.col_pos; + index as usize + } + + /// Get selected value from the menu + fn get_value(&self) -> Option { + self.get_values().get(self.index()).cloned() + } + + /// Calculates how many rows the Menu will use + fn get_rows(&self) -> u16 { + let values = self.get_values().len() as u16; + + if values == 0 { + // When the values are empty the no_records_msg is shown, taking 1 line + return 1; + } + + let rows = values / self.get_cols(); + if values % self.get_cols() != 0 { + rows + 1 + } else { + rows + } + } + + /// Returns working details col width + fn get_width(&self) -> usize { + self.working_details.col_width + } + + /// Reset menu position + fn reset_position(&mut self) { + self.col_pos = 0; + self.row_pos = 0; + self.skipped_rows = 0; + } + + fn no_records_msg(&self, use_ansi_coloring: bool) -> String { + let msg = "TYPE TO START SEARCH"; + if use_ansi_coloring { + format!( + "{}{}{}", + self.settings.color.selected_text_style.prefix(), + msg, + RESET + ) + } else { + msg.to_string() + } + } + + /// Returns working details columns + fn get_cols(&self) -> u16 { + self.working_details.columns.max(1) + } + + /// End of line for menu + fn end_of_line(&self, column: u16, index: usize) -> &str { + let is_last = index == self.values.len().saturating_sub(1); + if column == self.get_cols().saturating_sub(1) || is_last { + "\r\n" + } else { + "" + } + } + + /// Update list of examples from the actual value + fn update_examples(&mut self) { + self.examples = self + .get_value() + .and_then(|suggestion| suggestion.extra) + .unwrap_or_default(); + + self.example_index = None; + } + + /// Creates default string that represents one suggestion from the menu + fn create_entry_string( + &self, + suggestion: &Suggestion, + index: usize, + column: u16, + empty_space: usize, + use_ansi_coloring: bool, + ) -> String { + if use_ansi_coloring { + if index == self.index() { + format!( + "{}{}{}{:>empty$}{}", + self.settings.color.selected_text_style.prefix(), + &suggestion.value, + RESET, + "", + self.end_of_line(column, index), + empty = empty_space, + ) + } else { + format!( + "{}{}{}{:>empty$}{}", + self.settings.color.text_style.prefix(), + &suggestion.value, + RESET, + "", + self.end_of_line(column, index), + empty = empty_space, + ) + } + } else { + // If no ansi coloring is found, then the selection word is + // the line in uppercase + let (marker, empty_space) = if index == self.index() { + (">", empty_space.saturating_sub(1)) + } else { + ("", empty_space) + }; + + let line = format!( + "{}{}{:>empty$}{}", + marker, + &suggestion.value, + "", + self.end_of_line(column, index), + empty = empty_space, + ); + + if index == self.index() { + line.to_uppercase() + } else { + line + } + } + } + + /// Description string with color + fn create_description_string(&self, use_ansi_coloring: bool) -> String { + let description = self + .get_value() + .and_then(|suggestion| suggestion.description) + .unwrap_or_default() + .lines() + .skip(self.skipped_rows) + .take(self.working_details.description_rows) + .collect::>() + .join("\r\n"); + + if use_ansi_coloring && !description.is_empty() { + format!( + "{}{}{}", + self.settings.color.description_style.prefix(), + description, + RESET, + ) + } else { + description + } + } + + /// Selectable list of examples from the actual value + fn create_example_string(&self, use_ansi_coloring: bool) -> String { + if !self.show_examples { + return "".into(); + } + + let examples: String = self + .examples + .iter() + .enumerate() + .map(|(index, example)| { + if let Some(example_index) = self.example_index { + if index == example_index { + format!( + " {}{}{}\r\n", + self.settings.color.selected_text_style.prefix(), + example, + RESET + ) + } else { + format!(" {example}\r\n") + } + } else { + format!(" {example}\r\n") + } + }) + .collect(); + + if examples.is_empty() { + "".into() + } else if use_ansi_coloring { + format!( + "{}\r\n\r\nExamples:\r\n{}{}", + self.settings.color.description_style.prefix(), + RESET, + examples, + ) + } else { + format!("\r\n\r\nExamples:\r\n{examples}",) + } + } +} + +impl Menu for DescriptionMenu { + /// Menu settings + fn settings(&self) -> &MenuSettings { + &self.settings + } + + /// Deactivates context menu + fn is_active(&self) -> bool { + self.active + } + + /// The menu stays active even with one record + fn can_quick_complete(&self) -> bool { + false + } + + /// The menu does not need to partially complete + fn can_partially_complete( + &mut self, + _values_updated: bool, + _editor: &mut Editor, + _completer: &mut dyn Completer, + ) -> bool { + false + } + + /// Selects what type of event happened with the menu + 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(); + } + _ => {} + }; + + self.event = Some(event); + } + + /// Updates menu values + fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { + let (input, pos) = completer_input( + editor.get_buffer(), + editor.insertion_point(), + self.input.as_deref(), + self.settings.only_buffer_difference, + ); + self.values = completer.complete(&input, pos); + + self.reset_position(); + } + + /// The working details for the menu changes based on the size of the lines + /// collected from the completer + fn update_working_details( + &mut self, + editor: &mut Editor, + completer: &mut dyn Completer, + painter: &Painter, + ) { + if let Some(event) = self.event.take() { + // Updating all working parameters from the menu before executing any of the + // possible event + let max_width = self.get_values().iter().fold(0, |acc, suggestion| { + let str_len = suggestion.value.len() + self.default_details.col_padding; + if str_len > acc { + str_len + } else { + acc + } + }); + + // If no default width is found, then the total screen width is used to estimate + // the column width based on the default number of columns + let default_width = if let Some(col_width) = self.default_details.col_width { + col_width + } else { + let col_width = painter.screen_width() / self.default_details.columns; + col_width as usize + }; + + // Adjusting the working width of the column based the max line width found + // in the menu values + if max_width > default_width { + self.working_details.col_width = max_width; + } else { + self.working_details.col_width = default_width; + }; + + // The working columns is adjusted based on possible number of columns + // that could be fitted in the screen with the calculated column width + let possible_cols = painter.screen_width() / self.working_details.col_width as u16; + if possible_cols > self.default_details.columns { + self.working_details.columns = self.default_details.columns.max(1); + } else { + self.working_details.columns = possible_cols; + } + + // Updating the working rows to display the description + if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() { + self.working_details.description_rows = self.default_details.description_rows; + self.show_examples = true; + } else { + self.working_details.description_rows = painter + .remaining_lines() + .saturating_sub(self.default_details.selection_rows + 1) + as usize; + + self.show_examples = false; + } + + match event { + MenuEvent::Activate(_) => { + self.reset_position(); + self.input = Some(editor.get_buffer().to_string()); + self.update_values(editor, completer); + } + MenuEvent::Deactivate => self.active = false, + MenuEvent::Edit(_) => { + self.reset_position(); + self.update_values(editor, completer); + self.update_examples() + } + MenuEvent::NextElement => { + self.skipped_rows = 0; + self.move_next(); + self.update_examples(); + } + MenuEvent::PreviousElement => { + self.skipped_rows = 0; + self.move_previous(); + self.update_examples(); + } + MenuEvent::MoveUp => { + if let Some(example_index) = self.example_index { + if let Some(index) = example_index.checked_sub(1) { + self.example_index = Some(index); + } else { + self.example_index = Some(self.examples.len().saturating_sub(1)); + } + } else if !self.examples.is_empty() { + self.example_index = Some(0); + } + } + MenuEvent::MoveDown => { + if let Some(example_index) = self.example_index { + let index = example_index + 1; + if index < self.examples.len() { + self.example_index = Some(index); + } else { + self.example_index = Some(0); + } + } else if !self.examples.is_empty() { + self.example_index = Some(0); + } + } + MenuEvent::MoveLeft => self.skipped_rows = self.skipped_rows.saturating_sub(1), + MenuEvent::MoveRight => { + let skipped = self.skipped_rows + 1; + let description_rows = self + .get_value() + .and_then(|suggestion| suggestion.description) + .unwrap_or_default() + .lines() + .count(); + + let allowed_skips = + description_rows.saturating_sub(self.working_details.description_rows); + + if skipped < allowed_skips { + self.skipped_rows = skipped; + } else { + self.skipped_rows = allowed_skips; + } + } + MenuEvent::PreviousPage | MenuEvent::NextPage => {} + } + } + } + + /// The buffer gets replaced in the Span location + fn replace_in_buffer(&self, editor: &mut Editor) { + if let Some(mut suggestion) = self.get_value() { + if let Some(example_index) = self.example_index { + let example = self + .examples + .get(example_index) + .expect("the example index is always checked"); + suggestion.value = example.clone(); + } + replace_in_buffer(Some(suggestion), editor); + } + } + + /// Minimum rows that should be displayed by the menu + fn min_rows(&self) -> u16 { + self.get_rows().min(self.min_rows) + } + + /// Gets values from filler that will be displayed in the menu + fn get_values(&self) -> &[Suggestion] { + &self.values + } + + fn menu_required_lines(&self, _terminal_columns: u16) -> u16 { + let example_lines = self + .examples + .iter() + .fold(0, |acc, example| example.lines().count() + acc); + + self.default_details.selection_rows + + self.default_details.description_rows as u16 + + example_lines as u16 + + 3 + } + + fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String { + if self.get_values().is_empty() { + self.no_records_msg(use_ansi_coloring) + } else { + // The skip values represent the number of lines that should be skipped + // while printing the menu + let available_lines = self.default_details.selection_rows; + let skip_values = if self.row_pos >= available_lines { + let skip_lines = self.row_pos.saturating_sub(available_lines) + 1; + (skip_lines * self.get_cols()) as usize + } else { + 0 + }; + + // It seems that crossterm prefers to have a complete string ready to be printed + // rather than looping through the values and printing multiple things + // This reduces the flickering when printing the menu + let available_values = (available_lines * self.get_cols()) as usize; + let selection_values: String = self + .get_values() + .iter() + .skip(skip_values) + .take(available_values) + .enumerate() + .map(|(index, suggestion)| { + // Correcting the enumerate index based on the number of skipped values + let index = index + skip_values; + let column = index as u16 % self.get_cols(); + let empty_space = self.get_width().saturating_sub(suggestion.value.len()); + + self.create_entry_string( + suggestion, + index, + column, + empty_space, + use_ansi_coloring, + ) + }) + .collect(); + + format!( + "{}{}{}", + selection_values, + self.create_description_string(use_ansi_coloring), + self.create_example_string(use_ansi_coloring) + ) + } + } +} diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 478ecca9..2c5d42e2 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -1,13 +1,15 @@ -use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle}; +use super::{Menu, MenuBuilder, MenuEvent, MenuSettings}; use crate::{ - core_editor::Editor, menu_functions::string_difference, painting::Painter, Completer, - Suggestion, UndoBehavior, + core_editor::Editor, + menu_functions::{can_partially_complete, completer_input, replace_in_buffer}, + painting::Painter, + Completer, Suggestion, }; use itertools::{ EitherOrBoth::{Both, Left, Right}, Itertools, }; -use nu_ansi_term::{ansi::RESET, Style}; +use nu_ansi_term::ansi::RESET; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; @@ -130,12 +132,10 @@ struct IdeMenuDetails { /// Menu to present suggestions like similar to Ide completion menus pub struct IdeMenu { - /// Menu name - name: String, + /// Menu settings + settings: MenuSettings, /// Ide menu active status active: bool, - /// Menu coloring - color: MenuTextStyle, /// Default ide menu details that are set when creating the menu /// These values are the reference for the working details default_details: DefaultIdeMenuDetails, @@ -145,103 +145,75 @@ pub struct IdeMenu { values: Vec, /// Selected value. Starts at 0 selected: u16, - /// Menu marker when active - marker: String, /// Event sent to the menu event: Option, /// Longest suggestion found in the values longest_suggestion: usize, /// String collected after the menu is activated input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for IdeMenu { fn default() -> Self { Self { - name: "ide_completion_menu".to_string(), + settings: MenuSettings::default().with_name("ide_completion_menu"), active: false, - color: MenuTextStyle::default(), default_details: DefaultIdeMenuDetails::default(), working_details: IdeMenuDetails::default(), values: Vec::new(), selected: 0, - marker: "| ".to_string(), event: None, longest_suggestion: 0, input: None, - only_buffer_difference: false, } } } -impl IdeMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self +// Menu configuration functions +impl MenuBuilder for IdeMenu { + fn settings_mut(&mut self) -> &mut MenuSettings { + &mut self.settings } +} - /// Menu builder with new value for min completion width value +// Menu specific configuration functions +impl IdeMenu { + /// Menu builder with new value for min completion width #[must_use] pub fn with_min_completion_width(mut self, width: u16) -> Self { self.default_details.min_completion_width = width; self } - /// Menu builder with new value for max completion width value + /// Menu builder with new value for max completion width #[must_use] pub fn with_max_completion_width(mut self, width: u16) -> Self { self.default_details.max_completion_width = width; self } - /// Menu builder with new value for max completion height value + /// Menu builder with new value for max completion height #[must_use] pub fn with_max_completion_height(mut self, height: u16) -> Self { self.default_details.max_completion_height = height; self } - /// Menu builder with new value for padding value + /// Menu builder with new value for padding #[must_use] pub fn with_padding(mut self, padding: u16) -> Self { self.default_details.padding = padding; self } - /// Menu builder with the default border value + /// Menu builder with the default border #[must_use] pub fn with_default_border(mut self) -> Self { self.default_details.border = Some(BorderSymbols::default()); self } - /// Menu builder with new value for border value + /// Menu builder with new value for border #[must_use] pub fn with_border( mut self, @@ -263,27 +235,13 @@ impl IdeMenu { self } - /// Menu builder with new value for cursor offset value + /// Menu builder with new value for cursor offset #[must_use] pub fn with_cursor_offset(mut self, cursor_offset: i16) -> Self { self.default_details.cursor_offset = cursor_offset; self } - /// Menu builder with marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } - /// Menu builder with new description mode #[must_use] pub fn with_description_mode(mut self, description_mode: DescriptionMode) -> Self { @@ -399,7 +357,7 @@ impl IdeMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.settings.color.selected_text_style.prefix(), msg, RESET ) @@ -431,8 +389,6 @@ impl IdeMenu { let mut description_lines = split_string(&description, content_width as usize); - // panic!("{:?}", description_lines); - if description_lines.len() > content_height as usize { description_lines.truncate(content_height as usize); truncate_string_list(&mut description_lines, "..."); @@ -457,7 +413,7 @@ impl IdeMenu { *line = format!( "{}{}{}{}{}{}", border.vertical, - self.color.description_style.prefix(), + self.settings.color.description_style.prefix(), line, padding, RESET, @@ -486,7 +442,7 @@ impl IdeMenu { if use_ansi_coloring { *line = format!( "{}{}{}{}", - self.color.description_style.prefix(), + self.settings.color.description_style.prefix(), line, padding, RESET @@ -561,8 +517,11 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}{}", vertical_border, - suggestion.style.unwrap_or(self.color.text_style).prefix(), - self.color.selected_text_style.prefix(), + suggestion + .style + .unwrap_or(self.settings.color.text_style) + .prefix(), + self.settings.color.selected_text_style.prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -573,7 +532,10 @@ impl IdeMenu { format!( "{}{}{}{}{}{}{}", vertical_border, - suggestion.style.unwrap_or(self.color.text_style).prefix(), + suggestion + .style + .unwrap_or(self.settings.color.text_style) + .prefix(), " ".repeat(padding), string, " ".repeat(padding_right), @@ -598,14 +560,9 @@ impl IdeMenu { } impl Menu for IdeMenu { - /// Menu name - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() + /// Menu settings + fn settings(&self) -> &MenuSettings { + &self.settings } /// Deactivates context menu @@ -630,37 +587,12 @@ impl Menu for IdeMenu { self.update_values(editor, completer); } - let values = self.get_values(); - if let (Some(Suggestion { value, span, .. }), Some(index)) = find_common_string(values) { - let index = index.min(value.len()); - let matching = &value[0..index]; - - // make sure that the partial completion does not overwrite user entered input - let extends_input = matching.starts_with(&editor.get_buffer()[span.start..span.end]); - - if !matching.is_empty() && extends_input { - let mut line_buffer = editor.line_buffer().clone(); - line_buffer.replace_range(span.start..span.end, matching); - - let offset = if matching.len() < (span.end - span.start) { - line_buffer - .insertion_point() - .saturating_sub((span.end - span.start) - matching.len()) - } else { - line_buffer.insertion_point() + matching.len() - (span.end - span.start) - }; - - line_buffer.set_insertion_point(offset); - editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); - - // The values need to be updated because the spans need to be - // recalculated for accurate replacement in the string - self.update_values(editor, completer); + if can_partially_complete(self.get_values(), editor) { + // The values need to be updated because the spans need to be + // recalculated for accurate replacement in the string + self.update_values(editor, completer); - true - } else { - false - } + true } else { false } @@ -682,29 +614,13 @@ impl Menu for IdeMenu { /// Update menu values fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - 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_with_base_ranges(input, start + input.len()) - } else { - completer.complete_with_base_ranges("", editor.insertion_point()) - } - } else { - completer.complete_with_base_ranges("", editor.insertion_point()) - } - } else { - // If there is a new line character in the line buffer, the completer - // doesn't calculate the suggested values correctly. This happens when - // editing a multiline buffer. - // 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_with_base_ranges( - &trimmed_buffer[..editor.insertion_point()], - editor.insertion_point(), - ) - }; + let (input, pos) = completer_input( + editor.get_buffer(), + editor.insertion_point(), + self.input.as_deref(), + self.settings.only_buffer_difference, + ); + let (values, base_ranges) = completer.complete_with_base_ranges(&input, pos); self.values = values; self.working_details.base_strings = base_ranges @@ -730,7 +646,7 @@ impl Menu for IdeMenu { self.active = true; self.reset_position(); - self.input = if self.only_buffer_difference { + self.input = if self.settings.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None @@ -882,27 +798,7 @@ impl Menu for IdeMenu { /// The buffer gets replaced in the Span location fn replace_in_buffer(&self, editor: &mut Editor) { - if let Some(Suggestion { - mut value, - span, - append_whitespace, - .. - }) = self.get_value() - { - let start = span.start.min(editor.line_buffer().len()); - let end = span.end.min(editor.line_buffer().len()); - if append_whitespace { - value.push(' '); - } - let mut line_buffer = editor.line_buffer().clone(); - line_buffer.replace_range(start..end, &value); - - let mut offset = line_buffer.insertion_point(); - offset = offset.saturating_add(value.len()); - offset = offset.saturating_sub(end.saturating_sub(start)); - line_buffer.set_insertion_point(offset); - editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); - } + replace_in_buffer(self.get_value(), editor); } /// Minimum rows that should be displayed by the menu @@ -1169,7 +1065,7 @@ fn truncate_string_list(list: &mut [String], truncation_chars: &str) { #[cfg(test)] mod tests { - use crate::Span; + use crate::{Span, UndoBehavior}; use super::*; use pretty_assertions::assert_eq; diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index d90e3672..3ed16f6b 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -1,14 +1,12 @@ use { - super::{ - menu_functions::{parse_selection_char, string_difference}, - Menu, MenuEvent, MenuTextStyle, - }, + super::{menu_functions::parse_selection_char, Menu, MenuBuilder, MenuEvent, MenuSettings}, crate::{ core_editor::Editor, + menu_functions::{completer_input, replace_in_buffer}, painting::{estimate_single_line_wraps, Painter}, - Completer, Suggestion, UndoBehavior, + Completer, Suggestion, }, - nu_ansi_term::{ansi::RESET, Style}, + nu_ansi_term::ansi::RESET, std::{fmt::Write, iter::Sum}, unicode_width::UnicodeWidthStr, }; @@ -41,14 +39,10 @@ impl<'a> Sum<&'a Page> for Page { /// Struct to store the menu style /// Context menu definition pub struct ListMenu { - /// Menu name - name: String, - /// Menu coloring - color: MenuTextStyle, + /// Menu settings + settings: MenuSettings, /// Number of records pulled until page is full page_size: usize, - /// Menu marker displayed when the menu is active - marker: String, /// Menu active status active: bool, /// Cached values collected when querying the completer. @@ -73,63 +67,39 @@ pub struct ListMenu { event: Option, /// String collected after the menu is activated input: Option, - /// Calls the completer using only the line buffer difference difference - /// after the menu was activated - only_buffer_difference: bool, } impl Default for ListMenu { fn default() -> Self { Self { - name: "search_menu".to_string(), - color: MenuTextStyle::default(), + settings: MenuSettings::default() + .with_name("search_menu") + .with_marker("? ") + .with_only_buffer_difference(true), page_size: 10, active: false, values: Vec::new(), row_position: 0, page: 0, query_size: None, - marker: "? ".to_string(), max_lines: 5, multiline_marker: ":::".to_string(), pages: Vec::new(), event: None, input: None, - only_buffer_difference: true, } } } // Menu configuration functions -impl ListMenu { - /// Menu builder with new name - #[must_use] - pub fn with_name(mut self, name: &str) -> Self { - self.name = name.into(); - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_text_style(mut self, text_style: Style) -> Self { - self.color.text_style = text_style; - self - } - - /// Menu builder with new value for text style - #[must_use] - pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self { - self.color.selected_text_style = selected_text_style; - self - } - - /// Menu builder with new value for description style - #[must_use] - pub fn with_description_text_style(mut self, description_text_style: Style) -> Self { - self.color.description_style = description_text_style; - self +impl MenuBuilder for ListMenu { + fn settings_mut(&mut self) -> &mut MenuSettings { + &mut self.settings } +} +// Menu configuration functions +impl ListMenu { /// Menu builder with new page size #[must_use] pub fn with_page_size(mut self, page_size: usize) -> Self { @@ -137,30 +107,16 @@ impl ListMenu { self } - /// Menu builder with new only buffer difference - #[must_use] - pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { - self.only_buffer_difference = only_buffer_difference; - self - } -} - -// Menu functionality -impl ListMenu { - /// Menu builder with menu marker - #[must_use] - pub fn with_marker(mut self, marker: String) -> Self { - self.marker = marker; - self - } - /// Menu builder with max entry lines #[must_use] pub fn with_max_entry_lines(mut self, max_lines: u16) -> Self { self.max_lines = max_lines; self } +} +// Menu functionality +impl ListMenu { fn update_row_pos(&mut self, new_pos: Option) { if let (Some(row), Some(page)) = (new_pos, self.pages.get(self.page)) { let values_before_page = self.pages.iter().take(self.page).sum::().size; @@ -246,7 +202,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.settings.color.selected_text_style.prefix(), msg, RESET ) @@ -277,7 +233,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}{}{}", - self.color.selected_text_style.prefix(), + self.settings.color.selected_text_style.prefix(), status_bar, RESET, ) @@ -294,9 +250,9 @@ impl ListMenu { /// Text style for menu fn text_style(&self, index: usize) -> String { if index == self.index() { - self.color.selected_text_style.prefix().to_string() + self.settings.color.selected_text_style.prefix().to_string() } else { - self.color.text_style.prefix().to_string() + self.settings.color.text_style.prefix().to_string() } } @@ -313,7 +269,7 @@ impl ListMenu { if use_ansi_coloring { format!( "{}({}) {}", - self.color.description_style.prefix(), + self.settings.color.description_style.prefix(), desc, RESET ) @@ -348,13 +304,9 @@ impl ListMenu { } impl Menu for ListMenu { - fn name(&self) -> &str { - self.name.as_str() - } - - /// Menu indicator - fn indicator(&self) -> &str { - self.marker.as_str() + /// Menu settings + fn settings(&self) -> &MenuSettings { + &self.settings } /// Deactivates context menu @@ -394,27 +346,14 @@ impl Menu for ListMenu { /// Collecting the value from the completer to be shown in the menu fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) { - let line_buffer = editor.line_buffer(); - let (pos, input) = if self.only_buffer_difference { - match &self.input { - Some(old_string) => { - let (start, input) = string_difference(line_buffer.get_buffer(), old_string); - if input.is_empty() { - (line_buffer.insertion_point(), "") - } else { - (start + input.len(), input) - } - } - None => (line_buffer.insertion_point(), ""), - } - } else { - ( - line_buffer.insertion_point(), - &line_buffer.get_buffer()[..line_buffer.insertion_point()], - ) - }; + let (input, pos) = completer_input( + editor.get_buffer(), + editor.insertion_point(), + self.input.as_deref(), + self.settings.only_buffer_difference, + ); - let parsed = parse_selection_char(input, SELECTION_CHAR); + let parsed = parse_selection_char(&input, SELECTION_CHAR); self.update_row_pos(parsed.index); // If there are no row selector and the menu has an Edit event, this clears @@ -433,10 +372,10 @@ impl Menu for ListMenu { .map(|page| page.size) .unwrap_or(self.page_size); - completer.partial_complete(input, pos, skip, take) + completer.partial_complete(&input, pos, skip, take) } else { self.query_size = None; - completer.complete(input, pos) + completer.complete(&input, pos) } } @@ -469,27 +408,7 @@ impl Menu for ListMenu { /// The buffer gets cleared with the actual value fn replace_in_buffer(&self, editor: &mut Editor) { - if let Some(Suggestion { - mut value, - span, - append_whitespace, - .. - }) = self.get_value() - { - let buffer_len = editor.line_buffer().len(); - let start = span.start.min(buffer_len); - let end = span.end.min(buffer_len); - if append_whitespace { - value.push(' '); - } - let mut line_buffer = editor.line_buffer().clone(); - line_buffer.replace_range(start..end, &value); - - let mut offset = line_buffer.insertion_point(); - offset += value.len().saturating_sub(end.saturating_sub(start)); - line_buffer.set_insertion_point(offset); - editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); - } + replace_in_buffer(self.get_value(), editor); } fn update_working_details( @@ -503,7 +422,7 @@ impl Menu for ListMenu { MenuEvent::Activate(_) => { self.reset_position(); - self.input = if self.only_buffer_difference { + self.input = if self.settings.only_buffer_difference { Some(editor.get_buffer().to_string()) } else { None diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 247b8736..7a017cfa 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -1,5 +1,5 @@ //! Collection of common functions that can be used to create menus -use crate::Suggestion; +use crate::{Editor, Suggestion, UndoBehavior}; /// Index result obtained from parsing a string with an index marker /// For example, the next string: @@ -245,9 +245,101 @@ pub fn string_difference<'a>(new_string: &'a str, old_string: &str) -> (usize, & } } +/// Get the part of the line that should be given as input to the completer, as well +/// as the index of the end of that piece of text +/// +/// `prev_input` is the text in the buffer when the menu was activated. Needed if only_buffer_difference is true +pub fn completer_input( + buffer: &str, + insertion_point: usize, + prev_input: Option<&str>, + only_buffer_difference: bool, +) -> (String, usize) { + if only_buffer_difference { + if let Some(old_string) = prev_input { + let (start, input) = string_difference(buffer, old_string); + if !input.is_empty() { + (input.to_owned(), start + input.len()) + } else { + (String::new(), insertion_point) + } + } else { + (String::new(), insertion_point) + } + } else { + // TODO previously, all but the list menu replaced newlines with spaces here + // The completers should be adapted to account for this, and tests need to be added + (buffer[..insertion_point].to_owned(), insertion_point) + } +} + +/// Helper to accept a completion suggestion and edit the buffer +pub fn replace_in_buffer(value: Option, editor: &mut Editor) { + if let Some(Suggestion { + mut value, + span, + append_whitespace, + .. + }) = value + { + let start = span.start.min(editor.line_buffer().len()); + let end = span.end.min(editor.line_buffer().len()); + if append_whitespace { + value.push(' '); + } + + editor.edit_buffer( + |line_buffer| { + line_buffer.replace_range(start..end, &value); + + let mut offset = line_buffer.insertion_point(); + offset = offset.saturating_add(value.len()); + offset = offset.saturating_sub(end.saturating_sub(start)); + line_buffer.set_insertion_point(offset); + }, + UndoBehavior::CreateUndoPoint, + ); + } +} + +/// Helper for `Menu::can_partially_complete` +pub fn can_partially_complete(values: &[Suggestion], editor: &mut Editor) -> bool { + if let (Some(Suggestion { value, span, .. }), Some(index)) = find_common_string(values) { + let index = index.min(value.len()); + let matching = &value[0..index]; + + // make sure that the partial completion does not overwrite user entered input + let extends_input = matching.starts_with(&editor.get_buffer()[span.start..span.end]); + + if !matching.is_empty() && extends_input { + let mut line_buffer = editor.line_buffer().clone(); + line_buffer.replace_range(span.start..span.end, matching); + + let offset = if matching.len() < (span.end - span.start) { + line_buffer + .insertion_point() + .saturating_sub((span.end - span.start) - matching.len()) + } else { + line_buffer.insertion_point() + matching.len() - (span.end - span.start) + }; + + line_buffer.set_insertion_point(offset); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); + + true + } else { + false + } + } else { + false + } +} + #[cfg(test)] mod tests { use super::*; + use crate::Span; + use rstest::rstest; #[test] fn parse_row_test() { @@ -527,4 +619,59 @@ mod tests { assert!(matches!(res, (Some(elem), Some(6)) if elem == &input[0])); } + + #[rstest] + #[case("foobar", 6, None, false, "foobar", 6)] + #[case("foo\r\nbar", 5, None, false, "foo\r\n", 5)] + #[case("foobar", 6, None, true, "", 6)] + #[case("foobar", 3, Some("foobar"), true, "", 3)] + #[case("foobar", 6, Some("foo"), true, "bar", 6)] + #[case("foobar", 6, Some("for"), true, "oba", 5)] + fn test_completer_input( + #[case] buffer: String, + #[case] insertion_point: usize, + #[case] prev_input: Option<&str>, + #[case] only_buffer_difference: bool, + #[case] output: String, + #[case] pos: usize, + ) { + assert_eq!( + (output, pos), + completer_input(&buffer, insertion_point, prev_input, only_buffer_difference) + ) + } + + #[rstest] + #[case("foobar baz", 6, "foobleh baz", 7, "bleh", 3, 6)] + fn test_replace_in_buffer( + #[case] orig_buffer: &str, + #[case] orig_insertion_point: usize, + #[case] new_buffer: &str, + #[case] new_insertion_point: usize, + #[case] value: String, + #[case] start: usize, + #[case] end: usize, + ) { + let mut editor = Editor::default(); + editor.edit_buffer( + |lb| { + lb.set_buffer(orig_buffer.to_owned()); + lb.set_insertion_point(orig_insertion_point); + }, + UndoBehavior::CreateUndoPoint, + ); + replace_in_buffer( + Some(Suggestion { + value, + description: None, + style: None, + extra: None, + span: Span::new(start, end), + append_whitespace: false, + }), + &mut editor, + ); + assert_eq!(new_buffer, editor.get_buffer()); + assert_eq!(new_insertion_point, editor.insertion_point()); + } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index b9c1d98d..5711e846 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -1,4 +1,5 @@ mod columnar_menu; +mod description_menu; mod ide_menu; mod list_menu; pub mod menu_functions; @@ -7,6 +8,7 @@ use crate::core_editor::Editor; use crate::History; use crate::{completion::history::HistoryCompleter, painting::Painter, Completer, Suggestion}; pub use columnar_menu::ColumnarMenu; +pub use description_menu::DescriptionMenu; pub use ide_menu::DescriptionMode; pub use ide_menu::IdeMenu; pub use list_menu::ListMenu; @@ -63,11 +65,18 @@ pub enum MenuEvent { /// Trait that defines how a menu will be printed by the painter pub trait Menu: Send { + /// Get MenuSettings + fn settings(&self) -> &MenuSettings; + /// Menu name - fn name(&self) -> &str; + fn name(&self) -> &str { + &self.settings().name + } /// Menu indicator - fn indicator(&self) -> &str; + fn indicator(&self) -> &str { + &self.settings().marker + } /// Checks if the menu is active fn is_active(&self) -> bool; @@ -128,6 +137,108 @@ pub trait Menu: Send { } } +pub struct MenuSettings { + /// Menu name + name: String, + /// Menu coloring + color: MenuTextStyle, + /// Menu marker when active + marker: String, + /// Calls the completer using only the line buffer difference difference + /// after the menu was activated + only_buffer_difference: bool, +} + +impl Default for MenuSettings { + fn default() -> Self { + Self { + name: "menu".to_string(), + color: MenuTextStyle::default(), + marker: "| ".to_string(), + only_buffer_difference: false, + } + } +} + +impl MenuSettings { + /// MenuSettings builder with name + #[must_use] + pub fn with_name(mut self, name: &str) -> Self { + self.name = name.to_string(); + self + } + + /// MenuSettings builder with color + #[must_use] + pub fn with_color(mut self, color: MenuTextStyle) -> Self { + self.color = color; + self + } + + /// MenuSettings builder with marker + #[must_use] + pub fn with_marker(mut self, marker: &str) -> Self { + self.marker = marker.to_string(); + self + } + + /// MenuSettings builder with only_buffer_difference + #[must_use] + pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.only_buffer_difference = only_buffer_difference; + self + } +} + +/// Common builder for all menus +pub trait MenuBuilder: Menu + Sized { + /// Get mutable MenuSettings + /// required for the builder functions + fn settings_mut(&mut self) -> &mut MenuSettings; + + /// Menu builder with new name + #[must_use] + fn with_name(mut self, name: &str) -> Self { + self.settings_mut().name = name.to_string(); + self + } + + /// Menu builder with new value for text style + #[must_use] + fn with_text_style(mut self, color: Style) -> Self { + self.settings_mut().color.text_style = color; + self + } + + /// Menu builder with new value for selected text style + #[must_use] + fn with_selected_text_style(mut self, color: Style) -> Self { + self.settings_mut().color.selected_text_style = color; + self + } + + /// Menu builder with new value for description style + #[must_use] + fn with_description_text_style(mut self, color: Style) -> Self { + self.settings_mut().color.description_style = color; + self + } + + /// Menu builder with new value for marker + #[must_use] + fn with_marker(mut self, marker: &str) -> Self { + self.settings_mut().marker = marker.to_string(); + self + } + + /// Menu builder with new value for only_buffer_difference + #[must_use] + fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self { + self.settings_mut().only_buffer_difference = only_buffer_difference; + self + } +} + /// Allowed menus in Reedline pub enum ReedlineMenu { /// Menu that uses Reedline's completer to update its values @@ -229,6 +340,10 @@ impl ReedlineMenu { } impl Menu for ReedlineMenu { + fn settings(&self) -> &MenuSettings { + self.as_ref().settings() + } + fn name(&self) -> &str { self.as_ref().name() } From 23a07819fad5afbbfea0f658900a9aa87db247f1 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:40:52 +0100 Subject: [PATCH 18/23] add test case for unix newline --- src/menu/menu_functions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 7a017cfa..3af19de1 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -623,6 +623,7 @@ mod tests { #[rstest] #[case("foobar", 6, None, false, "foobar", 6)] #[case("foo\r\nbar", 5, None, false, "foo\r\n", 5)] + #[case("foo\nbar", 4, None, false, "foo\n", 4)] #[case("foobar", 6, None, true, "", 6)] #[case("foobar", 3, Some("foobar"), true, "", 3)] #[case("foobar", 6, Some("foo"), true, "bar", 6)] From 43bb16cd0797051db041204a27e4f88fbc146801 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:47:03 +0100 Subject: [PATCH 19/23] more tests --- src/menu/menu_functions.rs | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/menu/menu_functions.rs b/src/menu/menu_functions.rs index 3af19de1..cc48d85a 100644 --- a/src/menu/menu_functions.rs +++ b/src/menu/menu_functions.rs @@ -288,17 +288,13 @@ pub fn replace_in_buffer(value: Option, editor: &mut Editor) { value.push(' '); } - editor.edit_buffer( - |line_buffer| { - line_buffer.replace_range(start..end, &value); - - let mut offset = line_buffer.insertion_point(); - offset = offset.saturating_add(value.len()); - offset = offset.saturating_sub(end.saturating_sub(start)); - line_buffer.set_insertion_point(offset); - }, - UndoBehavior::CreateUndoPoint, - ); + let mut line_buffer = editor.line_buffer().clone(); + line_buffer.replace_range(start..end, &value); + let mut offset = line_buffer.insertion_point(); + offset = offset.saturating_add(value.len()); + offset = offset.saturating_sub(end.saturating_sub(start)); + line_buffer.set_insertion_point(offset); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); } } @@ -338,7 +334,7 @@ pub fn can_partially_complete(values: &[Suggestion], editor: &mut Editor) -> boo #[cfg(test)] mod tests { use super::*; - use crate::Span; + use crate::{EditCommand, LineBuffer, Span}; use rstest::rstest; #[test] @@ -644,6 +640,8 @@ mod tests { #[rstest] #[case("foobar baz", 6, "foobleh baz", 7, "bleh", 3, 6)] + #[case("foobar baz", 6, "foo baz", 3, "", 3, 6)] + #[case("foobar baz", 10, "foobleh", 7, "bleh", 3, 1000)] fn test_replace_in_buffer( #[case] orig_buffer: &str, #[case] orig_insertion_point: usize, @@ -654,13 +652,10 @@ mod tests { #[case] end: usize, ) { let mut editor = Editor::default(); - editor.edit_buffer( - |lb| { - lb.set_buffer(orig_buffer.to_owned()); - lb.set_insertion_point(orig_insertion_point); - }, - UndoBehavior::CreateUndoPoint, - ); + let mut line_buffer = LineBuffer::new(); + line_buffer.set_buffer(orig_buffer.to_owned()); + line_buffer.set_insertion_point(orig_insertion_point); + editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint); replace_in_buffer( Some(Suggestion { value, @@ -674,5 +669,9 @@ mod tests { ); assert_eq!(new_buffer, editor.get_buffer()); assert_eq!(new_insertion_point, editor.insertion_point()); + + editor.run_edit_command(&EditCommand::Undo); + assert_eq!(orig_buffer, editor.get_buffer()); + assert_eq!(orig_insertion_point, editor.insertion_point()); } } From 0493c0db2b1900ddbf5206a697e073b4a1d7a13f Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:06:02 +0100 Subject: [PATCH 20/23] fix newline replace --- src/completion/default.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/completion/default.rs b/src/completion/default.rs index 42f4d8a8..882debb6 100644 --- a/src/completion/default.rs +++ b/src/completion/default.rs @@ -78,7 +78,7 @@ impl Completer for DefaultCompleter { // When editing a multiline buffer, there can be new line characters in it. // Also, by replacing the new line character with a space, the insert // position is maintain in the line buffer. - let line = line.replace("\r\n", " ").replace('\n', " "); + let line = line.replace("\r\n", " ").replace('\n', " "); let mut split = line.split(' ').rev(); let mut span_line: String = String::new(); for _ in 0..split.clone().count() { From d6ab845eb1cb8f045cf8e1c5b464f7540181131d Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:15:11 +0100 Subject: [PATCH 21/23] add explicit panic to stay backwards compatible --- src/menu/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 5711e846..0e594c37 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -66,7 +66,12 @@ pub enum MenuEvent { /// Trait that defines how a menu will be printed by the painter pub trait Menu: Send { /// Get MenuSettings - fn settings(&self) -> &MenuSettings; + fn settings(&self) -> &MenuSettings { + // We panic here, so this function has base implementation + // so existing menus will not break. + // if a breaking change is ok, this can be removed + panic!("`settings` requires a manual implementation per menu. It has a base implementation to not break existing menus") + } /// Menu name fn name(&self) -> &str { From 95e155819150db41cbd8d40b68f2c279f48c7e96 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:15:55 +0100 Subject: [PATCH 22/23] fix ci --- src/menu/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/mod.rs b/src/menu/mod.rs index 0e594c37..0a2eaa18 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -69,7 +69,7 @@ pub trait Menu: Send { fn settings(&self) -> &MenuSettings { // We panic here, so this function has base implementation // so existing menus will not break. - // if a breaking change is ok, this can be removed + // if a breaking change is ok, this can be removed panic!("`settings` requires a manual implementation per menu. It has a base implementation to not break existing menus") } From 37268ed6ab7663ada1d196d95814a5e0d14b6369 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sun, 28 Jan 2024 08:51:39 +0100 Subject: [PATCH 23/23] Update columnar_menu.rs Co-authored-by: Yash Thakur <45539777+ysthakur@users.noreply.github.com> --- src/menu/columnar_menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 6378b456..fb9e3a53 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -71,7 +71,7 @@ pub struct ColumnarMenu { impl Default for ColumnarMenu { fn default() -> Self { Self { - settings: MenuSettings::default(), + settings: MenuSettings::default().with_name("columnar_menu"), active: false, default_details: DefaultColumnDetails::default(), min_rows: 3,