From 7a26fa18c7fee3fe031b991e18b55fd8f9c4eb1b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Jul 2022 18:09:24 -0700 Subject: [PATCH] Record start columns when writing to the clipboard from Zed --- 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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4543aa3b27..1f65451a83 100644 --- a/crates/editor/src/editor.rs +++ b/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::(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)); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a12285dbbe..be9972c3d5 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -305,7 +305,7 @@ impl MultiBuffer { pub fn edit( &mut self, edits: I, - autoindent_mode: Option, + mut autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, @@ -331,11 +331,17 @@ impl MultiBuffer { }); } - let mut buffer_edits: HashMap, Arc, 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, Arc, bool, u32)>> = Default::default(); let mut cursor = snapshot.excerpts.cursor::(); - for (range, new_text) in edits { + for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = 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 = "".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); }) } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ddacefec06..b2d45717f0 100644 --- a/crates/language/src/buffer.rs +++ b/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 }, Independent, } @@ -240,7 +240,7 @@ struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, 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, + start_column: Option, } #[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, })); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0657e25604..bee320a932 100644 --- a/crates/language/src/tests.rs +++ b/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!( diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index cb6a736c63..75bb7445ff 100644 --- a/crates/vim/src/utils.rs +++ b/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, + ), }); } }