Improve calculation of which lines are new when auto-indenting

Max Brunsfeld created

Change summary

crates/language/src/buffer.rs | 92 ++++++++++++++++++++----------------
crates/language/src/tests.rs  | 73 ++++++++++++++++++++++++++++-
2 files changed, 121 insertions(+), 44 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -232,7 +232,7 @@ struct SyntaxTree {
 struct AutoindentRequest {
     before_edit: BufferSnapshot,
     edited: Vec<Anchor>,
-    inserted: Option<Vec<Range<Anchor>>>,
+    inserted: Vec<Range<Anchor>>,
     indent_size: IndentSize,
 }
 
@@ -874,9 +874,10 @@ impl Buffer {
                     yield_now().await;
                 }
 
-                if let Some(inserted) = request.inserted.as_ref() {
+                if !request.inserted.is_empty() {
                     let inserted_row_ranges = contiguous_ranges(
-                        inserted
+                        request
+                            .inserted
                             .iter()
                             .map(|range| range.to_point(&snapshot))
                             .flat_map(|range| range.start.row..range.end.row + 1),
@@ -1203,52 +1204,61 @@ impl Buffer {
 
         self.start_transaction();
         self.pending_autoindent.take();
-        let autoindent_request =
-            self.language
-                .as_ref()
-                .and_then(|_| autoindent_size)
-                .map(|autoindent_size| {
-                    let before_edit = self.snapshot();
-                    let edited = edits
-                        .iter()
-                        .filter_map(|(range, new_text)| {
-                            let start = range.start.to_point(self);
-                            if new_text.starts_with('\n')
-                                && start.column == self.line_len(start.row)
-                            {
-                                None
-                            } else {
-                                Some(self.anchor_before(range.start))
-                            }
-                        })
-                        .collect();
-                    (before_edit, edited, autoindent_size)
-                });
+        let autoindent_request = self
+            .language
+            .as_ref()
+            .and_then(|_| autoindent_size)
+            .map(|autoindent_size| (self.snapshot(), autoindent_size));
 
         let edit_operation = self.text.edit(edits.iter().cloned());
         let edit_id = edit_operation.local_timestamp();
 
-        if let Some((before_edit, edited, size)) = autoindent_request {
-            let mut delta = 0isize;
+        if let Some((before_edit, size)) = autoindent_request {
+            let mut inserted = Vec::new();
+            let mut edited = Vec::new();
 
-            let inserted_ranges = edits
+            let mut delta = 0isize;
+            for ((range, _), new_text) in edits
                 .into_iter()
                 .zip(&edit_operation.as_edit().unwrap().new_text)
-                .filter_map(|((range, _), new_text)| {
-                    let first_newline_ix = new_text.find('\n')?;
-                    let new_text_len = new_text.len();
-                    let start = (delta + range.start as isize) as usize + first_newline_ix + 1;
-                    let end = (delta + range.start as isize) as usize + new_text_len;
-                    delta += new_text_len as isize - (range.end as isize - range.start as isize);
-                    Some(self.anchor_before(start)..self.anchor_after(end))
-                })
-                .collect::<Vec<Range<Anchor>>>();
+            {
+                let new_text_len = new_text.len();
+                let first_newline_ix = new_text.find('\n');
+                let old_start = range.start.to_point(&before_edit);
 
-            let inserted = if inserted_ranges.is_empty() {
-                None
-            } else {
-                Some(inserted_ranges)
-            };
+                let start = (delta + range.start as isize) as usize;
+                delta += new_text_len as isize - (range.end as isize - range.start as isize);
+
+                // When inserting multiple lines of text at the beginning of a line,
+                // treat all of the affected lines as newly-inserted.
+                if first_newline_ix.is_some()
+                    && old_start.column < before_edit.indent_size_for_line(old_start.row).len
+                {
+                    inserted
+                        .push(self.anchor_before(start)..self.anchor_after(start + new_text_len));
+                    continue;
+                }
+
+                // When inserting a newline at the end of an existing line, treat the following
+                // line as newly-inserted.
+                if first_newline_ix == Some(0)
+                    && old_start.column == before_edit.line_len(old_start.row)
+                {
+                    inserted.push(
+                        self.anchor_before(start + 1)..self.anchor_after(start + new_text_len),
+                    );
+                    continue;
+                }
+
+                // Otherwise, mark the start of the edit as edited, and any subsequent
+                // lines as newly inserted.
+                edited.push(before_edit.anchor_before(range.start));
+                if let Some(ix) = first_newline_ix {
+                    inserted.push(
+                        self.anchor_before(start + ix + 1)..self.anchor_after(start + new_text_len),
+                    );
+                }
+            }
 
             self.autoindent_requests.push(Arc::new(AutoindentRequest {
                 before_edit,

crates/language/src/tests.rs 🔗

@@ -756,8 +756,18 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
     });
 
     cx.add_model(|cx| {
-        let text = "fn a() {\n    {\n        b()?\n    }\n\n    Ok(())\n}";
+        let text = "
+            fn a() {
+                {
+                    b()?
+                }
+                Ok(())
+            }
+        "
+        .unindent();
         let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+
+        // Delete a closing curly brace changes the suggested indent for the line.
         buffer.edit_with_autoindent(
             [(Point::new(3, 4)..Point::new(3, 5), "")],
             IndentSize::spaces(4),
@@ -765,9 +775,19 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
         );
         assert_eq!(
             buffer.text(),
-            "fn a() {\n    {\n        b()?\n            \n\n    Ok(())\n}"
+            "
+            fn a() {
+                {
+                    b()?
+                        |
+                Ok(())
+            }
+            "
+            .replace("|", "") // included in the string to preserve trailing whites
+            .unindent()
         );
 
+        // Manually editing the leading whitespace
         buffer.edit_with_autoindent(
             [(Point::new(3, 0)..Point::new(3, 12), "")],
             IndentSize::spaces(4),
@@ -775,7 +795,15 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
         );
         assert_eq!(
             buffer.text(),
-            "fn a() {\n    {\n        b()?\n\n\n    Ok(())\n}"
+            "
+            fn a() {
+                {
+                    b()?
+
+                Ok(())
+            }
+            "
+            .unindent()
         );
         buffer
     });
@@ -832,6 +860,45 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) {
+    cx.add_model(|cx| {
+        let text = "
+            const a: usize = 1;
+            fn b() {
+                if c {
+                    let d = 2;
+                }
+            }
+        "
+        .unindent();
+
+        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        buffer.edit_with_autoindent(
+            [(Point::new(3, 0)..Point::new(3, 0), "e(\n    f()\n);\n")],
+            IndentSize::spaces(4),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+                const a: usize = 1;
+                fn b() {
+                    if c {
+                        e(
+                            f()
+                        );
+                        let d = 2;
+                    }
+                }
+            "
+            .unindent()
+        );
+
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_autoindent_disabled(cx: &mut MutableAppContext) {
     cx.add_model(|cx| {