Indent instead of accepting suggestion if cursor is in leading whitespace

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs       | 19 ++++++++++----
crates/editor/src/editor_tests.rs | 42 +++++++++++++++++++++++++++++---
2 files changed, 51 insertions(+), 10 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -3230,10 +3230,6 @@ impl Editor {
     }
 
     pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
-        if self.accept_copilot_suggestion(cx) {
-            return;
-        }
-
         if self.move_to_next_snippet_tabstop(cx) {
             return;
         }
@@ -3263,8 +3259,8 @@ impl Editor {
             // If the selection is empty and the cursor is in the leading whitespace before the
             // suggested indentation, then auto-indent the line.
             let cursor = selection.head();
+            let current_indent = snapshot.indent_size_for_line(cursor.row);
             if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
-                let current_indent = snapshot.indent_size_for_line(cursor.row);
                 if cursor.column < suggested_indent.len
                     && cursor.column <= current_indent.len
                     && current_indent.len <= suggested_indent.len
@@ -3283,6 +3279,16 @@ impl Editor {
                 }
             }
 
+            // Accept copilot suggestion if there is only one selection and the cursor is
+            // in the leading whitespace.
+            if self.selections.count() == 1
+                && selection.start.column >= current_indent.len
+                && self.has_active_copilot_suggestion(cx)
+            {
+                self.accept_copilot_suggestion(cx);
+                return;
+            }
+
             // Otherwise, insert a hard or soft tab.
             let settings = cx.global::<Settings>();
             let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
@@ -3306,7 +3312,8 @@ impl Editor {
 
         self.transact(cx, |this, cx| {
             this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
-            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections))
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+            this.refresh_copilot_suggestions(cx);
         });
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -5881,7 +5881,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
     );
 }
 
-#[gpui::test]
+#[gpui::test(iterations = 10)]
 async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
     let (copilot, copilot_lsp) = Copilot::fake(cx);
     cx.update(|cx| cx.set_global(copilot));
@@ -5918,7 +5918,6 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
         &copilot_lsp,
         vec![copilot::request::Completion {
             text: "copilot1".into(),
-            position: lsp::Position::new(0, 5),
             range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
             ..Default::default()
         }],
@@ -5962,7 +5961,6 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
         &copilot_lsp,
         vec![copilot::request::Completion {
             text: "one.copilot1".into(),
-            position: lsp::Position::new(0, 4),
             range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
             ..Default::default()
         }],
@@ -5996,7 +5994,6 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
         &copilot_lsp,
         vec![copilot::request::Completion {
             text: "one.copilot2".into(),
-            position: lsp::Position::new(0, 5),
             range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
             ..Default::default()
         }],
@@ -6062,6 +6059,43 @@ async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppC
         assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
         assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
     });
+
+    // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
+    cx.update_editor(|editor, cx| {
+        editor.set_text("fn foo() {\n  \n}", cx);
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
+        });
+    });
+    handle_copilot_completion_request(
+        &copilot_lsp,
+        vec![copilot::request::Completion {
+            text: "    let x = 4;".into(),
+            range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
+            ..Default::default()
+        }],
+        vec![],
+    );
+
+    cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
+    deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+    cx.update_editor(|editor, cx| {
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
+        assert_eq!(editor.text(cx), "fn foo() {\n  \n}");
+
+        // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
+        editor.tab(&Default::default(), cx);
+        assert!(editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.text(cx), "fn foo() {\n    \n}");
+        assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
+
+        // Tabbing again accepts the suggestion.
+        editor.tab(&Default::default(), cx);
+        assert!(!editor.has_active_copilot_suggestion(cx));
+        assert_eq!(editor.text(cx), "fn foo() {\n    let x = 4;\n}");
+        assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
+    });
 }
 
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {