Delete the autoclosing bracket when deleting the opening bracket

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs | 249 +++++++++++++++++++++++++++++++-------
1 file changed, 198 insertions(+), 51 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2132,6 +2132,41 @@ impl Editor {
         }
     }
 
+    fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let old_selections = self.selections.all::<usize>(cx);
+        let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() {
+            autoclose_pair
+        } else {
+            return false;
+        };
+
+        debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len());
+
+        let mut new_selections = Vec::new();
+        for (selection, autoclose_range) in old_selections
+            .iter()
+            .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer)))
+        {
+            if selection.is_empty() && autoclose_range.is_empty() && selection.start == autoclose_range.start {
+                new_selections.push(Selection {
+                    id: selection.id,
+                    start: selection.start - autoclose_pair.pair.start.len(),
+                    end: selection.end + autoclose_pair.pair.end.len(),
+                    reversed: true,
+                    goal: selection.goal,
+                });
+            } else {
+                return false;
+            }
+        }
+
+        self.change_selections(Some(Autoscroll::Fit), cx, |selections| {
+            selections.select(new_selections)
+        });
+        true
+    }
+
     fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
         let offset = position.to_offset(buffer);
         let (word_range, kind) = buffer.surrounding_word(offset);
@@ -2776,46 +2811,52 @@ impl Editor {
     }
 
     pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let mut selections = self.selections.all::<Point>(cx);
-        if !self.selections.line_mode {
-            for selection in &mut selections {
-                if selection.is_empty() {
-                    let old_head = selection.head();
-                    let mut new_head =
-                        movement::left(&display_map, old_head.to_display_point(&display_map))
+        self.transact(cx, |this, cx| {
+            if !this.select_autoclose_pair(cx) {
+                let mut selections = this.selections.all::<Point>(cx);
+                if !this.selections.line_mode {
+                    let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+                    for selection in &mut selections {
+                        if selection.is_empty() {
+                            let old_head = selection.head();
+                            let mut new_head = movement::left(
+                                &display_map,
+                                old_head.to_display_point(&display_map),
+                            )
                             .to_point(&display_map);
-                    if let Some((buffer, line_buffer_range)) = display_map
-                        .buffer_snapshot
-                        .buffer_line_for_row(old_head.row)
-                    {
-                        let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row);
-                        let language_name = buffer.language().map(|language| language.name());
-                        let indent_len = match indent_size.kind {
-                            IndentKind::Space => {
-                                cx.global::<Settings>().tab_size(language_name.as_deref())
+                            if let Some((buffer, line_buffer_range)) = display_map
+                                .buffer_snapshot
+                                .buffer_line_for_row(old_head.row)
+                            {
+                                let indent_size =
+                                    buffer.indent_size_for_line(line_buffer_range.start.row);
+                                let language_name =
+                                    buffer.language().map(|language| language.name());
+                                let indent_len = match indent_size.kind {
+                                    IndentKind::Space => {
+                                        cx.global::<Settings>().tab_size(language_name.as_deref())
+                                    }
+                                    IndentKind::Tab => NonZeroU32::new(1).unwrap(),
+                                };
+                                if old_head.column <= indent_size.len && old_head.column > 0 {
+                                    let indent_len = indent_len.get();
+                                    new_head = cmp::min(
+                                        new_head,
+                                        Point::new(
+                                            old_head.row,
+                                            ((old_head.column - 1) / indent_len) * indent_len,
+                                        ),
+                                    );
+                                }
                             }
-                            IndentKind::Tab => NonZeroU32::new(1).unwrap(),
-                        };
-                        if old_head.column <= indent_size.len && old_head.column > 0 {
-                            let indent_len = indent_len.get();
-                            new_head = cmp::min(
-                                new_head,
-                                Point::new(
-                                    old_head.row,
-                                    ((old_head.column - 1) / indent_len) * indent_len,
-                                ),
-                            );
+
+                            selection.set_head(new_head, SelectionGoal::None);
                         }
                     }
-
-                    selection.set_head(new_head, SelectionGoal::None);
                 }
-            }
-        }
 
-        self.transact(cx, |this, cx| {
-            this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
+                this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
+            }
             this.insert("", cx);
         });
     }
@@ -3749,15 +3790,17 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                let line_mode = s.line_mode;
-                s.move_with(|map, selection| {
-                    if selection.is_empty() && !line_mode {
-                        let cursor = movement::previous_word_start(map, selection.head());
-                        selection.set_head(cursor, SelectionGoal::None);
-                    }
+            if !this.select_autoclose_pair(cx) {
+                this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    let line_mode = s.line_mode;
+                    s.move_with(|map, selection| {
+                        if selection.is_empty() && !line_mode {
+                            let cursor = movement::previous_word_start(map, selection.head());
+                            selection.set_head(cursor, SelectionGoal::None);
+                        }
+                    });
                 });
-            });
+            }
             this.insert("", cx);
         });
     }
@@ -3768,15 +3811,17 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                let line_mode = s.line_mode;
-                s.move_with(|map, selection| {
-                    if selection.is_empty() && !line_mode {
-                        let cursor = movement::previous_subword_start(map, selection.head());
-                        selection.set_head(cursor, SelectionGoal::None);
-                    }
+            if !this.select_autoclose_pair(cx) {
+                this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                    let line_mode = s.line_mode;
+                    s.move_with(|map, selection| {
+                        if selection.is_empty() && !line_mode {
+                            let cursor = movement::previous_subword_start(map, selection.head());
+                            selection.set_head(cursor, SelectionGoal::None);
+                        }
+                    });
                 });
-            });
+            }
             this.insert("", cx);
         });
     }
@@ -8964,7 +9009,7 @@ mod tests {
             a
             b
             c
-            "#
+        "#
         .unindent();
 
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
@@ -9024,6 +9069,108 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
+        let language = Arc::new(Language::new(
+            LanguageConfig {
+                brackets: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                autoclose_before: "}".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        ));
+
+        let text = r#"
+            a
+            b
+            c
+        "#
+        .unindent();
+
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+        editor
+            .condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+            .await;
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    Point::new(0, 1)..Point::new(0, 1),
+                    Point::new(1, 1)..Point::new(1, 1),
+                    Point::new(2, 1)..Point::new(2, 1),
+                ])
+            });
+
+            editor.handle_input(&Input("{".to_string()), cx);
+            editor.handle_input(&Input("{".to_string()), cx);
+            editor.handle_input(&Input("_".to_string()), cx);
+            assert_eq!(
+                editor.text(cx),
+                "
+                a{{_}}
+                b{{_}}
+                c{{_}}
+                "
+                .unindent()
+            );
+            assert_eq!(
+                editor.selections.ranges::<Point>(cx),
+                [
+                    Point::new(0, 4)..Point::new(0, 4),
+                    Point::new(1, 4)..Point::new(1, 4),
+                    Point::new(2, 4)..Point::new(2, 4)
+                ]
+            );
+
+            editor.backspace(&Default::default(), cx);
+            editor.backspace(&Default::default(), cx);
+            assert_eq!(
+                editor.text(cx),
+                "
+                a{}
+                b{}
+                c{}
+                "
+                .unindent()
+            );
+            assert_eq!(
+                editor.selections.ranges::<Point>(cx),
+                [
+                    Point::new(0, 2)..Point::new(0, 2),
+                    Point::new(1, 2)..Point::new(1, 2),
+                    Point::new(2, 2)..Point::new(2, 2)
+                ]
+            );
+
+            editor.delete_to_previous_word_start(&Default::default(), cx);
+            assert_eq!(
+                editor.text(cx),
+                "
+                a
+                b
+                c
+                "
+                .unindent()
+            );
+            assert_eq!(
+                editor.selections.ranges::<Point>(cx),
+                [
+                    Point::new(0, 1)..Point::new(0, 1),
+                    Point::new(1, 1)..Point::new(1, 1),
+                    Point::new(2, 1)..Point::new(2, 1)
+                ]
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_snippets(cx: &mut gpui::TestAppContext) {
         cx.update(|cx| cx.set_global(Settings::test(cx)));