Introduce a `Tab` action to indent line or insert soft tabs

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/buffer/src/lib.rs | 19 +++++++++++-----
crates/editor/src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+), 7 deletions(-)

Detailed changes

crates/buffer/src/lib.rs 🔗

@@ -1086,12 +1086,15 @@ impl Buffer {
 
         let mut prev_row = prev_non_blank_row.unwrap_or(0);
         let mut prev_indent_column =
-            prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row, cx));
+            prev_non_blank_row.map_or(0, |prev_row| self.indent_column_for_line(prev_row));
         for row in row_range {
             let request = autoindent_requests.get(&row).unwrap();
-            let row_start = Point::new(row, self.indent_column_for_line(row, cx));
+            let row_start = Point::new(row, self.indent_column_for_line(row));
 
-            eprintln!("autoindent row: {:?}", row);
+            eprintln!(
+                "autoindent row: {:?}, prev_indent_column: {:?}",
+                row, prev_indent_column
+            );
 
             let mut increase_from_prev_row = false;
             let mut dedent_to_row = u32::MAX;
@@ -1114,7 +1117,7 @@ impl Buffer {
             if increase_from_prev_row {
                 indent_column += request.indent_size as u32;
             } else if dedent_to_row < row {
-                indent_column = self.indent_column_for_line(dedent_to_row, cx);
+                indent_column = self.indent_column_for_line(dedent_to_row);
             }
 
             self.set_indent_column_for_line(row, indent_column, cx);
@@ -1133,7 +1136,7 @@ impl Buffer {
         None
     }
 
-    fn indent_column_for_line(&mut self, row: u32, cx: &mut ModelContext<Self>) -> u32 {
+    pub fn indent_column_for_line(&self, row: u32) -> u32 {
         let mut result = 0;
         for c in self.chars_at(Point::new(row, 0)) {
             if c == ' ' {
@@ -1146,7 +1149,7 @@ impl Buffer {
     }
 
     fn set_indent_column_for_line(&mut self, row: u32, column: u32, cx: &mut ModelContext<Self>) {
-        let current_column = self.indent_column_for_line(row, cx);
+        let current_column = self.indent_column_for_line(row);
         if column > current_column {
             let offset = self.visible_text.to_offset(Point::new(row, 0));
 
@@ -1346,6 +1349,10 @@ impl Buffer {
         self.visible_text.chars_at(offset)
     }
 
+    pub fn chars_for_range<T: ToOffset>(&self, range: Range<T>) -> impl Iterator<Item = char> + '_ {
+        self.text_for_range(range).flat_map(str::chars)
+    }
+
     pub fn bytes_at<T: ToOffset>(&self, position: T) -> impl Iterator<Item = u8> + '_ {
         let offset = position.to_offset(self);
         self.visible_text.bytes_at(offset)

crates/editor/src/lib.rs 🔗

@@ -38,6 +38,7 @@ action!(Cancel);
 action!(Backspace);
 action!(Delete);
 action!(Input, String);
+action!(Tab);
 action!(DeleteLine);
 action!(DeleteToPreviousWordBoundary);
 action!(DeleteToNextWordBoundary);
@@ -101,7 +102,7 @@ pub fn init(cx: &mut MutableAppContext) {
             Input("\n".into()),
             Some("Editor && mode == auto_height"),
         ),
-        Binding::new("tab", Input("\t".into()), Some("Editor")),
+        Binding::new("tab", Tab, Some("Editor")),
         Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
         Binding::new(
             "alt-backspace",
@@ -195,6 +196,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::handle_input);
     cx.add_action(Editor::backspace);
     cx.add_action(Editor::delete);
+    cx.add_action(Editor::tab);
     cx.add_action(Editor::delete_line);
     cx.add_action(Editor::delete_to_previous_word_boundary);
     cx.add_action(Editor::delete_to_next_word_boundary);
@@ -962,6 +964,51 @@ impl Editor {
         self.end_transaction(cx);
     }
 
+    pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        self.start_transaction(cx);
+        let tab_size = self.build_settings.borrow()(cx).tab_size;
+        let mut selections = self.selections(cx).to_vec();
+        self.buffer.update(cx, |buffer, cx| {
+            let mut last_indented_row = None;
+            for selection in &mut selections {
+                let mut range = selection.point_range(buffer);
+                if range.is_empty() {
+                    let char_column = buffer
+                        .chars_for_range(Point::new(range.start.row, 0)..range.start)
+                        .count();
+                    let chars_to_next_tab_stop = tab_size - (char_column % tab_size);
+                    buffer.edit(
+                        [range.start..range.start],
+                        " ".repeat(chars_to_next_tab_stop),
+                        cx,
+                    );
+                    range.start.column += chars_to_next_tab_stop as u32;
+
+                    let head = buffer.anchor_before(range.start);
+                    selection.start = head.clone();
+                    selection.end = head;
+                } else {
+                    for row in range.start.row..=range.end.row {
+                        if last_indented_row != Some(row) {
+                            let char_column = buffer.indent_column_for_line(row) as usize;
+                            let chars_to_next_tab_stop = tab_size - (char_column % tab_size);
+                            let row_start = Point::new(row, 0);
+                            buffer.edit(
+                                [row_start..row_start],
+                                " ".repeat(chars_to_next_tab_stop),
+                                cx,
+                            );
+                            last_indented_row = Some(row);
+                        }
+                    }
+                }
+            }
+        });
+
+        self.update_selections(selections, true, cx);
+        self.end_transaction(cx);
+    }
+
     pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
         self.start_transaction(cx);