Require `alt-tab` for `AcceptEditPrediction` when `tab` inserting whitespace is desired (#24705)

Michael Sloan , Ben , and Joao created

Moves tab whitespace insertion logic out of `AcceptEditPrediction`
handler.

`edit_prediction_requires_modifier` context will now be true when on a
line with leading whitespace, so that `alt-tab` is used to accept
predictions in this case. This way leading indentation can be typed when
edit predictions are visible

Release Notes:

- N/A

Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Joao <joao@zed.dev>

Change summary

crates/editor/src/editor.rs                  | 30 ++++--------
crates/editor/src/inline_completion_tests.rs | 51 ---------------------
crates/multi_buffer/src/multi_buffer.rs      | 15 ++++++
3 files changed, 27 insertions(+), 69 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -709,6 +709,7 @@ pub struct Editor {
     /// Used to prevent flickering as the user types while the menu is open
     stale_inline_completion_in_menu: Option<InlineCompletionState>,
     edit_prediction_settings: EditPredictionSettings,
+    edit_prediction_cursor_on_leading_whitespace: bool,
     inline_completions_hidden_for_vim_mode: bool,
     show_inline_completions_override: Option<bool>,
     menu_inline_completions_policy: MenuInlineCompletionsPolicy,
@@ -1423,6 +1424,7 @@ impl Editor {
             show_inline_completions_override: None,
             menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider,
             edit_prediction_settings: EditPredictionSettings::Disabled,
+            edit_prediction_cursor_on_leading_whitespace: false,
             custom_context_menu: None,
             show_git_blame_gutter: false,
             show_git_blame_inline: false,
@@ -1567,8 +1569,12 @@ impl Editor {
         if has_active_edit_prediction {
             key_context.add("copilot_suggestion");
             key_context.add(EDIT_PREDICTION_KEY_CONTEXT);
-
-            if showing_completions || self.edit_prediction_requires_modifier() {
+            if showing_completions
+                || self.edit_prediction_requires_modifier()
+                // Require modifier key when the cursor is on leading whitespace, to allow `tab`
+                // bindings to insert tab characters.
+                || self.edit_prediction_cursor_on_leading_whitespace
+            {
                 key_context.add(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT);
             }
         }
@@ -4931,23 +4937,6 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<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(), window, cx);
-                return;
-            }
-        }
-
         if self.show_edit_predictions_in_menu() {
             self.hide_context_menu(window, cx);
         }
@@ -5298,6 +5287,9 @@ impl Editor {
             return None;
         }
 
+        self.edit_prediction_cursor_on_leading_whitespace =
+            multibuffer.is_line_whitespace_upto(cursor);
+
         let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
         let edits = inline_completion
             .edits

crates/editor/src/inline_completion_tests.rs 🔗

@@ -1,10 +1,9 @@
 use gpui::{prelude::*, Entity};
 use indoc::indoc;
 use inline_completion::EditPredictionProvider;
-use language::{Language, LanguageConfig};
 use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
 use project::Project;
-use std::{num::NonZeroU32, ops::Range, sync::Arc};
+use std::ops::Range;
 use text::{Point, ToOffset};
 
 use crate::{
@@ -124,54 +123,6 @@ 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(|_| 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, window, cx| editor.update_visible_inline_completion(window, 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, |_| {});

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -4236,6 +4236,21 @@ impl MultiBufferSnapshot {
         indent
     }
 
+    pub fn is_line_whitespace_upto<T>(&self, position: T) -> bool
+    where
+        T: ToOffset,
+    {
+        for char in self.reversed_chars_at(position) {
+            if !char.is_whitespace() {
+                return false;
+            }
+            if char == '\n' {
+                return true;
+            }
+        }
+        return true;
+    }
+
     pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option<MultiBufferRow> {
         while row.0 > 0 {
             row.0 -= 1;