diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 91d02b1aa0..4d49fb7149 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2638,11 +2638,25 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); for selection in &mut selections { if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); + let old_head = selection.head(); + let (buffer, line_buffer_range) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.row) + .unwrap(); + let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .unwrap() + .to_point(&display_map); + if old_head.column <= indent_column && old_head.column > 0 { + let indent = buffer.indent_size(); + new_head = cmp::min( + new_head, + Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), + ); + } + + selection.set_head(new_head); selection.goal = SelectionGoal::None; } } @@ -7153,14 +7167,13 @@ mod tests { #[gpui::test] fn test_backspace(cx: &mut gpui::MutableAppContext) { - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); let settings = Settings::test(&cx); let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) + build_editor(MultiBuffer::build_simple("", cx), settings, cx) }); view.update(cx, |view, cx| { + view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx); view.select_display_ranges( &[ // an empty selection - the preceding character is deleted @@ -7173,12 +7186,28 @@ mod tests { cx, ); view.backspace(&Backspace, cx); - }); + assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n"); - assert_eq!( - buffer.read(cx).read(cx).text(), - "oe two three\nfou five six\nseven ten\n" - ); + view.set_text(" one\n two\n three\n four", cx); + view.select_display_ranges( + &[ + // cursors at the the end of leading indent - last indent is deleted + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8), + // cursors inside leading indent - overlapping indent deletions are coalesced + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6), + // cursor at the beginning of a line - preceding newline is deleted + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + // selection inside leading indent - only the selected character is deleted + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3), + ], + cx, + ); + view.backspace(&Backspace, cx); + assert_eq!(view.text(cx), "one\n two\n three four"); + }); } #[gpui::test] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 64683faa96..3678f8f116 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1657,7 +1657,7 @@ impl MultiBufferSnapshot { } } - fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range)> { + pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range)> { let mut cursor = self.excerpts.cursor::(); cursor.seek(&Point::new(row, 0), Bias::Right, &()); if let Some(excerpt) = cursor.item() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3d79ecadd6..5f3ddb8b99 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -47,9 +47,6 @@ lazy_static! { static ref QUERY_CURSORS: Mutex> = Default::default(); } -// TODO - Make this configurable -const INDENT_SIZE: u32 = 4; - pub struct Buffer { text: TextBuffer, file: Option>, @@ -70,6 +67,7 @@ pub struct Buffer { file_update_count: usize, completion_triggers: Vec, deferred_ops: OperationQueue, + indent_size: u32, } pub struct BufferSnapshot { @@ -81,9 +79,9 @@ pub struct BufferSnapshot { file_update_count: usize, remote_selections: TreeMap, selections_update_count: usize, - is_parsing: bool, language: Option>, parse_count: usize, + indent_size: u32, } #[derive(Clone, Debug)] @@ -416,6 +414,8 @@ impl Buffer { file_update_count: 0, completion_triggers: Default::default(), deferred_ops: OperationQueue::new(), + // TODO: make this configurable + indent_size: 4, } } @@ -428,10 +428,10 @@ impl Buffer { diagnostics: self.diagnostics.clone(), diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, - is_parsing: self.parsing_in_background, language: self.language.clone(), parse_count: self.parse_count, selections_update_count: self.selections_update_count, + indent_size: self.indent_size, } } @@ -768,7 +768,11 @@ impl Buffer { .before_edit .indent_column_for_line(suggestion.basis_row) }); - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let delta = if suggestion.indent { + snapshot.indent_size + } else { + 0 + }; old_suggestions.insert( *old_to_new_rows.get(&old_row).unwrap(), indentation_basis + delta, @@ -787,7 +791,11 @@ impl Buffer { .into_iter() .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let delta = if suggestion.indent { + snapshot.indent_size + } else { + 0 + }; let new_indentation = indent_columns .get(&suggestion.basis_row) .copied() @@ -819,7 +827,11 @@ impl Buffer { .into_iter() .flatten(); for (row, suggestion) in inserted_row_range.zip(suggestions) { - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let delta = if suggestion.indent { + snapshot.indent_size + } else { + 0 + }; let new_indentation = indent_columns .get(&suggestion.basis_row) .copied() @@ -1868,6 +1880,10 @@ impl BufferSnapshot { pub fn file_update_count(&self) -> usize { self.file_update_count } + + pub fn indent_size(&self) -> u32 { + self.indent_size + } } impl Clone for BufferSnapshot { @@ -1881,9 +1897,9 @@ impl Clone for BufferSnapshot { selections_update_count: self.selections_update_count, diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, - is_parsing: self.is_parsing, language: self.language.clone(), parse_count: self.parse_count, + indent_size: self.indent_size, } } }