diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index a6ee7f05d..5b62d5ca4 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -31,6 +31,7 @@ pub struct Prompt { selection: Option, history_register: Option, history_pos: Option, + history_substring: Option, completion_fn: CompletionFn, callback_fn: CallbackFn, pub doc_fn: DocFn, @@ -83,6 +84,7 @@ impl Prompt { selection: None, history_register, history_pos: None, + history_substring: None, completion_fn: Box::new(completion_fn), callback_fn: Box::new(callback_fn), doc_fn: Box::new(|_| None), @@ -96,6 +98,7 @@ impl Prompt { self.line = line; self.cursor = cursor; self.recalculate_completion(editor); + self.reset_history(); self } @@ -232,12 +235,14 @@ impl Prompt { self.cursor = pos; } self.recalculate_completion(cx.editor); + self.reset_history(); } pub fn insert_str(&mut self, s: &str, editor: &Editor) { self.line.insert_str(self.cursor, s); self.cursor += s.len(); self.recalculate_completion(editor); + self.reset_history(); } pub fn move_cursor(&mut self, movement: Movement) { @@ -259,6 +264,7 @@ impl Prompt { self.cursor = pos; self.recalculate_completion(editor); + self.reset_history(); } pub fn delete_char_forwards(&mut self, editor: &Editor) { @@ -266,6 +272,7 @@ impl Prompt { self.line.replace_range(self.cursor..pos, ""); self.recalculate_completion(editor); + self.reset_history(); } pub fn delete_word_backwards(&mut self, editor: &Editor) { @@ -274,6 +281,7 @@ impl Prompt { self.cursor = pos; self.recalculate_completion(editor); + self.reset_history(); } pub fn delete_word_forwards(&mut self, editor: &Editor) { @@ -281,6 +289,7 @@ impl Prompt { self.line.replace_range(self.cursor..pos, ""); self.recalculate_completion(editor); + self.reset_history(); } pub fn kill_to_start_of_line(&mut self, editor: &Editor) { @@ -289,6 +298,7 @@ impl Prompt { self.cursor = pos; self.recalculate_completion(editor); + self.reset_history(); } pub fn kill_to_end_of_line(&mut self, editor: &Editor) { @@ -296,12 +306,19 @@ impl Prompt { self.line.replace_range(self.cursor..pos, ""); self.recalculate_completion(editor); + self.reset_history(); } pub fn clear(&mut self, editor: &Editor) { self.line.clear(); self.cursor = 0; self.recalculate_completion(editor); + self.reset_history(); + } + + pub fn reset_history(&mut self) { + self.history_pos = None; + self.history_substring = None; } pub fn change_history( @@ -312,11 +329,15 @@ impl Prompt { ) { (self.callback_fn)(cx, &self.line, PromptEvent::Abort); let mut values = match cx.editor.registers.read(register, cx.editor) { - Some(values) if values.len() > 0 => values.rev(), + Some(values) if values.len() > 0 => values.rev().enumerate(), _ => return, }; - let end = values.len().saturating_sub(1); + // Using `history_pos` as the trigger so that we only consider updating + // the substring to match on when entering history + if self.history_pos.is_none() && !self.line.is_empty() { + self.history_substring = Some(self.line.clone()) + } let index = match direction { CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1), @@ -324,18 +345,41 @@ impl Prompt { .history_pos .unwrap_or_else(|| values.len()) .saturating_sub(1), - } - .min(end); + }; + + let history_line = if let Some(substr) = self.history_substring.as_ref() { + match direction { + CompletionDirection::Forward => { + if index > 0 { + // Same as skip but without taking ownership + let _ = values.nth(index - 1); + } + values.find(|prev| prev.1.find(substr).is_some()) + } + CompletionDirection::Backward => { + let r_index = values.len() - 1 - index; + if r_index > 0 { + // Same as skip but without taking ownership + let _ = values.nth_back(r_index - 1); + } + values.rfind(|prev| prev.1.find(substr).is_some()) + } + } + } else { + values.nth(index) + }; - self.line = values.nth(index).unwrap().to_string(); - // Appease the borrow checker. - drop(values); + if let Some((index, line)) = history_line { + self.line = line.to_string(); + // Appease the borrow checker. + drop(values); - self.history_pos = Some(index); + self.history_pos = Some(index); - self.move_end(); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); - self.recalculate_completion(cx.editor); + self.move_end(); + (self.callback_fn)(cx, &self.line, PromptEvent::Update); + self.recalculate_completion(cx.editor); + } } pub fn change_completion_selection(&mut self, direction: CompletionDirection) { @@ -357,6 +401,7 @@ impl Prompt { self.line.replace_range(range.clone(), item); self.move_end(); + self.reset_history(); } pub fn exit_selection(&mut self) { @@ -492,6 +537,28 @@ impl Prompt { ) .into(); text.render(line_area, surface, cx); + } else if let Some((pre, substr, post)) = + self.history_substring.as_ref().and_then(|substr| { + self.line + .split_once(substr) + .map(|(pre, post)| (pre, substr, post)) + }) + { + surface.set_string(line_area.x, line_area.y, pre, prompt_color); + + surface.set_string( + line_area.x + pre.len() as u16, + line_area.y, + substr, + selected_color, + ); + + surface.set_string( + line_area.x + (pre.len() + substr.len()) as u16, + line_area.y, + post, + prompt_color, + ); } else { surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color); } diff --git a/helix-term/tests/test/prompt.rs b/helix-term/tests/test/prompt.rs index 4f3bf7632..911cac478 100644 --- a/helix-term/tests/test/prompt.rs +++ b/helix-term/tests/test/prompt.rs @@ -4,7 +4,7 @@ use super::*; async fn test_history_completion() -> anyhow::Result<()> { test_key_sequence( &mut AppBuilder::new().build()?, - Some(":asdf:theme d"), + Some(":asdf:"), Some(&|app| { assert!(!app.editor.is_err()); }),