Enhance the Vim Mode toggle discoverability (#21589)

Danilo Leal and Thorsten Ball created

Closes https://github.com/zed-industries/zed/issues/21522

This PR adds an info tooltip on the Welcome screen, informing users how
Vim Mode can be toggled on and off. It also adds the Vim Mode toggle in
the Editor Controls menu. This is all so that folks who accidentally
turn it on better know how to turn it off. We're of course already able
to toggle this setting via the command palette, but that may be harder
to reach for beginners. So, maybe that's enough to close the linked
issue? Open to feedback.

(Note: I also added a max-width to the tooltip's label in this PR. I'm
confident that this won't make any tooltip look weird/broken, but if it
does, it may be because of this new property).

| Welcome Page | Editor Controls |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-05 at 11 20 04"
src="https://github.com/user-attachments/assets/1229f866-6be5-45cd-a6b8-c805f72144a6">
| <img width="800" alt="Screenshot 2024-12-05 at 11 12 15"
src="https://github.com/user-attachments/assets/f082d7f9-7d56-41d1-bc86-c333ad6264c7">
|

Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

assets/icons/info.svg                  |  12 +
crates/ui/src/components/icon.rs       |   1 
crates/ui/src/components/tooltip.rs    |   2 
crates/welcome/src/welcome.rs          |  48 +++--
crates/zed/src/zed.rs                  |   1 
crates/zed/src/zed/quick_action_bar.rs | 236 +++++++++++++++------------
6 files changed, 174 insertions(+), 126 deletions(-)

Detailed changes

assets/icons/info.svg 🔗

@@ -0,0 +1,12 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2131_1193)">
+<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
+<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<circle cx="7" cy="4.5" r="1" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_2131_1193">
+<rect width="14" height="14" fill="white"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -88,7 +88,7 @@ impl Render for Tooltip {
             el.child(
                 h_flex()
                     .gap_4()
-                    .child(self.title.clone())
+                    .child(div().max_w_72().child(self.title.clone()))
                     .when_some(self.key_binding.clone(), |this, key_binding| {
                         this.justify_between().child(key_binding)
                     }),

crates/welcome/src/welcome.rs 🔗

@@ -11,7 +11,7 @@ use gpui::{
 };
 use settings::{Settings, SettingsStore};
 use std::sync::Arc;
-use ui::{prelude::*, CheckboxWithLabel};
+use ui::{prelude::*, CheckboxWithLabel, Tooltip};
 use vim_mode_setting::VimModeSetting;
 use workspace::{
     dock::DockPosition,
@@ -266,24 +266,34 @@ impl Render for WelcomePage {
                     .child(
                         v_group()
                             .gap_2()
-                            .child(CheckboxWithLabel::new(
-                                "enable-vim",
-                                Label::new("Enable Vim Mode"),
-                                if VimModeSetting::get_global(cx).0 {
-                                    ui::Selection::Selected
-                                } else {
-                                    ui::Selection::Unselected
-                                },
-                                cx.listener(move |this, selection, cx| {
-                                    this.telemetry
-                                        .report_app_event("welcome page: toggle vim".to_string());
-                                    this.update_settings::<VimModeSetting>(
-                                        selection,
-                                        cx,
-                                        |setting, value| *setting = Some(value),
-                                    );
-                                }),
-                            ))
+                            .child(
+                                h_flex()
+                                    .justify_between()
+                                    .child(CheckboxWithLabel::new(
+                                        "enable-vim",
+                                        Label::new("Enable Vim Mode"),
+                                        if VimModeSetting::get_global(cx).0 {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
+                                        cx.listener(move |this, selection, cx| {
+                                            this.telemetry
+                                                .report_app_event("welcome page: toggle vim".to_string());
+                                            this.update_settings::<VimModeSetting>(
+                                                selection,
+                                                cx,
+                                                |setting, value| *setting = Some(value),
+                                            );
+                                        }),
+                                    ))
+                                    .child(
+                                        IconButton::new("vim-mode", IconName::Info)
+                                            .icon_size(IconSize::XSmall)
+                                            .icon_color(Color::Muted)
+                                            .tooltip(|cx| Tooltip::text("You can also toggle Vim Mode via the command palette or Editor Controls menu.", cx)),
+                                    )
+                            )
                             .child(CheckboxWithLabel::new(
                                 "enable-crash",
                                 Label::new("Send Crash Reports"),

crates/zed/src/zed.rs 🔗

@@ -3447,6 +3447,7 @@ mod tests {
 
             app_state.languages.add(markdown_language());
 
+            vim_mode_setting::init(cx);
             theme::init(theme::LoadThemes::JustBase, cx);
             audio::init((), cx);
             channel::init(&app_state.client, app_state.user_store.clone(), cx);

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

@@ -19,6 +19,7 @@ use ui::{
     prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize,
     PopoverMenu, PopoverMenuHandle, Tooltip,
 };
+use vim_mode_setting::VimModeSetting;
 use workspace::{
     item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 };
@@ -154,6 +155,7 @@ impl Render for QuickActionBar {
 
         let editor_selections_dropdown = selection_menu_enabled.then(|| {
             let focus = editor.focus_handle(cx);
+
             PopoverMenu::new("editor-selections-dropdown")
                 .trigger(
                     IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
@@ -201,34 +203,78 @@ impl Render for QuickActionBar {
         });
 
         let editor = editor.downgrade();
-        let editor_settings_dropdown = PopoverMenu::new("editor-settings")
-            .trigger(
-                IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
-                    .shape(IconButtonShape::Square)
-                    .icon_size(IconSize::Small)
-                    .style(ButtonStyle::Subtle)
-                    .selected(self.toggle_settings_handle.is_deployed())
-                    .when(!self.toggle_settings_handle.is_deployed(), |this| {
-                        this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
-                    }),
-            )
-            .anchor(AnchorCorner::TopRight)
-            .with_handle(self.toggle_settings_handle.clone())
-            .menu(move |cx| {
-                let menu = ContextMenu::build(cx, |mut menu, _| {
-                    if supports_inlay_hints {
+        let editor_settings_dropdown = {
+            let vim_mode_enabled = VimModeSetting::get_global(cx).0;
+
+            PopoverMenu::new("editor-settings")
+                .trigger(
+                    IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
+                        .shape(IconButtonShape::Square)
+                        .icon_size(IconSize::Small)
+                        .style(ButtonStyle::Subtle)
+                        .selected(self.toggle_settings_handle.is_deployed())
+                        .when(!self.toggle_settings_handle.is_deployed(), |this| {
+                            this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
+                        }),
+                )
+                .anchor(AnchorCorner::TopRight)
+                .with_handle(self.toggle_settings_handle.clone())
+                .menu(move |cx| {
+                    let menu = ContextMenu::build(cx, |mut menu, _| {
+                        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 |cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_inlay_hints(
+                                                    &editor::actions::ToggleInlayHints,
+                                                    cx,
+                                                );
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
+                        }
+
+                        menu = menu.toggleable_entry(
+                            "Selection Menu",
+                            selection_menu_enabled,
+                            IconPosition::Start,
+                            Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
+                            {
+                                let editor = editor.clone();
+                                move |cx| {
+                                    editor
+                                        .update(cx, |editor, cx| {
+                                            editor.toggle_selection_menu(
+                                                &editor::actions::ToggleSelectionMenu,
+                                                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 |cx| {
                                     editor
                                         .update(cx, |editor, cx| {
-                                            editor.toggle_inlay_hints(
-                                                &editor::actions::ToggleInlayHints,
+                                            editor.toggle_auto_signature_help_menu(
+                                                &editor::actions::ToggleAutoSignatureHelp,
                                                 cx,
                                             );
                                         })
@@ -236,92 +282,70 @@ 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 |cx| {
-                                editor
-                                    .update(cx, |editor, cx| {
-                                        editor.toggle_selection_menu(
-                                            &editor::actions::ToggleSelectionMenu,
-                                            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 |cx| {
-                                editor
-                                    .update(cx, |editor, cx| {
-                                        editor.toggle_auto_signature_help_menu(
-                                            &editor::actions::ToggleAutoSignatureHelp,
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            }
-                        },
-                    );
-
-                    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 |cx| {
-                                editor
-                                    .update(cx, |editor, cx| {
-                                        editor.toggle_git_blame_inline(
-                                            &editor::actions::ToggleGitBlameInline,
-                                            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 |cx| {
-                                editor
-                                    .update(cx, |editor, cx| {
-                                        editor
-                                            .toggle_git_blame(&editor::actions::ToggleGitBlame, cx)
-                                    })
-                                    .ok();
-                            }
-                        },
-                    );
-
-                    menu
-                });
-                Some(menu)
-            });
+                        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 |cx| {
+                                    editor
+                                        .update(cx, |editor, cx| {
+                                            editor.toggle_git_blame_inline(
+                                                &editor::actions::ToggleGitBlameInline,
+                                                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 |cx| {
+                                    editor
+                                        .update(cx, |editor, cx| {
+                                            editor.toggle_git_blame(
+                                                &editor::actions::ToggleGitBlame,
+                                                cx,
+                                            )
+                                        })
+                                        .ok();
+                                }
+                            },
+                        );
+
+                        menu = menu.separator();
+
+                        menu = menu.toggleable_entry(
+                            "Vim Mode",
+                            vim_mode_enabled,
+                            IconPosition::Start,
+                            None,
+                            {
+                                move |cx| {
+                                    let new_value = !vim_mode_enabled;
+                                    VimModeSetting::override_global(VimModeSetting(new_value), cx);
+                                    cx.refresh();
+                                }
+                            },
+                        );
+
+                        menu
+                    });
+                    Some(menu)
+                })
+        };
 
         h_flex()
             .id("quick action bar")