Improve handling tab when inline completion is visible (#22892)

Thorsten Ball and Antonio created

This changes the behaviour of `<tab>` when inline completion is visible.
When the cursor is before the suggested indentation level, accepting a
completion should just indent.

cc @nathansobo @maxdeviant 

Release Notes:

- Changed the behavior of `<tab>` at start of line when an inline
completion (Copilot, Supermaven, ...) is visible. If the cursor is
before the suggested indentation, `<tab>` now indents the line instead
of accepting the visible completion.

Co-authored-by: Antonio <antonio@zed.dev>

Change summary

crates/editor/src/editor.rs                  | 17 +++++++
crates/editor/src/inline_completion_tests.rs | 51 +++++++++++++++++++++
2 files changed, 67 insertions(+), 1 deletion(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -4585,6 +4585,23 @@ impl Editor {
         _: &AcceptInlineCompletion,
         cx: &mut ViewContext<Self>,
     ) {
+        let buffer = self.buffer.read(cx);
+        let snapshot = buffer.snapshot(cx);
+        let selection = self.selections.newest_adjusted(cx);
+        let cursor = selection.head();
+        let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row));
+        let suggested_indents = snapshot.suggested_indents([cursor.row], cx);
+        if let Some(suggested_indent) = suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
+        {
+            if cursor.column < suggested_indent.len
+                && cursor.column <= current_indent.len
+                && current_indent.len <= suggested_indent.len
+            {
+                self.tab(&Default::default(), cx);
+                return;
+            }
+        }
+
         if self.show_inline_completions_in_menu(cx) {
             self.hide_context_menu(cx);
         }

crates/editor/src/inline_completion_tests.rs 🔗

@@ -1,8 +1,9 @@
 use gpui::{prelude::*, Model};
 use indoc::indoc;
 use inline_completion::InlineCompletionProvider;
+use language::{Language, LanguageConfig};
 use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
-use std::ops::Range;
+use std::{num::NonZeroU32, ops::Range, sync::Arc};
 use text::{Point, ToOffset};
 
 use crate::{
@@ -122,6 +123,54 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
     "});
 }
 
+#[gpui::test]
+async fn test_indentation(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::LANGUAGE.into()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
+    assign_editor_completion_provider(provider.clone(), &mut cx);
+
+    cx.set_state(indoc! {"
+        const a: A = (
+        ˇ
+        );
+    "});
+
+    propose_edits(
+        &provider,
+        vec![(Point::new(1, 0)..Point::new(1, 0), "    const function()")],
+        &mut cx,
+    );
+    cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
+
+    assert_editor_active_edit_completion(&mut cx, |_, edits| {
+        assert_eq!(edits.len(), 1);
+        assert_eq!(edits[0].1.as_str(), "    const function()");
+    });
+
+    // When the cursor is before the suggested indentation level, accepting a
+    // completion should just indent.
+    accept_completion(&mut cx);
+    cx.assert_editor_state(indoc! {"
+        const a: A = (
+            ˇ
+        );
+    "});
+}
+
 #[gpui::test]
 async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});