edit predictions: Cache settings across renders (#24581)

Agus Zubiaga created

We were reading edit prediction settings too often, causing frames to be
dropped. We'll now cache them and update them from
`update_visible_inline_completion`.

Release Notes:

- N/A

Change summary

crates/assistant/src/inline_assistant.rs                        |   2 
crates/assistant2/src/inline_assistant.rs                       |   2 
crates/editor/src/editor.rs                                     | 193 +-
crates/editor/src/element.rs                                    |   9 
crates/feedback/src/feedback_modal.rs                           |   2 
crates/inline_completion_button/src/inline_completion_button.rs |   2 
crates/language_tools/src/lsp_log.rs                            |   4 
crates/prompt_library/src/prompt_library.rs                     |   4 
crates/zed/src/zed/quick_action_bar.rs                          |   4 
crates/zeta/src/rate_completion_modal.rs                        |   2 
10 files changed, 123 insertions(+), 101 deletions(-)

Detailed changes

crates/assistant/src/inline_assistant.rs 🔗

@@ -1255,7 +1255,7 @@ impl InlineAssistant {
                     editor.scroll_manager.set_forbid_vertical_scroll(true);
                     editor.set_show_scrollbars(false, cx);
                     editor.set_read_only(true);
-                    editor.set_show_inline_completions(Some(false), window, cx);
+                    editor.set_show_edit_predictions(Some(false), window, cx);
                     editor.highlight_rows::<DeletedLines>(
                         Anchor::min()..Anchor::max(),
                         cx.theme().status().deleted_background,

crates/assistant2/src/inline_assistant.rs 🔗

@@ -1345,7 +1345,7 @@ impl InlineAssistant {
                     editor.scroll_manager.set_forbid_vertical_scroll(true);
                     editor.set_show_scrollbars(false, cx);
                     editor.set_read_only(true);
-                    editor.set_show_inline_completions(Some(false), window, cx);
+                    editor.set_show_edit_predictions(Some(false), window, cx);
                     editor.highlight_rows::<DeletedLines>(
                         Anchor::min()..Anchor::max(),
                         cx.theme().status().deleted_background,

crates/editor/src/editor.rs 🔗

@@ -497,6 +497,23 @@ struct InlineCompletionState {
     invalidation_range: Range<Anchor>,
 }
 
+enum EditPredictionSettings {
+    Disabled,
+    Enabled {
+        show_in_menu: bool,
+        preview_requires_modifier: bool,
+    },
+}
+
+impl EditPredictionSettings {
+    pub fn is_enabled(&self) -> bool {
+        match self {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled { .. } => true,
+        }
+    }
+}
+
 enum InlineCompletionHighlight {}
 
 pub enum MenuInlineCompletionsPolicy {
@@ -683,6 +700,7 @@ pub struct Editor {
     active_inline_completion: Option<InlineCompletionState>,
     /// Used to prevent flickering as the user types while the menu is open
     stale_inline_completion_in_menu: Option<InlineCompletionState>,
+    edit_prediction_settings: EditPredictionSettings,
     inline_completions_hidden_for_vim_mode: bool,
     show_inline_completions_override: Option<bool>,
     menu_inline_completions_policy: MenuInlineCompletionsPolicy,
@@ -1395,6 +1413,7 @@ impl Editor {
             inline_completions_hidden_for_vim_mode: false,
             show_inline_completions_override: None,
             menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider,
+            edit_prediction_settings: EditPredictionSettings::Disabled,
             custom_context_menu: None,
             show_git_blame_gutter: false,
             show_git_blame_inline: false,
@@ -1530,7 +1549,7 @@ impl Editor {
             key_context.add("copilot_suggestion");
             key_context.add("edit_prediction");
 
-            if showing_completions || self.edit_prediction_requires_modifier(cx) {
+            if showing_completions || self.edit_prediction_requires_modifier() {
                 key_context.add(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT);
             }
         }
@@ -1886,23 +1905,14 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         if self.show_inline_completions_override.is_some() {
-            self.set_show_inline_completions(None, window, cx);
+            self.set_show_edit_predictions(None, window, cx);
         } else {
-            let cursor = self.selections.newest_anchor().head();
-            if let Some((buffer, cursor_buffer_position)) =
-                self.buffer.read(cx).text_anchor_for_position(cursor, cx)
-            {
-                let show_inline_completions = !self.should_show_inline_completions_in_buffer(
-                    &buffer,
-                    cursor_buffer_position,
-                    cx,
-                );
-                self.set_show_inline_completions(Some(show_inline_completions), window, cx);
-            }
+            let show_edit_predictions = !self.edit_predictions_enabled();
+            self.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
         }
     }
 
-    pub fn set_show_inline_completions(
+    pub fn set_show_edit_predictions(
         &mut self,
         show_edit_predictions: Option<bool>,
         window: &mut Window,
@@ -3019,7 +3029,7 @@ impl Editor {
             }
 
             let trigger_in_words =
-                this.show_edit_predictions_in_menu(cx) || !had_active_inline_completion;
+                this.show_edit_predictions_in_menu() || !had_active_inline_completion;
             this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
             linked_editing_ranges::refresh_linked_ranges(this, window, cx);
             this.refresh_inline_completion(true, false, window, cx);
@@ -3912,7 +3922,7 @@ impl Editor {
                         *editor.context_menu.borrow_mut() =
                             Some(CodeContextMenu::Completions(menu));
 
-                        if editor.show_edit_predictions_in_menu(cx) {
+                        if editor.show_edit_predictions_in_menu() {
                             editor.update_visible_inline_completion(window, cx);
                         } else {
                             editor.discard_inline_completion(false, cx);
@@ -3926,7 +3936,7 @@ impl Editor {
                         // If it was already hidden and we don't show inline
                         // completions in the menu, we should also show the
                         // inline-completion when available.
-                        if was_hidden && editor.show_edit_predictions_in_menu(cx) {
+                        if was_hidden && editor.show_edit_predictions_in_menu() {
                             editor.update_visible_inline_completion(window, cx);
                         }
                     }
@@ -3976,7 +3986,7 @@ impl Editor {
 
         let entries = completions_menu.entries.borrow();
         let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
-        if self.show_edit_predictions_in_menu(cx) {
+        if self.show_edit_predictions_in_menu() {
             self.discard_inline_completion(true, cx);
         }
         let candidate_id = mat.candidate_id;
@@ -4668,7 +4678,7 @@ impl Editor {
         }
 
         if !user_requested
-            && (!self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx)
+            && (!self.should_show_edit_predictions()
                 || !self.is_focused(window)
                 || buffer.read(cx).is_empty())
         {
@@ -4687,58 +4697,79 @@ impl Editor {
         Some(())
     }
 
-    pub fn should_show_inline_completions(&self, cx: &App) -> bool {
-        let cursor = self.selections.newest_anchor().head();
-        if let Some((buffer, cursor_position)) =
-            self.buffer.read(cx).text_anchor_for_position(cursor, cx)
-        {
-            self.should_show_inline_completions_in_buffer(&buffer, cursor_position, cx)
-        } else {
-            false
+    fn show_edit_predictions_in_menu(&self) -> bool {
+        match self.edit_prediction_settings {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu,
         }
     }
 
-    fn edit_prediction_requires_modifier(&self, cx: &App) -> bool {
-        let cursor = self.selections.newest_anchor().head();
+    pub fn edit_predictions_enabled(&self) -> bool {
+        match self.edit_prediction_settings {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled { .. } => true,
+        }
+    }
 
-        self.buffer
-            .read(cx)
-            .text_anchor_for_position(cursor, cx)
-            .map(|(buffer, _)| {
-                all_language_settings(buffer.read(cx).file(), cx).inline_completions_preview_mode()
-                    == InlineCompletionPreviewMode::WhenHoldingModifier
-            })
-            .unwrap_or(false)
+    fn edit_prediction_requires_modifier(&self) -> bool {
+        match self.edit_prediction_settings {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled {
+                preview_requires_modifier,
+                ..
+            } => preview_requires_modifier,
+        }
     }
 
-    fn should_show_inline_completions_in_buffer(
+    fn edit_prediction_settings_at_position(
         &self,
         buffer: &Entity<Buffer>,
         buffer_position: language::Anchor,
         cx: &App,
-    ) -> bool {
-        if !self.snippet_stack.is_empty() {
-            return false;
+    ) -> EditPredictionSettings {
+        if self.mode != EditorMode::Full
+            || !self.show_inline_completions_override.unwrap_or(true)
+            || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx)
+        {
+            return EditPredictionSettings::Disabled;
         }
 
-        if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) {
-            return false;
-        }
+        let buffer = buffer.read(cx);
 
-        if let Some(show_inline_completions) = self.show_inline_completions_override {
-            show_inline_completions
-        } else {
-            let buffer = buffer.read(cx);
-            self.mode == EditorMode::Full
-                && language_settings(
-                    buffer.language_at(buffer_position).map(|l| l.name()),
-                    buffer.file(),
-                    cx,
-                )
-                .show_edit_predictions
+        let file = buffer.file();
+
+        if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions {
+            return EditPredictionSettings::Disabled;
+        };
+
+        let by_provider = matches!(
+            self.menu_inline_completions_policy,
+            MenuInlineCompletionsPolicy::ByProvider
+        );
+
+        let show_in_menu = by_provider
+            && EditorSettings::get_global(cx).show_edit_predictions_in_menu
+            && self
+                .edit_prediction_provider
+                .as_ref()
+                .map_or(false, |provider| {
+                    provider.provider.show_completions_in_menu()
+                });
+
+        let preview_requires_modifier = all_language_settings(file, cx)
+            .inline_completions_preview_mode()
+            == InlineCompletionPreviewMode::WhenHoldingModifier;
+
+        EditPredictionSettings::Enabled {
+            show_in_menu,
+            preview_requires_modifier,
         }
     }
 
+    fn should_show_edit_predictions(&self) -> bool {
+        self.snippet_stack.is_empty() && self.edit_predictions_enabled()
+    }
+
     pub fn inline_completions_enabled(&self, cx: &App) -> bool {
         let cursor = self.selections.newest_anchor().head();
         if let Some((buffer, cursor_position)) =
@@ -4781,9 +4812,7 @@ impl Editor {
         let cursor = self.selections.newest_anchor().head();
         let (buffer, cursor_buffer_position) =
             self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
-        if self.inline_completions_hidden_for_vim_mode
-            || !self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx)
-        {
+        if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
             return None;
         }
 
@@ -4889,7 +4918,7 @@ impl Editor {
             }
         }
 
-        if self.show_edit_predictions_in_menu(cx) {
+        if self.show_edit_predictions_in_menu() {
             self.hide_context_menu(window, cx);
         }
 
@@ -5076,14 +5105,10 @@ impl Editor {
     /// Returns true when we're displaying the inline completion popover below the cursor
     /// like we are not previewing and the LSP autocomplete menu is visible
     /// or we are in `when_holding_modifier` mode.
-    pub fn inline_completion_visible_in_cursor_popover(
-        &self,
-        has_completion: bool,
-        cx: &App,
-    ) -> bool {
+    pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool {
         if self.previewing_inline_completion
-            || !self.show_edit_predictions_in_menu(cx)
-            || !self.should_show_inline_completions(cx)
+            || !self.show_edit_predictions_in_menu()
+            || !self.edit_predictions_enabled()
         {
             return false;
         }
@@ -5092,7 +5117,7 @@ impl Editor {
             return true;
         }
 
-        has_completion && self.edit_prediction_requires_modifier(cx)
+        has_completion && self.edit_prediction_requires_modifier()
     }
 
     fn handle_modifiers_changed(
@@ -5102,7 +5127,7 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if self.show_edit_predictions_in_menu(cx) {
+        if self.show_edit_predictions_in_menu() {
             let accept_binding =
                 AcceptEditPredictionBinding::resolve(self.focus_handle(cx), window);
             if let Some(accept_keystroke) = accept_binding.keystroke() {
@@ -5140,10 +5165,11 @@ impl Editor {
         let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
         let excerpt_id = cursor.excerpt_id;
 
-        let show_in_menu = self.show_edit_predictions_in_menu(cx);
+        let show_in_menu = self.show_edit_predictions_in_menu();
         let completions_menu_has_precedence = !show_in_menu
             && (self.context_menu.borrow().is_some()
                 || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
+
         if completions_menu_has_precedence
             || !offset_selection.is_empty()
             || self
@@ -5160,11 +5186,22 @@ impl Editor {
         }
 
         self.take_active_inline_completion(cx);
-        let provider = self.edit_prediction_provider()?;
+        let Some(provider) = self.edit_prediction_provider() else {
+            self.edit_prediction_settings = EditPredictionSettings::Disabled;
+            return None;
+        };
 
         let (buffer, cursor_buffer_position) =
             self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
 
+        self.edit_prediction_settings =
+            self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
+
+        if !self.edit_prediction_settings.is_enabled() {
+            self.discard_inline_completion(false, cx);
+            return None;
+        }
+
         let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
         let edits = inline_completion
             .edits
@@ -5223,8 +5260,7 @@ impl Editor {
                 snapshot,
             }
         } else {
-            let show_completions_in_buffer = !self
-                .inline_completion_visible_in_cursor_popover(true, cx)
+            let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true)
                 && !self.inline_completions_hidden_for_vim_mode;
             if show_completions_in_buffer {
                 if edits
@@ -5300,19 +5336,6 @@ impl Editor {
         Some(self.edit_prediction_provider.as_ref()?.provider.clone())
     }
 
-    fn show_edit_predictions_in_menu(&self, cx: &App) -> bool {
-        let by_provider = matches!(
-            self.menu_inline_completions_policy,
-            MenuInlineCompletionsPolicy::ByProvider
-        );
-
-        by_provider
-            && EditorSettings::get_global(cx).show_edit_predictions_in_menu
-            && self
-                .edit_prediction_provider()
-                .map_or(false, |provider| provider.show_completions_in_menu())
-    }
-
     fn render_code_actions_indicator(
         &self,
         _style: &EditorStyle,

crates/editor/src/element.rs 🔗

@@ -3088,10 +3088,9 @@ impl EditorElement {
 
         {
             let editor = self.editor.read(cx);
-            if editor.inline_completion_visible_in_cursor_popover(
-                editor.has_active_inline_completion(),
-                cx,
-            ) {
+            if editor
+                .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion())
+            {
                 height_above_menu +=
                     editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
                 edit_prediction_popover_visible = true;
@@ -3557,7 +3556,7 @@ impl EditorElement {
         let editor = self.editor.read(cx);
         let active_inline_completion = editor.active_inline_completion.as_ref()?;
 
-        if editor.inline_completion_visible_in_cursor_popover(true, cx) {
+        if editor.edit_prediction_visible_in_cursor_popover(true) {
             return None;
         }
 

crates/feedback/src/feedback_modal.rs 🔗

@@ -191,7 +191,7 @@ impl FeedbackModal {
             );
             editor.set_show_gutter(false, cx);
             editor.set_show_indent_guides(false, cx);
-            editor.set_show_inline_completions(Some(false), window, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
             editor.set_vertical_scroll_margin(5, cx);
             editor.set_use_modal_editing(false);
             editor.set_soft_wrap();

crates/inline_completion_button/src/inline_completion_button.rs 🔗

@@ -611,7 +611,7 @@ impl InlineCompletionButton {
                 .unwrap_or(true),
             )
         };
-        self.editor_show_predictions = editor.should_show_inline_completions(cx);
+        self.editor_show_predictions = editor.edit_predictions_enabled();
         self.edit_prediction_provider = editor.edit_prediction_provider();
         self.language = language.cloned();
         self.file = file;

crates/language_tools/src/lsp_log.rs 🔗

@@ -708,7 +708,7 @@ impl LspLogView {
             editor.set_text(log_contents, window, cx);
             editor.move_to_end(&MoveToEnd, window, cx);
             editor.set_read_only(true);
-            editor.set_show_inline_completions(Some(false), window, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor
         });
@@ -751,7 +751,7 @@ impl LspLogView {
             );
             editor.set_text(server_info, window, cx);
             editor.set_read_only(true);
-            editor.set_show_inline_completions(Some(false), window, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor
         });

crates/prompt_library/src/prompt_library.rs 🔗

@@ -563,7 +563,7 @@ impl PromptLibrary {
                             editor.set_text(prompt_metadata.title.unwrap_or_default(), window, cx);
                             if prompt_id.is_built_in() {
                                 editor.set_read_only(true);
-                                editor.set_show_inline_completions(Some(false), window, cx);
+                                editor.set_show_edit_predictions(Some(false), window, cx);
                             }
                             editor
                         });
@@ -578,7 +578,7 @@ impl PromptLibrary {
                             let mut editor = Editor::for_buffer(buffer, None, window, cx);
                             if prompt_id.is_built_in() {
                                 editor.set_read_only(true);
-                                editor.set_show_inline_completions(Some(false), window, cx);
+                                editor.set_show_edit_predictions(Some(false), window, cx);
                             }
                             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
                             editor.set_show_gutter(false, cx);

crates/zed/src/zed/quick_action_bar.rs 🔗

@@ -104,7 +104,7 @@ impl Render for QuickActionBar {
             let git_blame_inline_enabled = editor.git_blame_inline_enabled();
             let show_git_blame_gutter = editor.show_git_blame_gutter();
             let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
-            let show_inline_completions = editor.should_show_inline_completions(cx);
+            let show_edit_predictions = editor.edit_predictions_enabled();
             let inline_completion_enabled = editor.inline_completions_enabled(cx);
 
             (
@@ -114,7 +114,7 @@ impl Render for QuickActionBar {
                 git_blame_inline_enabled,
                 show_git_blame_gutter,
                 auto_signature_help_enabled,
-                show_inline_completions,
+                show_edit_predictions,
                 inline_completion_enabled,
             )
         };

crates/zeta/src/rate_completion_modal.rs 🔗

@@ -279,7 +279,7 @@ impl RateCompletionModal {
                 editor.set_show_runnables(false, cx);
                 editor.set_show_wrap_guides(false, cx);
                 editor.set_show_indent_guides(false, cx);
-                editor.set_show_inline_completions(Some(false), window, cx);
+                editor.set_show_edit_predictions(Some(false), window, cx);
                 editor.set_placeholder_text("Add your feedback…", cx);
                 if focus {
                     cx.focus_self(window);