From 88bded3417e7f6c1242b444f403448de583357f0 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Tue, 5 Oct 2021 10:32:21 +1300 Subject: [PATCH] Add list style completions (#162) --- README.md | 4 +- .../{tab_handler.rs => circular.rs} | 27 ++- src/completion/default.rs | 4 +- src/completion/list.rs | 194 ++++++++++++++++++ src/completion/mod.rs | 6 +- src/engine.rs | 8 +- src/lib.rs | 7 +- src/main.rs | 9 +- 8 files changed, 226 insertions(+), 33 deletions(-) rename src/completion/{tab_handler.rs => circular.rs} (85%) create mode 100644 src/completion/list.rs diff --git a/README.md b/README.md index 9ae930c..0d4eae7 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Reedline::create()?.with_highlighter(Box::new(DefaultHighlighter::new(commands)) ```rust,no_run // Create a reedline object with tab completions support -use reedline::{DefaultCompleter, DefaultCompletionActionHandler, Reedline}; +use reedline::{DefaultCompleter, CircularCompletionHandler, Reedline}; let commands = vec![ "test".into(), @@ -115,7 +115,7 @@ let commands = vec![ let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2)); let mut line_editor = Reedline::create()?.with_completion_action_handler(Box::new( - DefaultCompletionActionHandler::default().with_completer(completer), + CircularCompletionHandler::default().with_completer(completer), )); ``` diff --git a/src/completion/tab_handler.rs b/src/completion/circular.rs similarity index 85% rename from src/completion/tab_handler.rs rename to src/completion/circular.rs index 62c7598..8aa2d70 100644 --- a/src/completion/tab_handler.rs +++ b/src/completion/circular.rs @@ -1,7 +1,7 @@ use crate::{core_editor::LineBuffer, Completer, CompletionActionHandler, DefaultCompleter}; /// A simple handler that will do a cycle-based rotation through the options given by the Completer -pub struct DefaultCompletionActionHandler { +pub struct CircularCompletionHandler { completer: Box, initial_line: LineBuffer, index: usize, @@ -9,8 +9,8 @@ pub struct DefaultCompletionActionHandler { last_buffer: Option, } -impl DefaultCompletionActionHandler { - /// Build a `DefaultCompletionActionHandler` configured to use a specific completer +impl CircularCompletionHandler { + /// Build a `CircularCompletionHandler` configured to use a specific completer /// /// # Arguments /// @@ -18,7 +18,7 @@ impl DefaultCompletionActionHandler { /// /// # Example /// ``` - /// use reedline::{DefaultCompletionActionHandler, DefaultCompleter, Completer, Span}; + /// use reedline::{CircularCompletionHandler, DefaultCompleter, Completer, Span}; /// /// let mut completer = DefaultCompleter::default(); /// completer.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect()); @@ -26,19 +26,16 @@ impl DefaultCompletionActionHandler { /// completer.complete("te",2), /// vec![(Span { start: 0, end: 2 }, "test".into())]); /// - /// let mut completions = DefaultCompletionActionHandler::default().with_completer(Box::new(completer)); + /// let mut completions = CircularCompletionHandler::default().with_completer(Box::new(completer)); /// ``` - pub fn with_completer( - mut self, - completer: Box, - ) -> DefaultCompletionActionHandler { + pub fn with_completer(mut self, completer: Box) -> CircularCompletionHandler { self.completer = completer; self } } -impl Default for DefaultCompletionActionHandler { +impl Default for CircularCompletionHandler { fn default() -> Self { - DefaultCompletionActionHandler { + CircularCompletionHandler { completer: Box::new(DefaultCompleter::default()), initial_line: LineBuffer::new(), index: 0, @@ -47,13 +44,13 @@ impl Default for DefaultCompletionActionHandler { } } -impl DefaultCompletionActionHandler { +impl CircularCompletionHandler { fn reset_index(&mut self) { self.index = 0; } } -impl CompletionActionHandler for DefaultCompletionActionHandler { +impl CompletionActionHandler for CircularCompletionHandler { // With this function we handle the tab events. // // If completions vector is not empty we proceed to replace @@ -106,11 +103,11 @@ mod test { use super::*; use pretty_assertions::assert_eq; - fn get_tab_handler_with(values: Vec<&'_ str>) -> DefaultCompletionActionHandler { + fn get_tab_handler_with(values: Vec<&'_ str>) -> CircularCompletionHandler { let mut completer = DefaultCompleter::default(); completer.insert(values.iter().map(|s| s.to_string()).collect()); - DefaultCompletionActionHandler::default().with_completer(Box::new(completer)) + CircularCompletionHandler::default().with_completer(Box::new(completer)) } fn buffer_with(content: &str) -> LineBuffer { diff --git a/src/completion/default.rs b/src/completion/default.rs index 7f81764..02f819f 100644 --- a/src/completion/default.rs +++ b/src/completion/default.rs @@ -11,7 +11,7 @@ use crate::{Completer, Span}; /// # Example /// /// ```rust, no_run -/// use reedline::{DefaultCompleter, DefaultCompletionActionHandler, Reedline}; +/// use reedline::{DefaultCompleter, CircularCompletionHandler, Reedline}; /// /// let commands = vec![ /// "test".into(), @@ -23,7 +23,7 @@ use crate::{Completer, Span}; /// /// let mut line_editor = Reedline::create().unwrap() /// .with_completion_action_handler(Box::new( -/// DefaultCompletionActionHandler::default().with_completer(completer), +/// CircularCompletionHandler::default().with_completer(completer), /// )); /// ``` #[derive(Debug, Clone)] diff --git a/src/completion/list.rs b/src/completion/list.rs new file mode 100644 index 0000000..0e950df --- /dev/null +++ b/src/completion/list.rs @@ -0,0 +1,194 @@ +use crate::{core_editor::LineBuffer, Completer, CompletionActionHandler, DefaultCompleter, Span}; + +/// A simple handler that will do a cycle-based rotation through the options given by the Completer +pub struct ListCompletionHandler { + completer: Box, + complete: bool, +} + +impl ListCompletionHandler { + /// Build a `ListCompletionHandler` configured to use a specific completer + /// + /// # Arguments + /// + /// * `completer` The completion logic to use + /// + /// # Example + /// ``` + /// use reedline::{ListCompletionHandler, DefaultCompleter, Completer, Span}; + /// + /// let mut completer = DefaultCompleter::default(); + /// completer.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect()); + /// assert_eq!( + /// completer.complete("te",2), + /// vec![(Span { start: 0, end: 2 }, "test".into())]); + /// + /// let mut completions = ListCompletionHandler::default().with_completer(Box::new(completer)); + /// ``` + pub fn with_completer(mut self, completer: Box) -> ListCompletionHandler { + self.completer = completer; + self + } +} +impl Default for ListCompletionHandler { + fn default() -> Self { + ListCompletionHandler { + completer: Box::new(DefaultCompleter::default()), + complete: true, + } + } +} + +impl CompletionActionHandler for ListCompletionHandler { + // With this function we handle the tab events. + // + // If completions vector is not empty we proceed to replace + // in the line_buffer only the specified range of characters. + // If internal index is 0 it means that is the first tab event pressed. + // If internal index is greater than completions vector, we bring it back to 0. + fn handle(&mut self, present_buffer: &mut LineBuffer) { + // if let Some(last_buffer) = &self.last_buffer { + // if last_buffer != present_buffer { + // self.reset_index(); + // } + // } + + // // NOTE: This is required to cycle through the tabs for what is presently present in the + // // buffer. Without this `repetitive_calls_to_handle_works` will not work + // if self.index == 0 { + // self.initial_line = present_buffer.clone(); + // } else { + // *present_buffer = self.initial_line.clone(); + // } + + let completions = self + .completer + .complete(present_buffer.get_buffer(), present_buffer.offset()); + + if completions.is_empty() { + // do nothing + } else if completions.len() == 1 { + let span = completions[0].0; + + let mut offset = present_buffer.offset(); + offset += completions[0].1.len() - (span.end - span.start); + + // TODO improve the support for multiline replace + present_buffer.replace(span.start..span.end, &completions[0].1); + present_buffer.set_insertion_point(offset); + self.complete = true; + } else { + let prefix = calculate_prefix(&completions); + + let span = completions[0].0; + let mut offset = present_buffer.offset(); + offset += prefix.len() - (span.end - span.start); + + present_buffer.replace(span.start..span.end, &prefix); + present_buffer.set_insertion_point(offset); + + print!("\r\n"); + for completion in completions { + // TODO: make this list pretty + print!("{}\r\n", completion.1); + } + print!("\r\n"); + } + } +} + +fn calculate_prefix(inputs: &[(Span, String)]) -> String { + let mut iter = inputs.iter(); + + if let Some(first) = iter.next() { + let prefix = first.1.clone(); + let prefix_bytes = prefix.as_bytes(); + + let mut longest_match = prefix.len(); + + for i in iter { + longest_match = std::cmp::min( + longest_match, + i.1.as_bytes() + .iter() + .zip(prefix_bytes) + .take_while(|(x, y)| x == y) + .count(), + ); + } + + prefix[0..longest_match].to_string() + } else { + String::new() + } +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + fn get_tab_handler_with(values: Vec<&'_ str>) -> ListCompletionHandler { + let mut completer = DefaultCompleter::default(); + completer.insert(values.iter().map(|s| s.to_string()).collect()); + + ListCompletionHandler::default().with_completer(Box::new(completer)) + } + + fn buffer_with(content: &str) -> LineBuffer { + let mut line_buffer = LineBuffer::new(); + line_buffer.insert_str(content); + + line_buffer + } + + #[test] + fn repetitive_calls_to_handle_works() { + let mut tab = get_tab_handler_with(vec!["login", "logout"]); + let mut buf = buffer_with("lo"); + tab.handle(&mut buf); + + assert_eq!(buf, buffer_with("log")); + tab.handle(&mut buf); + assert_eq!(buf, buffer_with("log")); + } + + #[test] + fn behaviour_with_hyphens_and_underscores() { + let mut tab = get_tab_handler_with(vec!["test-hyphen", "test_underscore"]); + let mut buf = buffer_with("te"); + tab.handle(&mut buf); + + assert_eq!(buf, buffer_with("test")); + } + + #[test] + fn auto_resets_on_new_query() { + let mut tab = get_tab_handler_with(vec!["login", "exit"]); + let mut buf = buffer_with("log"); + tab.handle(&mut buf); + + assert_eq!(buf, buffer_with("login")); + let mut new_buf = buffer_with("ex"); + tab.handle(&mut new_buf); + assert_eq!(new_buf, buffer_with("exit")); + } + + #[test] + fn same_string_different_places() { + let mut tab = get_tab_handler_with(vec!["that", "another"]); + let mut buf = buffer_with("th is my test th"); + + // Hitting tab after `th` fills the first completion `that` + buf.set_insertion_point(2); + tab.handle(&mut buf); + let mut expected_buffer = buffer_with("that is my test th"); + expected_buffer.set_insertion_point(4); + assert_eq!(buf, expected_buffer); + + // updating the cursor to end should reset the completions + buf.set_insertion_point(18); + tab.handle(&mut buf); + assert_eq!(buf, buffer_with("that is my test that")); + } +} diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 0c5bccd..f230f74 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,7 +1,9 @@ mod base; +mod circular; mod default; -mod tab_handler; +mod list; pub use base::{Completer, CompletionActionHandler, Span}; +pub use circular::CircularCompletionHandler; pub use default::DefaultCompleter; -pub use tab_handler::DefaultCompletionActionHandler; +pub use list::ListCompletionHandler; diff --git a/src/engine.rs b/src/engine.rs index 5064c0a..f464fed 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ use { crate::{ - completion::{CompletionActionHandler, DefaultCompletionActionHandler}, + completion::{CircularCompletionHandler, CompletionActionHandler}, core_editor::Editor, edit_mode::{EditMode, Emacs}, enums::ReedlineEvent, @@ -128,7 +128,7 @@ impl Reedline { input_mode: InputMode::Regular, painter, edit_mode, - tab_handler: Box::new(DefaultCompletionActionHandler::default()), + tab_handler: Box::new(CircularCompletionHandler::default()), terminal_size, prompt_widget, highlighter: buffer_highlighter, @@ -178,7 +178,7 @@ impl Reedline { /// // Create a reedline object with tab completions support /// /// use std::io; - /// use reedline::{DefaultCompleter, DefaultCompletionActionHandler, Reedline}; + /// use reedline::{DefaultCompleter, CircularCompletionHandler, Reedline}; /// /// let commands = vec![ /// "test".into(), @@ -189,7 +189,7 @@ impl Reedline { /// let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2)); /// /// let mut line_editor = Reedline::create()?.with_completion_action_handler(Box::new( - /// DefaultCompletionActionHandler::default().with_completer(completer), + /// CircularCompletionHandler::default().with_completer(completer), /// )); /// # Ok::<(), io::Error>(()) /// ``` diff --git a/src/lib.rs b/src/lib.rs index 16d2557..60255ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,7 +107,7 @@ //! // Create a reedline object with tab completions support //! //! use std::io; -//! use reedline::{DefaultCompleter, DefaultCompletionActionHandler, Reedline}; +//! use reedline::{DefaultCompleter, CircularCompletionHandler, Reedline}; //! //! let commands = vec![ //! "test".into(), @@ -118,7 +118,7 @@ //! let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 2)); //! //! let mut line_editor = Reedline::create()?.with_completion_action_handler(Box::new( -//! DefaultCompletionActionHandler::default().with_completer(completer), +//! CircularCompletionHandler::default().with_completer(completer), //! )); //! # Ok::<(), io::Error>(()) //! ``` @@ -218,7 +218,8 @@ pub use styled_text::StyledText; mod completion; pub use completion::{ - Completer, CompletionActionHandler, DefaultCompleter, DefaultCompletionActionHandler, Span, + CircularCompletionHandler, Completer, CompletionActionHandler, DefaultCompleter, + ListCompletionHandler, Span, }; mod hinter; diff --git a/src/main.rs b/src/main.rs index 022ff13..08c92bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use reedline::{EditMode, Emacs, Vi}; +use reedline::{EditMode, Emacs, ListCompletionHandler, Vi}; use { crossterm::{ @@ -7,9 +7,8 @@ use { }, nu_ansi_term::{Color, Style}, reedline::{ - default_emacs_keybindings, DefaultCompleter, DefaultCompletionActionHandler, - DefaultHighlighter, DefaultHinter, DefaultPrompt, EditCommand, FileBackedHistory, Reedline, - ReedlineEvent, Signal, + default_emacs_keybindings, DefaultCompleter, DefaultHighlighter, DefaultHinter, + DefaultPrompt, EditCommand, FileBackedHistory, Reedline, ReedlineEvent, Signal, }, std::{ io::{stdout, Write}, @@ -60,7 +59,7 @@ fn main() -> Result<()> { .with_edit_mode(edit_mode) .with_highlighter(Box::new(DefaultHighlighter::new(commands))) .with_completion_action_handler(Box::new( - DefaultCompletionActionHandler::default().with_completer(completer.clone()), + ListCompletionHandler::default().with_completer(completer.clone()), )) .with_hinter(Box::new( DefaultHinter::default()