edit predictions: Add binding to the prediction toggle (#24468)

Danilo Leal and Bennet Bo Fenner created

This PR primary goal is to add a keybinding to the (ephemeral)
prediction toggle. In doing that, we also standardized the keybinding to
open the status bar menu with it.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>

Change summary

assets/icons/lock_outlined.svg                                  |   6 
assets/keymaps/default-linux.json                               |   9 
assets/keymaps/default-macos.json                               |  13 
crates/inline_completion_button/src/inline_completion_button.rs |  63 
crates/ui/src/components/context_menu.rs                        |   3 
crates/ui/src/components/icon.rs                                |   1 
crates/zed/src/zed/quick_action_bar.rs                          | 280 +-
7 files changed, 214 insertions(+), 161 deletions(-)

Detailed changes

assets/icons/lock_outlined.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M8 9V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<circle cx="8" cy="9" r="1" fill="black"/>
+<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+</svg>

assets/keymaps/default-linux.json 🔗

@@ -122,7 +122,8 @@
       "ctrl-i": "editor::ShowSignatureHelp",
       "alt-g b": "editor::ToggleGitBlame",
       "menu": "editor::OpenContextMenu",
-      "shift-f10": "editor::OpenContextMenu"
+      "shift-f10": "editor::OpenContextMenu",
+      "ctrl-shift-e": "editor::ToggleEditPrediction"
     }
   },
   {
@@ -535,8 +536,7 @@
   {
     "bindings": {
       "ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
-      "ctrl-alt-i": "zed::DebugElements",
-      "ctrl-:": "editor::ToggleInlayHints"
+      "ctrl-alt-i": "zed::DebugElements"
     }
   },
   {
@@ -554,7 +554,8 @@
       "ctrl-shift-e": "pane::RevealInProjectPanel",
       "ctrl-f8": "editor::GoToHunk",
       "ctrl-shift-f8": "editor::GoToPrevHunk",
-      "ctrl-enter": "assistant::InlineAssist"
+      "ctrl-enter": "assistant::InlineAssist",
+      "ctrl-:": "editor::ToggleInlayHints"
     }
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -39,8 +39,8 @@
       "cmd-m": "zed::Minimize",
       "fn-f": "zed::ToggleFullScreen",
       "ctrl-cmd-f": "zed::ToggleFullScreen",
-      "ctrl-shift-z": "zeta::RateCompletions",
-      "ctrl-shift-i": "edit_prediction::ToggleMenu"
+      "ctrl-cmd-z": "zeta::RateCompletions",
+      "ctrl-cmd-i": "edit_prediction::ToggleMenu"
     }
   },
   {
@@ -132,7 +132,8 @@
       "cmd-alt-g b": "editor::ToggleGitBlame",
       "cmd-i": "editor::ShowSignatureHelp",
       "ctrl-f12": "editor::GoToDeclaration",
-      "alt-ctrl-f12": "editor::GoToDeclarationSplit"
+      "alt-ctrl-f12": "editor::GoToDeclarationSplit",
+      "ctrl-cmd-e": "editor::ToggleEditPrediction"
     }
   },
   {
@@ -619,8 +620,7 @@
       "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
       // TODO: Move this to a dock open action
       "cmd-shift-c": "collab_panel::ToggleFocus",
-      "cmd-alt-i": "zed::DebugElements",
-      "ctrl-:": "editor::ToggleInlayHints"
+      "cmd-alt-i": "zed::DebugElements"
     }
   },
   {
@@ -633,7 +633,8 @@
       "cmd-shift-e": "pane::RevealInProjectPanel",
       "cmd-f8": "editor::GoToHunk",
       "cmd-shift-f8": "editor::GoToPrevHunk",
-      "ctrl-enter": "assistant::InlineAssist"
+      "ctrl-enter": "assistant::InlineAssist",
+      "ctrl-:": "editor::ToggleInlayHints"
     }
   },
   {

crates/inline_completion_button/src/inline_completion_button.rs 🔗

@@ -1,7 +1,11 @@
 use anyhow::Result;
 use client::UserStore;
 use copilot::{Copilot, Status};
-use editor::{actions::ShowEditPrediction, scroll::Autoscroll, Editor};
+use editor::{
+    actions::{ShowEditPrediction, ToggleEditPrediction},
+    scroll::Autoscroll,
+    Editor,
+};
 use feature_flags::{
     FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
 };
@@ -44,6 +48,7 @@ struct CopilotErrorToast;
 pub struct InlineCompletionButton {
     editor_subscription: Option<(Subscription, usize)>,
     editor_enabled: Option<bool>,
+    editor_show_predictions: bool,
     editor_focus_handle: Option<FocusHandle>,
     language: Option<Arc<Language>>,
     file: Option<Arc<dyn File>>,
@@ -275,15 +280,29 @@ impl Render for InlineCompletionButton {
                     );
                 }
 
+                let show_editor_predictions = self.editor_show_predictions;
+
                 let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
                     .shape(IconButtonShape::Square)
+                    .when(enabled && !show_editor_predictions, |this| {
+                        this.indicator(Indicator::dot().color(Color::Muted))
+                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
+                    })
                     .when(!self.popover_menu_handle.is_deployed(), |element| {
-                        if enabled {
-                            element.tooltip(|window, cx| {
-                                Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
-                            })
-                        } else {
-                            element.tooltip(|window, cx| {
+                        element.tooltip(move |window, cx| {
+                            if enabled {
+                                if show_editor_predictions {
+                                    Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
+                                } else {
+                                    Tooltip::with_meta(
+                                        "Edit Prediction",
+                                        Some(&ToggleMenu),
+                                        "Hidden For This File",
+                                        window,
+                                        cx,
+                                    )
+                                }
+                            } else {
                                 Tooltip::with_meta(
                                     "Edit Prediction",
                                     Some(&ToggleMenu),
@@ -291,8 +310,8 @@ impl Render for InlineCompletionButton {
                                     window,
                                     cx,
                                 )
-                            })
-                        }
+                            }
+                        })
                     });
 
                 let this = cx.entity().clone();
@@ -347,6 +366,7 @@ impl InlineCompletionButton {
         Self {
             editor_subscription: None,
             editor_enabled: None,
+            editor_show_predictions: true,
             editor_focus_handle: None,
             language: None,
             file: None,
@@ -384,6 +404,21 @@ impl InlineCompletionButton {
 
         menu = menu.header("Show Edit Predictions For");
 
+        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
+            menu = menu.toggleable_entry(
+                "This File",
+                self.editor_show_predictions,
+                IconPosition::Start,
+                Some(Box::new(ToggleEditPrediction)),
+                {
+                    let editor_focus_handle = editor_focus_handle.clone();
+                    move |window, cx| {
+                        editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx);
+                    }
+                },
+            );
+        }
+
         if let Some(language) = self.language.clone() {
             let fs = fs.clone();
             let language_enabled =
@@ -393,7 +428,7 @@ impl InlineCompletionButton {
             menu = menu.toggleable_entry(
                 language.name(),
                 language_enabled,
-                IconPosition::End,
+                IconPosition::Start,
                 None,
                 move |_, cx| {
                     toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx)
@@ -406,7 +441,7 @@ impl InlineCompletionButton {
         menu = menu.toggleable_entry(
             "All Files",
             globally_enabled,
-            IconPosition::End,
+            IconPosition::Start,
             None,
             move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
         );
@@ -422,7 +457,7 @@ impl InlineCompletionButton {
                     // TODO: We want to add something later that communicates whether
                     // the current project is open-source.
                     ContextMenuEntry::new("Share Training Data")
-                        .toggleable(IconPosition::End, data_collection.is_enabled())
+                        .toggleable(IconPosition::Start, data_collection.is_enabled())
                         .documentation_aside(|_| {
                             Label::new(indoc!{"
                                 Help us improve our open model by sharing data from open source repositories. \
@@ -450,6 +485,8 @@ impl InlineCompletionButton {
 
         menu = menu.item(
             ContextMenuEntry::new("Configure Excluded Files")
+                .icon(IconName::LockOutlined)
+                .icon_color(Color::Muted)
                 .documentation_aside(|_| {
                     Label::new(indoc!{"
                         Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
@@ -486,7 +523,6 @@ impl InlineCompletionButton {
                     Some(Box::new(ShowEditPrediction)),
                     {
                         let editor_focus_handle = editor_focus_handle.clone();
-
                         move |window, cx| {
                             editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
                         }
@@ -571,6 +607,7 @@ impl InlineCompletionButton {
                 .unwrap_or(true),
             )
         };
+        self.editor_show_predictions = editor.should_show_inline_completions(cx);
         self.edit_prediction_provider = editor.edit_prediction_provider();
         self.language = language.cloned();
         self.file = file;

crates/ui/src/components/context_menu.rs 🔗

@@ -674,7 +674,8 @@ impl Render for ContextMenu {
                                                                 let contents = if toggled {
                                                                     v_flex().flex_none().child(
                                                                         Icon::new(IconName::Check)
-                                                                            .color(Color::Accent),
+                                                                            .color(Color::Accent)
+                                                                            .size(*icon_size)
                                                                     )
                                                                 } else {
                                                                     v_flex().flex_none().size(

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

@@ -213,6 +213,7 @@ impl Render for QuickActionBar {
                 })
         });
 
+        let editor_focus_handle = editor.focus_handle(cx);
         let editor = editor.downgrade();
         let editor_settings_dropdown = {
             let vim_mode_enabled = VimModeSetting::get_global(cx).0;
@@ -231,20 +232,67 @@ impl Render for QuickActionBar {
                 .anchor(Corner::TopRight)
                 .with_handle(self.toggle_settings_handle.clone())
                 .menu(move |window, cx| {
-                    let menu = ContextMenu::build(window, cx, |mut menu, _, _| {
-                        if supports_inlay_hints {
+                    let menu = ContextMenu::build(window, cx, {
+                        let focus_handle = editor_focus_handle.clone();
+                        |mut menu, _, _| {
+                            menu = menu.context(focus_handle);
+
+                            if supports_inlay_hints {
+                                menu = menu.toggleable_entry(
+                                    "Inlay Hints",
+                                    inlay_hints_enabled,
+                                    IconPosition::Start,
+                                    Some(editor::actions::ToggleInlayHints.boxed_clone()),
+                                    {
+                                        let editor = editor.clone();
+                                        move |window, cx| {
+                                            editor
+                                                .update(cx, |editor, cx| {
+                                                    editor.toggle_inlay_hints(
+                                                        &editor::actions::ToggleInlayHints,
+                                                        window,
+                                                        cx,
+                                                    );
+                                                })
+                                                .ok();
+                                        }
+                                    },
+                                );
+                            }
+
+                            menu = menu.toggleable_entry(
+                                "Selection Menu",
+                                selection_menu_enabled,
+                                IconPosition::Start,
+                                Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
+                                {
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_selection_menu(
+                                                    &editor::actions::ToggleSelectionMenu,
+                                                    window,
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
+
                             menu = menu.toggleable_entry(
-                                "Inlay Hints",
-                                inlay_hints_enabled,
+                                "Auto Signature Help",
+                                auto_signature_help_enabled,
                                 IconPosition::Start,
-                                Some(editor::actions::ToggleInlayHints.boxed_clone()),
+                                Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
                                 {
                                     let editor = editor.clone();
                                     move |window, cx| {
                                         editor
                                             .update(cx, |editor, cx| {
-                                                editor.toggle_inlay_hints(
-                                                    &editor::actions::ToggleInlayHints,
+                                                editor.toggle_auto_signature_help_menu(
+                                                    &editor::actions::ToggleAutoSignatureHelp,
                                                     window,
                                                     cx,
                                                 );
@@ -253,138 +301,96 @@ impl Render for QuickActionBar {
                                     }
                                 },
                             );
-                        }
 
-                        menu = menu.toggleable_entry(
-                            "Selection Menu",
-                            selection_menu_enabled,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_selection_menu(
-                                                &editor::actions::ToggleSelectionMenu,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        menu = menu.toggleable_entry(
-                            "Auto Signature Help",
-                            auto_signature_help_enabled,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_auto_signature_help_menu(
-                                                &editor::actions::ToggleAutoSignatureHelp,
-                                                window,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
-                            .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
-                            .disabled(!inline_completion_enabled)
-                            .action(Some(
-                                editor::actions::ToggleEditPrediction.boxed_clone(),
-                            )).handler({
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_inline_completions(
-                                                &editor::actions::ToggleEditPrediction,
-                                                window,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }
-                            });
-                        if !inline_completion_enabled {
-                            inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
-                                Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
-                            });
-                        }
+                            let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
+                                .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
+                                .disabled(!inline_completion_enabled)
+                                .action(Some(
+                                    editor::actions::ToggleEditPrediction.boxed_clone(),
+                                )).handler({
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_inline_completions(
+                                                    &editor::actions::ToggleEditPrediction,
+                                                    window,
+                                                    cx,
+                                                );
+                                            })
+                                            .ok();
+                                    }
+                                });
+                            if !inline_completion_enabled {
+                                inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
+                                    Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
+                                });
+                            }
+
+                            menu = menu.item(inline_completion_entry);
+
+                            menu = menu.separator();
+
+                            menu = menu.toggleable_entry(
+                                "Inline Git Blame",
+                                git_blame_inline_enabled,
+                                IconPosition::Start,
+                                Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
+                                {
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_git_blame_inline(
+                                                    &editor::actions::ToggleGitBlameInline,
+                                                    window,
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
 
-                        menu = menu.item(inline_completion_entry);
-
-                        menu = menu.separator();
-
-                        menu = menu.toggleable_entry(
-                            "Inline Git Blame",
-                            git_blame_inline_enabled,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_git_blame_inline(
-                                                &editor::actions::ToggleGitBlameInline,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        menu = menu.toggleable_entry(
-                            "Column Git Blame",
-                            show_git_blame_gutter,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleGitBlame.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_git_blame(
-                                                &editor::actions::ToggleGitBlame,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        menu = menu.separator();
-
-                        menu = menu.toggleable_entry(
-                            "Vim Mode",
-                            vim_mode_enabled,
-                            IconPosition::Start,
-                            None,
-                            {
-                                move |window, cx| {
-                                    let new_value = !vim_mode_enabled;
-                                    VimModeSetting::override_global(VimModeSetting(new_value), cx);
-                                    window.refresh();
-                                }
-                            },
-                        );
-
-                        menu
+                            menu = menu.toggleable_entry(
+                                "Column Git Blame",
+                                show_git_blame_gutter,
+                                IconPosition::Start,
+                                Some(editor::actions::ToggleGitBlame.boxed_clone()),
+                                {
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_git_blame(
+                                                    &editor::actions::ToggleGitBlame,
+                                                    window,
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
+
+                            menu = menu.separator();
+
+                            menu = menu.toggleable_entry(
+                                "Vim Mode",
+                                vim_mode_enabled,
+                                IconPosition::Start,
+                                None,
+                                {
+                                    move |window, cx| {
+                                        let new_value = !vim_mode_enabled;
+                                        VimModeSetting::override_global(VimModeSetting(new_value), cx);
+                                        window.refresh();
+                                    }
+                                },
+                            );
+
+                            menu
+                        }
                     });
                     Some(menu)
                 })