Record start columns when writing to the clipboard from Zed

This commit is contained in:
Max Brunsfeld 2022-07-28 18:09:24 -07:00
parent 2d05f906f1
commit 7a26fa18c7
5 changed files with 214 additions and 45 deletions

View file

@ -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));

View file

@ -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);
})
}
}

View file

@ -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,
}));
}

View file

@ -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!(

View file

@ -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,
),
});
}
}