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/editor/src/editor.rs b/crates/editor/src/editor.rs index 84f2cf56d8..f861b6ac60 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); @@ -121,6 +122,8 @@ action!(ToggleComments); action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); +action!(UndoSelection); +action!(RedoSelection); action!(GoToDiagnostic, Direction); action!(GoToDefinition); action!(FindAllReferences); @@ -283,6 +286,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")), @@ -360,6 +365,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); @@ -459,8 +466,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]>>, @@ -512,11 +518,105 @@ 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: VecDeque, + redo_stack: VecDeque, +} + +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) + } + + 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 + .back() + .map_or(true, |e| e.selections != entry.selections) + { + 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 + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } + } + } +} + +#[derive(Clone)] struct AddSelectionsState { above: bool, stack: Vec, } +#[derive(Clone)] struct SelectNextState { query: AhoCorasick, wordwise: bool, @@ -3463,7 +3563,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); @@ -3473,7 +3573,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); @@ -3937,6 +4038,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(|| { @@ -4023,13 +4125,14 @@ 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); } } 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; @@ -4314,6 +4417,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_back() { + 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; + } + + 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_back() { + 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; + } + pub fn go_to_diagnostic( &mut self, &GoToDiagnostic(direction): &GoToDiagnostic, @@ -5212,6 +5339,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() { @@ -5277,6 +5405,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(); @@ -5304,7 +5440,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()); } } @@ -5313,7 +5449,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"); @@ -6315,7 +6451,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, ItemHandle}; #[gpui::test] @@ -8110,6 +8246,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| { @@ -8242,6 +8393,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); 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)