Display case-sensitive keybindings for vim commands (#24322)

This Pull Request tackles the issue outline in #14287 by changing the
way `KeyBinding`s for vim mode are displayed in the command palette.
It's worth pointing out that this whole thing was pretty much
implemented by Conrad Irwin during a pairing session, I just tried to
clean up some other changes introduced for a different issue, while
improving some comments.

Here's a quick list of the changes introduced:

- Update `KeyBinding` with a new `vim_mode` field to determine whether
the keybinding should be displayed in vim mode.
- Update the way `KeyBinding` is rendered, so as to detect if the
keybinding is for vim mode, if it is, only display keys in uppercase if
they require the shift key.
- Introduce a new global state – `VimStyle(bool)` - use to determine
whether `vim_mode` should be enabled or disabled when creating a new
`KeyBinding` struct. This global state is automatically set by the `vim`
crate whenever vim mode is enabled or disabled.
- Since the app's context is now required when building a `KeyBinding` ,
update a lot of callers to correctly pass this context.

And before and after screenshots, for comparison:

| before | after |
|--------|-------|
| <img width="1050" alt="SCR-20250205-tyeq"
src="https://github.com/user-attachments/assets/e577206d-2a3d-4e06-a96f-a98899cc15c0"
/> | <img width="1050" alt="SCR-20250205-tylh"
src="https://github.com/user-attachments/assets/ebbf70a9-e838-4d32-aee5-0ffde94d65fb"
/> |

Closes #14287 

Release Notes:

- Fix rendering of vim commands to preserve case sensitivity

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Dino 2025-02-15 05:03:59 +00:00 committed by GitHub
parent 14289b5a6e
commit e0fc767c11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 236 additions and 165 deletions

View file

@ -37,9 +37,9 @@
"[ [": "vim::PreviousSectionStart", "[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd", "[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart", "] m": "vim::NextMethodStart",
"] M": "vim::NextMethodEnd", "] shift-m": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart", "[ m": "vim::PreviousMethodStart",
"[ M": "vim::PreviousMethodEnd", "[ shift-m": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment", "[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment", "[ /": "vim::PreviousComment",
"] *": "vim::NextComment", "] *": "vim::NextComment",

View file

@ -1704,7 +1704,7 @@ impl PromptEditor {
// always show the cursor (even when it isn't focused) because // always show the cursor (even when it isn't focused) because
// typing in one will make what you typed appear in all of them. // typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx); 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 editor
}); });
@ -1783,7 +1783,10 @@ impl PromptEditor {
self.editor = cx.new(|cx| { self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, 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_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_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, window, cx); editor.set_text(prompt, window, cx);
if focus { if focus {
@ -1794,8 +1797,8 @@ impl PromptEditor {
self.subscribe_to_editor(window, cx); self.subscribe_to_editor(window, cx);
} }
fn placeholder_text(codegen: &Codegen, window: &Window) -> String { fn placeholder_text(codegen: &Codegen, window: &Window, cx: &App) -> String {
let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window) let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} for context")) .map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default(); .unwrap_or_default();
@ -2084,12 +2087,13 @@ impl PromptEditor {
.tooltip({ .tooltip({
let focus_handle = self.editor.focus_handle(cx); let focus_handle = self.editor.focus_handle(cx);
move |window, cx| { move |window, cx| {
cx.new(|_| { cx.new(|cx| {
let mut tooltip = Tooltip::new("Previous Alternative").key_binding( let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
KeyBinding::for_action_in( KeyBinding::for_action_in(
&CyclePreviousInlineAssist, &CyclePreviousInlineAssist,
&focus_handle, &focus_handle,
window, window,
cx,
), ),
); );
if !disabled && current_index != 0 { if !disabled && current_index != 0 {
@ -2126,12 +2130,13 @@ impl PromptEditor {
.tooltip({ .tooltip({
let focus_handle = self.editor.focus_handle(cx); let focus_handle = self.editor.focus_handle(cx);
move |window, cx| { move |window, cx| {
cx.new(|_| { cx.new(|cx| {
let mut tooltip = Tooltip::new("Next Alternative").key_binding( let mut tooltip = Tooltip::new("Next Alternative").key_binding(
KeyBinding::for_action_in( KeyBinding::for_action_in(
&CycleNextInlineAssist, &CycleNextInlineAssist,
&focus_handle, &focus_handle,
window, window,
cx,
), ),
); );
if !disabled && current_index != total_models - 1 { if !disabled && current_index != total_models - 1 {

View file

@ -725,7 +725,7 @@ impl PromptEditor {
cx, cx,
); );
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, 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 editor
}); });
@ -774,8 +774,8 @@ impl PromptEditor {
this this
} }
fn placeholder_text(window: &Window) -> String { fn placeholder_text(window: &Window, cx: &App) -> String {
let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window) let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} for context")) .map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default(); .unwrap_or_default();

View file

@ -849,6 +849,7 @@ impl AssistantPanel {
&OpenHistory, &OpenHistory,
&self.focus_handle(cx), &self.focus_handle(cx),
window, window,
cx
)) ))
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx); window.dispatch_action(OpenHistory.boxed_clone(), cx);

View file

@ -453,6 +453,7 @@ impl Render for ContextStrip {
&ToggleContextPicker, &ToggleContextPicker,
&focus_handle, &focus_handle,
window, window,
cx,
) )
.map(|binding| binding.into_any_element()), .map(|binding| binding.into_any_element()),
), ),

View file

@ -271,7 +271,7 @@ impl<T: 'static> PromptEditor<T> {
}; };
let assistant_panel_keybinding = 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 ― ")) .map(|keybinding| format!("{keybinding} to chat ― "))
.unwrap_or_default(); .unwrap_or_default();
@ -618,12 +618,13 @@ impl<T: 'static> PromptEditor<T> {
.tooltip({ .tooltip({
let focus_handle = self.editor.focus_handle(cx); let focus_handle = self.editor.focus_handle(cx);
move |window, cx| { move |window, cx| {
cx.new(|_| { cx.new(|cx| {
let mut tooltip = Tooltip::new("Previous Alternative").key_binding( let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
KeyBinding::for_action_in( KeyBinding::for_action_in(
&CyclePreviousInlineAssist, &CyclePreviousInlineAssist,
&focus_handle, &focus_handle,
window, window,
cx,
), ),
); );
if !disabled && current_index != 0 { if !disabled && current_index != 0 {
@ -659,12 +660,13 @@ impl<T: 'static> PromptEditor<T> {
.tooltip({ .tooltip({
let focus_handle = self.editor.focus_handle(cx); let focus_handle = self.editor.focus_handle(cx);
move |window, cx| { move |window, cx| {
cx.new(|_| { cx.new(|cx| {
let mut tooltip = Tooltip::new("Next Alternative").key_binding( let mut tooltip = Tooltip::new("Next Alternative").key_binding(
KeyBinding::for_action_in( KeyBinding::for_action_in(
&CycleNextInlineAssist, &CycleNextInlineAssist,
&focus_handle, &focus_handle,
window, window,
cx,
), ),
); );
if !disabled && current_index != total_models - 1 { if !disabled && current_index != total_models - 1 {

View file

@ -390,6 +390,7 @@ impl Render for MessageEditor {
&ChatMode, &ChatMode,
&focus_handle, &focus_handle,
window, window,
cx,
)), )),
) )
.child(h_flex().gap_1().child(self.model_selector.clone()).child( .child(h_flex().gap_1().child(self.model_selector.clone()).child(
@ -419,6 +420,7 @@ impl Render for MessageEditor {
&editor::actions::Cancel, &editor::actions::Cancel,
&focus_handle, &focus_handle,
window, window,
cx,
) )
.map(|binding| binding.into_any_element()), .map(|binding| binding.into_any_element()),
), ),
@ -449,6 +451,7 @@ impl Render for MessageEditor {
&Chat, &Chat,
&focus_handle, &focus_handle,
window, window,
cx,
) )
.map(|binding| binding.into_any_element()), .map(|binding| binding.into_any_element()),
), ),

View file

@ -2290,7 +2290,7 @@ impl ContextEditor {
}, },
)) ))
.children( .children(
KeyBinding::for_action_in(&Assist, &focus_handle, window) KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
.map(|binding| binding.into_any_element()), .map(|binding| binding.into_any_element()),
) )
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
@ -2343,7 +2343,7 @@ impl ContextEditor {
.layer(ElevationIndex::ModalSurface) .layer(ElevationIndex::ModalSurface)
.child(Label::new("Suggest Edits")) .child(Label::new("Suggest Edits"))
.children( .children(
KeyBinding::for_action_in(&Edit, &focus_handle, window) KeyBinding::for_action_in(&Edit, &focus_handle, window, cx)
.map(|binding| binding.into_any_element()), .map(|binding| binding.into_any_element()),
) )
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {

View file

@ -992,6 +992,7 @@ impl Render for ChatPanel {
.key_binding(KeyBinding::for_action( .key_binding(KeyBinding::for_action(
&collab_panel::ToggleFocus, &collab_panel::ToggleFocus,
window, window,
cx,
)) ))
.on_click(|_, window, cx| { .on_click(|_, window, cx| {
window.dispatch_action( window.dispatch_action(

View file

@ -402,7 +402,7 @@ impl PickerDelegate for CommandPaletteDelegate {
ix: usize, ix: usize,
selected: bool, selected: bool,
window: &mut Window, window: &mut Window,
_: &mut Context<Picker<Self>>, cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let r#match = self.matches.get(ix)?; let r#match = self.matches.get(ix)?;
let command = self.commands.get(r#match.candidate_id)?; let command = self.commands.get(r#match.candidate_id)?;
@ -424,6 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
&*command.action, &*command.action,
&self.previous_focus_handle, &self.previous_focus_handle,
window, window,
cx,
)), )),
), ),
) )

View file

@ -2731,6 +2731,7 @@ impl EditorElement {
&OpenExcerpts, &OpenExcerpts,
&focus_handle, &focus_handle,
window, window,
cx,
) )
.map(|binding| binding.into_any_element()), .map(|binding| binding.into_any_element()),
), ),

View file

@ -387,7 +387,7 @@ impl Render for ProposedChangesEditorToolbar {
Some(editor) => { Some(editor) => {
let focus_handle = editor.focus_handle(cx); let focus_handle = editor.focus_handle(cx);
let keybinding = 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()); .map(|binding| binding.into_any_element());
button_like.children(keybinding).on_click({ button_like.children(keybinding).on_click({

View file

@ -1317,7 +1317,7 @@ impl PickerDelegate for FileFinderDelegate {
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.child( .child(
Button::new("open-selection", "Open") 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| { .on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx) window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}), }),
@ -1334,6 +1334,7 @@ impl PickerDelegate for FileFinderDelegate {
&ToggleMenu, &ToggleMenu,
&context, &context,
window, window,
cx,
)), )),
) )
.menu({ .menu({

View file

@ -216,6 +216,7 @@ impl Render for KeyContextView {
.key_binding(ui::KeyBinding::for_action( .key_binding(ui::KeyBinding::for_action(
&zed_actions::OpenDefaultKeymap, &zed_actions::OpenDefaultKeymap,
window, window,
cx
)) ))
.on_click(|_, window, cx| { .on_click(|_, window, cx| {
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx); window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
@ -225,7 +226,7 @@ impl Render for KeyContextView {
.child( .child(
Button::new("default", "Edit your keymap") Button::new("default", "Edit your keymap")
.style(ButtonStyle::Filled) .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| { .on_click(|_, window, cx| {
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx); window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);

View file

@ -4643,7 +4643,7 @@ impl Render for ProjectPanel {
.child( .child(
Button::new("open_project", "Open a project") Button::new("open_project", "Open a project")
.full_width() .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| { .on_click(cx.listener(|this, _, window, cx| {
this.workspace this.workspace
.update(cx, |_, cx| { .update(cx, |_, cx| {

View file

@ -1268,7 +1268,7 @@ impl Render for PromptLibrary {
Button::new("create-prompt", "New Prompt") Button::new("create-prompt", "New Prompt")
.full_width() .full_width()
.key_binding(KeyBinding::for_action( .key_binding(KeyBinding::for_action(
&NewPrompt, window, &NewPrompt, window, cx,
)) ))
.on_click(|_, window, cx| { .on_click(|_, window, cx| {
window.dispatch_action( window.dispatch_action(

View file

@ -465,14 +465,14 @@ impl PickerDelegate for RecentProjectsDelegate {
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.child( .child(
Button::new("remote", "Open Remote Folder") 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| { .on_click(|_, window, cx| {
window.dispatch_action(OpenRemote.boxed_clone(), cx) window.dispatch_action(OpenRemote.boxed_clone(), cx)
}), }),
) )
.child( .child(
Button::new("local", "Open Local Folder") 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| { .on_click(|_, window, cx| {
window.dispatch_action(workspace::Open.boxed_clone(), cx) window.dispatch_action(workspace::Open.boxed_clone(), cx)
}), }),

View file

@ -249,7 +249,7 @@ impl Render for ReplSessionsPage {
return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child( return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child(
v_flex() v_flex()
.child(Label::new(instructions)) .child(Label::new(instructions))
.children(KeyBinding::for_action(&Run, window)), .children(KeyBinding::for_action(&Run, window, cx)),
); );
} }

View file

@ -364,7 +364,7 @@ impl Render for ProjectSearchView {
None None
} }
} else { } 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)); let page_content = page_content.map(|text| div().child(text));
@ -1231,7 +1231,7 @@ impl ProjectSearchView {
self.active_match_index.is_some() 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(); let focus_handle = self.focus_handle.clone();
v_flex() v_flex()
.gap_1() .gap_1()
@ -1249,6 +1249,7 @@ impl ProjectSearchView {
&ToggleFilters, &ToggleFilters,
&focus_handle, &focus_handle,
window, window,
cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleFilters.boxed_clone(), cx) window.dispatch_action(ToggleFilters.boxed_clone(), cx)
@ -1263,6 +1264,7 @@ impl ProjectSearchView {
&ToggleReplace, &ToggleReplace,
&focus_handle, &focus_handle,
window, window,
cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleReplace.boxed_clone(), cx) window.dispatch_action(ToggleReplace.boxed_clone(), cx)
@ -1277,6 +1279,7 @@ impl ProjectSearchView {
&ToggleRegex, &ToggleRegex,
&focus_handle, &focus_handle,
window, window,
cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleRegex.boxed_clone(), cx) window.dispatch_action(ToggleRegex.boxed_clone(), cx)
@ -1291,6 +1294,7 @@ impl ProjectSearchView {
&ToggleCaseSensitive, &ToggleCaseSensitive,
&focus_handle, &focus_handle,
window, window,
cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx) window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx)
@ -1305,6 +1309,7 @@ impl ProjectSearchView {
&ToggleWholeWord, &ToggleWholeWord,
&focus_handle, &focus_handle,
window, window,
cx,
)) ))
.on_click(|_event, window, cx| { .on_click(|_event, window, cx| {
window.dispatch_action(ToggleWholeWord.boxed_clone(), cx) window.dispatch_action(ToggleWholeWord.boxed_clone(), cx)

View file

@ -511,7 +511,7 @@ impl PickerDelegate for TasksModalDelegate {
.child( .child(
left_button left_button
.map(|(label, action)| { .map(|(label, action)| {
let keybind = KeyBinding::for_action(&*action, window); let keybind = KeyBinding::for_action(&*action, window, cx);
Button::new("edit-current-task", label) Button::new("edit-current-task", label)
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
@ -530,7 +530,7 @@ impl PickerDelegate for TasksModalDelegate {
secondary: current_modifiers.secondary(), secondary: current_modifiers.secondary(),
} }
.boxed_clone(); .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() { let spawn_oneshot_label = if current_modifiers.secondary() {
"Spawn Oneshot Without History" "Spawn Oneshot Without History"
} else { } else {
@ -545,26 +545,28 @@ impl PickerDelegate for TasksModalDelegate {
}) })
})) }))
} else if current_modifiers.secondary() { } else if current_modifiers.secondary() {
this.children(KeyBinding::for_action(&menu::SecondaryConfirm, window).map( this.children(
|keybind| { KeyBinding::for_action(&menu::SecondaryConfirm, window, cx).map(
let label = if is_recent_selected { |keybind| {
"Rerun Without History" let label = if is_recent_selected {
} else { "Rerun Without History"
"Spawn Without History" } else {
}; "Spawn Without History"
Button::new("spawn", label) };
.label_size(LabelSize::Small) Button::new("spawn", label)
.key_binding(keybind) .label_size(LabelSize::Small)
.on_click(move |_, window, cx| { .key_binding(keybind)
window.dispatch_action( .on_click(move |_, window, cx| {
menu::SecondaryConfirm.boxed_clone(), window.dispatch_action(
cx, menu::SecondaryConfirm.boxed_clone(),
) cx,
}) )
}, })
)) },
),
)
} else { } else {
this.children(KeyBinding::for_action(&menu::Confirm, window).map( this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
|keybind| { |keybind| {
let run_entry_label = let run_entry_label =
if is_recent_selected { "Rerun" } else { "Spawn" }; if is_recent_selected { "Rerun" } else { "Spawn" };

View file

@ -716,11 +716,12 @@ impl Render for ContextMenu {
KeyBinding::for_action_in( KeyBinding::for_action_in(
&**action, focus, &**action, focus,
window, window,
cx
) )
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
KeyBinding::for_action( KeyBinding::for_action(
&**action, window, &**action, window, cx
) )
}) })
.map(|binding| { .map(|binding| {

View file

@ -2,8 +2,10 @@
use crate::PlatformStyle; use crate::PlatformStyle;
use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
use gpui::{ 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)] #[derive(Debug, IntoElement, Clone)]
pub struct KeyBinding { pub struct KeyBinding {
@ -16,18 +18,24 @@ pub struct KeyBinding {
/// The [`PlatformStyle`] to use when displaying this keybinding. /// The [`PlatformStyle`] to use when displaying this keybinding.
platform_style: PlatformStyle, platform_style: PlatformStyle,
size: Option<AbsoluteLength>, size: Option<AbsoluteLength>,
/// Determines whether the keybinding is meant for vim mode.
vim_mode: bool,
} }
struct VimStyle(bool);
impl Global for VimStyle {}
impl KeyBinding { impl KeyBinding {
/// Returns the highest precedence keybinding for an action. This is the last binding added to /// 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. /// 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<Self> { pub fn for_action(action: &dyn Action, window: &mut Window, cx: &App) -> Option<Self> {
let key_binding = window let key_binding = window
.bindings_for_action(action) .bindings_for_action(action)
.into_iter() .into_iter()
.rev() .rev()
.next()?; .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. /// Like `for_action`, but lets you specify the context from which keybindings are matched.
@ -35,20 +43,30 @@ impl KeyBinding {
action: &dyn Action, action: &dyn Action,
focus: &FocusHandle, focus: &FocusHandle,
window: &mut Window, window: &mut Window,
cx: &App,
) -> Option<Self> { ) -> Option<Self> {
let key_binding = window let key_binding = window
.bindings_for_action_in(action, focus) .bindings_for_action_in(action, focus)
.into_iter() .into_iter()
.rev() .rev()
.next()?; .next()?;
Some(Self::new(key_binding)) Some(Self::new(key_binding, cx))
} }
pub fn new(key_binding: gpui::KeyBinding) -> Self { 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::<VimStyle>().is_some_and(|g| g.0)
}
pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self {
Self { Self {
key_binding, key_binding,
platform_style: PlatformStyle::platform(), platform_style: PlatformStyle::platform(),
size: None, size: None,
vim_mode: KeyBinding::is_vim_mode(cx),
} }
} }
@ -63,6 +81,30 @@ impl KeyBinding {
self.size = Some(size.into()); self.size = Some(size.into());
self self
} }
pub fn vim_mode(mut self, enabled: bool) -> Self {
self.vim_mode = enabled;
self
}
fn render_key(&self, keystroke: &Keystroke, color: Option<Color>) -> 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 { impl RenderOnce for KeyBinding {
@ -94,28 +136,11 @@ impl RenderOnce for KeyBinding {
self.size, self.size,
true, true,
)) ))
.map(|el| { .map(|el| el.child(self.render_key(&keystroke, None)))
el.child(render_key(&keystroke, self.platform_style, None, self.size))
})
})) }))
} }
} }
pub fn render_key(
keystroke: &Keystroke,
platform_style: PlatformStyle,
color: Option<Color>,
size: Option<AbsoluteLength>,
) -> 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<IconName> { fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
match keystroke.key.as_str() { match keystroke.key.as_str() {
"left" => Some(IconName::ArrowLeft), "left" => Some(IconName::ArrowLeft),
@ -312,39 +337,33 @@ impl KeyIcon {
} }
/// Returns a textual representation of the key binding for the given [`Action`]. /// Returns a textual representation of the key binding for the given [`Action`].
pub fn text_for_action(action: &dyn Action, window: &Window) -> Option<String> { pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
let bindings = window.bindings_for_action(action); let bindings = window.bindings_for_action(action);
let key_binding = bindings.last()?; 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`] pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
/// as if the provided [`FocusHandle`] was focused. let platform_style = PlatformStyle::platform();
pub fn text_for_action_in( let vim_enabled = cx.try_global::<VimStyle>().is_some();
action: &dyn Action, keystrokes
focus: &FocusHandle,
window: &mut Window,
) -> Option<String> {
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()
.iter() .iter()
.map(|keystroke| text_for_keystroke(keystroke, platform_style)) .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
.collect::<Vec<_>>()
.join(" ") .join(" ")
} }
pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
let platform_style = PlatformStyle::platform();
let vim_enabled = cx.try_global::<VimStyle>().is_some();
keystroke_text(keystroke, platform_style, vim_enabled)
}
/// Returns a textual representation of the given [`Keystroke`]. /// 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 mut text = String::new();
let delimiter = match platform_style { let delimiter = match platform_style {
@ -354,7 +373,7 @@ pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle)
if keystroke.modifiers.function { if keystroke.modifiers.function {
match platform_style { match platform_style {
PlatformStyle::Mac => text.push_str("fn"), PlatformStyle::Mac => text.push_str("Fn"),
PlatformStyle::Linux | PlatformStyle::Windows => 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 { if keystroke.modifiers.shift {
match platform_style { if !(vim_enabled && keystroke.key.len() == 1) {
PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => { match platform_style {
text.push_str("Shift") PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
text.push_str("Shift")
}
} }
text.push(delimiter);
} }
text.push(delimiter);
} }
let key = match keystroke.key.as_str() { let key = match keystroke.key.as_str() {
"pageup" => "PageUp", "pageup" => "PageUp",
"pagedown" => "PageDown", "pagedown" => "PageDown",
key if vim_enabled => {
if !keystroke.modifiers.shift && key.len() == 1 {
key
} else {
&util::capitalize(key)
}
}
key => &util::capitalize(key), key => &util::capitalize(key),
}; };
@ -417,58 +444,76 @@ mod tests {
#[test] #[test]
fn test_text_for_keystroke() { fn test_text_for_keystroke() {
assert_eq!( 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() "Command-C".to_string()
); );
assert_eq!( 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() "Super+C".to_string()
); );
assert_eq!( 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() "Win+C".to_string()
); );
assert_eq!( assert_eq!(
text_for_keystroke( keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(), &Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Mac PlatformStyle::Mac,
false
), ),
"Control-Option-Delete".to_string() "Control-Option-Delete".to_string()
); );
assert_eq!( assert_eq!(
text_for_keystroke( keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(), &Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Linux PlatformStyle::Linux,
false
), ),
"Ctrl+Alt+Delete".to_string() "Ctrl+Alt+Delete".to_string()
); );
assert_eq!( assert_eq!(
text_for_keystroke( keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(), &Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Windows PlatformStyle::Windows,
false
), ),
"Ctrl+Alt+Delete".to_string() "Ctrl+Alt+Delete".to_string()
); );
assert_eq!( assert_eq!(
text_for_keystroke( keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(), &Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Mac PlatformStyle::Mac,
false
), ),
"Shift-PageUp".to_string() "Shift-PageUp".to_string()
); );
assert_eq!( assert_eq!(
text_for_keystroke( keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(), &Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Linux PlatformStyle::Linux,
false,
), ),
"Shift+PageUp".to_string() "Shift+PageUp".to_string()
); );
assert_eq!( assert_eq!(
text_for_keystroke( keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(), &Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Windows PlatformStyle::Windows,
false
), ),
"Shift+PageUp".to_string() "Shift+PageUp".to_string()
); );

View file

@ -207,10 +207,10 @@ impl RenderOnce for KeybindingHint {
// View this component preview using `workspace: open component-preview` // View this component preview using `workspace: open component-preview`
impl ComponentPreview for KeybindingHint { 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_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
let enter = KeyBinding::for_action(&menu::Confirm, window) let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
.unwrap_or(KeyBinding::new(enter_fallback)); .unwrap_or(KeyBinding::new(enter_fallback, cx));
v_flex() v_flex()
.gap_6() .gap_6()

View file

@ -12,22 +12,22 @@ pub fn binding(key: &str) -> gpui::KeyBinding {
} }
impl Render for KeybindingStory { impl Render for KeybindingStory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
Story::container() Story::container()
.child(Story::title_for::<KeyBinding>()) .child(Story::title_for::<KeyBinding>())
.child(Story::label("Single Key")) .child(Story::label("Single Key"))
.child(KeyBinding::new(binding("Z"))) .child(KeyBinding::new(binding("Z"), cx))
.child(Story::label("Single Key with Modifier")) .child(Story::label("Single Key with Modifier"))
.child( .child(
div() div()
.flex() .flex()
.gap_3() .gap_3()
.child(KeyBinding::new(binding("ctrl-c"))) .child(KeyBinding::new(binding("ctrl-c"), cx))
.child(KeyBinding::new(binding("alt-c"))) .child(KeyBinding::new(binding("alt-c"), cx))
.child(KeyBinding::new(binding("cmd-c"))) .child(KeyBinding::new(binding("cmd-c"), cx))
.child(KeyBinding::new(binding("shift-c"))), .child(KeyBinding::new(binding("shift-c"), cx)),
) )
.child(Story::label("Single Key with Modifier (Permuted)")) .child(Story::label("Single Key with Modifier (Permuted)"))
.child( .child(
@ -41,39 +41,42 @@ impl Render for KeybindingStory {
.gap_4() .gap_4()
.py_3() .py_3()
.children(chunk.map(|permutation| { .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(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(Story::label("Chord"))
.child(KeyBinding::new(binding("a z"))) .child(KeyBinding::new(binding("a z"), cx))
.child(Story::label("Chord with Modifier")) .child(Story::label("Chord with Modifier"))
.child(KeyBinding::new(binding("ctrl-a shift-z"))) .child(KeyBinding::new(binding("ctrl-a shift-z"), cx))
.child(KeyBinding::new(binding("fn-s"))) .child(KeyBinding::new(binding("fn-s"), cx))
.child(Story::label("Single Key with All Modifiers (Linux)")) .child(Story::label("Single Key with All Modifiers (Linux)"))
.child( .child(
KeyBinding::new(binding("ctrl-alt-cmd-shift-z")) KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)
.platform_style(PlatformStyle::Linux), .platform_style(PlatformStyle::Linux),
) )
.child(Story::label("Chord (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(Story::label("Chord with Modifier (Linux)"))
.child(KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Linux)) .child(
.child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Linux)) 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(Story::label("Single Key with All Modifiers (Windows)"))
.child( .child(
KeyBinding::new(binding("ctrl-alt-cmd-shift-z")) KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)
.platform_style(PlatformStyle::Windows), .platform_style(PlatformStyle::Windows),
) )
.child(Story::label("Chord (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(Story::label("Chord with Modifier (Windows)"))
.child( .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))
} }
} }

View file

@ -43,10 +43,10 @@ impl Tooltip {
let title = title.into(); let title = title.into();
let action = action.boxed_clone(); let action = action.boxed_clone();
move |window, cx| { move |window, cx| {
cx.new(|_| Self { cx.new(|cx| Self {
title: title.clone(), title: title.clone(),
meta: None, meta: None,
key_binding: KeyBinding::for_action(action.as_ref(), window), key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
}) })
.into() .into()
} }
@ -58,10 +58,10 @@ impl Tooltip {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> AnyView { ) -> AnyView {
cx.new(|_| Self { cx.new(|cx| Self {
title: title.into(), title: title.into(),
meta: None, meta: None,
key_binding: KeyBinding::for_action(action, window), key_binding: KeyBinding::for_action(action, window, cx),
}) })
.into() .into()
} }
@ -73,10 +73,10 @@ impl Tooltip {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> AnyView { ) -> AnyView {
cx.new(|_| Self { cx.new(|cx| Self {
title: title.into(), title: title.into(),
meta: None, meta: None,
key_binding: KeyBinding::for_action_in(action, focus_handle, window), key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
}) })
.into() .into()
} }
@ -88,10 +88,10 @@ impl Tooltip {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> AnyView { ) -> AnyView {
cx.new(|_| Self { cx.new(|cx| Self {
title: title.into(), title: title.into(),
meta: Some(meta.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() .into()
} }
@ -104,11 +104,11 @@ impl Tooltip {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> AnyView { ) -> AnyView {
cx.new(|_| Self { cx.new(|cx| Self {
title: title.into(), title: title.into(),
meta: Some(meta.into()), meta: Some(meta.into()),
key_binding: action 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() .into()
} }

View file

@ -1,5 +1,5 @@
use gpui::{div, Context, Element, Entity, Render, Subscription, WeakEntity, Window}; 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 workspace::{item::ItemHandle, ui::prelude::*, StatusItemView};
use crate::{Vim, VimEvent, VimGlobals}; use crate::{Vim, VimEvent, VimGlobals};
@ -15,7 +15,7 @@ impl ModeIndicator {
/// Construct a new mode indicator in this window. /// Construct a new mode indicator in this window.
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.observe_pending_input(window, |this: &mut Self, window, cx| { cx.observe_pending_input(window, |this: &mut Self, window, cx| {
this.update_pending_keys(window); this.update_pending_keys(window, cx);
cx.notify(); cx.notify();
}) })
.detach(); .detach();
@ -50,13 +50,10 @@ impl ModeIndicator {
} }
} }
fn update_pending_keys(&mut self, window: &mut Window) { fn update_pending_keys(&mut self, window: &mut Window, cx: &App) {
self.pending_keys = window.pending_input_keystrokes().map(|keystrokes| { self.pending_keys = window
keystrokes .pending_input_keystrokes()
.iter() .map(|keystrokes| text_for_keystrokes(keystrokes, cx));
.map(|keystroke| format!("{}", keystroke))
.join(" ")
});
} }
fn vim(&self) -> Option<Entity<Vim>> { fn vim(&self) -> Option<Entity<Vim>> {

View file

@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::{fmt::Display, ops::Range, sync::Arc}; use std::{fmt::Display, ops::Range, sync::Arc};
use ui::{Context, SharedString}; use ui::{Context, KeyBinding, SharedString};
use workspace::searchable::Direction; use workspace::searchable::Direction;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
@ -217,6 +217,7 @@ impl VimGlobals {
cx.observe_global::<SettingsStore>(move |cx| { cx.observe_global::<SettingsStore>(move |cx| {
if Vim::enabled(cx) { if Vim::enabled(cx) {
KeyBinding::set_vim_mode(cx, true);
CommandPaletteFilter::update_global(cx, |filter, _| { CommandPaletteFilter::update_global(cx, |filter, _| {
filter.show_namespace(Vim::NAMESPACE); filter.show_namespace(Vim::NAMESPACE);
}); });
@ -224,6 +225,7 @@ impl VimGlobals {
interceptor.set(Box::new(command_interceptor)); interceptor.set(Box::new(command_interceptor));
}); });
} else { } else {
KeyBinding::set_vim_mode(cx, true);
*Vim::globals(cx) = VimGlobals::default(); *Vim::globals(cx) = VimGlobals::default();
CommandPaletteInterceptor::update_global(cx, |interceptor, _| { CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
interceptor.clear(); interceptor.clear();

View file

@ -41,10 +41,7 @@ impl QuickActionBar {
Tooltip::with_meta( Tooltip::with_meta(
"Preview Markdown", "Preview Markdown",
Some(&markdown_preview::OpenPreview), Some(&markdown_preview::OpenPreview),
format!( format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
"{} to open in a split",
text_for_keystroke(&alt_click, PlatformStyle::platform())
),
window, window,
cx, cx,
) )

View file

@ -489,6 +489,7 @@ impl RateCompletionModal {
&ThumbsDownActiveCompletion, &ThumbsDownActiveCompletion,
focus_handle, focus_handle,
window, window,
cx
)) ))
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.thumbs_down_active( this.thumbs_down_active(
@ -507,6 +508,7 @@ impl RateCompletionModal {
&ThumbsUpActiveCompletion, &ThumbsUpActiveCompletion,
focus_handle, focus_handle,
window, window,
cx
)) ))
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.thumbs_up_active(&ThumbsUpActiveCompletion, window, cx); this.thumbs_up_active(&ThumbsUpActiveCompletion, window, cx);

View file

@ -408,8 +408,8 @@ The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for qui
{ {
"context": "vim_mode == normal || vim_mode == visual", "context": "vim_mode == normal || vim_mode == visual",
"bindings": { "bindings": {
"s": ["vim::PushSneak", {}], "s": "vim::PushSneak",
"S": ["vim::PushSneakBackward", {}] "shift-s": "vim::PushSneakBackward"
} }
} }
``` ```