From 345fad3e9d522d0e1c8c91674606ca8c5e20de37 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:32:34 +0200 Subject: [PATCH] editor: add select previous command (#2556) Added a `select previous` command to complement `select next`. Release Notes: - Added "Select previous" editor command, mirroring `Select next`. Ticket number: Z-366 --- assets/keymaps/atom.json | 6 ++ assets/keymaps/default.json | 12 ++++ assets/keymaps/jetbrains.json | 6 ++ crates/editor/src/editor.rs | 110 ++++++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 51 ++++++++++++++ crates/editor/src/multi_buffer.rs | 87 ++++++++++++++++++++++- crates/rope/src/rope.rs | 48 ++++++++++--- crates/text/src/text.rs | 6 ++ 8 files changed, 316 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 634aed322a..25143914cc 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -16,6 +16,12 @@ "replace_newest": true } ], + "ctrl-cmd-g": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], "ctrl-shift-down": "editor::AddSelectionBelow", "ctrl-shift-up": "editor::AddSelectionAbove", "cmd-shift-backspace": "editor::DeleteToBeginningOfLine", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7e1a8429bf..46a3fb5a5a 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -250,12 +250,24 @@ "replace_newest": false } ], + "ctrl-cmd-d": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], "cmd-k cmd-d": [ "editor::SelectNext", { "replace_newest": true } ], + "cmd-k ctrl-cmd-d": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], "cmd-k cmd-i": "editor::Hover", "cmd-/": [ "editor::ToggleComments", diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 4825d3e8b5..b3e8f989a4 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -26,6 +26,12 @@ "replace_newest": false } ], + "ctrl-cmd-g": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], "cmd-/": [ "editor::ToggleComments", { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5c19bb0121..af7344a91f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,6 +111,12 @@ pub struct SelectNext { pub replace_newest: bool, } +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct SelectPrevious { + #[serde(default)] + pub replace_newest: bool, +} + #[derive(Clone, Deserialize, PartialEq)] pub struct SelectToBeginningOfLine { #[serde(default)] @@ -272,6 +278,7 @@ impl_actions!( editor, [ SelectNext, + SelectPrevious, SelectToBeginningOfLine, SelectToEndOfLine, ToggleCodeActions, @@ -367,6 +374,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::add_selection_above); cx.add_action(Editor::add_selection_below); cx.add_action(Editor::select_next); + cx.add_action(Editor::select_previous); cx.add_action(Editor::toggle_comments); cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); @@ -484,6 +492,7 @@ pub struct Editor { columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, + select_prev_state: Option, selection_history: SelectionHistory, autoclose_regions: Vec, snippet_stack: InvalidationStack, @@ -539,6 +548,7 @@ pub struct EditorSnapshot { struct SelectionHistoryEntry { selections: Arc<[Selection]>, select_next_state: Option, + select_prev_state: Option, add_selections_state: Option, } @@ -1286,6 +1296,7 @@ impl Editor { columnar_selection_tail: None, add_selections_state: None, select_next_state: None, + select_prev_state: None, selection_history: Default::default(), autoclose_regions: Default::default(), snippet_stack: Default::default(), @@ -1507,6 +1518,7 @@ impl Editor { let buffer = &display_map.buffer_snapshot; self.add_selections_state = None; self.select_next_state = None; + self.select_prev_state = None; self.select_larger_syntax_node_stack.clear(); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.snippet_stack @@ -5213,6 +5225,101 @@ impl Editor { } } + pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext) { + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_prev_state) = self.select_prev_state.take() { + let query = &select_prev_state.query; + if !select_prev_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. + let bytes_before_last_selection = + buffer.reversed_bytes_in_range(0..last_selection.start); + let bytes_after_first_selection = + buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); + let query_matches = query + .stream_find_iter(bytes_before_last_selection) + .map(|result| (last_selection.start, result)) + .chain( + query + .stream_find_iter(bytes_after_first_selection) + .map(|result| (buffer.len(), result)), + ); + for (end_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + end_offset - query_match.end()..end_offset - query_match.start(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_prev_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + self.unfold_ranges([next_selected_range.clone()], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + if action.replace_newest { + s.delete(s.newest_anchor().id); + } + s.insert_range(next_selected_range); + }); + } else { + select_prev_state.done = true; + } + } + + self.select_prev_state = Some(select_prev_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + let select_state = SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: true, + done: false, + }; + self.unfold_ranges([selection.start..selection.end], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select(selections); + }); + self.select_prev_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + self.select_prev_state = Some(SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: false, + done: false, + }); + self.select_previous(action, cx); + } + } + } + pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); @@ -5586,6 +5693,7 @@ impl Editor { if let Some(entry) = self.selection_history.undo_stack.pop_back() { self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::newest(), cx); } @@ -5598,6 +5706,7 @@ impl Editor { if let Some(entry) = self.selection_history.redo_stack.pop_back() { self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::newest(), cx); } @@ -6375,6 +6484,7 @@ impl Editor { self.selection_history.push(SelectionHistoryEntry { selections: self.selections.disjoint_anchors(), select_next_state: self.select_next_state.clone(), + select_prev_state: self.select_prev_state.clone(), add_selections_state: self.add_selections_state.clone(), }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index bc671b9ffc..969b9c882d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3107,6 +3107,57 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } +#[gpui::test] +async fn test_select_previous(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + { + // `Select previous` without a selection (selects wordwise) + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + } + { + // `Select previous` with a selection + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); + } +} + #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index b7b2b89c8c..af0003ec2d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -196,6 +196,13 @@ pub struct MultiBufferBytes<'a> { chunk: &'a [u8], } +pub struct ReversedMultiBufferBytes<'a> { + range: Range, + excerpts: Cursor<'a, Excerpt, usize>, + excerpt_bytes: Option>, + chunk: &'a [u8], +} + struct ExcerptChunks<'a> { content_chunks: BufferChunks<'a>, footer_height: usize, @@ -1967,7 +1974,6 @@ impl MultiBufferSnapshot { } else { None }; - MultiBufferBytes { range, excerpts, @@ -1976,6 +1982,33 @@ impl MultiBufferSnapshot { } } + pub fn reversed_bytes_in_range( + &self, + range: Range, + ) -> ReversedMultiBufferBytes { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut excerpts = self.excerpts.cursor::(); + excerpts.seek(&range.end, Bias::Left, &()); + + let mut chunk = &[][..]; + let excerpt_bytes = if let Some(excerpt) = excerpts.item() { + let mut excerpt_bytes = excerpt.reversed_bytes_in_range( + range.start - excerpts.start()..range.end - excerpts.start(), + ); + chunk = excerpt_bytes.next().unwrap_or(&[][..]); + Some(excerpt_bytes) + } else { + None + }; + + ReversedMultiBufferBytes { + range, + excerpts, + excerpt_bytes, + chunk, + } + } + pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { let mut result = MultiBufferRows { buffer_row_range: 0..0, @@ -3409,6 +3442,26 @@ impl Excerpt { } } + fn reversed_bytes_in_range(&self, range: Range) -> ExcerptBytes { + let content_start = self.range.context.start.to_offset(&self.buffer); + let bytes_start = content_start + range.start; + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); + let footer_height = if self.has_trailing_newline + && range.start <= self.text_summary.len + && range.end > self.text_summary.len + { + 1 + } else { + 0 + }; + let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end); + + ExcerptBytes { + content_bytes, + footer_height, + } + } + fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { if text_anchor .cmp(&self.range.context.start, &self.buffer) @@ -3727,6 +3780,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> { } } +impl<'a> ReversedMultiBufferBytes<'a> { + fn consume(&mut self, len: usize) { + self.range.end -= len; + self.chunk = &self.chunk[..self.chunk.len() - len]; + + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { + self.chunk = chunk; + } else { + self.excerpts.next(&()); + if let Some(excerpt) = self.excerpts.item() { + let mut excerpt_bytes = + excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); + self.chunk = excerpt_bytes.next().unwrap(); + self.excerpt_bytes = Some(excerpt_bytes); + } + } + } + } +} + +impl<'a> io::Read for ReversedMultiBufferBytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let len = cmp::min(buf.len(), self.chunk.len()); + buf[..len].copy_from_slice(&self.chunk[..len]); + buf[..len].reverse(); + if len > 0 { + self.consume(len); + } + Ok(len) + } +} impl<'a> Iterator for ExcerptBytes<'a> { type Item = &'a [u8]; diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 797fb39317..6b6f364fdb 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -179,7 +179,11 @@ impl Rope { } pub fn bytes_in_range(&self, range: Range) -> Bytes { - Bytes::new(self, range) + Bytes::new(self, range, false) + } + + pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, true) } pub fn chunks(&self) -> Chunks { @@ -579,22 +583,33 @@ impl<'a> Iterator for Chunks<'a> { pub struct Bytes<'a> { chunks: sum_tree::Cursor<'a, Chunk, usize>, range: Range, + reversed: bool, } impl<'a> Bytes<'a> { - pub fn new(rope: &'a Rope, range: Range) -> Self { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { let mut chunks = rope.chunks.cursor(); - chunks.seek(&range.start, Bias::Right, &()); - Self { chunks, range } + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } } pub fn peek(&self) -> Option<&'a [u8]> { let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } let chunk_start = *self.chunks.start(); if self.range.end <= chunk_start { return None; } - let start = self.range.start.saturating_sub(chunk_start); let end = self.range.end - chunk_start; Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)]) @@ -607,7 +622,11 @@ impl<'a> Iterator for Bytes<'a> { fn next(&mut self) -> Option { let result = self.peek(); if result.is_some() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } result } @@ -617,10 +636,21 @@ impl<'a> io::Read for Bytes<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if let Some(chunk) = self.peek() { let len = cmp::min(buf.len(), chunk.len()); - buf[..len].copy_from_slice(&chunk[..len]); - self.range.start += len; + if self.reversed { + buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); + buf[..len].reverse(); + self.range.end -= len; + } else { + buf[..len].copy_from_slice(&chunk[..len]); + self.range.start += len; + } + if len == chunk.len() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } Ok(len) } else { diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index dcfaf818d1..2693add8ed 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1749,6 +1749,12 @@ impl BufferSnapshot { self.visible_text.bytes_in_range(start..end) } + pub fn reversed_bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.reversed_bytes_in_range(start..end) + } + pub fn text_for_range(&self, range: Range) -> Chunks<'_> { let start = range.start.to_offset(self); let end = range.end.to_offset(self);