Fixed some weirdness in the logic for scrolling through command history
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user