Highlight matching bracket when newest selection head is on a bracket

Keith Simmons created

Change summary

assets/keymaps/vim.json                         |  1 
crates/editor/src/editor.rs                     |  3 +
crates/editor/src/highlight_matching_bracket.rs | 40 +++++++++++++++++++
crates/vim/src/motion.rs                        | 23 +++++++++
4 files changed, 65 insertions(+), 2 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -37,6 +37,7 @@
                     "ignorePunctuation": true
                 }
             ],
+            "shift-%": "vim::Matching",
             "escape": "editor::Cancel"
         }
     },

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);

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>) {
+    editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+
+    let newest_selection = editor.selections.newest::<usize>(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::<MatchingBracketHighlight>(
+                vec![anchor_range],
+                |theme| theme.editor.document_highlight_read_background,
+                cx,
+            )
+        }
+    }
+}

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
+    }
+}