diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 14cd5dfe4c..f141a9b6e3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3126,21 +3126,27 @@ impl Editor { for selection in &mut selections { let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let tab_size = cx.global::().tab_size(language_name.as_deref()); - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); + let settings = cx.global::(); + let tab_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + let tab_size = settings.tab_size(language_name.as_deref()); + let char_column = buffer + .read(cx) + .text_for_range(Point::new(selection.start.row, 0)..selection.start) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); + IndentSize::spaces(chars_to_next_tab_stop) + }; buffer.edit( [( selection.start..selection.start, - " ".repeat(chars_to_next_tab_stop as usize), + tab_size.chars().collect::(), )], cx, ); - selection.start.column += chars_to_next_tab_stop; + selection.start.column += tab_size.len; selection.end = selection.start; } }); @@ -3161,7 +3167,14 @@ impl Editor { let snapshot = buffer.snapshot(cx); for selection in &mut selections { let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let tab_size = cx.global::().tab_size(language_name.as_deref()); + let settings = &cx.global::(); + let tab_size = settings.tab_size(language_name.as_deref()); + let indent_kind = if settings.hard_tabs(language_name.as_deref()) { + IndentKind::Tab + } else { + IndentKind::Space + }; + let mut start_row = selection.start.row; let mut end_row = selection.end.row + 1; @@ -3186,14 +3199,16 @@ impl Editor { for row in start_row..end_row { let current_indent = snapshot.indent_size_for_line(row); - let indent_delta = match current_indent.kind { - IndentKind::Space => { + let indent_delta = match (current_indent.kind, indent_kind) { + (IndentKind::Space, IndentKind::Space) => { let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); IndentSize::spaces(columns_to_next_tab_stop) } - IndentKind::Tab => IndentSize::tab(), + (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), + (_, IndentKind::Tab) => IndentSize::tab(), }; + let row_start = Point::new(row, 0); buffer.edit( [( @@ -7696,6 +7711,88 @@ mod tests { four"}); } + #[gpui::test] + async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + cx.update(|cx| { + cx.update_global::(|settings, _| { + settings.hard_tabs = true; + }); + }); + + // select two ranges on one line + cx.set_state(indoc! {" + [one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t[one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t\t[one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + \t[one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + [one} [two} + three + four"}); + + // select across a line ending + cx.set_state(indoc! {" + one two + t[hree + }four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt[hree + }four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t\tt[hree + }four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt[hree + }four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t[hree + }four"}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + |three + four"}); + cx.assert_editor_state(indoc! {" + one two + |three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t|three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + |three + four"}); + } + #[gpui::test] fn test_indent_outdent_with_excerpts(cx: &mut gpui::MutableAppContext) { cx.set_global( diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 4c9ceed9ae..0f49576936 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -109,9 +109,10 @@ impl<'a> EditorTestContext<'a> { self.editor.update(self.cx, update) } - pub fn editor_text(&mut self) -> String { - self.editor - .update(self.cx, |editor, cx| editor.snapshot(cx).text()) + pub fn buffer_text(&mut self) -> String { + self.editor.read_with(self.cx, |editor, cx| { + editor.buffer.read(cx).snapshot(cx).text() + }) } pub fn simulate_keystroke(&mut self, keystroke_text: &str) { @@ -171,10 +172,10 @@ impl<'a> EditorTestContext<'a> { &text, vec!['|'.into(), ('[', '}').into(), ('{', ']').into()], ); - let editor_text = self.editor_text(); + let buffer_text = self.buffer_text(); assert_eq!( - editor_text, unmarked_text, - "Unmarked text doesn't match editor text" + buffer_text, unmarked_text, + "Unmarked text doesn't match buffer text" ); let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default(); @@ -254,7 +255,7 @@ impl<'a> EditorTestContext<'a> { let actual_selections = self.insert_markers(&empty_selections, &reverse_selections, &forward_selections); - let unmarked_text = self.editor_text(); + let unmarked_text = self.buffer_text(); let all_eq: Result<(), SetEqError> = set_eq!(expected_empty_selections, empty_selections) .map_err(|err| { @@ -322,7 +323,7 @@ impl<'a> EditorTestContext<'a> { reverse_selections: &Vec>, forward_selections: &Vec>, ) -> String { - let mut editor_text_with_selections = self.editor_text(); + let mut editor_text_with_selections = self.buffer_text(); let mut selection_marks = BTreeMap::new(); for range in empty_selections { selection_marks.insert(&range.start, '|'); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a1b6bb2127..d420722f49 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2002,15 +2002,15 @@ impl BufferSnapshot { pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { let mut result = IndentSize::spaces(0); for c in text.chars_at(Point::new(row, 0)) { - match (c, &result.kind) { - (' ', IndentKind::Space) | ('\t', IndentKind::Tab) => result.len += 1, - ('\t', IndentKind::Space) => { - if result.len == 0 { - result = IndentSize::tab(); - } - } + let kind = match c { + ' ' => IndentKind::Space, + '\t' => IndentKind::Tab, _ => break, + }; + if result.len == 0 { + result.kind = kind; } + result.len += 1; } result } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index b653f4198f..3f54954ed5 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -202,7 +202,7 @@ mod test { cx.enable_vim(); assert_eq!(cx.mode(), Mode::Normal); cx.simulate_keystrokes(["h", "h", "h", "l"]); - assert_eq!(cx.editor_text(), "hjkl".to_owned()); + assert_eq!(cx.buffer_text(), "hjkl".to_owned()); cx.assert_editor_state("h|jkl"); cx.simulate_keystrokes(["i", "T", "e", "s", "t"]); cx.assert_editor_state("hTest|jkl");