Skip to content

Commit

Permalink
add cwd aware hinter (#647)
Browse files Browse the repository at this point in the history
* add cwd aware hinter

towards nushell/nushell#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
  • Loading branch information
p00f authored Oct 19, 2023
1 parent adc20cb commit 973dbb5
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 3 deletions.
86 changes: 86 additions & 0 deletions examples/cwd_aware_hinter.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 108 additions & 0 deletions src/hinter/cwd_aware.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 1 addition & 3 deletions src/hinter/default.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/hinter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod cwd_aware;
mod default;
pub use cwd_aware::CwdAwareHinter;
pub use default::DefaultHinter;

use crate::History;
Expand Down
32 changes: 32 additions & 0 deletions src/history/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 973dbb5

Please sign in to comment.