From 9b36e3d0091cad38b9b7bfcae21e0d17ddd95586 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Fri, 1 Jul 2022 15:06:16 -0700 Subject: [PATCH 1/4] Highlight matching bracket when newest selection head is on a bracket --- assets/keymaps/vim.json | 1 + crates/editor/src/editor.rs | 3 ++ .../editor/src/highlight_matching_bracket.rs | 40 +++++++++++++++++++ crates/vim/src/motion.rs | 23 ++++++++++- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 crates/editor/src/highlight_matching_bracket.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 0d2a611d4614f64a24be0584f404f51030ec088b..e8bf95061ce576b2113739aac1b352146bed4aca 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -37,6 +37,7 @@ "ignorePunctuation": true } ], + "shift-%": "vim::Matching", "escape": "editor::Cancel" } }, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9243df0004f3dd16b12643913ae3368044946756..3a17e53559b0d273aef8b11f07fe03cf39733260 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,5 +1,6 @@ pub mod display_map; mod element; +mod highlight_matching_bracket; mod hover_popover; pub mod items; mod link_go_to_definition; @@ -31,6 +32,7 @@ use gpui::{ ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; pub use language::{char_kind, CharKind}; use language::{ @@ -1422,6 +1424,7 @@ impl Editor { } self.refresh_code_actions(cx); self.refresh_document_highlights(cx); + refresh_matching_bracket_highlights(self, cx); } self.pause_cursor_blinking(cx); diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs new file mode 100644 index 0000000000000000000000000000000000000000..3edeb80003f6134e7a11a7340afca8c86f8507cf --- /dev/null +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -0,0 +1,40 @@ +use gpui::ViewContext; + +use crate::Editor; + +enum MatchingBracketHighlight {} + +pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext) { + editor.clear_background_highlights::(cx); + + let newest_selection = editor.selections.newest::(cx); + let snapshot = editor.snapshot(cx); + if let Some((opening_range, closing_range)) = snapshot + .buffer_snapshot + .enclosing_bracket_ranges(newest_selection.range()) + { + let head = newest_selection.head(); + let range_to_highlight = if opening_range.contains(&head) { + Some(closing_range) + } else if closing_range.contains(&head) { + Some(opening_range) + } else { + None + }; + + if let Some(range_to_highlight) = range_to_highlight { + let anchor_range = snapshot + .buffer_snapshot + .anchor_before(range_to_highlight.start) + ..snapshot + .buffer_snapshot + .anchor_after(range_to_highlight.end); + + editor.highlight_background::( + vec![anchor_range], + |theme| theme.editor.document_highlight_read_background, + cx, + ) + } + } +} diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 30c6c78c05d292012c527baf03fd25c3bd611d32..d9e4bf084c69d994c6655b1bae01b952da160cdb 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -30,6 +30,7 @@ pub enum Motion { EndOfLine, StartOfDocument, EndOfDocument, + Matching, } #[derive(Clone, Deserialize, PartialEq)] @@ -65,7 +66,8 @@ actions!( EndOfLine, CurrentLine, StartOfDocument, - EndOfDocument + EndOfDocument, + Matching, ] ); impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]); @@ -85,6 +87,7 @@ pub fn init(cx: &mut MutableAppContext) { motion(Motion::StartOfDocument, cx) }); cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx)); + cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx)); cx.add_action( |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| { @@ -136,7 +139,7 @@ impl Motion { } match self { - EndOfLine | NextWordEnd { .. } => true, + EndOfLine | NextWordEnd { .. } | Matching => true, Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false, _ => panic!("Exclusivity not defined for {self:?}"), } @@ -172,6 +175,7 @@ impl Motion { CurrentLine => (end_of_line(map, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point), SelectionGoal::None), EndOfDocument => (end_of_document(map, point), SelectionGoal::None), + Matching => (matching(map, point), SelectionGoal::None), } } @@ -341,3 +345,18 @@ fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { *new_point.column_mut() = point.column(); map.clip_point(new_point, Bias::Left) } + +fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let offset = point.to_offset(map, Bias::Left); + if let Some((open_range, close_range)) = + map.buffer_snapshot.enclosing_bracket_ranges(offset..offset) + { + if open_range.contains(&offset) { + close_range.start.to_display_point(map) + } else { + open_range.start.to_display_point(map) + } + } else { + point + } +} From 1f3dc2f5344e6da097dcc74f80d3ca1431cbcf85 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 5 Jul 2022 15:19:05 -0700 Subject: [PATCH 2/4] highlight both brackets, only when empty selection, and add test --- crates/editor/src/editor.rs | 12 +- .../editor/src/highlight_matching_bracket.rs | 150 +++++++++++++++--- crates/editor/src/multi_buffer.rs | 2 +- crates/editor/src/test.rs | 64 +++++--- crates/util/src/test/assertions.rs | 4 +- crates/util/src/test/marked_text.rs | 17 ++ 6 files changed, 200 insertions(+), 49 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3a17e53559b0d273aef8b11f07fe03cf39733260..6f6d6f3ff0022613310d9927fdc8a194b4d0b098 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5372,7 +5372,7 @@ impl Editor { .map(|h| &h.1); let write_highlights = self .background_highlights - .get(&TypeId::of::()) + .get(&TypeId::of::()) .map(|h| &h.1); let left_position = position.bias_left(buffer); let right_position = position.bias_right(buffer); @@ -10281,3 +10281,13 @@ impl RangeExt for Range { self.start.clone()..=self.end.clone() } } + +trait RangeToAnchorExt { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; +} + +impl RangeToAnchorExt for Range { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range { + snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end) + } +} diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 3edeb80003f6134e7a11a7340afca8c86f8507cf..30bf1091f28ad4ca588e6f63494e297abaebd1e2 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,6 +1,6 @@ use gpui::ViewContext; -use crate::Editor; +use crate::{Editor, RangeToAnchorExt}; enum MatchingBracketHighlight {} @@ -8,33 +8,135 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon editor.clear_background_highlights::(cx); let newest_selection = editor.selections.newest::(cx); + // Don't highlight brackets if the selection isn't empty + if !newest_selection.is_empty() { + return; + } + + let head = newest_selection.head(); let snapshot = editor.snapshot(cx); if let Some((opening_range, closing_range)) = snapshot .buffer_snapshot - .enclosing_bracket_ranges(newest_selection.range()) + .enclosing_bracket_ranges(head..head) { - let head = newest_selection.head(); - let range_to_highlight = if opening_range.contains(&head) { - Some(closing_range) - } else if closing_range.contains(&head) { - Some(opening_range) - } else { - None - }; - - if let Some(range_to_highlight) = range_to_highlight { - let anchor_range = snapshot - .buffer_snapshot - .anchor_before(range_to_highlight.start) - ..snapshot - .buffer_snapshot - .anchor_after(range_to_highlight.end); - - editor.highlight_background::( - vec![anchor_range], - |theme| theme.editor.document_highlight_read_background, - cx, + editor.highlight_background::( + vec![ + opening_range.to_anchors(&snapshot.buffer_snapshot), + closing_range.to_anchors(&snapshot.buffer_snapshot), + ], + |theme| theme.editor.document_highlight_read_background, + cx, + ) + } +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + + use language::{BracketPair, Language, LanguageConfig}; + + use crate::test::EditorLspTestContext; + + use super::*; + + #[gpui::test] + async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), ) - } + .with_brackets_query(indoc! {r#" + ("{" @open "}" @close) + ("(" @open ")" @close) + "#}) + .unwrap(), + Default::default(), + cx, + ) + .await; + + // positioning cursor inside bracket highlights both + cx.set_state_by( + vec!['|'.into()], + indoc! {r#" + pub fn test("Test |argument") { + another_test(1, 2, 3); + }"#}, + ); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test[(]"Test argument"[)] { + another_test(1, 2, 3); + }"#}); + + cx.set_state_by( + vec!['|'.into()], + indoc! {r#" + pub fn test("Test argument") { + another_test(1, |2, 3); + }"#}, + ); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test[(]1, 2, 3[)]; + }"#}); + + cx.set_state_by( + vec!['|'.into()], + indoc! {r#" + pub fn test("Test argument") { + another|_test(1, 2, 3); + }"#}, + ); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") [{] + another_test(1, 2, 3); + [}]"#}); + + // positioning outside of brackets removes highlight + cx.set_state_by( + vec!['|'.into()], + indoc! {r#" + pub f|n test("Test argument") { + another_test(1, 2, 3); + }"#}, + ); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test(1, 2, 3); + }"#}); + + // non empty selection dismisses highlight + // positioning outside of brackets removes highlight + cx.set_state_by( + vec![('<', '>').into()], + indoc! {r#" + pub fn test("Teument") { + another_test(1, 2, 3); + }"#}, + ); + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { + another_test(1, 2, 3); + }"#}); } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 51bed91fc6db35230327d89512d5afb9df67477f..5f069d223f8eb643a97789d802830a73ce05b70e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2321,7 +2321,7 @@ impl MultiBufferSnapshot { .enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?; if start_bracket_range.start >= excerpt_buffer_start - && end_bracket_range.end < excerpt_buffer_end + && end_bracket_range.end <= excerpt_buffer_end { start_bracket_range.start = cursor.start() + (start_bracket_range.start - excerpt_buffer_start); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 91480ca6a9d4d0a5dfcd0079e45e76c88294dd84..d1316a85a0bf38afe5c39cb596b6647b89119252 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,4 +1,5 @@ use std::{ + any::TypeId, ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -13,7 +14,7 @@ use project::Project; use settings::Settings; use util::{ assert_set_eq, set_eq, - test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError}, + test::{marked_text, marked_text_ranges, marked_text_ranges_by, SetEqError, TextRangeMarker}, }; use workspace::{pane, AppState, Workspace, WorkspaceHandle}; @@ -159,29 +160,30 @@ impl<'a> EditorTestContext<'a> { // `[` to `}` represents a non empty selection with the head at `}` // `{` to `]` represents a non empty selection with the head at `{` pub fn set_state(&mut self, text: &str) { + self.set_state_by( + vec![ + '|'.into(), + ('[', '}').into(), + TextRangeMarker::ReverseRange('{', ']'), + ], + text, + ); + } + + pub fn set_state_by(&mut self, range_markers: Vec, text: &str) { self.editor.update(self.cx, |editor, cx| { - let (unmarked_text, mut selection_ranges) = marked_text_ranges_by( - &text, - vec!['|'.into(), ('[', '}').into(), ('{', ']').into()], - ); + let (unmarked_text, selection_ranges) = marked_text_ranges_by(&text, range_markers); editor.set_text(unmarked_text, cx); - let mut selections: Vec> = - selection_ranges.remove(&'|'.into()).unwrap_or_default(); - selections.extend( - selection_ranges - .remove(&('{', ']').into()) - .unwrap_or_default() - .into_iter() - .map(|range| range.end..range.start), - ); - selections.extend( - selection_ranges - .remove(&('[', '}').into()) - .unwrap_or_default(), - ); - - editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.select_ranges(selections)); + let selection_ranges: Vec> = selection_ranges + .values() + .into_iter() + .flatten() + .cloned() + .collect(); + editor.change_selections(Some(Autoscroll::Fit), cx, |s| { + s.select_ranges(selection_ranges) + }) }) } @@ -216,6 +218,26 @@ impl<'a> EditorTestContext<'a> { ) } + pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { + let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); + assert_eq!(unmarked, self.buffer_text()); + + let asserted_ranges = ranges.remove(&('[', ']').into()).unwrap(); + let actual_ranges: Vec> = self.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .background_highlights + .get(&TypeId::of::()) + .map(|h| h.1.clone()) + .unwrap_or_default() + .into_iter() + .map(|range| range.to_offset(&snapshot.buffer_snapshot)) + .collect() + }); + + assert_set_eq!(asserted_ranges, actual_ranges); + } + pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); assert_eq!(unmarked, self.buffer_text()); diff --git a/crates/util/src/test/assertions.rs b/crates/util/src/test/assertions.rs index c393104ae3f7ba8da5dcd1d2b3aded32a380eb7d..afb1397fa9ea770c3fd37a460d33ad6b3fb99934 100644 --- a/crates/util/src/test/assertions.rs +++ b/crates/util/src/test/assertions.rs @@ -51,10 +51,10 @@ macro_rules! assert_set_eq { match set_eq!(&left, &right) { Err(SetEqError::LeftMissing(missing)) => { - panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing); + panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing); }, Err(SetEqError::RightMissing(missing)) => { - panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing); + panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing); }, _ => {} } diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index 733feeb3f8e6739607b304d3c3b2d82c963cfe97..3428728fabb3e6159f1170ac8fed66c511c513ca 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -28,6 +28,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec) { pub enum TextRangeMarker { Empty(char), Range(char, char), + ReverseRange(char, char), } impl TextRangeMarker { @@ -35,6 +36,7 @@ impl TextRangeMarker { match self { Self::Empty(m) => vec![*m], Self::Range(l, r) => vec![*l, *r], + Self::ReverseRange(l, r) => vec![*l, *r], } } } @@ -85,6 +87,21 @@ pub fn marked_text_ranges_by( .collect::>>(); (marker, ranges) } + TextRangeMarker::ReverseRange(start_marker, end_marker) => { + let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); + let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); + assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); + + let ranges = starts + .into_iter() + .zip(ends) + .map(|(start, end)| { + assert!(start >= end, "marked ranges must be disjoint"); + end..start + }) + .collect::>>(); + (marker, ranges) + } }) .collect(); From 956dd0c2bc4a430094fb60bb8210c9bbac510d36 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 5 Jul 2022 16:44:45 -0700 Subject: [PATCH 3/4] fix error with reverse range marked text ranges --- crates/util/src/test/marked_text.rs | 2 +- crates/vim/src/normal.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index 3428728fabb3e6159f1170ac8fed66c511c513ca..4529c8c803d35017e969220a3bccc43e8894affc 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -96,7 +96,7 @@ pub fn marked_text_ranges_by( .into_iter() .zip(ends) .map(|(start, end)| { - assert!(start >= end, "marked ranges must be disjoint"); + assert!(end >= start, "marked ranges must be disjoint"); end..start }) .collect::>>(); diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 4c6dfd2d608cb447dd7fdedf73131f90a4fafc69..0f5aca7965bff9ee6a2ada12017a5c32a919caf8 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1181,4 +1181,7 @@ mod test { fox jumps|jumps over the lazy dog"}); } + + #[gpui::test] + async fn test_ } From 229bc94ac3b290f1335ec6d18f801bc5980bfc7b Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 5 Jul 2022 16:48:12 -0700 Subject: [PATCH 4/4] remove partial edit from normal.rs --- crates/vim/src/normal.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 0f5aca7965bff9ee6a2ada12017a5c32a919caf8..4c6dfd2d608cb447dd7fdedf73131f90a4fafc69 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1181,7 +1181,4 @@ mod test { fox jumps|jumps over the lazy dog"}); } - - #[gpui::test] - async fn test_ }