From 0a16b05232b815d148f74df6b47bb3957a4fde70 Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 6 Jul 2026 22:43:08 +0300 Subject: [PATCH] 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 field (max 50 entries) - Editor::goto_history_cursor: Option 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. --- .../tui/tlc/source/src/editor/handlers.rs | 34 +++++++ .../recipes/tui/tlc/source/src/editor/mod.rs | 91 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/local/recipes/tui/tlc/source/src/editor/handlers.rs b/local/recipes/tui/tlc/source/src/editor/handlers.rs index ef3e45cc17..93741628e9 100644 --- a/local/recipes/tui/tlc/source/src/editor/handlers.rs +++ b/local/recipes/tui/tlc/source/src/editor/handlers.rs @@ -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 diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 250537f87a..e12f69ba8b 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -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, + /// Position within `goto_history` when navigating with Up/Down. + /// `None` means "not navigating — show the live input". + goto_history_cursor: Option, /// Status line message (set by the last action). message: Option, /// 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 = 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 { + 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> { + 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]); + } }