feat: navigate command and search input history
This commit is contained in:
+86
-7
@@ -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() {
|
||||
|
||||
@@ -56,6 +56,7 @@ Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+
|
||||
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
||||
Outside snippets Enter inserts newline, Tab inserts a tab or configured spaces; Esc returns to Normal
|
||||
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
||||
Command/Search: Up/Ctrl+k older history; Down/Ctrl+j newer history or unfinished input; Enter submits
|
||||
Search: Enter accepts current/next literal match, Esc cancels (case-sensitive, wraps)
|
||||
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
|
||||
:save_and_quit [PATH], :force_save_and_quit [PATH], :plugins, :PLUGIN [ARGS]
|
||||
|
||||
Reference in New Issue
Block a user