diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 098ab8a6e01cf9..1f2e6b26e35d28 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -37,9 +37,9 @@ "[ [": "vim::PreviousSectionStart", "[ ]": "vim::PreviousSectionEnd", "] m": "vim::NextMethodStart", - "] M": "vim::NextMethodEnd", + "] shift-m": "vim::NextMethodEnd", "[ m": "vim::PreviousMethodStart", - "[ M": "vim::PreviousMethodEnd", + "[ shift-m": "vim::PreviousMethodEnd", "[ *": "vim::PreviousComment", "[ /": "vim::PreviousComment", "] *": "vim::NextComment", diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 370d6d1119dfd6..b9d99cb19e51c9 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1704,7 +1704,7 @@ impl PromptEditor { // always show the cursor (even when it isn't focused) because // typing in one will make what you typed appear in all of them. editor.set_show_cursor_when_unfocused(true, cx); - editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), window), cx); + editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), window, cx), cx); editor }); @@ -1783,7 +1783,10 @@ impl PromptEditor { self.editor = cx.new(|cx| { let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), window), cx); + editor.set_placeholder_text( + Self::placeholder_text(self.codegen.read(cx), window, cx), + cx, + ); editor.set_placeholder_text("Add a prompt…", cx); editor.set_text(prompt, window, cx); if focus { @@ -1794,8 +1797,8 @@ impl PromptEditor { self.subscribe_to_editor(window, cx); } - fn placeholder_text(codegen: &Codegen, window: &Window) -> String { - let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window) + fn placeholder_text(codegen: &Codegen, window: &Window, cx: &App) -> String { + let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window, cx) .map(|keybinding| format!(" • {keybinding} for context")) .unwrap_or_default(); @@ -2084,12 +2087,13 @@ impl PromptEditor { .tooltip({ let focus_handle = self.editor.focus_handle(cx); move |window, cx| { - cx.new(|_| { + cx.new(|cx| { let mut tooltip = Tooltip::new("Previous Alternative").key_binding( KeyBinding::for_action_in( &CyclePreviousInlineAssist, &focus_handle, window, + cx, ), ); if !disabled && current_index != 0 { @@ -2126,12 +2130,13 @@ impl PromptEditor { .tooltip({ let focus_handle = self.editor.focus_handle(cx); move |window, cx| { - cx.new(|_| { + cx.new(|cx| { let mut tooltip = Tooltip::new("Next Alternative").key_binding( KeyBinding::for_action_in( &CycleNextInlineAssist, &focus_handle, window, + cx, ), ); if !disabled && current_index != total_models - 1 { diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index a7f1d196676930..e9fb54028a1420 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -725,7 +725,7 @@ impl PromptEditor { cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text(Self::placeholder_text(window), cx); + editor.set_placeholder_text(Self::placeholder_text(window, cx), cx); editor }); @@ -774,8 +774,8 @@ impl PromptEditor { this } - fn placeholder_text(window: &Window) -> String { - let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window) + fn placeholder_text(window: &Window, cx: &App) -> String { + let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window, cx) .map(|keybinding| format!(" • {keybinding} for context")) .unwrap_or_default(); diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index bfdeeb545bb939..d8788d187e5753 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -849,6 +849,7 @@ impl AssistantPanel { &OpenHistory, &self.focus_handle(cx), window, + cx )) .on_click(move |_event, window, cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx); diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index 317eaad8a1547d..2baabf4b5623e9 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -453,6 +453,7 @@ impl Render for ContextStrip { &ToggleContextPicker, &focus_handle, window, + cx, ) .map(|binding| binding.into_any_element()), ), diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index a8c4c3d0f9f996..c1764cf30d285b 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -271,7 +271,7 @@ impl PromptEditor { }; let assistant_panel_keybinding = - ui::text_for_action(&zed_actions::assistant::ToggleFocus, window) + ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx) .map(|keybinding| format!("{keybinding} to chat ― ")) .unwrap_or_default(); @@ -618,12 +618,13 @@ impl PromptEditor { .tooltip({ let focus_handle = self.editor.focus_handle(cx); move |window, cx| { - cx.new(|_| { + cx.new(|cx| { let mut tooltip = Tooltip::new("Previous Alternative").key_binding( KeyBinding::for_action_in( &CyclePreviousInlineAssist, &focus_handle, window, + cx, ), ); if !disabled && current_index != 0 { @@ -659,12 +660,13 @@ impl PromptEditor { .tooltip({ let focus_handle = self.editor.focus_handle(cx); move |window, cx| { - cx.new(|_| { + cx.new(|cx| { let mut tooltip = Tooltip::new("Next Alternative").key_binding( KeyBinding::for_action_in( &CycleNextInlineAssist, &focus_handle, window, + cx, ), ); if !disabled && current_index != total_models - 1 { diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 5c7c7cff66d40b..1533679ef9cc42 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -390,6 +390,7 @@ impl Render for MessageEditor { &ChatMode, &focus_handle, window, + cx, )), ) .child(h_flex().gap_1().child(self.model_selector.clone()).child( @@ -419,6 +420,7 @@ impl Render for MessageEditor { &editor::actions::Cancel, &focus_handle, window, + cx, ) .map(|binding| binding.into_any_element()), ), @@ -449,6 +451,7 @@ impl Render for MessageEditor { &Chat, &focus_handle, window, + cx, ) .map(|binding| binding.into_any_element()), ), diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index fcfce741c1e812..bc1294e8615b11 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -2290,7 +2290,7 @@ impl ContextEditor { }, )) .children( - KeyBinding::for_action_in(&Assist, &focus_handle, window) + KeyBinding::for_action_in(&Assist, &focus_handle, window, cx) .map(|binding| binding.into_any_element()), ) .on_click(move |_event, window, cx| { @@ -2343,7 +2343,7 @@ impl ContextEditor { .layer(ElevationIndex::ModalSurface) .child(Label::new("Suggest Edits")) .children( - KeyBinding::for_action_in(&Edit, &focus_handle, window) + KeyBinding::for_action_in(&Edit, &focus_handle, window, cx) .map(|binding| binding.into_any_element()), ) .on_click(move |_event, window, cx| { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index c17f3ff052e317..58eb2b576654b0 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -992,6 +992,7 @@ impl Render for ChatPanel { .key_binding(KeyBinding::for_action( &collab_panel::ToggleFocus, window, + cx, )) .on_click(|_, window, cx| { window.dispatch_action( diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 68467ff4b01201..a431b36736d1c0 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -402,7 +402,7 @@ impl PickerDelegate for CommandPaletteDelegate { ix: usize, selected: bool, window: &mut Window, - _: &mut Context>, + cx: &mut Context>, ) -> Option { let r#match = self.matches.get(ix)?; let command = self.commands.get(r#match.candidate_id)?; @@ -424,6 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate { &*command.action, &self.previous_focus_handle, window, + cx, )), ), ) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 06c4b24641e416..87602c72ad6395 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2731,6 +2731,7 @@ impl EditorElement { &OpenExcerpts, &focus_handle, window, + cx, ) .map(|binding| binding.into_any_element()), ), diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index a5dd583d6de918..db25ce8d0ea2ca 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -387,7 +387,7 @@ impl Render for ProposedChangesEditorToolbar { Some(editor) => { let focus_handle = editor.focus_handle(cx); let keybinding = - KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window) + KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window, cx) .map(|binding| binding.into_any_element()); button_like.children(keybinding).on_click({ diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c4a43626004731..20d3503e0b4904 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1317,7 +1317,7 @@ impl PickerDelegate for FileFinderDelegate { .border_color(cx.theme().colors().border_variant) .child( Button::new("open-selection", "Open") - .key_binding(KeyBinding::for_action(&menu::Confirm, window)) + .key_binding(KeyBinding::for_action(&menu::Confirm, window, cx)) .on_click(|_, window, cx| { window.dispatch_action(menu::Confirm.boxed_clone(), cx) }), @@ -1334,6 +1334,7 @@ impl PickerDelegate for FileFinderDelegate { &ToggleMenu, &context, window, + cx, )), ) .menu({ diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index c34663e3edd1a9..88a68956d351b4 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -216,6 +216,7 @@ impl Render for KeyContextView { .key_binding(ui::KeyBinding::for_action( &zed_actions::OpenDefaultKeymap, window, + cx )) .on_click(|_, window, cx| { window.dispatch_action(workspace::SplitRight.boxed_clone(), cx); @@ -225,7 +226,7 @@ impl Render for KeyContextView { .child( Button::new("default", "Edit your keymap") .style(ButtonStyle::Filled) - .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window)) + .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window, cx)) .on_click(|_, window, cx| { window.dispatch_action(workspace::SplitRight.boxed_clone(), cx); window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 758ae4f7c8c011..9542287c980a15 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4643,7 +4643,7 @@ impl Render for ProjectPanel { .child( Button::new("open_project", "Open a project") .full_width() - .key_binding(KeyBinding::for_action(&workspace::Open, window)) + .key_binding(KeyBinding::for_action(&workspace::Open, window, cx)) .on_click(cx.listener(|this, _, window, cx| { this.workspace .update(cx, |_, cx| { diff --git a/crates/prompt_library/src/prompt_library.rs b/crates/prompt_library/src/prompt_library.rs index a37957bfd6bdad..a3ec59cad71b2a 100644 --- a/crates/prompt_library/src/prompt_library.rs +++ b/crates/prompt_library/src/prompt_library.rs @@ -1268,7 +1268,7 @@ impl Render for PromptLibrary { Button::new("create-prompt", "New Prompt") .full_width() .key_binding(KeyBinding::for_action( - &NewPrompt, window, + &NewPrompt, window, cx, )) .on_click(|_, window, cx| { window.dispatch_action( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index f93194b24020a5..f06b3ae701528f 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -465,14 +465,14 @@ impl PickerDelegate for RecentProjectsDelegate { .border_color(cx.theme().colors().border_variant) .child( Button::new("remote", "Open Remote Folder") - .key_binding(KeyBinding::for_action(&OpenRemote, window)) + .key_binding(KeyBinding::for_action(&OpenRemote, window, cx)) .on_click(|_, window, cx| { window.dispatch_action(OpenRemote.boxed_clone(), cx) }), ) .child( Button::new("local", "Open Local Folder") - .key_binding(KeyBinding::for_action(&workspace::Open, window)) + .key_binding(KeyBinding::for_action(&workspace::Open, window, cx)) .on_click(|_, window, cx| { window.dispatch_action(workspace::Open.boxed_clone(), cx) }), diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index b878c659217bdb..3655ed01363ab6 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -249,7 +249,7 @@ impl Render for ReplSessionsPage { return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child( v_flex() .child(Label::new(instructions)) - .children(KeyBinding::for_action(&Run, window)), + .children(KeyBinding::for_action(&Run, window, cx)), ); } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 71887c067ddb62..f4c04447fee57a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -364,7 +364,7 @@ impl Render for ProjectSearchView { None } } else { - Some(self.landing_text_minor(window).into_any_element()) + Some(self.landing_text_minor(window, cx).into_any_element()) }; let page_content = page_content.map(|text| div().child(text)); @@ -1231,7 +1231,7 @@ impl ProjectSearchView { self.active_match_index.is_some() } - fn landing_text_minor(&self, window: &mut Window) -> impl IntoElement { + fn landing_text_minor(&self, window: &mut Window, cx: &App) -> impl IntoElement { let focus_handle = self.focus_handle.clone(); v_flex() .gap_1() @@ -1249,6 +1249,7 @@ impl ProjectSearchView { &ToggleFilters, &focus_handle, window, + cx, )) .on_click(|_event, window, cx| { window.dispatch_action(ToggleFilters.boxed_clone(), cx) @@ -1263,6 +1264,7 @@ impl ProjectSearchView { &ToggleReplace, &focus_handle, window, + cx, )) .on_click(|_event, window, cx| { window.dispatch_action(ToggleReplace.boxed_clone(), cx) @@ -1277,6 +1279,7 @@ impl ProjectSearchView { &ToggleRegex, &focus_handle, window, + cx, )) .on_click(|_event, window, cx| { window.dispatch_action(ToggleRegex.boxed_clone(), cx) @@ -1291,6 +1294,7 @@ impl ProjectSearchView { &ToggleCaseSensitive, &focus_handle, window, + cx, )) .on_click(|_event, window, cx| { window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx) @@ -1305,6 +1309,7 @@ impl ProjectSearchView { &ToggleWholeWord, &focus_handle, window, + cx, )) .on_click(|_event, window, cx| { window.dispatch_action(ToggleWholeWord.boxed_clone(), cx) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index d24a5b13709abb..32e9add67c38c5 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -511,7 +511,7 @@ impl PickerDelegate for TasksModalDelegate { .child( left_button .map(|(label, action)| { - let keybind = KeyBinding::for_action(&*action, window); + let keybind = KeyBinding::for_action(&*action, window, cx); Button::new("edit-current-task", label) .label_size(LabelSize::Small) @@ -530,7 +530,7 @@ impl PickerDelegate for TasksModalDelegate { secondary: current_modifiers.secondary(), } .boxed_clone(); - this.children(KeyBinding::for_action(&*action, window).map(|keybind| { + this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { let spawn_oneshot_label = if current_modifiers.secondary() { "Spawn Oneshot Without History" } else { @@ -545,26 +545,28 @@ impl PickerDelegate for TasksModalDelegate { }) })) } else if current_modifiers.secondary() { - this.children(KeyBinding::for_action(&menu::SecondaryConfirm, window).map( - |keybind| { - let label = if is_recent_selected { - "Rerun Without History" - } else { - "Spawn Without History" - }; - Button::new("spawn", label) - .label_size(LabelSize::Small) - .key_binding(keybind) - .on_click(move |_, window, cx| { - window.dispatch_action( - menu::SecondaryConfirm.boxed_clone(), - cx, - ) - }) - }, - )) + this.children( + KeyBinding::for_action(&menu::SecondaryConfirm, window, cx).map( + |keybind| { + let label = if is_recent_selected { + "Rerun Without History" + } else { + "Spawn Without History" + }; + Button::new("spawn", label) + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action( + menu::SecondaryConfirm.boxed_clone(), + cx, + ) + }) + }, + ), + ) } else { - this.children(KeyBinding::for_action(&menu::Confirm, window).map( + this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map( |keybind| { let run_entry_label = if is_recent_selected { "Rerun" } else { "Spawn" }; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index cd39638638b244..03e5fa407db791 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -716,11 +716,12 @@ impl Render for ContextMenu { KeyBinding::for_action_in( &**action, focus, window, + cx ) }) .unwrap_or_else(|| { KeyBinding::for_action( - &**action, window, + &**action, window, cx ) }) .map(|binding| { diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 8883dce8993288..3682afa972faeb 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -2,8 +2,10 @@ use crate::PlatformStyle; use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; use gpui::{ - relative, Action, AnyElement, App, FocusHandle, IntoElement, Keystroke, Modifiers, Window, + relative, Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, + Window, }; +use itertools::Itertools; #[derive(Debug, IntoElement, Clone)] pub struct KeyBinding { @@ -16,18 +18,24 @@ pub struct KeyBinding { /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, size: Option, + + /// Determines whether the keybinding is meant for vim mode. + vim_mode: bool, } +struct VimStyle(bool); +impl Global for VimStyle {} + impl KeyBinding { /// Returns the highest precedence keybinding for an action. This is the last binding added to /// the keymap. User bindings are added after built-in bindings so that they take precedence. - pub fn for_action(action: &dyn Action, window: &mut Window) -> Option { + pub fn for_action(action: &dyn Action, window: &mut Window, cx: &App) -> Option { let key_binding = window .bindings_for_action(action) .into_iter() .rev() .next()?; - Some(Self::new(key_binding)) + Some(Self::new(key_binding, cx)) } /// Like `for_action`, but lets you specify the context from which keybindings are matched. @@ -35,20 +43,30 @@ impl KeyBinding { action: &dyn Action, focus: &FocusHandle, window: &mut Window, + cx: &App, ) -> Option { let key_binding = window .bindings_for_action_in(action, focus) .into_iter() .rev() .next()?; - Some(Self::new(key_binding)) + Some(Self::new(key_binding, cx)) + } + + pub fn set_vim_mode(cx: &mut App, enabled: bool) { + cx.set_global(VimStyle(enabled)); + } + + fn is_vim_mode(cx: &App) -> bool { + cx.try_global::().is_some_and(|g| g.0) } - pub fn new(key_binding: gpui::KeyBinding) -> Self { + pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self { Self { key_binding, platform_style: PlatformStyle::platform(), size: None, + vim_mode: KeyBinding::is_vim_mode(cx), } } @@ -63,6 +81,30 @@ impl KeyBinding { self.size = Some(size.into()); self } + + pub fn vim_mode(mut self, enabled: bool) -> Self { + self.vim_mode = enabled; + self + } + + fn render_key(&self, keystroke: &Keystroke, color: Option) -> AnyElement { + let key_icon = icon_for_key(keystroke, self.platform_style); + match key_icon { + Some(icon) => KeyIcon::new(icon, color).size(self.size).into_any_element(), + None => { + let key = if self.vim_mode { + if keystroke.modifiers.shift && keystroke.key.len() == 1 { + keystroke.key.to_ascii_uppercase().to_string() + } else { + keystroke.key.to_string() + } + } else { + util::capitalize(&keystroke.key) + }; + Key::new(&key, color).size(self.size).into_any_element() + } + } + } } impl RenderOnce for KeyBinding { @@ -94,28 +136,11 @@ impl RenderOnce for KeyBinding { self.size, true, )) - .map(|el| { - el.child(render_key(&keystroke, self.platform_style, None, self.size)) - }) + .map(|el| el.child(self.render_key(&keystroke, None))) })) } } -pub fn render_key( - keystroke: &Keystroke, - platform_style: PlatformStyle, - color: Option, - size: Option, -) -> AnyElement { - let key_icon = icon_for_key(keystroke, platform_style); - match key_icon { - Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), - None => Key::new(util::capitalize(&keystroke.key), color) - .size(size) - .into_any_element(), - } -} - fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), @@ -312,39 +337,33 @@ impl KeyIcon { } /// Returns a textual representation of the key binding for the given [`Action`]. -pub fn text_for_action(action: &dyn Action, window: &Window) -> Option { +pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { let bindings = window.bindings_for_action(action); let key_binding = bindings.last()?; - Some(text_for_key_binding(key_binding, PlatformStyle::platform())) + Some(text_for_keystrokes(key_binding.keystrokes(), cx)) } -/// Returns a textual representation of the key binding for the given [`Action`] -/// as if the provided [`FocusHandle`] was focused. -pub fn text_for_action_in( - action: &dyn Action, - focus: &FocusHandle, - window: &mut Window, -) -> Option { - let bindings = window.bindings_for_action_in(action, focus); - let key_binding = bindings.last()?; - Some(text_for_key_binding(key_binding, PlatformStyle::platform())) -} - -/// Returns a textual representation of the given key binding for the specified platform. -pub fn text_for_key_binding( - key_binding: &gpui::KeyBinding, - platform_style: PlatformStyle, -) -> String { - key_binding - .keystrokes() +pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { + let platform_style = PlatformStyle::platform(); + let vim_enabled = cx.try_global::().is_some(); + keystrokes .iter() - .map(|keystroke| text_for_keystroke(keystroke, platform_style)) - .collect::>() + .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) .join(" ") } +pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { + let platform_style = PlatformStyle::platform(); + let vim_enabled = cx.try_global::().is_some(); + keystroke_text(keystroke, platform_style, vim_enabled) +} + /// Returns a textual representation of the given [`Keystroke`]. -pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String { +fn keystroke_text( + keystroke: &Keystroke, + platform_style: PlatformStyle, + vim_enabled: bool, +) -> String { let mut text = String::new(); let delimiter = match platform_style { @@ -354,7 +373,7 @@ pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) if keystroke.modifiers.function { match platform_style { - PlatformStyle::Mac => text.push_str("fn"), + PlatformStyle::Mac => text.push_str("Fn"), PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"), } @@ -390,18 +409,26 @@ pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) } if keystroke.modifiers.shift { - match platform_style { - PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => { - text.push_str("Shift") + if !(vim_enabled && keystroke.key.len() == 1) { + match platform_style { + PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => { + text.push_str("Shift") + } } + text.push(delimiter); } - - text.push(delimiter); } let key = match keystroke.key.as_str() { "pageup" => "PageUp", "pagedown" => "PageDown", + key if vim_enabled => { + if !keystroke.modifiers.shift && key.len() == 1 { + key + } else { + &util::capitalize(key) + } + } key => &util::capitalize(key), }; @@ -417,58 +444,76 @@ mod tests { #[test] fn test_text_for_keystroke() { assert_eq!( - text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac), + keystroke_text( + &Keystroke::parse("cmd-c").unwrap(), + PlatformStyle::Mac, + false + ), "Command-C".to_string() ); assert_eq!( - text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux), + keystroke_text( + &Keystroke::parse("cmd-c").unwrap(), + PlatformStyle::Linux, + false + ), "Super+C".to_string() ); assert_eq!( - text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows), + keystroke_text( + &Keystroke::parse("cmd-c").unwrap(), + PlatformStyle::Windows, + false + ), "Win+C".to_string() ); assert_eq!( - text_for_keystroke( + keystroke_text( &Keystroke::parse("ctrl-alt-delete").unwrap(), - PlatformStyle::Mac + PlatformStyle::Mac, + false ), "Control-Option-Delete".to_string() ); assert_eq!( - text_for_keystroke( + keystroke_text( &Keystroke::parse("ctrl-alt-delete").unwrap(), - PlatformStyle::Linux + PlatformStyle::Linux, + false ), "Ctrl+Alt+Delete".to_string() ); assert_eq!( - text_for_keystroke( + keystroke_text( &Keystroke::parse("ctrl-alt-delete").unwrap(), - PlatformStyle::Windows + PlatformStyle::Windows, + false ), "Ctrl+Alt+Delete".to_string() ); assert_eq!( - text_for_keystroke( + keystroke_text( &Keystroke::parse("shift-pageup").unwrap(), - PlatformStyle::Mac + PlatformStyle::Mac, + false ), "Shift-PageUp".to_string() ); assert_eq!( - text_for_keystroke( + keystroke_text( &Keystroke::parse("shift-pageup").unwrap(), - PlatformStyle::Linux + PlatformStyle::Linux, + false, ), "Shift+PageUp".to_string() ); assert_eq!( - text_for_keystroke( + keystroke_text( &Keystroke::parse("shift-pageup").unwrap(), - PlatformStyle::Windows + PlatformStyle::Windows, + false ), "Shift+PageUp".to_string() ); diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index ef355336e6aa8d..9df64be5f6a507 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -207,10 +207,10 @@ impl RenderOnce for KeybindingHint { // View this component preview using `workspace: open component-preview` impl ComponentPreview for KeybindingHint { - fn preview(window: &mut Window, _cx: &App) -> AnyElement { + fn preview(window: &mut Window, cx: &App) -> AnyElement { let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None); - let enter = KeyBinding::for_action(&menu::Confirm, window) - .unwrap_or(KeyBinding::new(enter_fallback)); + let enter = KeyBinding::for_action(&menu::Confirm, window, cx) + .unwrap_or(KeyBinding::new(enter_fallback, cx)); v_flex() .gap_6() diff --git a/crates/ui/src/components/stories/keybinding.rs b/crates/ui/src/components/stories/keybinding.rs index 61cd7fdbaaa208..2e2b952a4d93dd 100644 --- a/crates/ui/src/components/stories/keybinding.rs +++ b/crates/ui/src/components/stories/keybinding.rs @@ -12,22 +12,22 @@ pub fn binding(key: &str) -> gpui::KeyBinding { } impl Render for KeybindingStory { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); Story::container() .child(Story::title_for::()) .child(Story::label("Single Key")) - .child(KeyBinding::new(binding("Z"))) + .child(KeyBinding::new(binding("Z"), cx)) .child(Story::label("Single Key with Modifier")) .child( div() .flex() .gap_3() - .child(KeyBinding::new(binding("ctrl-c"))) - .child(KeyBinding::new(binding("alt-c"))) - .child(KeyBinding::new(binding("cmd-c"))) - .child(KeyBinding::new(binding("shift-c"))), + .child(KeyBinding::new(binding("ctrl-c"), cx)) + .child(KeyBinding::new(binding("alt-c"), cx)) + .child(KeyBinding::new(binding("cmd-c"), cx)) + .child(KeyBinding::new(binding("shift-c"), cx)), ) .child(Story::label("Single Key with Modifier (Permuted)")) .child( @@ -41,39 +41,42 @@ impl Render for KeybindingStory { .gap_4() .py_3() .children(chunk.map(|permutation| { - KeyBinding::new(binding(&(permutation.join("-") + "-x"))) + KeyBinding::new(binding(&(permutation.join("-") + "-x")), cx) })) }), ), ) .child(Story::label("Single Key with All Modifiers")) - .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"))) + .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)) .child(Story::label("Chord")) - .child(KeyBinding::new(binding("a z"))) + .child(KeyBinding::new(binding("a z"), cx)) .child(Story::label("Chord with Modifier")) - .child(KeyBinding::new(binding("ctrl-a shift-z"))) - .child(KeyBinding::new(binding("fn-s"))) + .child(KeyBinding::new(binding("ctrl-a shift-z"), cx)) + .child(KeyBinding::new(binding("fn-s"), cx)) .child(Story::label("Single Key with All Modifiers (Linux)")) .child( - KeyBinding::new(binding("ctrl-alt-cmd-shift-z")) + KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx) .platform_style(PlatformStyle::Linux), ) .child(Story::label("Chord (Linux)")) - .child(KeyBinding::new(binding("a z")).platform_style(PlatformStyle::Linux)) + .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Linux)) .child(Story::label("Chord with Modifier (Linux)")) - .child(KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Linux)) - .child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Linux)) + .child( + KeyBinding::new(binding("ctrl-a shift-z"), cx).platform_style(PlatformStyle::Linux), + ) + .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Linux)) .child(Story::label("Single Key with All Modifiers (Windows)")) .child( - KeyBinding::new(binding("ctrl-alt-cmd-shift-z")) + KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx) .platform_style(PlatformStyle::Windows), ) .child(Story::label("Chord (Windows)")) - .child(KeyBinding::new(binding("a z")).platform_style(PlatformStyle::Windows)) + .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Windows)) .child(Story::label("Chord with Modifier (Windows)")) .child( - KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Windows), + KeyBinding::new(binding("ctrl-a shift-z"), cx) + .platform_style(PlatformStyle::Windows), ) - .child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Windows)) + .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Windows)) } } diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index a4fbdcb5964e89..cd391bb7f1a0ab 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -43,10 +43,10 @@ impl Tooltip { let title = title.into(); let action = action.boxed_clone(); move |window, cx| { - cx.new(|_| Self { + cx.new(|cx| Self { title: title.clone(), meta: None, - key_binding: KeyBinding::for_action(action.as_ref(), window), + key_binding: KeyBinding::for_action(action.as_ref(), window, cx), }) .into() } @@ -58,10 +58,10 @@ impl Tooltip { window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|_| Self { + cx.new(|cx| Self { title: title.into(), meta: None, - key_binding: KeyBinding::for_action(action, window), + key_binding: KeyBinding::for_action(action, window, cx), }) .into() } @@ -73,10 +73,10 @@ impl Tooltip { window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|_| Self { + cx.new(|cx| Self { title: title.into(), meta: None, - key_binding: KeyBinding::for_action_in(action, focus_handle, window), + key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx), }) .into() } @@ -88,10 +88,10 @@ impl Tooltip { window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|_| Self { + cx.new(|cx| Self { title: title.into(), meta: Some(meta.into()), - key_binding: action.and_then(|action| KeyBinding::for_action(action, window)), + key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)), }) .into() } @@ -104,11 +104,11 @@ impl Tooltip { window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|_| Self { + cx.new(|cx| Self { title: title.into(), meta: Some(meta.into()), key_binding: action - .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window)), + .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)), }) .into() } diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 8292970a53fcea..9bd37478bcd6c3 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -1,5 +1,5 @@ use gpui::{div, Context, Element, Entity, Render, Subscription, WeakEntity, Window}; -use itertools::Itertools; +use ui::text_for_keystrokes; use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; use crate::{Vim, VimEvent, VimGlobals}; @@ -15,7 +15,7 @@ impl ModeIndicator { /// Construct a new mode indicator in this window. pub fn new(window: &mut Window, cx: &mut Context) -> Self { cx.observe_pending_input(window, |this: &mut Self, window, cx| { - this.update_pending_keys(window); + this.update_pending_keys(window, cx); cx.notify(); }) .detach(); @@ -50,13 +50,10 @@ impl ModeIndicator { } } - fn update_pending_keys(&mut self, window: &mut Window) { - self.pending_keys = window.pending_input_keystrokes().map(|keystrokes| { - keystrokes - .iter() - .map(|keystroke| format!("{}", keystroke)) - .join(" ") - }); + fn update_pending_keys(&mut self, window: &mut Window, cx: &App) { + self.pending_keys = window + .pending_input_keystrokes() + .map(|keystrokes| text_for_keystrokes(keystrokes, cx)); } fn vim(&self) -> Option> { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 4c09984a753f46..8940e21185cee6 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::borrow::BorrowMut; use std::{fmt::Display, ops::Range, sync::Arc}; -use ui::{Context, SharedString}; +use ui::{Context, KeyBinding, SharedString}; use workspace::searchable::Direction; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] @@ -217,6 +217,7 @@ impl VimGlobals { cx.observe_global::(move |cx| { if Vim::enabled(cx) { + KeyBinding::set_vim_mode(cx, true); CommandPaletteFilter::update_global(cx, |filter, _| { filter.show_namespace(Vim::NAMESPACE); }); @@ -224,6 +225,7 @@ impl VimGlobals { interceptor.set(Box::new(command_interceptor)); }); } else { + KeyBinding::set_vim_mode(cx, true); *Vim::globals(cx) = VimGlobals::default(); CommandPaletteInterceptor::update_global(cx, |interceptor, _| { interceptor.clear(); diff --git a/crates/zed/src/zed/quick_action_bar/markdown_preview.rs b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs index 1ce8fa65e87227..cf9f7073410b7f 100644 --- a/crates/zed/src/zed/quick_action_bar/markdown_preview.rs +++ b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs @@ -41,10 +41,7 @@ impl QuickActionBar { Tooltip::with_meta( "Preview Markdown", Some(&markdown_preview::OpenPreview), - format!( - "{} to open in a split", - text_for_keystroke(&alt_click, PlatformStyle::platform()) - ), + format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), window, cx, ) diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 3c556253236838..28c1dad1f18bbe 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -489,6 +489,7 @@ impl RateCompletionModal { &ThumbsDownActiveCompletion, focus_handle, window, + cx )) .on_click(cx.listener(move |this, _, window, cx| { this.thumbs_down_active( @@ -507,6 +508,7 @@ impl RateCompletionModal { &ThumbsUpActiveCompletion, focus_handle, window, + cx )) .on_click(cx.listener(move |this, _, window, cx| { this.thumbs_up_active(&ThumbsUpActiveCompletion, window, cx); diff --git a/docs/src/vim.md b/docs/src/vim.md index e0eba0def203bf..bd8bd2780a3af5 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -408,8 +408,8 @@ The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for qui { "context": "vim_mode == normal || vim_mode == visual", "bindings": { - "s": ["vim::PushSneak", {}], - "S": ["vim::PushSneakBackward", {}] + "s": "vim::PushSneak", + "shift-s": "vim::PushSneakBackward" } } ```