Skip to content

Commit 973dbb5

Browse files
authored
add cwd aware hinter (#647)
* 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
1 parent adc20cb commit 973dbb5

File tree

6 files changed

+230
-3
lines changed

6 files changed

+230
-3
lines changed

examples/cwd_aware_hinter.rs

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Create a reedline object with in-line hint support.
2+
// cargo run --example cwd_aware_hinter
3+
//
4+
// Fish-style cwd history based hinting
5+
// assuming history ["abc", "ade"]
6+
// pressing "a" hints to abc.
7+
// Up/Down or Ctrl p/n, to select next/previous match
8+
9+
use std::io;
10+
11+
fn create_item(cwd: &str, cmd: &str, exit_status: i64) -> reedline::HistoryItem {
12+
use std::time::Duration;
13+
14+
use reedline::HistoryItem;
15+
HistoryItem {
16+
id: None,
17+
start_timestamp: None,
18+
command_line: cmd.to_string(),
19+
session_id: None,
20+
hostname: Some("foohost".to_string()),
21+
cwd: Some(cwd.to_string()),
22+
duration: Some(Duration::from_millis(1000)),
23+
exit_status: Some(exit_status),
24+
more_info: None,
25+
}
26+
}
27+
28+
fn create_filled_example_history(home_dir: &str, orig_dir: &str) -> Box<dyn reedline::History> {
29+
use reedline::History;
30+
#[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
31+
let mut history = Box::new(reedline::FileBackedHistory::new(100));
32+
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
33+
let mut history = Box::new(reedline::SqliteBackedHistory::in_memory().unwrap());
34+
35+
history.save(create_item(orig_dir, "dummy", 0)).unwrap(); // add dummy item so ids start with 1
36+
history.save(create_item(orig_dir, "ls /usr", 0)).unwrap();
37+
history.save(create_item(orig_dir, "pwd", 0)).unwrap();
38+
39+
history.save(create_item(home_dir, "cat foo", 0)).unwrap();
40+
history.save(create_item(home_dir, "ls bar", 0)).unwrap();
41+
history.save(create_item(home_dir, "rm baz", 0)).unwrap();
42+
43+
history
44+
}
45+
46+
fn main() -> io::Result<()> {
47+
use nu_ansi_term::{Color, Style};
48+
use reedline::{CwdAwareHinter, DefaultPrompt, Reedline, Signal};
49+
50+
let orig_dir = std::env::current_dir().unwrap();
51+
#[allow(deprecated)]
52+
let home_dir = std::env::home_dir().unwrap();
53+
54+
let history = create_filled_example_history(
55+
&home_dir.to_string_lossy().to_string(),
56+
&orig_dir.to_string_lossy().to_string(),
57+
);
58+
59+
let mut line_editor = Reedline::create()
60+
.with_hinter(Box::new(
61+
CwdAwareHinter::default().with_style(Style::new().italic().fg(Color::Yellow)),
62+
))
63+
.with_history(history);
64+
65+
let prompt = DefaultPrompt::default();
66+
67+
let mut iterations = 0;
68+
loop {
69+
if iterations % 2 == 0 {
70+
std::env::set_current_dir(&orig_dir).unwrap();
71+
} else {
72+
std::env::set_current_dir(&home_dir).unwrap();
73+
}
74+
let sig = line_editor.read_line(&prompt)?;
75+
match sig {
76+
Signal::Success(buffer) => {
77+
println!("We processed: {buffer}");
78+
}
79+
Signal::CtrlD | Signal::CtrlC => {
80+
println!("\nAborted!");
81+
break Ok(());
82+
}
83+
}
84+
iterations += 1;
85+
}
86+
}

src/hinter/cwd_aware.rs

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use crate::{
2+
history::SearchQuery,
3+
result::{ReedlineError, ReedlineErrorVariants::HistoryFeatureUnsupported},
4+
Hinter, History,
5+
};
6+
use nu_ansi_term::{Color, Style};
7+
8+
/// A hinter that uses the completions or the history to show a hint to the user
9+
///
10+
/// Similar to `fish` autosuggestions
11+
pub struct CwdAwareHinter {
12+
style: Style,
13+
current_hint: String,
14+
min_chars: usize,
15+
}
16+
17+
impl Hinter for CwdAwareHinter {
18+
fn handle(
19+
&mut self,
20+
line: &str,
21+
#[allow(unused_variables)] pos: usize,
22+
history: &dyn History,
23+
use_ansi_coloring: bool,
24+
) -> String {
25+
self.current_hint = if line.chars().count() >= self.min_chars {
26+
history
27+
.search(SearchQuery::last_with_prefix_and_cwd(
28+
line.to_string(),
29+
history.session(),
30+
))
31+
.or_else(|err| {
32+
if let ReedlineError(HistoryFeatureUnsupported { .. }) = err {
33+
history.search(SearchQuery::last_with_prefix(
34+
line.to_string(),
35+
history.session(),
36+
))
37+
} else {
38+
Err(err)
39+
}
40+
})
41+
.expect("todo: error handling")
42+
.get(0)
43+
.map_or_else(String::new, |entry| {
44+
entry
45+
.command_line
46+
.get(line.len()..)
47+
.unwrap_or_default()
48+
.to_string()
49+
})
50+
} else {
51+
String::new()
52+
};
53+
54+
if use_ansi_coloring && !self.current_hint.is_empty() {
55+
self.style.paint(&self.current_hint).to_string()
56+
} else {
57+
self.current_hint.clone()
58+
}
59+
}
60+
61+
fn complete_hint(&self) -> String {
62+
self.current_hint.clone()
63+
}
64+
65+
fn next_hint_token(&self) -> String {
66+
let mut reached_content = false;
67+
let result: String = self
68+
.current_hint
69+
.chars()
70+
.take_while(|c| match (c.is_whitespace(), reached_content) {
71+
(true, true) => false,
72+
(true, false) => true,
73+
(false, true) => true,
74+
(false, false) => {
75+
reached_content = true;
76+
true
77+
}
78+
})
79+
.collect();
80+
result
81+
}
82+
}
83+
84+
impl Default for CwdAwareHinter {
85+
fn default() -> Self {
86+
CwdAwareHinter {
87+
style: Style::new().fg(Color::LightGray),
88+
current_hint: String::new(),
89+
min_chars: 1,
90+
}
91+
}
92+
}
93+
94+
impl CwdAwareHinter {
95+
/// A builder that sets the style applied to the hint as part of the buffer
96+
#[must_use]
97+
pub fn with_style(mut self, style: Style) -> Self {
98+
self.style = style;
99+
self
100+
}
101+
102+
/// A builder that sets the number of characters that have to be present to enable history hints
103+
#[must_use]
104+
pub fn with_min_chars(mut self, min_chars: usize) -> Self {
105+
self.min_chars = min_chars;
106+
self
107+
}
108+
}

src/hinter/default.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
use crate::{history::SearchQuery, Hinter, History};
22
use nu_ansi_term::{Color, Style};
33

4-
/// A hinter that use the completions or the history to show a hint to the user
5-
///
6-
/// Similar to `fish` autosuggestins
4+
/// A hinter that uses the completions or the history to show a hint to the user
75
pub struct DefaultHinter {
86
style: Style,
97
current_hint: String,

src/hinter/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
mod cwd_aware;
12
mod default;
3+
pub use cwd_aware::CwdAwareHinter;
24
pub use default::DefaultHinter;
35

46
use crate::History;

src/history/base.rs

+32
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ impl SearchFilter {
6666
s
6767
}
6868

69+
/// Create a search filter with a [`CommandLineSearch`] and `cwd`
70+
pub fn from_text_search_cwd(
71+
cwd: String,
72+
cmd: CommandLineSearch,
73+
session: Option<HistorySessionId>,
74+
) -> SearchFilter {
75+
let mut s = SearchFilter::anything(session);
76+
s.command_line = Some(cmd);
77+
s.cwd_exact = Some(cwd);
78+
s
79+
}
80+
6981
/// anything within this session
7082
pub fn anything(session: Option<HistorySessionId>) -> SearchFilter {
7183
SearchFilter {
@@ -134,6 +146,26 @@ impl SearchQuery {
134146
))
135147
}
136148

149+
/// Get the most recent entry starting with the `prefix` and `cwd` same as the current cwd
150+
pub fn last_with_prefix_and_cwd(
151+
prefix: String,
152+
session: Option<HistorySessionId>,
153+
) -> SearchQuery {
154+
let cwd = std::env::current_dir();
155+
if let Ok(cwd) = cwd {
156+
SearchQuery::last_with_search(SearchFilter::from_text_search_cwd(
157+
cwd.to_string_lossy().to_string(),
158+
CommandLineSearch::Prefix(prefix),
159+
session,
160+
))
161+
} else {
162+
SearchQuery::last_with_search(SearchFilter::from_text_search(
163+
CommandLineSearch::Prefix(prefix),
164+
session,
165+
))
166+
}
167+
}
168+
137169
/// Query to get all entries in the given [`SearchDirection`]
138170
pub fn everything(
139171
direction: SearchDirection,

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ mod completion;
269269
pub use completion::{Completer, DefaultCompleter, Span, Suggestion};
270270

271271
mod hinter;
272+
pub use hinter::CwdAwareHinter;
272273
pub use hinter::{DefaultHinter, Hinter};
273274

274275
mod validator;

0 commit comments

Comments
 (0)