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 {
type = lib.types.bool;
default = false;
default = true;
description = "Whether to automatically add commands to the history as they are executed";
};
bellStyle = lib.mkOption {

View File

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

View File

@@ -359,12 +359,13 @@ impl LineBuf {
}
pub fn set_hint(&mut self, hint: Option<String>) {
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() {
self.hint = Some(hint.to_string())
} else {
self.hint = None
}
}
} else {
self.hint = None
}

View File

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