From b668dab52243526407e68046da389d548b120ee0 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Thu, 19 Feb 2026 20:12:51 -0500 Subject: [PATCH] Fixed some weirdness in the logic for scrolling through command history --- nix/hm-module.nix | 2 +- src/prompt/readline/history.rs | 72 ++++++++++++++-------------------- src/prompt/readline/linebuf.rs | 13 +++--- src/prompt/readline/mod.rs | 70 ++++++++++++++++----------------- 4 files changed, 73 insertions(+), 84 deletions(-) diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 748e95e..96943eb 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -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 { diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs index 1481a0e..7002cc7 100644 --- a/src/prompt/readline/history.rs +++ b/src/prompt/readline/history.rs @@ -207,9 +207,11 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec { pub struct History { path: PathBuf, + pub pending: Option, entries: Vec, search_mask: Vec, - cursor: usize, + no_matches: bool, + pub cursor: usize, search_direction: Direction, ignore_dups: bool, max_size: Option, @@ -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 { - 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(()) } diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 9346280..cf4b46d 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -359,12 +359,13 @@ impl LineBuf { } pub fn set_hint(&mut self, hint: Option) { if let Some(hint) = hint { - let hint = hint.strip_prefix(&self.buffer).unwrap(); // If this ever panics, I will eat my hat - if !hint.is_empty() { - self.hint = Some(hint.to_string()) - } else { - self.hint = None - } + 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 } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 5aa8bae..f603965 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -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) { - self.history.push(buf.clone()); - if let Err(e) = self.history.save() { - eprintln!("Failed to save history: {e}"); - } - } + 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); - } - } - } else { - let target = (col).min(line_end); - buf.cursor.add(target); - } - - self.editor = buf + 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); + } } 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 {