Fix auto-indent when pasting multi-line content that was copied start… (#26246)

Max Brunsfeld created

Closes https://github.com/zed-industries/zed/issues/24914 (again)

Release Notes:

- Fixed an issue where multi-line pasted content was auto-indented
incorrectly if copied from the middle of an existing line.

Change summary

crates/assistant_context_editor/src/patch.rs |  2 
crates/editor/src/editor.rs                  | 32 +++++++++++----------
crates/editor/src/editor_tests.rs            | 28 +++++++++++++++++++
crates/language/src/buffer.rs                | 30 +++++++++++--------
crates/language/src/buffer_tests.rs          | 12 ++++----
crates/multi_buffer/src/multi_buffer.rs      | 28 +++++++++---------
crates/vim/src/normal/paste.rs               | 18 ++++++------
crates/vim/src/normal/yank.rs                |  2 
8 files changed, 93 insertions(+), 59 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1012,8 +1012,8 @@ pub struct ClipboardSelection {
     pub len: usize,
     /// Whether this was a full-line selection.
     pub is_entire_line: bool,
-    /// The column where this selection originally started.
-    pub start_column: u32,
+    /// The indentation of the first line when this content was originally copied.
+    pub first_line_indent: u32,
 }
 
 #[derive(Debug)]
@@ -2354,7 +2354,7 @@ impl Editor {
     pub fn edit_with_block_indent<I, S, T>(
         &mut self,
         edits: I,
-        original_start_columns: Vec<u32>,
+        original_indent_columns: Vec<Option<u32>>,
         cx: &mut Context<Self>,
     ) where
         I: IntoIterator<Item = (Range<S>, T)>,
@@ -2369,7 +2369,7 @@ impl Editor {
             buffer.edit(
                 edits,
                 Some(AutoindentMode::Block {
-                    original_start_columns,
+                    original_indent_columns,
                 }),
                 cx,
             )
@@ -3480,7 +3480,7 @@ impl Editor {
 
     pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
         let autoindent = text.is_empty().not().then(|| AutoindentMode::Block {
-            original_start_columns: Vec::new(),
+            original_indent_columns: Vec::new(),
         });
         self.insert_with_autoindent_mode(text, autoindent, window, cx);
     }
@@ -8704,7 +8704,9 @@ impl Editor {
                 clipboard_selections.push(ClipboardSelection {
                     len,
                     is_entire_line,
-                    start_column: selection.start.column,
+                    first_line_indent: buffer
+                        .indent_size_for_line(MultiBufferRow(selection.start.row))
+                        .len,
                 });
             }
         }
@@ -8783,7 +8785,7 @@ impl Editor {
                 clipboard_selections.push(ClipboardSelection {
                     len,
                     is_entire_line,
-                    start_column: start.column,
+                    first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
                 });
             }
         }
@@ -8813,8 +8815,8 @@ impl Editor {
                 let old_selections = this.selections.all::<usize>(cx);
                 let all_selections_were_entire_line =
                     clipboard_selections.iter().all(|s| s.is_entire_line);
-                let first_selection_start_column =
-                    clipboard_selections.first().map(|s| s.start_column);
+                let first_selection_indent_column =
+                    clipboard_selections.first().map(|s| s.first_line_indent);
                 if clipboard_selections.len() != old_selections.len() {
                     clipboard_selections.drain(..);
                 }
@@ -8829,21 +8831,21 @@ impl Editor {
 
                     let mut start_offset = 0;
                     let mut edits = Vec::new();
-                    let mut original_start_columns = Vec::new();
+                    let mut original_indent_columns = Vec::new();
                     for (ix, selection) in old_selections.iter().enumerate() {
                         let to_insert;
                         let entire_line;
-                        let original_start_column;
+                        let original_indent_column;
                         if let Some(clipboard_selection) = clipboard_selections.get(ix) {
                             let end_offset = start_offset + clipboard_selection.len;
                             to_insert = &clipboard_text[start_offset..end_offset];
                             entire_line = clipboard_selection.is_entire_line;
                             start_offset = end_offset + 1;
-                            original_start_column = Some(clipboard_selection.start_column);
+                            original_indent_column = Some(clipboard_selection.first_line_indent);
                         } else {
                             to_insert = clipboard_text.as_str();
                             entire_line = all_selections_were_entire_line;
-                            original_start_column = first_selection_start_column
+                            original_indent_column = first_selection_indent_column
                         }
 
                         // If the corresponding selection was empty when this slice of the
@@ -8859,7 +8861,7 @@ impl Editor {
                         };
 
                         edits.push((range, to_insert));
-                        original_start_columns.extend(original_start_column);
+                        original_indent_columns.push(original_indent_column);
                     }
                     drop(snapshot);
 
@@ -8867,7 +8869,7 @@ impl Editor {
                         edits,
                         if auto_indent_on_paste {
                             Some(AutoindentMode::Block {
-                                original_start_columns,
+                                original_indent_columns,
                             })
                         } else {
                             None

crates/editor/src/editor_tests.rs 🔗

@@ -4931,6 +4931,34 @@ async fn test_paste_multiline(cx: &mut TestAppContext) {
             )
         );
     "});
+
+    // Copy an indented block, starting mid-line
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(),
+            somethin«g(
+                e,
+                f
+            )ˇ»
+        );
+    "});
+    cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+
+    // Paste it on a line with a lower indent level
+    cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
+    cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(),
+            something(
+                e,
+                f
+            )
+        );
+        g(
+            e,
+            f
+        )ˇ"});
 }
 
 #[gpui::test]

crates/language/src/buffer.rs 🔗

@@ -401,17 +401,16 @@ pub enum AutoindentMode {
     /// Apply the same indentation adjustment to all of the lines
     /// in a given insertion.
     Block {
-        /// The original start column of each insertion, if it was
-        /// copied from elsewhere.
+        /// The original indentation column of the first line of each
+        /// insertion, if it has been copied.
         ///
-        /// Knowing this start column makes it possible to preserve the
-        /// relative indentation of every line in the insertion from
-        /// when it was copied.
+        /// Knowing this makes it possible to preserve the relative indentation
+        /// of every line in the insertion from when it was copied.
         ///
-        /// If the start column is `a`, and the first line of insertion
+        /// If the original indent column is `a`, and the first line of insertion
         /// is then auto-indented to column `b`, then every other line of
         /// the insertion will be auto-indented to column `b - a`
-        original_start_columns: Vec<u32>,
+        original_indent_columns: Vec<Option<u32>>,
     },
 }
 
@@ -2206,15 +2205,20 @@ impl Buffer {
 
                     let mut original_indent_column = None;
                     if let AutoindentMode::Block {
-                        original_start_columns,
+                        original_indent_columns,
                     } = &mode
                     {
                         original_indent_column = Some(
-                            original_start_columns.get(ix).copied().unwrap_or(0)
-                                + indent_size_for_text(
-                                    new_text[range_of_insertion_to_indent.clone()].chars(),
-                                )
-                                .len,
+                            original_indent_columns
+                                .get(ix)
+                                .copied()
+                                .flatten()
+                                .unwrap_or_else(|| {
+                                    indent_size_for_text(
+                                        new_text[range_of_insertion_to_indent.clone()].chars(),
+                                    )
+                                    .len
+                                }),
                         );
 
                         // Avoid auto-indenting the line after the edit.

crates/language/src/buffer_tests.rs 🔗

@@ -1643,7 +1643,7 @@ fn test_autoindent_block_mode(cx: &mut App) {
         // indent level, but the indentation of the first line was not included in
         // the copied text. This information is retained in the
         // 'original_indent_columns' vector.
-        let original_indent_columns = vec![4];
+        let original_indent_columns = vec![Some(4)];
         let inserted_text = r#"
             "
                   c
@@ -1658,7 +1658,7 @@ fn test_autoindent_block_mode(cx: &mut App) {
         buffer.edit(
             [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
             Some(AutoindentMode::Block {
-                original_start_columns: original_indent_columns.clone(),
+                original_indent_columns: original_indent_columns.clone(),
             }),
             cx,
         );
@@ -1686,7 +1686,7 @@ fn test_autoindent_block_mode(cx: &mut App) {
         buffer.edit(
             [(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
             Some(AutoindentMode::Block {
-                original_start_columns: original_indent_columns.clone(),
+                original_indent_columns: original_indent_columns.clone(),
             }),
             cx,
         );
@@ -1735,7 +1735,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) {
         buffer.edit(
             [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
             Some(AutoindentMode::Block {
-                original_start_columns: original_indent_columns.clone(),
+                original_indent_columns: original_indent_columns.clone(),
             }),
             cx,
         );
@@ -1766,7 +1766,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) {
         buffer.edit(
             [(Point::new(2, 12)..Point::new(2, 12), inserted_text)],
             Some(AutoindentMode::Block {
-                original_start_columns: Vec::new(),
+                original_indent_columns: Vec::new(),
             }),
             cx,
         );
@@ -1822,7 +1822,7 @@ fn test_autoindent_block_mode_multiple_adjacent_ranges(cx: &mut App) {
                 (ranges_to_replace[2].clone(), "fn three() {\n    103\n}\n"),
             ],
             Some(AutoindentMode::Block {
-                original_start_columns: vec![0, 0, 0],
+                original_indent_columns: vec![Some(0), Some(0), Some(0)],
             }),
             cx,
         );

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -495,7 +495,7 @@ struct BufferEdit {
     range: Range<usize>,
     new_text: Arc<str>,
     is_insertion: bool,
-    original_start_column: u32,
+    original_indent_column: Option<u32>,
     excerpt_id: ExcerptId,
 }
 
@@ -751,15 +751,15 @@ impl MultiBuffer {
                 return;
             }
 
-            let original_start_columns = match &mut autoindent_mode {
+            let original_indent_columns = match &mut autoindent_mode {
                 Some(AutoindentMode::Block {
-                    original_start_columns,
-                }) => mem::take(original_start_columns),
+                    original_indent_columns,
+                }) => mem::take(original_indent_columns),
                 _ => Default::default(),
             };
 
             let (buffer_edits, edited_excerpt_ids) =
-                this.convert_edits_to_buffer_edits(edits, &snapshot, &original_start_columns);
+                this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
             drop(snapshot);
 
             let mut buffer_ids = Vec::new();
@@ -778,7 +778,7 @@ impl MultiBuffer {
                             mut range,
                             mut new_text,
                             mut is_insertion,
-                            original_start_column: original_indent_column,
+                            original_indent_column,
                             excerpt_id,
                         }) = edits.next()
                         {
@@ -821,7 +821,7 @@ impl MultiBuffer {
                         let deletion_autoindent_mode =
                             if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
                                 Some(AutoindentMode::Block {
-                                    original_start_columns: Default::default(),
+                                    original_indent_columns: Default::default(),
                                 })
                             } else {
                                 autoindent_mode.clone()
@@ -829,7 +829,7 @@ impl MultiBuffer {
                         let insertion_autoindent_mode =
                             if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
                                 Some(AutoindentMode::Block {
-                                    original_start_columns: original_indent_columns,
+                                    original_indent_columns,
                                 })
                             } else {
                                 autoindent_mode.clone()
@@ -851,13 +851,13 @@ impl MultiBuffer {
         &self,
         edits: Vec<(Range<usize>, Arc<str>)>,
         snapshot: &MultiBufferSnapshot,
-        original_start_columns: &[u32],
+        original_indent_columns: &[Option<u32>],
     ) -> (HashMap<BufferId, Vec<BufferEdit>>, Vec<ExcerptId>) {
         let mut buffer_edits: HashMap<BufferId, Vec<BufferEdit>> = Default::default();
         let mut edited_excerpt_ids = Vec::new();
         let mut cursor = snapshot.cursor::<usize>();
         for (ix, (range, new_text)) in edits.into_iter().enumerate() {
-            let original_start_column = original_start_columns.get(ix).copied().unwrap_or(0);
+            let original_indent_column = original_indent_columns.get(ix).copied().flatten();
 
             cursor.seek(&range.start);
             let mut start_region = cursor.region().expect("start offset out of bounds");
@@ -908,7 +908,7 @@ impl MultiBuffer {
                             range: buffer_start..buffer_end,
                             new_text,
                             is_insertion: true,
-                            original_start_column,
+                            original_indent_column,
                             excerpt_id: start_region.excerpt.id,
                         });
                 }
@@ -924,7 +924,7 @@ impl MultiBuffer {
                             range: start_excerpt_range,
                             new_text: new_text.clone(),
                             is_insertion: true,
-                            original_start_column,
+                            original_indent_column,
                             excerpt_id: start_region.excerpt.id,
                         });
                 }
@@ -937,7 +937,7 @@ impl MultiBuffer {
                             range: end_excerpt_range,
                             new_text: new_text.clone(),
                             is_insertion: false,
-                            original_start_column,
+                            original_indent_column,
                             excerpt_id: end_region.excerpt.id,
                         });
                 }
@@ -957,7 +957,7 @@ impl MultiBuffer {
                                 range: region.buffer_range,
                                 new_text: new_text.clone(),
                                 is_insertion: false,
-                                original_start_column,
+                                original_indent_column,
                                 excerpt_id: region.excerpt.id,
                             });
                     }

crates/vim/src/normal/paste.rs 🔗

@@ -81,32 +81,32 @@ impl Vim {
                     }
                 }
 
-                let first_selection_start_column =
+                let first_selection_indent_column =
                     clipboard_selections.as_ref().and_then(|zed_selections| {
                         zed_selections
                             .first()
-                            .map(|selection| selection.start_column)
+                            .map(|selection| selection.first_line_indent)
                     });
                 let before = action.before || vim.mode == Mode::VisualLine;
 
                 let mut edits = Vec::new();
                 let mut new_selections = Vec::new();
-                let mut original_start_columns = Vec::new();
+                let mut original_indent_columns = Vec::new();
                 let mut start_offset = 0;
 
                 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
-                    let (mut to_insert, original_start_column) =
+                    let (mut to_insert, original_indent_column) =
                         if let Some(clipboard_selections) = &clipboard_selections {
                             if let Some(clipboard_selection) = clipboard_selections.get(ix) {
                                 let end_offset = start_offset + clipboard_selection.len;
                                 let text = text[start_offset..end_offset].to_string();
                                 start_offset = end_offset + 1;
-                                (text, Some(clipboard_selection.start_column))
+                                (text, Some(clipboard_selection.first_line_indent))
                             } else {
-                                ("".to_string(), first_selection_start_column)
+                                ("".to_string(), first_selection_indent_column)
                             }
                         } else {
-                            (text.to_string(), first_selection_start_column)
+                            (text.to_string(), first_selection_indent_column)
                         };
                     let line_mode = to_insert.ends_with('\n');
                     let is_multiline = to_insert.contains('\n');
@@ -152,7 +152,7 @@ impl Vim {
                         new_selections.push((anchor, line_mode, is_multiline));
                     }
                     edits.push((point_range, to_insert.repeat(count)));
-                    original_start_columns.extend(original_start_column);
+                    original_indent_columns.push(original_indent_column);
                 }
 
                 let cursor_offset = editor.selections.last::<usize>(cx).head();
@@ -163,7 +163,7 @@ impl Vim {
                     .language_settings_at(cursor_offset, cx)
                     .auto_indent_on_paste
                 {
-                    editor.edit_with_block_indent(edits, original_start_columns, cx);
+                    editor.edit_with_block_indent(edits, original_indent_columns, cx);
                 } else {
                     editor.edit(edits, cx);
                 }

crates/vim/src/normal/yank.rs 🔗

@@ -188,7 +188,7 @@ impl Vim {
                 clipboard_selections.push(ClipboardSelection {
                     len: text.len() - initial_len,
                     is_entire_line: linewise,
-                    start_column: start.column,
+                    first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
                 });
             }
         }