Record start columns when writing to the clipboard from Zed

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs       | 132 ++++++++++++++++++++++++++++++++
crates/editor/src/multi_buffer.rs |  51 +++++++++---
crates/language/src/buffer.rs     |  63 ++++++++-------
crates/language/src/tests.rs      |   8 +
crates/vim/src/utils.rs           |   5 +
5 files changed, 214 insertions(+), 45 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -879,6 +879,7 @@ struct ActiveDiagnosticGroup {
 pub struct ClipboardSelection {
     pub len: usize,
     pub is_entire_line: bool,
+    pub first_line_indent: u32,
 }
 
 #[derive(Debug)]
@@ -1926,7 +1927,7 @@ impl Editor {
                     old_selections
                         .iter()
                         .map(|s| (s.start..s.end, text.clone())),
-                    Some(AutoindentMode::Block),
+                    Some(AutoindentMode::Independent),
                     cx,
                 );
                 anchors
@@ -2368,7 +2369,7 @@ impl Editor {
                 this.buffer.update(cx, |buffer, cx| {
                     buffer.edit(
                         ranges.iter().map(|range| (range.clone(), text)),
-                        Some(AutoindentMode::Block),
+                        Some(AutoindentMode::Independent),
                         cx,
                     );
                 });
@@ -3512,6 +3513,10 @@ impl Editor {
                 clipboard_selections.push(ClipboardSelection {
                     len,
                     is_entire_line,
+                    first_line_indent: cmp::min(
+                        selection.start.column,
+                        buffer.indent_size_for_line(selection.start.row).len,
+                    ),
                 });
             }
         }
@@ -3549,6 +3554,10 @@ impl Editor {
                 clipboard_selections.push(ClipboardSelection {
                     len,
                     is_entire_line,
+                    first_line_indent: cmp::min(
+                        start.column,
+                        buffer.indent_size_for_line(start.row).len,
+                    ),
                 });
             }
         }
@@ -3583,18 +3592,22 @@ impl Editor {
                         let snapshot = buffer.read(cx);
                         let mut start_offset = 0;
                         let mut edits = Vec::new();
+                        let mut start_columns = Vec::new();
                         let line_mode = this.selections.line_mode;
                         for (ix, selection) in old_selections.iter().enumerate() {
                             let to_insert;
                             let entire_line;
+                            let start_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;
+                                start_column = clipboard_selection.first_line_indent;
                             } else {
                                 to_insert = clipboard_text.as_str();
                                 entire_line = all_selections_were_entire_line;
+                                start_column = 0;
                             }
 
                             // If the corresponding selection was empty when this slice of the
@@ -3610,9 +3623,10 @@ impl Editor {
                             };
 
                             edits.push((range, to_insert));
+                            start_columns.push(start_column);
                         }
                         drop(snapshot);
-                        buffer.edit(edits, Some(AutoindentMode::Block), cx);
+                        buffer.edit(edits, Some(AutoindentMode::Block { start_columns }), cx);
                     });
 
                     let selections = this.selections.all::<usize>(cx);
@@ -8649,6 +8663,118 @@ mod tests {
             t|he lazy dog"});
     }
 
+    #[gpui::test]
+    async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorTestContext::new(cx).await;
+        let language = Arc::new(Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        ));
+        cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+        // Cut an indented block, without the leading whitespace.
+        cx.set_state(indoc! {"
+            const a = (
+                b(),
+                [c(
+                    d,
+                    e
+                )}
+            );
+        "});
+        cx.update_editor(|e, cx| e.cut(&Cut, cx));
+        cx.assert_editor_state(indoc! {"
+            const a = (
+                b(),
+                |
+            );
+        "});
+
+        // Paste it at the same position.
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.assert_editor_state(indoc! {"
+            const a = (
+                b(),
+                c(
+                    d,
+                    e
+                )|
+            );
+        "});
+
+        // Paste it at a line with a lower indent level.
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.set_state(indoc! {"
+            |
+            const a = (
+                b(),
+            );
+        "});
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.assert_editor_state(indoc! {"
+            c(
+                d,
+                e
+            )|
+            const a = (
+                b(),
+            );
+        "});
+
+        // Cut an indented block, with the leading whitespace.
+        cx.set_state(indoc! {"
+            const a = (
+                b(),
+            [    c(
+                    d,
+                    e
+                )
+            });
+        "});
+        cx.update_editor(|e, cx| e.cut(&Cut, cx));
+        cx.assert_editor_state(indoc! {"
+            const a = (
+                b(),
+            |);
+        "});
+
+        // Paste it at the same position.
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.assert_editor_state(indoc! {"
+            const a = (
+                b(),
+                c(
+                    d,
+                    e
+                )
+            |);
+        "});
+
+        // Paste it at a line with a higher indent level.
+        cx.set_state(indoc! {"
+            const a = (
+                b(),
+                c(
+                    d,
+                    e|
+                )
+            );
+        "});
+        cx.update_editor(|e, cx| e.paste(&Paste, cx));
+        cx.set_state(indoc! {"
+            const a = (
+                b(),
+                c(
+                    d,
+                    ec(
+                        d,
+                        e
+                    )|
+                )
+            );
+        "});
+    }
+
     #[gpui::test]
     fn test_select_all(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));

crates/editor/src/multi_buffer.rs 🔗

@@ -305,7 +305,7 @@ impl MultiBuffer {
     pub fn edit<I, S, T>(
         &mut self,
         edits: I,
-        autoindent_mode: Option<AutoindentMode>,
+        mut autoindent_mode: Option<AutoindentMode>,
         cx: &mut ModelContext<Self>,
     ) where
         I: IntoIterator<Item = (Range<S>, T)>,
@@ -331,11 +331,17 @@ impl MultiBuffer {
             });
         }
 
-        let mut buffer_edits: HashMap<usize, Vec<(Range<usize>, Arc<str>, bool)>> =
+        let indent_start_columns = match &mut autoindent_mode {
+            Some(AutoindentMode::Block { start_columns }) => mem::take(start_columns),
+            _ => Default::default(),
+        };
+
+        let mut buffer_edits: HashMap<usize, Vec<(Range<usize>, Arc<str>, bool, u32)>> =
             Default::default();
         let mut cursor = snapshot.excerpts.cursor::<usize>();
-        for (range, new_text) in edits {
+        for (ix, (range, new_text)) in edits.enumerate() {
             let new_text: Arc<str> = new_text.into();
+            let start_column = indent_start_columns.get(ix).copied().unwrap_or(0);
             cursor.seek(&range.start, Bias::Right, &());
             if cursor.item().is_none() && range.start == *cursor.start() {
                 cursor.prev(&());
@@ -366,7 +372,7 @@ impl MultiBuffer {
                 buffer_edits
                     .entry(start_excerpt.buffer_id)
                     .or_insert(Vec::new())
-                    .push((buffer_start..buffer_end, new_text, true));
+                    .push((buffer_start..buffer_end, new_text, true, start_column));
             } else {
                 let start_excerpt_range = buffer_start
                     ..start_excerpt
@@ -383,11 +389,11 @@ impl MultiBuffer {
                 buffer_edits
                     .entry(start_excerpt.buffer_id)
                     .or_insert(Vec::new())
-                    .push((start_excerpt_range, new_text.clone(), true));
+                    .push((start_excerpt_range, new_text.clone(), true, start_column));
                 buffer_edits
                     .entry(end_excerpt.buffer_id)
                     .or_insert(Vec::new())
-                    .push((end_excerpt_range, new_text.clone(), false));
+                    .push((end_excerpt_range, new_text.clone(), false, start_column));
 
                 cursor.seek(&range.start, Bias::Right, &());
                 cursor.next(&());
@@ -402,6 +408,7 @@ impl MultiBuffer {
                             excerpt.range.context.to_offset(&excerpt.buffer),
                             new_text.clone(),
                             false,
+                            start_column,
                         ));
                     cursor.next(&());
                 }
@@ -409,19 +416,21 @@ impl MultiBuffer {
         }
 
         for (buffer_id, mut edits) in buffer_edits {
-            edits.sort_unstable_by_key(|(range, _, _)| range.start);
+            edits.sort_unstable_by_key(|(range, _, _, _)| range.start);
             self.buffers.borrow()[&buffer_id]
                 .buffer
                 .update(cx, |buffer, cx| {
                     let mut edits = edits.into_iter().peekable();
                     let mut insertions = Vec::new();
+                    let mut insertion_start_columns = Vec::new();
                     let mut deletions = Vec::new();
                     let empty_str: Arc<str> = "".into();
-                    while let Some((mut range, new_text, mut is_insertion)) = edits.next() {
-                        while let Some((next_range, _, next_is_insertion)) = edits.peek() {
+                    while let Some((mut range, new_text, mut is_insertion, start_column)) =
+                        edits.next()
+                    {
+                        while let Some((next_range, _, next_is_insertion, _)) = edits.peek() {
                             if range.end >= next_range.start {
                                 range.end = cmp::max(next_range.end, range.end);
-
                                 is_insertion |= *next_is_insertion;
                                 edits.next();
                             } else {
@@ -430,6 +439,7 @@ impl MultiBuffer {
                         }
 
                         if is_insertion {
+                            insertion_start_columns.push(start_column);
                             insertions.push((
                                 buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
                                 new_text.clone(),
@@ -442,8 +452,25 @@ impl MultiBuffer {
                         }
                     }
 
-                    buffer.edit(deletions, autoindent_mode, cx);
-                    buffer.edit(insertions, autoindent_mode, cx);
+                    let deletion_autoindent_mode =
+                        if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
+                            Some(AutoindentMode::Block {
+                                start_columns: Default::default(),
+                            })
+                        } else {
+                            None
+                        };
+                    let insertion_autoindent_mode =
+                        if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
+                            Some(AutoindentMode::Block {
+                                start_columns: insertion_start_columns,
+                            })
+                        } else {
+                            None
+                        };
+
+                    buffer.edit(deletions, deletion_autoindent_mode, cx);
+                    buffer.edit(insertions, insertion_autoindent_mode, cx);
                 })
         }
     }

crates/language/src/buffer.rs 🔗

@@ -229,9 +229,9 @@ struct SyntaxTree {
     version: clock::Global,
 }
 
-#[derive(Clone, Copy)]
+#[derive(Clone, Debug)]
 pub enum AutoindentMode {
-    Block,
+    Block { start_columns: Vec<u32> },
     Independent,
 }
 
@@ -240,7 +240,7 @@ struct AutoindentRequest {
     before_edit: BufferSnapshot,
     entries: Vec<AutoindentRequestEntry>,
     indent_size: IndentSize,
-    mode: AutoindentMode,
+    is_block_mode: bool,
 }
 
 #[derive(Clone)]
@@ -252,8 +252,7 @@ struct AutoindentRequestEntry {
     /// only be adjusted if the suggested indentation level has *changed*
     /// since the edit was made.
     first_line_is_new: bool,
-    /// The original indentation of the text that was inserted into this range.
-    original_indent: Option<IndentSize>,
+    start_column: Option<u32>,
 }
 
 #[derive(Debug)]
@@ -828,7 +827,7 @@ impl Buffer {
                         let old_row = position.to_point(&request.before_edit).row;
                         old_to_new_rows.insert(old_row, new_row);
                     }
-                    row_ranges.push((new_row..new_end_row, entry.original_indent));
+                    row_ranges.push((new_row..new_end_row, entry.start_column));
                 }
 
                 // Build a map containing the suggested indentation for each of the edited lines
@@ -864,9 +863,12 @@ impl Buffer {
                 // In block mode, only compute indentation suggestions for the first line
                 // of each insertion. Otherwise, compute suggestions for every inserted line.
                 let new_edited_row_ranges = contiguous_ranges(
-                    row_ranges.iter().flat_map(|(range, _)| match request.mode {
-                        AutoindentMode::Block => range.start..range.start + 1,
-                        AutoindentMode::Independent => range.clone(),
+                    row_ranges.iter().flat_map(|(range, _)| {
+                        if request.is_block_mode {
+                            range.start..range.start + 1
+                        } else {
+                            range.clone()
+                        }
                     }),
                     max_rows_between_yields,
                 );
@@ -902,24 +904,22 @@ impl Buffer {
 
                 // For each block of inserted text, adjust the indentation of the remaining
                 // lines of the block by the same amount as the first line was adjusted.
-                if matches!(request.mode, AutoindentMode::Block) {
-                    for (row_range, original_indent) in
-                        row_ranges
-                            .into_iter()
-                            .filter_map(|(range, original_indent)| {
-                                if range.len() > 1 {
-                                    Some((range, original_indent?))
-                                } else {
-                                    None
-                                }
-                            })
+                if request.is_block_mode {
+                    for (row_range, start_column) in
+                        row_ranges.into_iter().filter_map(|(range, start_column)| {
+                            if range.len() > 1 {
+                                Some((range, start_column?))
+                            } else {
+                                None
+                            }
+                        })
                     {
                         let new_indent = indent_sizes
                             .get(&row_range.start)
                             .copied()
                             .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start));
-                        let delta = new_indent.len as i64 - original_indent.len as i64;
-                        if new_indent.kind == original_indent.kind && delta != 0 {
+                        let delta = new_indent.len as i64 - start_column as i64;
+                        if delta != 0 {
                             for row in row_range.skip(1) {
                                 indent_sizes.entry(row).or_insert_with(|| {
                                     let mut size = snapshot.indent_size_for_line(row);
@@ -1223,12 +1223,17 @@ impl Buffer {
             } else {
                 IndentSize::spaces(settings.tab_size(language_name.as_deref()).get())
             };
+            let (start_columns, is_block_mode) = match mode {
+                AutoindentMode::Block { start_columns } => (start_columns, true),
+                AutoindentMode::Independent => (Default::default(), false),
+            };
 
             let mut delta = 0isize;
             let entries = edits
                 .into_iter()
+                .enumerate()
                 .zip(&edit_operation.as_edit().unwrap().new_text)
-                .map(|((range, _), new_text)| {
+                .map(|((ix, (range, _)), new_text)| {
                     let new_text_len = new_text.len();
                     let old_start = range.start.to_point(&before_edit);
                     let new_start = (delta + range.start as isize) as usize;
@@ -1236,7 +1241,7 @@ impl Buffer {
 
                     let mut range_of_insertion_to_indent = 0..new_text_len;
                     let mut first_line_is_new = false;
-                    let mut original_indent = None;
+                    let mut start_column = None;
 
                     // When inserting an entire line at the beginning of an existing line,
                     // treat the insertion as new.
@@ -1254,8 +1259,10 @@ impl Buffer {
                     }
 
                     // Avoid auto-indenting before the insertion.
-                    if matches!(mode, AutoindentMode::Block) {
-                        original_indent = Some(indent_size_for_text(new_text.chars()));
+                    if is_block_mode {
+                        start_column = start_columns
+                            .get(ix)
+                            .map(|start| start + indent_size_for_text(new_text.chars()).len);
                         if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
                             range_of_insertion_to_indent.end -= 1;
                         }
@@ -1263,7 +1270,7 @@ impl Buffer {
 
                     AutoindentRequestEntry {
                         first_line_is_new,
-                        original_indent,
+                        start_column,
                         range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
                             ..self.anchor_after(new_start + range_of_insertion_to_indent.end),
                     }
@@ -1274,7 +1281,7 @@ impl Buffer {
                 before_edit,
                 entries,
                 indent_size,
-                mode,
+                is_block_mode,
             }));
         }
 

crates/language/src/tests.rs 🔗

@@ -944,7 +944,9 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
         // so that the first line matches the previous line's indentation.
         buffer.edit(
             [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
-            Some(AutoindentMode::Block),
+            Some(AutoindentMode::Block {
+                start_columns: vec![0],
+            }),
             cx,
         );
         assert_eq!(
@@ -967,7 +969,9 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
         buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "        ")], None, cx);
         buffer.edit(
             [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())],
-            Some(AutoindentMode::Block),
+            Some(AutoindentMode::Block {
+                start_columns: vec![0],
+            }),
             cx,
         );
         assert_eq!(

crates/vim/src/utils.rs 🔗

@@ -1,5 +1,6 @@
 use editor::{ClipboardSelection, Editor};
 use gpui::{ClipboardItem, MutableAppContext};
+use std::cmp;
 
 pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut MutableAppContext) {
     let selections = editor.selections.all_adjusted(cx);
@@ -17,6 +18,10 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut Mut
             clipboard_selections.push(ClipboardSelection {
                 len: text.len() - initial_len,
                 is_entire_line: linewise,
+                first_line_indent: cmp::min(
+                    start.column,
+                    buffer.indent_size_for_line(start.row).len,
+                ),
             });
         }
     }