Fixed some weirdness in the logic for scrolling through command history

This commit is contained in:
2026-02-19 20:12:51 -05:00
parent 18e36622a0
commit b668dab522
4 changed files with 73 additions and 84 deletions

View File

@@ -55,7 +55,7 @@ in
}; };
autoHistory = lib.mkOption { autoHistory = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = true;
description = "Whether to automatically add commands to the history as they are executed"; description = "Whether to automatically add commands to the history as they are executed";
}; };
bellStyle = lib.mkOption { bellStyle = lib.mkOption {

View File

@@ -207,9 +207,11 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
pub struct History { pub struct History {
path: PathBuf, path: PathBuf,
pub pending: Option<String>,
entries: Vec<HistEntry>, entries: Vec<HistEntry>,
search_mask: Vec<HistEntry>, search_mask: Vec<HistEntry>,
cursor: usize, no_matches: bool,
pub cursor: usize,
search_direction: Direction, search_direction: Direction,
ignore_dups: bool, ignore_dups: bool,
max_size: Option<u32>, max_size: Option<u32>,
@@ -228,20 +230,14 @@ impl History {
if entries.len() > max_hist { if entries.len() > max_hist {
entries = entries.split_off(entries.len() - max_hist); entries = entries.split_off(entries.len() - max_hist);
} }
// Create pending entry for current input
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
entries.push(HistEntry {
id,
timestamp: SystemTime::now(),
command: String::new(),
new: true,
});
let search_mask = dedupe_entries(&entries); let search_mask = dedupe_entries(&entries);
let cursor = search_mask.len().saturating_sub(1); let cursor = search_mask.len();
Ok(Self { Ok(Self {
path, path,
entries, entries,
pending: None,
search_mask, search_mask,
no_matches: false,
cursor, cursor,
search_direction: Direction::Backward, search_direction: Direction::Backward,
ignore_dups, ignore_dups,
@@ -251,7 +247,7 @@ impl History {
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.search_mask = dedupe_entries(&self.entries); self.search_mask = dedupe_entries(&self.entries);
self.cursor = self.search_mask.len().saturating_sub(1); self.cursor = self.search_mask.len();
} }
pub fn entries(&self) -> &[HistEntry] { pub fn entries(&self) -> &[HistEntry] {
@@ -262,30 +258,26 @@ impl History {
&self.search_mask &self.search_mask
} }
pub fn push_empty_entry(&mut self) {
let timestamp = SystemTime::now();
let id = self.get_new_id();
self.entries.push(HistEntry {
id,
timestamp,
command: String::new(),
new: true,
});
}
pub fn cursor_entry(&self) -> Option<&HistEntry> { pub fn cursor_entry(&self) -> Option<&HistEntry> {
self.search_mask.get(self.cursor) self.search_mask.get(self.cursor)
} }
pub fn at_pending(&self) -> bool {
self.cursor >= self.search_mask.len()
}
pub fn reset_to_pending(&mut self) {
self.cursor = self.search_mask.len();
}
pub fn update_pending_cmd(&mut self, command: &str) { pub fn update_pending_cmd(&mut self, command: &str) {
let Some(ent) = self.last_mut() else { return };
let cmd = command.to_string(); let cmd = command.to_string();
let constraint = SearchConstraint { let constraint = SearchConstraint {
kind: SearchKind::Prefix, kind: SearchKind::Prefix,
term: cmd.clone(), term: cmd.clone(),
}; };
ent.command = cmd; self.pending = Some(cmd);
self.constrain_entries(constraint); self.constrain_entries(constraint);
} }
@@ -323,25 +315,26 @@ impl History {
.collect(); .collect();
self.search_mask = dedupe_entries(&filtered); self.search_mask = dedupe_entries(&filtered);
self.no_matches = self.search_mask.is_empty();
if self.no_matches {
// If no matches, reset to full history so user can still scroll through it
self.search_mask = dedupe_entries(&self.entries);
}
} }
self.cursor = self.search_mask.len().saturating_sub(1); self.cursor = self.search_mask.len();
} }
SearchKind::Fuzzy => todo!(), SearchKind::Fuzzy => todo!(),
} }
} }
pub fn hint_entry(&self) -> Option<&HistEntry> { pub fn hint_entry(&self) -> Option<&HistEntry> {
let second_to_last = self.search_mask.len().checked_sub(2)?; if self.no_matches { return None };
self.search_mask.get(second_to_last) self.search_mask.last()
} }
pub fn get_hint(&self) -> Option<String> { pub fn get_hint(&self) -> Option<String> {
if self if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) {
.cursor_entry()
.is_some_and(|ent| ent.is_new() && !ent.command().is_empty())
{
let entry = self.hint_entry()?; let entry = self.hint_entry()?;
let prefix = self.cursor_entry()?.command();
Some(entry.command().to_string()) Some(entry.command().to_string())
} else { } else {
None None
@@ -349,15 +342,10 @@ impl History {
} }
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
let new_idx = self self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len());
.cursor
.saturating_add_signed(offset)
.clamp(0, self.search_mask.len().saturating_sub(1));
let ent = self.search_mask.get(new_idx)?;
self.cursor = new_idx; log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor);
self.search_mask.get(self.cursor)
Some(ent)
} }
pub fn push(&mut self, command: String) { pub fn push(&mut self, command: String) {
@@ -411,8 +399,8 @@ impl History {
} }
file.write_all(data.as_bytes())?; file.write_all(data.as_bytes())?;
self.push_empty_entry(); // Prepare for next command self.pending = None;
self.reset(); // Reset search mask to include new pending entry self.reset();
Ok(()) Ok(())
} }

View File

@@ -359,12 +359,13 @@ impl LineBuf {
} }
pub fn set_hint(&mut self, hint: Option<String>) { pub fn set_hint(&mut self, hint: Option<String>) {
if let Some(hint) = hint { if let Some(hint) = hint {
let hint = hint.strip_prefix(&self.buffer).unwrap(); // If this ever panics, I will eat my hat if let Some(hint) = hint.strip_prefix(&self.buffer) {
if !hint.is_empty() { if !hint.is_empty() {
self.hint = Some(hint.to_string()) self.hint = Some(hint.to_string())
} else { } else {
self.hint = None self.hint = None
} }
}
} else { } else {
self.hint = None self.hint = None
} }

View File

@@ -170,6 +170,8 @@ impl FernVi {
self.mode = Box::new(ViInsert::new()); self.mode = Box::new(ViInsert::new());
self.old_layout = None; self.old_layout = None;
self.needs_redraw = true; self.needs_redraw = true;
self.history.pending = None;
self.history.reset();
} }
/// Process any available input and return readline event /// Process any available input and return readline event
@@ -186,6 +188,9 @@ impl FernVi {
if self.should_accept_hint(&key) { if self.should_accept_hint(&key) {
self.editor.accept_hint(); self.editor.accept_hint();
if !self.history.at_pending() {
self.history.reset_to_pending();
}
self.history.update_pending_cmd(self.editor.as_str()); self.history.update_pending_cmd(self.editor.as_str());
self.needs_redraw = true; self.needs_redraw = true;
continue; continue;
@@ -207,6 +212,9 @@ impl FernVi {
self.editor.set_buffer(line); self.editor.set_buffer(line);
self.editor.cursor.set(new_cursor); self.editor.cursor.set(new_cursor);
if !self.history.at_pending() {
self.history.reset_to_pending();
}
self.history.update_pending_cmd(self.editor.as_str()); self.history.update_pending_cmd(self.editor.as_str());
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
@@ -244,12 +252,14 @@ impl FernVi {
self.writer.flush_write("\n")?; self.writer.flush_write("\n")?;
let buf = self.editor.take_buf(); let buf = self.editor.take_buf();
// Save command to history if auto_hist is enabled // Save command to history if auto_hist is enabled
if crate::state::read_shopts(|s| s.core.auto_hist) { if crate::state::read_shopts(|s| s.core.auto_hist)
self.history.push(buf.clone()); && !buf.is_empty() {
if let Err(e) = self.history.save() { self.history.push(buf.clone());
eprintln!("Failed to save history: {e}"); if let Err(e) = self.history.save() {
} eprintln!("Failed to save history: {e}");
} }
}
self.history.reset();
return Ok(ReadlineEvent::Line(buf)); return Ok(ReadlineEvent::Line(buf));
} }
@@ -300,39 +310,30 @@ impl FernVi {
*/ */
let count = &cmd.motion().unwrap().0; let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1; let motion = &cmd.motion().unwrap().1;
let entry = match motion { let count = match motion {
Motion::LineUpCharwise => { Motion::LineUpCharwise => {
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { -(*count as isize)
return;
};
hist_entry
} }
Motion::LineDownCharwise => { Motion::LineDownCharwise => {
let Some(hist_entry) = self.history.scroll(*count as isize) else { *count as isize
return;
};
hist_entry
} }
_ => unreachable!(), _ => unreachable!(),
}; };
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col()); let entry = self.history.scroll(count);
let mut buf = LineBuf::new().with_initial(entry.command(), 0); log::info!("Scrolled history, got entry: {:?}", entry.as_ref());
let line_end = buf.end_of_line(); if let Some(entry) = entry {
if let Some(dest) = self.mode.hist_scroll_start_pos() { log::info!("Setting buffer to history entry: {}", entry.command());
match dest { let pending = self.editor.take_buf();
To::Start => { /* Already at 0 */ } self.editor.set_buffer(entry.command().to_string());
To::End => { if self.history.pending.is_none() {
// History entries cannot be empty self.history.pending = Some(pending);
// So this subtraction is safe (maybe) }
buf.cursor.add(line_end); self.editor.set_hint(None);
} } else if let Some(pending) = self.history.pending.take() {
} log::info!("Setting buffer to pending command: {}", &pending);
} else { self.editor.set_buffer(pending);
let target = (col).min(line_end); self.editor.set_hint(None);
buf.cursor.add(target); }
}
self.editor = buf
} }
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.editor.cursor_at_max() && self.editor.has_hint() { if self.editor.cursor_at_max() && self.editor.has_hint() {
@@ -361,8 +362,7 @@ impl FernVi {
|| (cmd || (cmd
.motion() .motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
&& self.editor.end_of_line() == self.editor.cursor_max() && self.editor.end_of_line() == self.editor.cursor_max())
&& !self.history.cursor_entry().is_some_and(|ent| ent.is_new()))
} }
pub fn line_text(&mut self) -> String { pub fn line_text(&mut self) -> String {