W34: Goto line history with Up/Down arrows
Similar to the search history (W31), the goto-line prompt now keeps a history of recently-visited line numbers. Up/Down arrows cycle through the history; Down past the newest entry clears back to the live input. - Editor::goto_history: Vec<u32> field (max 50 entries) - Editor::goto_history_cursor: Option<usize> navigation position - Editor::push_goto_history(line) / goto_history_up() / goto_history_down() / reset_goto_history_cursor() - GotoLine prompt commit pushes the line to history - Up/Down arrows in the GotoLine prompt cycle through history - push_goto_history skips consecutive duplicates Added 2 unit tests covering: walk backwards/forwards, dedup. Tests: 1399 pass (was 1397), zero warnings.
This commit is contained in:
@@ -889,6 +889,38 @@ impl Editor {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Up/Down arrows in the GotoLine prompt cycle through the
|
||||
// goto-line history.
|
||||
if let Mode::Prompt(PromptKind::GotoLine) = self.mode {
|
||||
match key {
|
||||
Key::UP => {
|
||||
if let Some(line) = self.goto_history_up() {
|
||||
self.prompt_input.text = line.to_string();
|
||||
self.prompt_input.cursor = self.prompt_input.text.chars().count();
|
||||
}
|
||||
return EditorResult::Running;
|
||||
}
|
||||
Key::DOWN => {
|
||||
if let Some(p) = self.goto_history_down() {
|
||||
match p {
|
||||
Some(line) => {
|
||||
self.prompt_input.text = line.to_string();
|
||||
self.prompt_input.cursor =
|
||||
self.prompt_input.text.chars().count();
|
||||
}
|
||||
None => {
|
||||
// Past newest entry: clear the
|
||||
// prompt back to the live input.
|
||||
self.prompt_input.clear();
|
||||
self.reset_goto_history_cursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
return EditorResult::Running;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Alt-/ opens the search-history popup when the active
|
||||
// prompt is Find or Replace. Other prompt kinds ignore it.
|
||||
if key.mods == Modifiers::ALT && key.code == b'/' as u32 {
|
||||
@@ -972,9 +1004,11 @@ impl Editor {
|
||||
if let Ok(off) = crate::editor::goto::line_to_offset(&self.buffer, n) {
|
||||
self.buffer.set_cursor(off);
|
||||
self.cursor.set_position(self.buffer.cursor(), &self.buffer);
|
||||
self.push_goto_history(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.reset_goto_history_cursor();
|
||||
}
|
||||
PromptKind::GotoCol => {
|
||||
// 1-based column on the **current** line. We read
|
||||
|
||||
@@ -120,6 +120,14 @@ pub struct Editor {
|
||||
modified: bool,
|
||||
/// Recently opened files (most recent last).
|
||||
history: History,
|
||||
/// Recently visited line numbers via the goto-line prompt
|
||||
/// (most recent last). Up/Down arrows in the prompt cycle
|
||||
/// through this list. The cursor position is `None` when the
|
||||
/// user is editing the live input.
|
||||
goto_history: Vec<u32>,
|
||||
/// Position within `goto_history` when navigating with Up/Down.
|
||||
/// `None` means "not navigating — show the live input".
|
||||
goto_history_cursor: Option<usize>,
|
||||
/// Status line message (set by the last action).
|
||||
message: Option<String>,
|
||||
/// Current mode — Normal/Insert/Prompt(PromptKind). The
|
||||
@@ -254,6 +262,7 @@ impl Editor {
|
||||
let title = format!(" {} {} ", crate::locale::t("dialog_title_editor"), path.display());
|
||||
let mut history = History::with_capacity(20);
|
||||
history.push(&path);
|
||||
let goto_history: Vec<u32> = Vec::new();
|
||||
#[cfg(feature = "syntect")]
|
||||
let hl = crate::editor::syntax::Highlighter::new(&path);
|
||||
Self {
|
||||
@@ -264,6 +273,8 @@ impl Editor {
|
||||
path: Some(path),
|
||||
modified: false,
|
||||
history,
|
||||
goto_history,
|
||||
goto_history_cursor: None,
|
||||
message: None,
|
||||
mode: Mode::Insert,
|
||||
prompt_input: PromptInput::new(),
|
||||
@@ -314,6 +325,8 @@ impl Editor {
|
||||
path: None,
|
||||
modified: false,
|
||||
history: History::with_capacity(20),
|
||||
goto_history: Vec::new(),
|
||||
goto_history_cursor: None,
|
||||
message: Some("New buffer — Ctrl-S to save".to_string()),
|
||||
mode: Mode::Insert,
|
||||
prompt_input: PromptInput::new(),
|
||||
@@ -856,6 +869,58 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a successful goto-line result to the history. No-op if
|
||||
/// the line is identical to the most recent entry.
|
||||
pub fn push_goto_history(&mut self, line: u32) {
|
||||
if self.goto_history.last() == Some(&line) {
|
||||
return;
|
||||
}
|
||||
self.goto_history.push(line);
|
||||
if self.goto_history.len() > 50 {
|
||||
let drop = self.goto_history.len() - 50;
|
||||
self.goto_history.drain(0..drop);
|
||||
}
|
||||
self.goto_history_cursor = None;
|
||||
}
|
||||
|
||||
/// Move backward through the goto history (Up arrow). Returns
|
||||
/// the line number to display, or `None` if empty.
|
||||
pub fn goto_history_up(&mut self) -> Option<u32> {
|
||||
if self.goto_history.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let next = match self.goto_history_cursor {
|
||||
None => self.goto_history.len().checked_sub(1)?,
|
||||
Some(0) => 0,
|
||||
Some(c) => c - 1,
|
||||
};
|
||||
self.goto_history_cursor = Some(next);
|
||||
Some(self.goto_history[next])
|
||||
}
|
||||
|
||||
/// Move forward through the goto history (Down arrow). Returns
|
||||
/// `Some(Some(n))` to show a line, `Some(None)` to clear back
|
||||
/// to the live input, or `None` if not navigating.
|
||||
pub fn goto_history_down(&mut self) -> Option<Option<u32>> {
|
||||
match self.goto_history_cursor {
|
||||
None => Some(None),
|
||||
Some(c) if c + 1 >= self.goto_history.len() => {
|
||||
self.goto_history_cursor = None;
|
||||
Some(None)
|
||||
}
|
||||
Some(c) => {
|
||||
self.goto_history_cursor = Some(c + 1);
|
||||
Some(Some(self.goto_history[c + 1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset goto history navigation (call when the goto prompt
|
||||
/// is closed or the line is committed).
|
||||
pub fn reset_goto_history_cursor(&mut self) {
|
||||
self.goto_history_cursor = None;
|
||||
}
|
||||
|
||||
fn update_bracket_flash(&mut self) {
|
||||
let pos = self.cursor.position();
|
||||
let text = self.buffer.as_string();
|
||||
@@ -3747,5 +3812,31 @@ mod tests {
|
||||
// still the same (or slightly less, since time passed).
|
||||
assert!(frac > 0.0 && frac <= 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_history_walks_backwards_and_forwards() {
|
||||
let mut e = make_empty();
|
||||
e.push_goto_history(10);
|
||||
e.push_goto_history(20);
|
||||
e.push_goto_history(30);
|
||||
// Up walks from newest to oldest.
|
||||
assert_eq!(e.goto_history_up(), Some(30));
|
||||
assert_eq!(e.goto_history_up(), Some(20));
|
||||
assert_eq!(e.goto_history_up(), Some(10));
|
||||
// Down walks back forward through every entry, then
|
||||
// signals stop past the newest.
|
||||
assert_eq!(e.goto_history_down(), Some(Some(20)));
|
||||
assert_eq!(e.goto_history_down(), Some(Some(30)));
|
||||
assert_eq!(e.goto_history_down(), Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_history_skips_duplicates() {
|
||||
let mut e = make_empty();
|
||||
e.push_goto_history(10);
|
||||
e.push_goto_history(10);
|
||||
e.push_goto_history(20);
|
||||
assert_eq!(e.goto_history, vec![10, 20]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user