From 973dbb5f5f2338c18c25ce951bfa42c8d8cacfdf Mon Sep 17 00:00:00 2001 From: Chinmay Dalal Date: Fri, 20 Oct 2023 02:11:09 +0530 Subject: [PATCH] add cwd aware hinter (#647) * add cwd aware hinter towards https://github.com/nushell/nushell/issues/8883 * handle the case where get_current_dir returns Err * WIP cwd aware hinter - guard CwdAwareHinter with feature flag - remove references to fish from DefaultHinter as fish is cwd aware - add example * document that CwdAwareHinter is only compatible with sqlite history * handle non-sqlite history * handle no sqlite feature in example * fix warnings --- examples/cwd_aware_hinter.rs | 86 ++++++++++++++++++++++++++++ src/hinter/cwd_aware.rs | 108 +++++++++++++++++++++++++++++++++++ src/hinter/default.rs | 4 +- src/hinter/mod.rs | 2 + src/history/base.rs | 32 +++++++++++ src/lib.rs | 1 + 6 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 examples/cwd_aware_hinter.rs create mode 100644 src/hinter/cwd_aware.rs diff --git a/examples/cwd_aware_hinter.rs b/examples/cwd_aware_hinter.rs new file mode 100644 index 00000000..d84d24f1 --- /dev/null +++ b/examples/cwd_aware_hinter.rs @@ -0,0 +1,86 @@ +// Create a reedline object with in-line hint support. +// cargo run --example cwd_aware_hinter +// +// Fish-style cwd history based hinting +// assuming history ["abc", "ade"] +// pressing "a" hints to abc. +// Up/Down or Ctrl p/n, to select next/previous match + +use std::io; + +fn create_item(cwd: &str, cmd: &str, exit_status: i64) -> reedline::HistoryItem { + use std::time::Duration; + + use reedline::HistoryItem; + HistoryItem { + id: None, + start_timestamp: None, + command_line: cmd.to_string(), + session_id: None, + hostname: Some("foohost".to_string()), + cwd: Some(cwd.to_string()), + duration: Some(Duration::from_millis(1000)), + exit_status: Some(exit_status), + more_info: None, + } +} + +fn create_filled_example_history(home_dir: &str, orig_dir: &str) -> Box { + use reedline::History; + #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))] + let mut history = Box::new(reedline::FileBackedHistory::new(100)); + #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))] + let mut history = Box::new(reedline::SqliteBackedHistory::in_memory().unwrap()); + + history.save(create_item(orig_dir, "dummy", 0)).unwrap(); // add dummy item so ids start with 1 + history.save(create_item(orig_dir, "ls /usr", 0)).unwrap(); + history.save(create_item(orig_dir, "pwd", 0)).unwrap(); + + history.save(create_item(home_dir, "cat foo", 0)).unwrap(); + history.save(create_item(home_dir, "ls bar", 0)).unwrap(); + history.save(create_item(home_dir, "rm baz", 0)).unwrap(); + + history +} + +fn main() -> io::Result<()> { + use nu_ansi_term::{Color, Style}; + use reedline::{CwdAwareHinter, DefaultPrompt, Reedline, Signal}; + + let orig_dir = std::env::current_dir().unwrap(); + #[allow(deprecated)] + let home_dir = std::env::home_dir().unwrap(); + + let history = create_filled_example_history( + &home_dir.to_string_lossy().to_string(), + &orig_dir.to_string_lossy().to_string(), + ); + + let mut line_editor = Reedline::create() + .with_hinter(Box::new( + CwdAwareHinter::default().with_style(Style::new().italic().fg(Color::Yellow)), + )) + .with_history(history); + + let prompt = DefaultPrompt::default(); + + let mut iterations = 0; + loop { + if iterations % 2 == 0 { + std::env::set_current_dir(&orig_dir).unwrap(); + } else { + std::env::set_current_dir(&home_dir).unwrap(); + } + let sig = line_editor.read_line(&prompt)?; + match sig { + Signal::Success(buffer) => { + println!("We processed: {buffer}"); + } + Signal::CtrlD | Signal::CtrlC => { + println!("\nAborted!"); + break Ok(()); + } + } + iterations += 1; + } +} diff --git a/src/hinter/cwd_aware.rs b/src/hinter/cwd_aware.rs new file mode 100644 index 00000000..bed5f5d9 --- /dev/null +++ b/src/hinter/cwd_aware.rs @@ -0,0 +1,108 @@ +use crate::{ + history::SearchQuery, + result::{ReedlineError, ReedlineErrorVariants::HistoryFeatureUnsupported}, + Hinter, History, +}; +use nu_ansi_term::{Color, Style}; + +/// A hinter that uses the completions or the history to show a hint to the user +/// +/// Similar to `fish` autosuggestions +pub struct CwdAwareHinter { + style: Style, + current_hint: String, + min_chars: usize, +} + +impl Hinter for CwdAwareHinter { + fn handle( + &mut self, + line: &str, + #[allow(unused_variables)] pos: usize, + history: &dyn History, + use_ansi_coloring: bool, + ) -> String { + self.current_hint = if line.chars().count() >= self.min_chars { + history + .search(SearchQuery::last_with_prefix_and_cwd( + line.to_string(), + history.session(), + )) + .or_else(|err| { + if let ReedlineError(HistoryFeatureUnsupported { .. }) = err { + history.search(SearchQuery::last_with_prefix( + line.to_string(), + history.session(), + )) + } else { + Err(err) + } + }) + .expect("todo: error handling") + .get(0) + .map_or_else(String::new, |entry| { + entry + .command_line + .get(line.len()..) + .unwrap_or_default() + .to_string() + }) + } else { + String::new() + }; + + if use_ansi_coloring && !self.current_hint.is_empty() { + self.style.paint(&self.current_hint).to_string() + } else { + self.current_hint.clone() + } + } + + fn complete_hint(&self) -> String { + self.current_hint.clone() + } + + fn next_hint_token(&self) -> String { + let mut reached_content = false; + let result: String = self + .current_hint + .chars() + .take_while(|c| match (c.is_whitespace(), reached_content) { + (true, true) => false, + (true, false) => true, + (false, true) => true, + (false, false) => { + reached_content = true; + true + } + }) + .collect(); + result + } +} + +impl Default for CwdAwareHinter { + fn default() -> Self { + CwdAwareHinter { + style: Style::new().fg(Color::LightGray), + current_hint: String::new(), + min_chars: 1, + } + } +} + +impl CwdAwareHinter { + /// A builder that sets the style applied to the hint as part of the buffer + #[must_use] + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + + /// A builder that sets the number of characters that have to be present to enable history hints + #[must_use] + pub fn with_min_chars(mut self, min_chars: usize) -> Self { + self.min_chars = min_chars; + self + } +} diff --git a/src/hinter/default.rs b/src/hinter/default.rs index e1aa31e4..87955198 100644 --- a/src/hinter/default.rs +++ b/src/hinter/default.rs @@ -1,9 +1,7 @@ use crate::{history::SearchQuery, Hinter, History}; use nu_ansi_term::{Color, Style}; -/// A hinter that use the completions or the history to show a hint to the user -/// -/// Similar to `fish` autosuggestins +/// A hinter that uses the completions or the history to show a hint to the user pub struct DefaultHinter { style: Style, current_hint: String, diff --git a/src/hinter/mod.rs b/src/hinter/mod.rs index 4b63b5d3..d0a86eca 100644 --- a/src/hinter/mod.rs +++ b/src/hinter/mod.rs @@ -1,4 +1,6 @@ +mod cwd_aware; mod default; +pub use cwd_aware::CwdAwareHinter; pub use default::DefaultHinter; use crate::History; diff --git a/src/history/base.rs b/src/history/base.rs index 7eb27d76..26d4da05 100644 --- a/src/history/base.rs +++ b/src/history/base.rs @@ -66,6 +66,18 @@ impl SearchFilter { s } + /// Create a search filter with a [`CommandLineSearch`] and `cwd` + pub fn from_text_search_cwd( + cwd: String, + cmd: CommandLineSearch, + session: Option, + ) -> SearchFilter { + let mut s = SearchFilter::anything(session); + s.command_line = Some(cmd); + s.cwd_exact = Some(cwd); + s + } + /// anything within this session pub fn anything(session: Option) -> SearchFilter { SearchFilter { @@ -134,6 +146,26 @@ impl SearchQuery { )) } + /// Get the most recent entry starting with the `prefix` and `cwd` same as the current cwd + pub fn last_with_prefix_and_cwd( + prefix: String, + session: Option, + ) -> SearchQuery { + let cwd = std::env::current_dir(); + if let Ok(cwd) = cwd { + SearchQuery::last_with_search(SearchFilter::from_text_search_cwd( + cwd.to_string_lossy().to_string(), + CommandLineSearch::Prefix(prefix), + session, + )) + } else { + SearchQuery::last_with_search(SearchFilter::from_text_search( + CommandLineSearch::Prefix(prefix), + session, + )) + } + } + /// Query to get all entries in the given [`SearchDirection`] pub fn everything( direction: SearchDirection, diff --git a/src/lib.rs b/src/lib.rs index 25527356..83fb2a9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -269,6 +269,7 @@ mod completion; pub use completion::{Completer, DefaultCompleter, Span, Suggestion}; mod hinter; +pub use hinter::CwdAwareHinter; pub use hinter::{DefaultHinter, Hinter}; mod validator;