Add tests and fix bugs for editor indent/outdent commands w/ hard tabs

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs   | 123 +++++++++++++++++++++++++++++++++---
crates/editor/src/test.rs     |  17 ++--
crates/language/src/buffer.rs |  14 ++--
crates/vim/src/vim.rs         |   2 
4 files changed, 127 insertions(+), 29 deletions(-)

Detailed changes

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::<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);
+                        let settings = cx.global::<Settings>();
+                        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::<String>(),
                             )],
                             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::<Settings>().tab_size(language_name.as_deref());
+                    let settings = &cx.global::<Settings>();
+                    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, _| {
+                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(

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<String>> =
             set_eq!(expected_empty_selections, empty_selections)
                 .map_err(|err| {
@@ -322,7 +323,7 @@ impl<'a> EditorTestContext<'a> {
         reverse_selections: &Vec<Range<usize>>,
         forward_selections: &Vec<Range<usize>>,
     ) -> 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, '|');

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
 }

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