Fix relative indentation when pasting content copied from outside Zed (#25300)

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

Release Notes:

- Fixed incorrect indentation when pasting multi-line content that was
copied from another app.
This commit is contained in:
Max Brunsfeld 2025-02-20 17:25:33 -08:00 committed by GitHub
parent ee1a559827
commit c31c638006
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 233 additions and 190 deletions

View file

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

View file

@ -976,9 +976,12 @@ struct ActiveDiagnosticGroup {
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ClipboardSelection {
/// The number of bytes in this selection.
pub len: usize,
/// Whether this was a full-line selection.
pub is_entire_line: bool,
pub first_line_indent: u32,
/// The column where this selection originally started.
pub start_column: u32,
}
#[derive(Debug)]
@ -2273,7 +2276,7 @@ impl Editor {
pub fn edit_with_block_indent<I, S, T>(
&mut self,
edits: I,
original_indent_columns: Vec<u32>,
original_start_columns: Vec<u32>,
cx: &mut Context<Self>,
) where
I: IntoIterator<Item = (Range<S>, T)>,
@ -2288,7 +2291,7 @@ impl Editor {
buffer.edit(
edits,
Some(AutoindentMode::Block {
original_indent_columns,
original_start_columns,
}),
cx,
)
@ -3397,7 +3400,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_indent_columns: Vec::new(),
original_start_columns: Vec::new(),
});
self.insert_with_autoindent_mode(text, autoindent, window, cx);
}
@ -7943,9 +7946,7 @@ impl Editor {
clipboard_selections.push(ClipboardSelection {
len,
is_entire_line,
first_line_indent: buffer
.indent_size_for_line(MultiBufferRow(selection.start.row))
.len,
start_column: selection.start.column,
});
}
}
@ -8024,7 +8025,7 @@ impl Editor {
clipboard_selections.push(ClipboardSelection {
len,
is_entire_line,
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
start_column: start.column,
});
}
}
@ -8054,8 +8055,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_indent_column =
clipboard_selections.first().map(|s| s.first_line_indent);
let first_selection_start_column =
clipboard_selections.first().map(|s| s.start_column);
if clipboard_selections.len() != old_selections.len() {
clipboard_selections.drain(..);
}
@ -8069,21 +8070,21 @@ impl Editor {
let mut start_offset = 0;
let mut edits = Vec::new();
let mut original_indent_columns = Vec::new();
let mut original_start_columns = Vec::new();
for (ix, selection) in old_selections.iter().enumerate() {
let to_insert;
let entire_line;
let original_indent_column;
let original_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 + 1;
original_indent_column = Some(clipboard_selection.first_line_indent);
original_start_column = Some(clipboard_selection.start_column);
} else {
to_insert = clipboard_text.as_str();
entire_line = all_selections_were_entire_line;
original_indent_column = first_selection_indent_column
original_start_column = first_selection_start_column
}
// If the corresponding selection was empty when this slice of the
@ -8099,7 +8100,7 @@ impl Editor {
};
edits.push((range, to_insert));
original_indent_columns.extend(original_indent_column);
original_start_columns.extend(original_start_column);
}
drop(snapshot);
@ -8107,7 +8108,7 @@ impl Editor {
edits,
if auto_indent_on_paste {
Some(AutoindentMode::Block {
original_indent_columns,
original_start_columns,
})
} else {
None

File diff suppressed because it is too large Load diff

View file

@ -393,9 +393,17 @@ pub enum AutoindentMode {
/// Apply the same indentation adjustment to all of the lines
/// in a given insertion.
Block {
/// The original indentation level of the first line of each
/// insertion, if it has been copied.
original_indent_columns: Vec<u32>,
/// The original start column of each insertion, if it was
/// copied from elsewhere.
///
/// Knowing this start column 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
/// 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>,
},
}
@ -2191,16 +2199,16 @@ impl Buffer {
let mut original_indent_column = None;
if let AutoindentMode::Block {
original_indent_columns,
original_start_columns,
} = &mode
{
original_indent_column =
Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
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
}));
.len,
);
// Avoid auto-indenting the line after the edit.
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {

View file

@ -1657,7 +1657,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_indent_columns: original_indent_columns.clone(),
original_start_columns: original_indent_columns.clone(),
}),
cx,
);
@ -1685,7 +1685,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_indent_columns: original_indent_columns.clone(),
original_start_columns: original_indent_columns.clone(),
}),
cx,
);
@ -1734,7 +1734,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_indent_columns: original_indent_columns.clone(),
original_start_columns: original_indent_columns.clone(),
}),
cx,
);
@ -1765,7 +1765,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_indent_columns: Vec::new(),
original_start_columns: Vec::new(),
}),
cx,
);
@ -1821,7 +1821,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_indent_columns: vec![0, 0, 0],
original_start_columns: vec![0, 0, 0],
}),
cx,
);

View file

@ -488,7 +488,7 @@ struct BufferEdit {
range: Range<usize>,
new_text: Arc<str>,
is_insertion: bool,
original_indent_column: u32,
original_start_column: u32,
excerpt_id: ExcerptId,
}
@ -747,15 +747,15 @@ impl MultiBuffer {
return;
}
let original_indent_columns = match &mut autoindent_mode {
let original_start_columns = match &mut autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns,
}) => mem::take(original_indent_columns),
original_start_columns,
}) => mem::take(original_start_columns),
_ => Default::default(),
};
let (buffer_edits, edited_excerpt_ids) =
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_start_columns);
drop(snapshot);
for (buffer_id, mut edits) in buffer_edits {
@ -772,7 +772,7 @@ impl MultiBuffer {
mut range,
mut new_text,
mut is_insertion,
original_indent_column,
original_start_column: original_indent_column,
excerpt_id,
}) = edits.next()
{
@ -815,7 +815,7 @@ impl MultiBuffer {
let deletion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns: Default::default(),
original_start_columns: Default::default(),
})
} else {
autoindent_mode.clone()
@ -823,7 +823,7 @@ impl MultiBuffer {
let insertion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns,
original_start_columns: original_indent_columns,
})
} else {
autoindent_mode.clone()
@ -844,13 +844,13 @@ impl MultiBuffer {
&self,
edits: Vec<(Range<usize>, Arc<str>)>,
snapshot: &MultiBufferSnapshot,
original_indent_columns: &[u32],
original_start_columns: &[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_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0);
let original_start_column = original_start_columns.get(ix).copied().unwrap_or(0);
cursor.seek(&range.start);
let mut start_region = cursor.region().expect("start offset out of bounds");
@ -901,7 +901,7 @@ impl MultiBuffer {
range: buffer_start..buffer_end,
new_text,
is_insertion: true,
original_indent_column,
original_start_column,
excerpt_id: start_region.excerpt.id,
});
}
@ -917,7 +917,7 @@ impl MultiBuffer {
range: start_excerpt_range,
new_text: new_text.clone(),
is_insertion: true,
original_indent_column,
original_start_column,
excerpt_id: start_region.excerpt.id,
});
}
@ -930,7 +930,7 @@ impl MultiBuffer {
range: end_excerpt_range,
new_text: new_text.clone(),
is_insertion: false,
original_indent_column,
original_start_column,
excerpt_id: end_region.excerpt.id,
});
}
@ -950,7 +950,7 @@ impl MultiBuffer {
range: region.buffer_range,
new_text: new_text.clone(),
is_insertion: false,
original_indent_column,
original_start_column,
excerpt_id: region.excerpt.id,
});
}

View file

@ -81,32 +81,32 @@ impl Vim {
}
}
let first_selection_indent_column =
let first_selection_start_column =
clipboard_selections.as_ref().and_then(|zed_selections| {
zed_selections
.first()
.map(|selection| selection.first_line_indent)
.map(|selection| selection.start_column)
});
let before = action.before || vim.mode == Mode::VisualLine;
let mut edits = Vec::new();
let mut new_selections = Vec::new();
let mut original_indent_columns = Vec::new();
let mut original_start_columns = Vec::new();
let mut start_offset = 0;
for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
let (mut to_insert, original_indent_column) =
let (mut to_insert, original_start_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.first_line_indent))
(text, Some(clipboard_selection.start_column))
} else {
("".to_string(), first_selection_indent_column)
("".to_string(), first_selection_start_column)
}
} else {
(text.to_string(), first_selection_indent_column)
(text.to_string(), first_selection_start_column)
};
let line_mode = to_insert.ends_with('\n');
let is_multiline = to_insert.contains('\n');
@ -152,10 +152,10 @@ impl Vim {
new_selections.push((anchor, line_mode, is_multiline));
}
edits.push((point_range, to_insert.repeat(count)));
original_indent_columns.extend(original_indent_column);
original_start_columns.extend(original_start_column);
}
editor.edit_with_block_indent(edits, original_indent_columns, cx);
editor.edit_with_block_indent(edits, original_start_columns, cx);
// in line_mode vim will insert the new text on the next (or previous if before) line
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).

View file

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