W31: Search history with Up/Down arrow keys
MC has search history navigation in the find prompt via arrow keys. TLC's SearchState already had a history() list but no way to navigate it without using the Alt-/ popup. Add history_up() / history_down() / reset_history_cursor() to SearchState. Wire Up/Down in the Find/Replace prompt to cycle through the history. Down past the newest entry clears the prompt back to the live pattern. Added 3 unit tests covering: walk backwards, walk forward with stop signal, empty history. Tests: 1396 pass (was 1393), zero warnings.
This commit is contained in:
@@ -857,6 +857,38 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Up/Down arrows in the Find/Replace prompt cycle through
|
||||
// the search history.
|
||||
if let Mode::Prompt(PromptKind::Find | PromptKind::Replace) = self.mode {
|
||||
match key {
|
||||
Key::UP => {
|
||||
if let Some(p) = self.search.history_up() {
|
||||
self.prompt_input.text = p;
|
||||
self.prompt_input.cursor = self.prompt_input.text.chars().count();
|
||||
}
|
||||
return EditorResult::Running;
|
||||
}
|
||||
Key::DOWN => {
|
||||
if let Some(p) = self.search.history_down() {
|
||||
match p {
|
||||
Some(s) => {
|
||||
self.prompt_input.text = s;
|
||||
self.prompt_input.cursor =
|
||||
self.prompt_input.text.chars().count();
|
||||
}
|
||||
None => {
|
||||
// Past newest entry: clear the
|
||||
// prompt back to the live pattern.
|
||||
self.prompt_input.clear();
|
||||
self.search.reset_history_cursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
return EditorResult::Running;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Alt-/ opens the search-history popup when the active
|
||||
// prompt is Find or Replace. Other prompt kinds ignore it.
|
||||
if key.mods == Modifiers::ALT && key.code == b'/' as u32 {
|
||||
|
||||
@@ -63,6 +63,10 @@ pub struct SearchState {
|
||||
last_match: Option<Range<usize>>,
|
||||
history: Vec<String>,
|
||||
last_wrapped: bool,
|
||||
/// Position within `history` when the user is scrolling through
|
||||
/// past patterns with Up/Down. `None` means "not navigating"
|
||||
/// (showing the live pattern, not a history entry).
|
||||
history_cursor: Option<usize>,
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
@@ -148,6 +152,45 @@ impl SearchState {
|
||||
&self.history
|
||||
}
|
||||
|
||||
/// Move backward through the history (Up arrow in the find
|
||||
/// prompt). Returns the pattern to display, or `None` if the
|
||||
/// history is empty.
|
||||
pub fn history_up(&mut self) -> Option<String> {
|
||||
if self.history.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let next = match self.history_cursor {
|
||||
None => self.history.len().checked_sub(1)?,
|
||||
Some(0) => 0,
|
||||
Some(c) => c - 1,
|
||||
};
|
||||
self.history_cursor = Some(next);
|
||||
Some(self.history[next].clone())
|
||||
}
|
||||
|
||||
/// Move forward through the history (Down arrow). Returns the
|
||||
/// pattern to display, or `None` to signal "stop navigating,
|
||||
/// back to the live pattern".
|
||||
pub fn history_down(&mut self) -> Option<Option<String>> {
|
||||
match self.history_cursor {
|
||||
None => Some(None),
|
||||
Some(c) if c + 1 >= self.history.len() => {
|
||||
self.history_cursor = None;
|
||||
Some(None)
|
||||
}
|
||||
Some(c) => {
|
||||
self.history_cursor = Some(c + 1);
|
||||
Some(Some(self.history[c + 1].clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset history navigation (call when the find prompt is
|
||||
/// closed or the pattern is committed).
|
||||
pub fn reset_history_cursor(&mut self) {
|
||||
self.history_cursor = None;
|
||||
}
|
||||
|
||||
/// Find the next match starting from byte position `from` in
|
||||
/// `buf`. Wraps around: if no match is found at or after `from`,
|
||||
/// the search continues from position 0. Updates
|
||||
@@ -651,4 +694,42 @@ mod tests {
|
||||
assert_eq!(m2.len(), 0);
|
||||
assert!(m2.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_up_walks_backwards_through_entries() {
|
||||
let mut s = SearchState::new();
|
||||
s.set_pattern("a".to_string());
|
||||
s.push_history();
|
||||
s.set_pattern("b".to_string());
|
||||
s.push_history();
|
||||
s.set_pattern("c".to_string());
|
||||
s.push_history();
|
||||
assert_eq!(s.history_up(), Some("c".to_string()));
|
||||
assert_eq!(s.history_up(), Some("b".to_string()));
|
||||
assert_eq!(s.history_up(), Some("a".to_string()));
|
||||
// Stays at oldest entry on further Up.
|
||||
assert_eq!(s.history_up(), Some("a".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_down_walks_forward_and_signals_stop() {
|
||||
let mut s = SearchState::new();
|
||||
s.set_pattern("a".to_string());
|
||||
s.push_history();
|
||||
s.set_pattern("b".to_string());
|
||||
s.push_history();
|
||||
s.set_pattern("c".to_string());
|
||||
s.push_history();
|
||||
let _ = s.history_up(); // -> "c"
|
||||
let _ = s.history_up(); // -> "b"
|
||||
assert_eq!(s.history_down(), Some(Some("c".to_string())));
|
||||
// Past the newest entry: Down signals stop (None).
|
||||
assert_eq!(s.history_down(), Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_up_on_empty_history_returns_none() {
|
||||
let mut s = SearchState::new();
|
||||
assert!(s.history_up().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user