From 3cfa2c65b360adcf36679b3223832085b86f3e1f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 14:38:51 +0200 Subject: [PATCH 1/7] Autoscroll to newest cursor on cmd-d instead of fitting all selections --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9327ee251..60d6bbc699 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3998,7 +3998,7 @@ impl Editor { state.stack.pop(); } - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.update_selections(new_selections, Some(Autoscroll::Newest), cx); if state.stack.len() > 1 { self.add_selections_state = Some(state); } From 4ed0607e1e66e2dd112a7a74a7affbc8daa694e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 14:52:54 +0200 Subject: [PATCH 2/7] Extract `SelectionHistory` in preparation to store an undo/redo stack --- crates/editor/src/editor.rs | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 60d6bbc699..40cae9576e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -455,8 +455,7 @@ pub struct Editor { columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, - selection_history: - HashMap]>, Option]>>)>, + selection_history: SelectionHistory, autoclose_stack: InvalidationStack, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, @@ -508,6 +507,37 @@ pub struct PendingSelection { mode: SelectMode, } +#[derive(Default)] +struct SelectionHistory { + selections_by_transaction: + HashMap]>, Option]>>)>, +} + +impl SelectionHistory { + fn insert_transaction( + &mut self, + transaction_id: TransactionId, + selections: Arc<[Selection]>, + ) { + self.selections_by_transaction + .insert(transaction_id, (selections, None)); + } + + fn transaction( + &self, + transaction_id: TransactionId, + ) -> Option<&(Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get(&transaction_id) + } + + fn transaction_mut( + &mut self, + transaction_id: TransactionId, + ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get_mut(&transaction_id) + } +} + struct AddSelectionsState { above: bool, stack: Vec, @@ -3438,7 +3468,7 @@ impl Editor { pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { - if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { + if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() { self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); @@ -3448,7 +3478,8 @@ impl Editor { pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { - if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { + if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned() + { self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); @@ -5279,7 +5310,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) { self.selection_history - .insert(tx_id, (self.selections.clone(), None)); + .insert_transaction(tx_id, self.selections.clone()); } } @@ -5288,7 +5319,7 @@ impl Editor { .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) { - if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { + if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) { *end_selections = Some(self.selections.clone()); } else { log::error!("unexpectedly ended a transaction that wasn't started by this editor"); From 73c2f52158907efbc7812f97bbfdbbb64fa15222 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 16:05:44 +0200 Subject: [PATCH 3/7] Implement `cmd-u` and `cmd-shift-u` to undo and redo selections --- crates/editor/src/editor.rs | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 40cae9576e..5c3840fcf9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -120,6 +120,8 @@ action!(ToggleComments); action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); +action!(UndoSelection); +action!(RedoSelection); action!(GoToDiagnostic, Direction); action!(GoToDefinition); action!(FindAllReferences); @@ -280,6 +282,8 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("cmd-u", UndoSelection, Some("Editor")), + Binding::new("cmd-shift-U", RedoSelection, Some("Editor")), Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")), Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")), Binding::new("f2", Rename, Some("Editor")), @@ -356,6 +360,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::undo_selection); + cx.add_action(Editor::redo_selection); cx.add_action(Editor::go_to_diagnostic); cx.add_action(Editor::go_to_definition); cx.add_action(Editor::page_up); @@ -507,10 +513,32 @@ pub struct PendingSelection { mode: SelectMode, } +#[derive(Clone)] +struct SelectionHistoryEntry { + selections: Arc<[Selection]>, + select_next_state: Option, + add_selections_state: Option, +} + +enum SelectionHistoryMode { + Normal, + Undoing, + Redoing, +} + +impl Default for SelectionHistoryMode { + fn default() -> Self { + Self::Normal + } +} + #[derive(Default)] struct SelectionHistory { selections_by_transaction: HashMap]>, Option]>>)>, + mode: SelectionHistoryMode, + undo_stack: Vec, + redo_stack: Vec, } impl SelectionHistory { @@ -536,13 +564,48 @@ impl SelectionHistory { ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { self.selections_by_transaction.get_mut(&transaction_id) } + + fn push(&mut self, entry: SelectionHistoryEntry) { + if !entry.selections.is_empty() { + match self.mode { + SelectionHistoryMode::Normal => { + self.push_undo(entry); + self.redo_stack.clear(); + } + SelectionHistoryMode::Undoing => self.push_redo(entry), + SelectionHistoryMode::Redoing => self.push_undo(entry), + } + } + } + + fn push_undo(&mut self, entry: SelectionHistoryEntry) { + if self + .undo_stack + .last() + .map_or(true, |e| e.selections != entry.selections) + { + self.undo_stack.push(entry) + } + } + + fn push_redo(&mut self, entry: SelectionHistoryEntry) { + if self + .redo_stack + .last() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push(entry) + } + } } +#[derive(Clone)] struct AddSelectionsState { above: bool, stack: Vec, } +#[derive(Clone)] struct SelectNextState { query: AhoCorasick, wordwise: bool, @@ -3943,6 +4006,7 @@ impl Editor { } fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + self.push_to_selection_history(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.local_selections::(cx); let mut state = self.add_selections_state.take().unwrap_or_else(|| { @@ -4036,6 +4100,7 @@ impl Editor { } pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { + self.push_to_selection_history(); let replace_newest = action.0; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -4320,6 +4385,30 @@ impl Editor { self.update_selections(selections, Some(Autoscroll::Fit), cx); } + pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Undoing; + if let Some(entry) = self.selection_history.undo_stack.pop() { + self.set_selections(entry.selections.clone(), None, true, cx); + self.select_next_state = entry.select_next_state.clone(); + self.add_selections_state = entry.add_selections_state.clone(); + self.request_autoscroll(Autoscroll::Newest, cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Redoing; + if let Some(entry) = self.selection_history.redo_stack.pop() { + self.set_selections(entry.selections.clone(), None, true, cx); + self.select_next_state = entry.select_next_state.clone(); + self.add_selections_state = entry.add_selections_state.clone(); + self.request_autoscroll(Autoscroll::Newest, cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + pub fn go_to_diagnostic( &mut self, &GoToDiagnostic(direction): &GoToDiagnostic, @@ -5218,6 +5307,7 @@ impl Editor { let old_cursor_position = self.newest_anchor_selection().head(); + self.push_to_selection_history(); self.selections = selections; self.pending_selection = pending_selection; if self.focused && self.leader_replica_id.is_none() { @@ -5283,6 +5373,14 @@ impl Editor { cx.emit(Event::SelectionsChanged { local }); } + fn push_to_selection_history(&mut self) { + self.selection_history.push(SelectionHistoryEntry { + selections: self.selections.clone(), + select_next_state: self.select_next_state.clone(), + add_selections_state: self.add_selections_state.clone(), + }); + } + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { self.autoscroll_request = Some((autoscroll, true)); cx.notify(); From bbfb63ff892c0f1010b8d41efea0fe62c7b6a321 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 16:12:34 +0200 Subject: [PATCH 4/7] Cap selection history to 1024 entries --- crates/editor/src/editor.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5c3840fcf9..40a27752cd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10,7 +10,7 @@ mod test; use aho_corasick::AhoCorasick; use anyhow::Result; use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; +use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; @@ -62,6 +62,7 @@ use workspace::{settings, ItemNavHistory, Settings, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; +const MAX_SELECTION_HISTORY_LEN: usize = 1024; action!(Cancel); action!(Backspace); @@ -537,8 +538,8 @@ struct SelectionHistory { selections_by_transaction: HashMap]>, Option]>>)>, mode: SelectionHistoryMode, - undo_stack: Vec, - redo_stack: Vec, + undo_stack: VecDeque, + redo_stack: VecDeque, } impl SelectionHistory { @@ -581,20 +582,26 @@ impl SelectionHistory { fn push_undo(&mut self, entry: SelectionHistoryEntry) { if self .undo_stack - .last() + .back() .map_or(true, |e| e.selections != entry.selections) { - self.undo_stack.push(entry) + self.undo_stack.push_back(entry); + if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.undo_stack.pop_front(); + } } } fn push_redo(&mut self, entry: SelectionHistoryEntry) { if self .redo_stack - .last() + .back() .map_or(true, |e| e.selections != entry.selections) { - self.redo_stack.push(entry) + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } } } } @@ -4388,7 +4395,7 @@ impl Editor { pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Undoing; - if let Some(entry) = self.selection_history.undo_stack.pop() { + if let Some(entry) = self.selection_history.undo_stack.pop_back() { self.set_selections(entry.selections.clone(), None, true, cx); self.select_next_state = entry.select_next_state.clone(); self.add_selections_state = entry.add_selections_state.clone(); @@ -4400,7 +4407,7 @@ impl Editor { pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Redoing; - if let Some(entry) = self.selection_history.redo_stack.pop() { + if let Some(entry) = self.selection_history.redo_stack.pop_back() { self.set_selections(entry.selections.clone(), None, true, cx); self.select_next_state = entry.select_next_state.clone(); self.add_selections_state = entry.add_selections_state.clone(); From 45ecd8e0a61005da19893bd013fa2d35b45a6c70 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 17:11:35 +0200 Subject: [PATCH 5/7] Always use square brackets in `marked_text_ranges` Co-Authored-By: Nathan Sobo --- crates/editor/src/display_map.rs | 5 +---- crates/util/src/test.rs | 28 ++++++++++++---------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index ae2c4655a5..2bea851ec2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1081,10 +1081,7 @@ pub mod tests { ); language.set_theme(&theme); - let (text, highlighted_ranges) = marked_text_ranges( - r#"const{} : B = "c [d]""#, - vec![('{', '}'), ('<', '>'), ('[', ']')], - ); + let (text, highlighted_ranges) = marked_text_ranges(r#"const[] [a]: B = "c [d]""#); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index b4cf25274e..252383b347 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -77,22 +77,18 @@ pub fn marked_text(marked_text: &str) -> (String, Vec) { (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) } -pub fn marked_text_ranges( - marked_text: &str, - range_markers: Vec<(char, char)>, -) -> (String, Vec>) { - let mut marker_chars = Vec::new(); - for (start, end) in range_markers.iter() { - marker_chars.push(*start); - marker_chars.push(*end); - } - let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars); - let ranges = range_markers - .iter() - .map(|(start_marker, end_marker)| { - let start = markers.get(start_marker).unwrap()[0]; - let end = markers.get(end_marker).unwrap()[0]; - start..end +pub fn marked_text_ranges(marked_text: &str) -> (String, Vec>) { + let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']); + let opens = markers.remove(&'[').unwrap_or_default(); + let closes = markers.remove(&']').unwrap_or_default(); + assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced"); + + let ranges = opens + .into_iter() + .zip(closes) + .map(|(open, close)| { + assert!(close >= open, "marked ranges must be disjoint"); + open..close }) .collect(); (unmarked_text, ranges) From aec82ef71e35989721e1cd16388392c1c1ba18aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 17:20:52 +0200 Subject: [PATCH 6/7] Test selection history Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 40a27752cd..6423a7f72f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6426,7 +6426,7 @@ mod tests { use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; - use util::test::{marked_text_by, sample_text}; + use util::test::{marked_text_by, marked_text_ranges, sample_text}; use workspace::FollowableItem; #[gpui::test] @@ -8221,6 +8221,21 @@ mod tests { view.selected_display_ranges(cx), vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] ); + + view.undo_selection(&UndoSelection, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.redo_selection(&RedoSelection, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); }); view.update(cx, |view, cx| { @@ -8353,6 +8368,36 @@ mod tests { }); } + #[gpui::test] + fn test_select_next(cx: &mut gpui::MutableAppContext) { + populate_settings(cx); + + let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]"); + let buffer = MultiBuffer::build_simple(&text, cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); + + view.update(cx, |view, cx| { + view.select_ranges([ranges[1].start + 1..ranges[1].start + 1], None, cx); + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..2]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..3]); + + view.undo_selection(&UndoSelection, cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..2]); + + view.redo_selection(&RedoSelection, cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..3]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..4]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[0..4]); + }); + } + #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); From f274a6ab4f981e28e8167a727fc064d646cd7c18 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 17:47:14 +0200 Subject: [PATCH 7/7] Avoid unnecessary clones when undoing/redoing selections --- crates/editor/src/editor.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7f771f26e8..f861b6ac60 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4421,9 +4421,9 @@ impl Editor { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Undoing; if let Some(entry) = self.selection_history.undo_stack.pop_back() { - self.set_selections(entry.selections.clone(), None, true, cx); - self.select_next_state = entry.select_next_state.clone(); - self.add_selections_state = entry.add_selections_state.clone(); + self.set_selections(entry.selections, None, true, cx); + self.select_next_state = entry.select_next_state; + self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::Newest, cx); } self.selection_history.mode = SelectionHistoryMode::Normal; @@ -4433,9 +4433,9 @@ impl Editor { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Redoing; if let Some(entry) = self.selection_history.redo_stack.pop_back() { - self.set_selections(entry.selections.clone(), None, true, cx); - self.select_next_state = entry.select_next_state.clone(); - self.add_selections_state = entry.add_selections_state.clone(); + self.set_selections(entry.selections, None, true, cx); + self.select_next_state = entry.select_next_state; + self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::Newest, cx); } self.selection_history.mode = SelectionHistoryMode::Normal;