feat: navigate command and search input history

This commit is contained in:
2026-10-17 01:50:28 +02:00
parent 2cb3736b92
commit 5697a08c0a
7 changed files with 242 additions and 13 deletions
+86 -7
View File
@@ -28,6 +28,7 @@ use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr;
const MAX_EDIT_BYTES: usize = 4 * 1024 * 1024;
const PROMPT_HISTORY_LIMIT: usize = 100;
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
@@ -667,12 +668,24 @@ core_actions![
(
"core.down",
Action::Down,
&[Mode::Normal, Mode::Insert, Mode::Visual],
&[
Mode::Normal,
Mode::Insert,
Mode::Visual,
Mode::Command,
Mode::Search,
],
),
(
"core.up",
Action::Up,
&[Mode::Normal, Mode::Insert, Mode::Visual],
&[
Mode::Normal,
Mode::Insert,
Mode::Visual,
Mode::Command,
Mode::Search,
],
),
(
"core.scroll_up",
@@ -1552,11 +1565,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
),
(
Input::Ctrl('j'),
if mode == Mode::Insert {
Newline
} else {
Submit
},
if mode == Mode::Insert { Newline } else { Down },
),
] {
bindings
@@ -1575,6 +1584,14 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
.insert(key, Binding::Action(action));
}
}
for mode in [Mode::Command, Mode::Search] {
for (key, action) in [(Input::Up, Up), (Input::Ctrl('k'), Up), (Input::Down, Down)] {
bindings
.entry(mode)
.or_default()
.insert(key, Binding::Action(action));
}
}
for mode in [
Mode::Normal,
Mode::Insert,
@@ -1734,6 +1751,8 @@ struct ViewState {
search_highlight: bool,
search_match: Option<SearchMatch>,
command_line: String,
prompt_history_index: Option<usize>,
prompt_draft: String,
command_from_visual: bool,
caret_reveal_revision: u64,
lsp_center: Cell<Option<(u64, u64, Cursor)>>,
@@ -1756,6 +1775,8 @@ impl ViewState {
search_highlight: false,
search_match: None,
command_line: String::new(),
prompt_history_index: None,
prompt_draft: String::new(),
command_from_visual: false,
caret_reveal_revision: 0,
lsp_center: Cell::new(None),
@@ -1778,6 +1799,8 @@ impl ViewState {
search_highlight: self.search_highlight,
search_match: self.search_match,
command_line: self.command_line.clone(),
prompt_history_index: self.prompt_history_index,
prompt_draft: self.prompt_draft.clone(),
command_from_visual: self.command_from_visual,
caret_reveal_revision: self.caret_reveal_revision,
visual_motions: self.visual_motions.clone(),
@@ -2154,6 +2177,8 @@ pub struct Editor {
// Closed destinations stay until bounded eviction so they cannot shift jump_index.
jumps: VecDeque<JumpLocation>,
jump_index: usize,
command_history: VecDeque<String>,
search_history: VecDeque<String>,
clipboard: Vec<String>,
clipboard_linewise: bool,
clipboard_mode: ClipboardMode,
@@ -2269,6 +2294,8 @@ impl Editor {
); 3],
jumps: VecDeque::new(),
jump_index: 0,
command_history: VecDeque::new(),
search_history: VecDeque::new(),
clipboard: Vec::new(),
clipboard_linewise: false,
clipboard_mode: ClipboardMode::System,
@@ -7733,6 +7760,8 @@ impl Editor {
}
self.pane_close_picker_origin = None;
self.active.view.command_line = format!("{} ", entry.name);
self.active.view.prompt_history_index = None;
self.active.view.prompt_draft.clear();
self.palette = None;
self.active.view.command_from_visual = self.active.view.mode == Mode::Visual;
if self.diff.is_some() && !self.diff_interacting() {
@@ -8385,9 +8414,39 @@ impl Editor {
self.prepare_command();
}
self.active.view.command_line = format!("{} ", entry.ex.expect("prompt has Ex alias"));
self.active.view.prompt_history_index = None;
self.active.view.prompt_draft.clear();
self.active.view.mode = Mode::Command;
}
fn navigate_prompt_history(&mut self, previous: bool) {
let history = if self.active.view.mode == Mode::Search {
&self.search_history
} else {
&self.command_history
};
if history.is_empty() {
return;
}
let view = &mut self.active.view;
let index = match view.prompt_history_index {
None if !previous => return,
None => {
view.prompt_draft = std::mem::take(&mut view.command_line);
history.len() - 1
}
Some(index) if previous => index.saturating_sub(1).min(history.len() - 1),
Some(index) if index + 1 < history.len() => index + 1,
Some(_) => {
view.command_line = std::mem::take(&mut view.prompt_draft);
view.prompt_history_index = None;
return;
}
};
view.command_line.clone_from(&history[index]);
view.prompt_history_index = Some(index);
}
fn act(&mut self, action: Action) -> Option<Invocation> {
if (self.active.view.mode != Mode::Command || action == Action::Normal)
&& !matches!(
@@ -8620,6 +8679,11 @@ impl Editor {
self.start_insert(before, EditStart::Insert(action));
self.place_insert(action);
}
Action::Up | Action::Down
if matches!(self.active.view.mode, Mode::Command | Mode::Search) =>
{
self.navigate_prompt_history(action == Action::Up);
}
Action::Left
| Action::Right
| Action::Down
@@ -8684,6 +8748,8 @@ impl Editor {
Mode::Command
};
self.active.view.command_line.clear();
self.active.view.prompt_history_index = None;
self.active.view.prompt_draft.clear();
}
Action::SearchWord | Action::SearchNext | Action::SearchPrevious => {
self.seek_search(action, count);
@@ -8756,6 +8822,19 @@ impl Editor {
Action::Submit => {
let command = std::mem::take(&mut self.active.view.command_line);
let searching = self.active.view.mode == Mode::Search;
let history = if searching {
&mut self.search_history
} else {
&mut self.command_history
};
if !(command.is_empty() || (!searching && command.trim().is_empty()))
&& history.back() != Some(&command)
{
if history.len() == PROMPT_HISTORY_LIMIT {
history.pop_front();
}
history.push_back(command.clone());
}
self.active.view.mode = Mode::Normal;
if searching {
if !command.is_empty() {