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

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.
This commit is contained in:
Max Brunsfeld 2025-03-06 14:13:34 -08:00 committed by GitHub
parent f373383fc1
commit be75f17429
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 93 additions and 59 deletions

View file

@ -140,7 +140,7 @@ impl ResolvedPatch {
buffer.edit( buffer.edit(
edits, edits,
Some(AutoindentMode::Block { Some(AutoindentMode::Block {
original_start_columns: Vec::new(), original_indent_columns: Vec::new(),
}), }),
cx, cx,
); );

View file

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

View file

@ -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] #[gpui::test]

View file

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

View file

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

View file

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

View file

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

View file

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