From 6f49ff22294fa2c885cff696aa92a59778229c67 Mon Sep 17 00:00:00 2001 From: piotrkwarcinski Date: Sun, 8 Jun 2025 00:09:21 +0200 Subject: [PATCH 1/3] Add undo and redo selection --- helix-term/src/commands.rs | 16 +++++++- helix-term/src/keymap/default.rs | 2 + helix-view/src/document.rs | 65 ++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2cbdeb451..3c274309e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -42,7 +42,7 @@ use helix_core::{ Selection, SmallVec, Syntax, Tendril, Transaction, }; use helix_view::{ - document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, + document::{FormatterError, Mode, SelectionDirection, SCRATCH_BUFFER_NAME}, editor::Action, info::Info, input::KeyEvent, @@ -461,6 +461,8 @@ impl MappableCommand { extend_to_line_start, "Extend to line start", extend_to_first_nonwhitespace, "Extend to first non-blank in line", extend_to_line_end, "Extend to line end", + undo_selection, "Go to previous selection", + redo_selection, "Go to next selection", extend_to_line_end_newline, "Extend to line end", signature_help, "Show signature help", smart_tab, "Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command.", @@ -832,6 +834,18 @@ fn goto_line_end(cx: &mut Context) { ) } +fn undo_selection(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + doc.select_history(view.id, SelectionDirection::Undo(count)); +} + +fn redo_selection(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + doc.select_history(view.id, SelectionDirection::Redo(count)); +} + fn extend_to_line_end(cx: &mut Context) { let (view, doc) = current!(cx.editor); goto_line_end_impl(view, doc, Movement::Extend) diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 82baf336f..4f6573678 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -86,6 +86,8 @@ pub fn default() -> HashMap { "S" => split_selection, ";" => collapse_selection, "A-;" => flip_selections, + "A-w" => redo_selection, + "A-k" => undo_selection, "A-o" | "A-up" => expand_selection, "A-i" | "A-down" => shrink_selection, "A-I" | "A-S-down" => select_all_children, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index fb89e2e0c..b3317c628 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -142,6 +142,7 @@ pub struct Document { pub(crate) id: DocumentId, text: Rope, selections: HashMap, + selections_history: HashMap, view_data: HashMap, pub active_snippet: Option, @@ -216,6 +217,33 @@ pub struct Document { syn_loader: Arc>, } +#[derive(Debug, PartialEq, Eq)] +pub enum SelectionDirection { + Undo(usize), + Redo(usize), +} + +#[derive(Debug)] +pub struct SelectionHistory { + selections: Vec, + current: usize, +} + +impl SelectionHistory { + pub fn goto(&mut self, direction: SelectionDirection) -> Option<&Selection> { + let mut position = match direction { + SelectionDirection::Undo(count) => self.current.checked_sub(count)?, + SelectionDirection::Redo(count) => self.current.checked_add(count)?, + }; + let max = self.selections.len() - 1; + if position > max { + position = max + } + self.current = position; + self.selections.get(self.current) + } +} + #[derive(Debug, Clone, Default)] pub struct DocumentColorSwatches { pub color_swatches: Vec, @@ -300,6 +328,7 @@ impl fmt::Debug for Document { .field("inlay_hints_oudated", &self.inlay_hints_oudated) .field("text_annotations", &self.inlay_hints) .field("view_data", &self.view_data) + .field("selections_history", &self.selections_history) .field("path", &self.path) .field("encoding", &self.encoding) .field("restore_cursor", &self.restore_cursor) @@ -700,6 +729,7 @@ impl Document { has_bom, text, selections: HashMap::default(), + selections_history: HashMap::default(), inlay_hints: HashMap::default(), inlay_hints_oudated: false, view_data: Default::default(), @@ -1319,17 +1349,43 @@ impl Document { Ok(()) } - /// Select text within the [`Document`]. - pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { + fn select(&mut self, view_id: ViewId, selection: Selection) { // TODO: use a transaction? - self.selections - .insert(view_id, selection.ensure_invariants(self.text().slice(..))); + self.selections.insert(view_id, selection); helix_event::dispatch(SelectionDidChange { doc: self, view: view_id, }) } + /// Select text within the [`Document`]. + pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { + // TODO: use a transaction? + let selection = selection.ensure_invariants(self.text().slice(..)); + self.save_selection_to_history(view_id, selection.clone()); + self.select(view_id, selection); + } + + fn save_selection_to_history(&mut self, view_id: ViewId, selection: Selection) { + let entry = self + .selections_history + .entry(view_id) + .or_insert(SelectionHistory { + selections: vec![], + current: 0, + }); + entry.selections.push(selection); + entry.current = entry.selections.len() - 1; + } + + pub fn select_history(&mut self, view_id: ViewId, direction: SelectionDirection) { + self.selections_history + .get_mut(&view_id) + .and_then(|history| history.goto(direction).cloned()) + .into_iter() + .for_each(|selection| self.select(view_id, selection)); + } + /// Find the origin selection of the text in a document, i.e. where /// a single cursor would go if it were on the first grapheme. If /// the text is empty, returns (0, 0). @@ -1366,6 +1422,7 @@ impl Document { /// Remove a view's selection and inlay hints from this document. pub fn remove_view(&mut self, view_id: ViewId) { self.selections.remove(&view_id); + self.selections_history.remove(&view_id); self.inlay_hints.remove(&view_id); self.jump_labels.remove(&view_id); } From c0f8e18437b7d4c0878be4ef8981196d06fa17af Mon Sep 17 00:00:00 2001 From: piotrkwarcinski Date: Tue, 10 Jun 2025 01:22:06 +0200 Subject: [PATCH 2/3] Clear history after a change --- helix-view/src/document.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index b3317c628..049fdaf48 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1468,6 +1468,9 @@ impl Document { .ensure_invariants(self.text.slice(..)); } + // Reset the selection history after any change + self.selections_history.clear(); + for view_data in self.view_data.values_mut() { view_data.view_position.anchor = transaction .changes() From 7681cc93fd064450b5578ee8e4b5b44e285bc64b Mon Sep 17 00:00:00 2001 From: piotrkwarcinski Date: Tue, 10 Jun 2025 01:57:28 +0200 Subject: [PATCH 3/3] Gen docs --- book/src/generated/static-cmd.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/src/generated/static-cmd.md b/book/src/generated/static-cmd.md index 7ecb7f4e8..1bb39d0a0 100644 --- a/book/src/generated/static-cmd.md +++ b/book/src/generated/static-cmd.md @@ -163,6 +163,8 @@ | `extend_to_line_start` | Extend to line start | select: `` `` | | `extend_to_first_nonwhitespace` | Extend to first non-blank in line | | | `extend_to_line_end` | Extend to line end | select: `` `` | +| `undo_selection` | Go to previous selection | normal: `` ``, select: `` `` | +| `redo_selection` | Go to next selection | normal: `` ``, select: `` `` | | `extend_to_line_end_newline` | Extend to line end | | | `signature_help` | Show signature help | | | `smart_tab` | Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command. | insert: `` `` |