Merge pull request #577 from zed-industries/backspace-indent

Antonio Scandurra created

Delete till previous tabstop when backspacing within indent column

Change summary

crates/editor/src/editor.rs       | 55 +++++++++++++++++++++++++-------
crates/editor/src/multi_buffer.rs |  2 
crates/language/src/buffer.rs     | 34 +++++++++++++++-----
3 files changed, 68 insertions(+), 23 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2638,11 +2638,25 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         for selection in &mut selections {
             if selection.is_empty() {
-                let head = selection.head().to_display_point(&display_map);
-                let cursor = movement::left(&display_map, head)
-                    .unwrap()
-                    .to_point(&display_map);
-                selection.set_head(cursor);
+                let old_head = selection.head();
+                let (buffer, line_buffer_range) = display_map
+                    .buffer_snapshot
+                    .buffer_line_for_row(old_head.row)
+                    .unwrap();
+                let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
+                let mut new_head =
+                    movement::left(&display_map, old_head.to_display_point(&display_map))
+                        .unwrap()
+                        .to_point(&display_map);
+                if old_head.column <= indent_column && old_head.column > 0 {
+                    let indent = buffer.indent_size();
+                    new_head = cmp::min(
+                        new_head,
+                        Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
+                    );
+                }
+
+                selection.set_head(new_head);
                 selection.goal = SelectionGoal::None;
             }
         }
@@ -7153,14 +7167,13 @@ mod tests {
 
     #[gpui::test]
     fn test_backspace(cx: &mut gpui::MutableAppContext) {
-        let buffer =
-            MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
         let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            build_editor(buffer.clone(), settings, cx)
+            build_editor(MultiBuffer::build_simple("", cx), settings, cx)
         });
 
         view.update(cx, |view, cx| {
+            view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx);
             view.select_display_ranges(
                 &[
                     // an empty selection - the preceding character is deleted
@@ -7173,12 +7186,28 @@ mod tests {
                 cx,
             );
             view.backspace(&Backspace, cx);
-        });
+            assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n");
 
-        assert_eq!(
-            buffer.read(cx).read(cx).text(),
-            "oe two three\nfou five six\nseven ten\n"
-        );
+            view.set_text("    one\n        two\n        three\n   four", cx);
+            view.select_display_ranges(
+                &[
+                    // cursors at the the end of leading indent - last indent is deleted
+                    DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
+                    DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8),
+                    // cursors inside leading indent - overlapping indent deletions are coalesced
+                    DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
+                    DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                    DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6),
+                    // cursor at the beginning of a line - preceding newline is deleted
+                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+                    // selection inside leading indent - only the selected character is deleted
+                    DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3),
+                ],
+                cx,
+            );
+            view.backspace(&Backspace, cx);
+            assert_eq!(view.text(cx), "one\n    two\n  three  four");
+        });
     }
 
     #[gpui::test]

crates/editor/src/multi_buffer.rs 🔗

@@ -1657,7 +1657,7 @@ impl MultiBufferSnapshot {
         }
     }
 
-    fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
+    pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
         let mut cursor = self.excerpts.cursor::<Point>();
         cursor.seek(&Point::new(row, 0), Bias::Right, &());
         if let Some(excerpt) = cursor.item() {

crates/language/src/buffer.rs 🔗

@@ -47,9 +47,6 @@ lazy_static! {
     static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
 }
 
-// TODO - Make this configurable
-const INDENT_SIZE: u32 = 4;
-
 pub struct Buffer {
     text: TextBuffer,
     file: Option<Box<dyn File>>,
@@ -70,6 +67,7 @@ pub struct Buffer {
     file_update_count: usize,
     completion_triggers: Vec<String>,
     deferred_ops: OperationQueue<Operation>,
+    indent_size: u32,
 }
 
 pub struct BufferSnapshot {
@@ -81,9 +79,9 @@ pub struct BufferSnapshot {
     file_update_count: usize,
     remote_selections: TreeMap<ReplicaId, SelectionSet>,
     selections_update_count: usize,
-    is_parsing: bool,
     language: Option<Arc<Language>>,
     parse_count: usize,
+    indent_size: u32,
 }
 
 #[derive(Clone, Debug)]
@@ -416,6 +414,8 @@ impl Buffer {
             file_update_count: 0,
             completion_triggers: Default::default(),
             deferred_ops: OperationQueue::new(),
+            // TODO: make this configurable
+            indent_size: 4,
         }
     }
 
@@ -428,10 +428,10 @@ impl Buffer {
             diagnostics: self.diagnostics.clone(),
             diagnostics_update_count: self.diagnostics_update_count,
             file_update_count: self.file_update_count,
-            is_parsing: self.parsing_in_background,
             language: self.language.clone(),
             parse_count: self.parse_count,
             selections_update_count: self.selections_update_count,
+            indent_size: self.indent_size,
         }
     }
 
@@ -768,7 +768,11 @@ impl Buffer {
                                     .before_edit
                                     .indent_column_for_line(suggestion.basis_row)
                             });
-                        let delta = if suggestion.indent { INDENT_SIZE } else { 0 };
+                        let delta = if suggestion.indent {
+                            snapshot.indent_size
+                        } else {
+                            0
+                        };
                         old_suggestions.insert(
                             *old_to_new_rows.get(&old_row).unwrap(),
                             indentation_basis + delta,
@@ -787,7 +791,11 @@ impl Buffer {
                         .into_iter()
                         .flatten();
                     for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
-                        let delta = if suggestion.indent { INDENT_SIZE } else { 0 };
+                        let delta = if suggestion.indent {
+                            snapshot.indent_size
+                        } else {
+                            0
+                        };
                         let new_indentation = indent_columns
                             .get(&suggestion.basis_row)
                             .copied()
@@ -819,7 +827,11 @@ impl Buffer {
                             .into_iter()
                             .flatten();
                         for (row, suggestion) in inserted_row_range.zip(suggestions) {
-                            let delta = if suggestion.indent { INDENT_SIZE } else { 0 };
+                            let delta = if suggestion.indent {
+                                snapshot.indent_size
+                            } else {
+                                0
+                            };
                             let new_indentation = indent_columns
                                 .get(&suggestion.basis_row)
                                 .copied()
@@ -1868,6 +1880,10 @@ impl BufferSnapshot {
     pub fn file_update_count(&self) -> usize {
         self.file_update_count
     }
+
+    pub fn indent_size(&self) -> u32 {
+        self.indent_size
+    }
 }
 
 impl Clone for BufferSnapshot {
@@ -1881,9 +1897,9 @@ impl Clone for BufferSnapshot {
             selections_update_count: self.selections_update_count,
             diagnostics_update_count: self.diagnostics_update_count,
             file_update_count: self.file_update_count,
-            is_parsing: self.is_parsing,
             language: self.language.clone(),
             parse_count: self.parse_count,
+            indent_size: self.indent_size,
         }
     }
 }