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
This commit is contained in:
Chinmay Dalal 2023-10-20 02:11:09 +05:30 committed by GitHub
parent adc20cb371
commit 973dbb5f5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 230 additions and 3 deletions

View File

@ -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<dyn reedline::History> {
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;
}
}

108
src/hinter/cwd_aware.rs Normal file
View File

@ -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
}
}

View File

@ -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,

View File

@ -1,4 +1,6 @@
mod cwd_aware;
mod default;
pub use cwd_aware::CwdAwareHinter;
pub use default::DefaultHinter;
use crate::History;

View File

@ -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<HistorySessionId>,
) -> 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<HistorySessionId>) -> 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<HistorySessionId>,
) -> 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,

View File

@ -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;