windows: Fix keystroke & keymap (#36572)
Closes #36300 This PR follows Windows conventions by introducing `KeybindingKeystroke`, so shortcuts now show up as `ctrl-shift-4` instead of `ctrl-$`. It also fixes issues with keyboard layouts: when `use_key_equivalents` is set to true, keys are remapped based on their virtual key codes. For example, `ctrl-\` on a standard English layout will be mapped to `ctrl-ё` on a Russian layout. Release Notes: - N/A --------- Co-authored-by: Kate <kate@zed.dev>
This commit is contained in:
parent
b1b60bb7fe
commit
fff0ecead1
25 changed files with 3515 additions and 1721 deletions
1260
assets/keymaps/default-windows.json
Normal file
1260
assets/keymaps/default-windows.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
|
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||||
|
});
|
||||||
|
|
||||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||||
|
|
||||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||||
|
@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||||
let keymap = match os {
|
let keymap = match os {
|
||||||
"macos" => &KEYMAP_MACOS,
|
"macos" => &KEYMAP_MACOS,
|
||||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||||
|
"windows" => &KEYMAP_WINDOWS,
|
||||||
_ => unreachable!("Not a valid OS: {}", os),
|
_ => unreachable!("Not a valid OS: {}", os),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2588,7 +2588,7 @@ impl Editor {
|
||||||
|| binding
|
|| binding
|
||||||
.keystrokes()
|
.keystrokes()
|
||||||
.first()
|
.first()
|
||||||
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7686,16 +7686,16 @@ impl Editor {
|
||||||
.keystroke()
|
.keystroke()
|
||||||
{
|
{
|
||||||
modifiers_held = modifiers_held
|
modifiers_held = modifiers_held
|
||||||
|| (&accept_keystroke.modifiers == modifiers
|
|| (&accept_keystroke.display_modifiers == modifiers
|
||||||
&& accept_keystroke.modifiers.modified());
|
&& accept_keystroke.display_modifiers.modified());
|
||||||
};
|
};
|
||||||
if let Some(accept_partial_keystroke) = self
|
if let Some(accept_partial_keystroke) = self
|
||||||
.accept_edit_prediction_keybind(true, window, cx)
|
.accept_edit_prediction_keybind(true, window, cx)
|
||||||
.keystroke()
|
.keystroke()
|
||||||
{
|
{
|
||||||
modifiers_held = modifiers_held
|
modifiers_held = modifiers_held
|
||||||
|| (&accept_partial_keystroke.modifiers == modifiers
|
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
||||||
&& accept_partial_keystroke.modifiers.modified());
|
&& accept_partial_keystroke.display_modifiers.modified());
|
||||||
}
|
}
|
||||||
|
|
||||||
if modifiers_held {
|
if modifiers_held {
|
||||||
|
@ -9044,7 +9044,7 @@ impl Editor {
|
||||||
|
|
||||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||||
|
|
||||||
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
||||||
Color::Accent
|
Color::Accent
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
@ -9056,19 +9056,19 @@ impl Editor {
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.text_size(TextSize::XSmall.rems(cx))
|
.text_size(TextSize::XSmall.rems(cx))
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.modifiers,
|
&accept_keystroke.display_modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(modifiers_color),
|
Some(modifiers_color),
|
||||||
Some(IconSize::XSmall.rems().into()),
|
Some(IconSize::XSmall.rems().into()),
|
||||||
true,
|
true,
|
||||||
)))
|
)))
|
||||||
.when(is_platform_style_mac, |parent| {
|
.when(is_platform_style_mac, |parent| {
|
||||||
parent.child(accept_keystroke.key.clone())
|
parent.child(accept_keystroke.display_key.clone())
|
||||||
})
|
})
|
||||||
.when(!is_platform_style_mac, |parent| {
|
.when(!is_platform_style_mac, |parent| {
|
||||||
parent.child(
|
parent.child(
|
||||||
Key::new(
|
Key::new(
|
||||||
util::capitalize(&accept_keystroke.key),
|
util::capitalize(&accept_keystroke.display_key),
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
)
|
)
|
||||||
.size(Some(IconSize::XSmall.rems().into())),
|
.size(Some(IconSize::XSmall.rems().into())),
|
||||||
|
@ -9171,7 +9171,7 @@ impl Editor {
|
||||||
max_width: Pixels,
|
max_width: Pixels,
|
||||||
cursor_point: Point,
|
cursor_point: Point,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
accept_keystroke: Option<&gpui::Keystroke>,
|
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
||||||
_window: &Window,
|
_window: &Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
|
@ -9249,7 +9249,7 @@ impl Editor {
|
||||||
accept_keystroke.as_ref(),
|
accept_keystroke.as_ref(),
|
||||||
|el, accept_keystroke| {
|
|el, accept_keystroke| {
|
||||||
el.child(h_flex().children(ui::render_modifiers(
|
el.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.modifiers,
|
&accept_keystroke.display_modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
Some(IconSize::XSmall.rems().into()),
|
Some(IconSize::XSmall.rems().into()),
|
||||||
|
@ -9319,7 +9319,7 @@ impl Editor {
|
||||||
.child(completion),
|
.child(completion),
|
||||||
)
|
)
|
||||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||||
if !accept_keystroke.modifiers.modified() {
|
if !accept_keystroke.display_modifiers.modified() {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9338,7 +9338,7 @@ impl Editor {
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.modifiers,
|
&accept_keystroke.display_modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(if !has_completion {
|
Some(if !has_completion {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -43,10 +43,10 @@ use gpui::{
|
||||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
||||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||||
transparent_black,
|
transparent_black,
|
||||||
};
|
};
|
||||||
|
@ -7150,7 +7150,7 @@ fn header_jump_data(
|
||||||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||||
|
|
||||||
impl AcceptEditPredictionBinding {
|
impl AcceptEditPredictionBinding {
|
||||||
pub fn keystroke(&self) -> Option<&Keystroke> {
|
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
||||||
if let Some(binding) = self.0.as_ref() {
|
if let Some(binding) = self.0.as_ref() {
|
||||||
match &binding.keystrokes() {
|
match &binding.keystrokes() {
|
||||||
[keystroke, ..] => Some(keystroke),
|
[keystroke, ..] => Some(keystroke),
|
||||||
|
|
|
@ -37,10 +37,10 @@ use crate::{
|
||||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||||
WindowHandle, WindowId, WindowInvalidator,
|
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||||
colors::{Colors, GlobalColors},
|
colors::{Colors, GlobalColors},
|
||||||
current_platform, hash, init_app_menus,
|
current_platform, hash, init_app_menus,
|
||||||
};
|
};
|
||||||
|
@ -263,6 +263,7 @@ pub struct App {
|
||||||
pub(crate) focus_handles: Arc<FocusMap>,
|
pub(crate) focus_handles: Arc<FocusMap>,
|
||||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||||
|
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
||||||
pub(crate) global_action_listeners:
|
pub(crate) global_action_listeners:
|
||||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||||
pending_effects: VecDeque<Effect>,
|
pending_effects: VecDeque<Effect>,
|
||||||
|
@ -312,6 +313,7 @@ impl App {
|
||||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||||
let entities = EntityMap::new();
|
let entities = EntityMap::new();
|
||||||
let keyboard_layout = platform.keyboard_layout();
|
let keyboard_layout = platform.keyboard_layout();
|
||||||
|
let keyboard_mapper = platform.keyboard_mapper();
|
||||||
|
|
||||||
let app = Rc::new_cyclic(|this| AppCell {
|
let app = Rc::new_cyclic(|this| AppCell {
|
||||||
app: RefCell::new(App {
|
app: RefCell::new(App {
|
||||||
|
@ -337,6 +339,7 @@ impl App {
|
||||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||||
keyboard_layout,
|
keyboard_layout,
|
||||||
|
keyboard_mapper,
|
||||||
global_action_listeners: FxHashMap::default(),
|
global_action_listeners: FxHashMap::default(),
|
||||||
pending_effects: VecDeque::new(),
|
pending_effects: VecDeque::new(),
|
||||||
pending_notifications: FxHashSet::default(),
|
pending_notifications: FxHashSet::default(),
|
||||||
|
@ -376,6 +379,7 @@ impl App {
|
||||||
if let Some(app) = app.upgrade() {
|
if let Some(app) = app.upgrade() {
|
||||||
let cx = &mut app.borrow_mut();
|
let cx = &mut app.borrow_mut();
|
||||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||||
|
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||||
cx.keyboard_layout_observers
|
cx.keyboard_layout_observers
|
||||||
.clone()
|
.clone()
|
||||||
.retain(&(), move |callback| (callback)(cx));
|
.retain(&(), move |callback| (callback)(cx));
|
||||||
|
@ -424,6 +428,11 @@ impl App {
|
||||||
self.keyboard_layout.as_ref()
|
self.keyboard_layout.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current keyboard mapper.
|
||||||
|
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
&self.keyboard_mapper
|
||||||
|
}
|
||||||
|
|
||||||
/// Invokes a handler when the current keyboard layout changes
|
/// Invokes a handler when the current keyboard layout changes
|
||||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
|
|
|
@ -4,7 +4,7 @@ mod context;
|
||||||
pub use binding::*;
|
pub use binding::*;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
|
||||||
use crate::{Action, Keystroke, is_no_action};
|
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
@ -141,7 +141,7 @@ impl Keymap {
|
||||||
/// only.
|
/// only.
|
||||||
pub fn bindings_for_input(
|
pub fn bindings_for_input(
|
||||||
&self,
|
&self,
|
||||||
input: &[Keystroke],
|
input: &[impl AsKeystroke],
|
||||||
context_stack: &[KeyContext],
|
context_stack: &[KeyContext],
|
||||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||||
|
@ -192,7 +192,6 @@ impl Keymap {
|
||||||
|
|
||||||
(bindings, !pending.is_empty())
|
(bindings, !pending.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given binding is enabled, given a certain key context.
|
/// Check if the given binding is enabled, given a certain key context.
|
||||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||||
|
@ -639,7 +638,7 @@ mod tests {
|
||||||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||||
let actual = keymap
|
let actual = keymap
|
||||||
.bindings_for_action(action)
|
.bindings_for_action(action)
|
||||||
.map(|binding| binding.keystrokes[0].unparse())
|
.map(|binding| binding.keystrokes[0].inner.unparse())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(actual, expected, "{:?}", action);
|
assert_eq!(actual, expected, "{:?}", action);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use collections::HashMap;
|
use crate::{
|
||||||
|
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||||
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
/// A keybinding and its associated metadata, from the keymap.
|
/// A keybinding and its associated metadata, from the keymap.
|
||||||
pub struct KeyBinding {
|
pub struct KeyBinding {
|
||||||
pub(crate) action: Box<dyn Action>,
|
pub(crate) action: Box<dyn Action>,
|
||||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
||||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||||
/// The json input string used when building the keybinding, if any
|
/// The json input string used when building the keybinding, if any
|
||||||
|
@ -32,7 +33,15 @@ impl KeyBinding {
|
||||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||||
let context_predicate =
|
let context_predicate =
|
||||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
Self::load(
|
||||||
|
keystrokes,
|
||||||
|
Box::new(action),
|
||||||
|
context_predicate,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
&DummyKeyboardMapper,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a keybinding from the given raw data.
|
/// Load a keybinding from the given raw data.
|
||||||
|
@ -40,24 +49,22 @@ impl KeyBinding {
|
||||||
keystrokes: &str,
|
keystrokes: &str,
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
key_equivalents: Option<&HashMap<char, char>>,
|
use_key_equivalents: bool,
|
||||||
action_input: Option<SharedString>,
|
action_input: Option<SharedString>,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(Keystroke::parse)
|
.map(|source| {
|
||||||
|
let keystroke = Keystroke::parse(source)?;
|
||||||
|
Ok(KeybindingKeystroke::new(
|
||||||
|
keystroke,
|
||||||
|
use_key_equivalents,
|
||||||
|
keyboard_mapper,
|
||||||
|
))
|
||||||
|
})
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
|
||||||
if let Some(equivalents) = key_equivalents {
|
|
||||||
for keystroke in keystrokes.iter_mut() {
|
|
||||||
if keystroke.key.chars().count() == 1
|
|
||||||
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
|
||||||
{
|
|
||||||
keystroke.key = key.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
|
@ -79,13 +86,13 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given keystrokes match this binding.
|
/// Check if the given keystrokes match this binding.
|
||||||
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
||||||
if self.keystrokes.len() < typed.len() {
|
if self.keystrokes.len() < typed.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||||
if !typed.should_match(target) {
|
if !typed.as_keystroke().should_match(target) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +101,7 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the keystrokes associated with this binding
|
/// Get the keystrokes associated with this binding
|
||||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||||
self.keystrokes.as_slice()
|
self.keystrokes.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
|
||||||
|
|
||||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
|
||||||
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||||
|
@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
|
||||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
|
||||||
|
|
||||||
fn compositor_name(&self) -> &'static str {
|
fn compositor_name(&self) -> &'static str {
|
||||||
""
|
""
|
||||||
|
@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
|
||||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||||
|
|
||||||
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
|
||||||
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{KeybindingKeystroke, Keystroke};
|
||||||
|
|
||||||
/// A trait for platform-specific keyboard layouts
|
/// A trait for platform-specific keyboard layouts
|
||||||
pub trait PlatformKeyboardLayout {
|
pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout ID, which should be unique to the layout
|
/// Get the keyboard layout ID, which should be unique to the layout
|
||||||
|
@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout display name
|
/// Get the keyboard layout display name
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A trait for platform-specific keyboard mappings
|
||||||
|
pub trait PlatformKeyboardMapper {
|
||||||
|
/// Map a key equivalent to its platform-specific representation
|
||||||
|
fn map_key_equivalent(
|
||||||
|
&self,
|
||||||
|
keystroke: Keystroke,
|
||||||
|
use_key_equivalents: bool,
|
||||||
|
) -> KeybindingKeystroke;
|
||||||
|
/// Get the key equivalents for the current keyboard layout,
|
||||||
|
/// only used on macOS
|
||||||
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dummy implementation of the platform keyboard mapper
|
||||||
|
pub struct DummyKeyboardMapper;
|
||||||
|
|
||||||
|
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||||
|
fn map_key_equivalent(
|
||||||
|
&self,
|
||||||
|
keystroke: Keystroke,
|
||||||
|
_use_key_equivalents: bool,
|
||||||
|
) -> KeybindingKeystroke {
|
||||||
|
KeybindingKeystroke::from_keystroke(keystroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,14 @@ use std::{
|
||||||
fmt::{Display, Write},
|
fmt::{Display, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::PlatformKeyboardMapper;
|
||||||
|
|
||||||
|
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||||
|
pub trait AsKeystroke {
|
||||||
|
/// Returns the GPUI representation of the keystroke.
|
||||||
|
fn as_keystroke(&self) -> &Keystroke;
|
||||||
|
}
|
||||||
|
|
||||||
/// A keystroke and associated metadata generated by the platform
|
/// A keystroke and associated metadata generated by the platform
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||||
pub struct Keystroke {
|
pub struct Keystroke {
|
||||||
|
@ -24,6 +32,17 @@ pub struct Keystroke {
|
||||||
pub key_char: Option<String>,
|
pub key_char: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct KeybindingKeystroke {
|
||||||
|
/// The GPUI representation of the keystroke.
|
||||||
|
pub inner: Keystroke,
|
||||||
|
/// The modifiers to display.
|
||||||
|
pub display_modifiers: Modifiers,
|
||||||
|
/// The key to display.
|
||||||
|
pub display_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||||
/// markdown to display it.
|
/// markdown to display it.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -58,7 +77,7 @@ impl Keystroke {
|
||||||
///
|
///
|
||||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||||
/// both possibilities for self against the target.
|
/// both possibilities for self against the target.
|
||||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
if let Some(key_char) = self
|
if let Some(key_char) = self
|
||||||
.key_char
|
.key_char
|
||||||
|
@ -71,7 +90,7 @@ impl Keystroke {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,12 +102,12 @@ impl Keystroke {
|
||||||
.filter(|key_char| key_char != &&self.key)
|
.filter(|key_char| key_char != &&self.key)
|
||||||
{
|
{
|
||||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||||
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target.modifiers == self.modifiers && target.key == self.key
|
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// key syntax is:
|
/// key syntax is:
|
||||||
|
@ -200,31 +219,7 @@ impl Keystroke {
|
||||||
|
|
||||||
/// Produces a representation of this key that Parse can understand.
|
/// Produces a representation of this key that Parse can understand.
|
||||||
pub fn unparse(&self) -> String {
|
pub fn unparse(&self) -> String {
|
||||||
let mut str = String::new();
|
unparse(&self.modifiers, &self.key)
|
||||||
if self.modifiers.function {
|
|
||||||
str.push_str("fn-");
|
|
||||||
}
|
|
||||||
if self.modifiers.control {
|
|
||||||
str.push_str("ctrl-");
|
|
||||||
}
|
|
||||||
if self.modifiers.alt {
|
|
||||||
str.push_str("alt-");
|
|
||||||
}
|
|
||||||
if self.modifiers.platform {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
str.push_str("cmd-");
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
||||||
str.push_str("super-");
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
str.push_str("win-");
|
|
||||||
}
|
|
||||||
if self.modifiers.shift {
|
|
||||||
str.push_str("shift-");
|
|
||||||
}
|
|
||||||
str.push_str(&self.key);
|
|
||||||
str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this keystroke left
|
/// Returns true if this keystroke left
|
||||||
|
@ -266,6 +261,32 @@ impl Keystroke {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeybindingKeystroke {
|
||||||
|
/// Create a new keybinding keystroke from the given keystroke
|
||||||
|
pub fn new(
|
||||||
|
inner: Keystroke,
|
||||||
|
use_key_equivalents: bool,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
|
) -> Self {
|
||||||
|
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||||
|
let key = keystroke.key.clone();
|
||||||
|
let modifiers = keystroke.modifiers;
|
||||||
|
KeybindingKeystroke {
|
||||||
|
inner: keystroke,
|
||||||
|
display_modifiers: modifiers,
|
||||||
|
display_key: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a representation of this key that Parse can understand.
|
||||||
|
pub fn unparse(&self) -> String {
|
||||||
|
unparse(&self.display_modifiers, &self.display_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_printable_key(key: &str) -> bool {
|
fn is_printable_key(key: &str) -> bool {
|
||||||
!matches!(
|
!matches!(
|
||||||
key,
|
key,
|
||||||
|
@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
|
||||||
|
|
||||||
impl std::fmt::Display for Keystroke {
|
impl std::fmt::Display for Keystroke {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if self.modifiers.control {
|
display_modifiers(&self.modifiers, f)?;
|
||||||
#[cfg(target_os = "macos")]
|
display_key(&self.key, f)
|
||||||
f.write_char('^')?;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
impl std::fmt::Display for KeybindingKeystroke {
|
||||||
write!(f, "ctrl-")?;
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
}
|
display_modifiers(&self.display_modifiers, f)?;
|
||||||
if self.modifiers.alt {
|
display_key(&self.display_key, f)
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⌥')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "alt-")?;
|
|
||||||
}
|
|
||||||
if self.modifiers.platform {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⌘')?;
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
||||||
f.write_char('❖')?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
f.write_char('⊞')?;
|
|
||||||
}
|
|
||||||
if self.modifiers.shift {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⇧')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "shift-")?;
|
|
||||||
}
|
|
||||||
let key = match self.key.as_str() {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"backspace" => '⌫',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"up" => '↑',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"down" => '↓',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"left" => '←',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"right" => '→',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"tab" => '⇥',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"escape" => '⎋',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"shift" => '⇧',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"control" => '⌃',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"alt" => '⌥',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"platform" => '⌘',
|
|
||||||
|
|
||||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
|
||||||
key => return f.write_str(key),
|
|
||||||
};
|
|
||||||
f.write_char(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,3 +571,110 @@ pub struct Capslock {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub on: bool,
|
pub on: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsKeystroke for Keystroke {
|
||||||
|
fn as_keystroke(&self) -> &Keystroke {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsKeystroke for KeybindingKeystroke {
|
||||||
|
fn as_keystroke(&self) -> &Keystroke {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if modifiers.control {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('^')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "ctrl-")?;
|
||||||
|
}
|
||||||
|
if modifiers.alt {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌥')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "alt-")?;
|
||||||
|
}
|
||||||
|
if modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌘')?;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
f.write_char('❖')?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
f.write_char('⊞')?;
|
||||||
|
}
|
||||||
|
if modifiers.shift {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⇧')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "shift-")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let key = match key {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"backspace" => '⌫',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"up" => '↑',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"down" => '↓',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"left" => '←',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"right" => '→',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"tab" => '⇥',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"escape" => '⎋',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"shift" => '⇧',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"control" => '⌃',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"alt" => '⌥',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"platform" => '⌘',
|
||||||
|
|
||||||
|
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||||
|
key => return f.write_str(key),
|
||||||
|
};
|
||||||
|
f.write_char(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
if modifiers.function {
|
||||||
|
result.push_str("fn-");
|
||||||
|
}
|
||||||
|
if modifiers.control {
|
||||||
|
result.push_str("ctrl-");
|
||||||
|
}
|
||||||
|
if modifiers.alt {
|
||||||
|
result.push_str("alt-");
|
||||||
|
}
|
||||||
|
if modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
result.push_str("cmd-");
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
result.push_str("super-");
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
result.push_str("win-");
|
||||||
|
}
|
||||||
|
if modifiers.shift {
|
||||||
|
result.push_str("shift-");
|
||||||
|
}
|
||||||
|
result.push_str(&key);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||||
|
@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||||
self.keyboard_layout()
|
self.keyboard_layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
Rc::new(crate::DummyKeyboardMapper)
|
||||||
|
}
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
use super::{
|
use super::{
|
||||||
BoolExt, MacKeyboardLayout,
|
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||||
events::key_to_native,
|
events::key_to_native,
|
||||||
renderer,
|
renderer,
|
||||||
|
@ -8,8 +8,9 @@ use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||||
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
||||||
|
hash,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
|
@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
|
||||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||||
dock_menu: Option<id>,
|
dock_menu: Option<id>,
|
||||||
menus: Option<Vec<OwnedMenu>>,
|
menus: Option<Vec<OwnedMenu>>,
|
||||||
|
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MacPlatform {
|
impl Default for MacPlatform {
|
||||||
|
@ -189,6 +191,9 @@ impl MacPlatform {
|
||||||
#[cfg(not(feature = "font-kit"))]
|
#[cfg(not(feature = "font-kit"))]
|
||||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||||
|
|
||||||
|
let keyboard_layout = MacKeyboardLayout::new();
|
||||||
|
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||||
|
|
||||||
Self(Mutex::new(MacPlatformState {
|
Self(Mutex::new(MacPlatformState {
|
||||||
headless,
|
headless,
|
||||||
text_system,
|
text_system,
|
||||||
|
@ -209,6 +214,7 @@ impl MacPlatform {
|
||||||
dock_menu: None,
|
dock_menu: None,
|
||||||
on_keyboard_layout_change: None,
|
on_keyboard_layout_change: None,
|
||||||
menus: None,
|
menus: None,
|
||||||
|
keyboard_mapper,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,19 +354,19 @@ impl MacPlatform {
|
||||||
let mut mask = NSEventModifierFlags::empty();
|
let mut mask = NSEventModifierFlags::empty();
|
||||||
for (modifier, flag) in &[
|
for (modifier, flag) in &[
|
||||||
(
|
(
|
||||||
keystroke.modifiers.platform,
|
keystroke.display_modifiers.platform,
|
||||||
NSEventModifierFlags::NSCommandKeyMask,
|
NSEventModifierFlags::NSCommandKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.modifiers.control,
|
keystroke.display_modifiers.control,
|
||||||
NSEventModifierFlags::NSControlKeyMask,
|
NSEventModifierFlags::NSControlKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.modifiers.alt,
|
keystroke.display_modifiers.alt,
|
||||||
NSEventModifierFlags::NSAlternateKeyMask,
|
NSEventModifierFlags::NSAlternateKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.modifiers.shift,
|
keystroke.display_modifiers.shift,
|
||||||
NSEventModifierFlags::NSShiftKeyMask,
|
NSEventModifierFlags::NSShiftKeyMask,
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
@ -373,7 +379,7 @@ impl MacPlatform {
|
||||||
.initWithTitle_action_keyEquivalent_(
|
.initWithTitle_action_keyEquivalent_(
|
||||||
ns_string(name),
|
ns_string(name),
|
||||||
selector,
|
selector,
|
||||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
||||||
)
|
)
|
||||||
.autorelease();
|
.autorelease();
|
||||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||||
|
@ -882,6 +888,10 @@ impl Platform for MacPlatform {
|
||||||
Box::new(MacKeyboardLayout::new())
|
Box::new(MacKeyboardLayout::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
self.0.lock().keyboard_mapper.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn app_path(&self) -> Result<PathBuf> {
|
fn app_path(&self) -> Result<PathBuf> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let bundle: id = NSBundle::mainBundle();
|
let bundle: id = NSBundle::mainBundle();
|
||||||
|
@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||||
let platform = unsafe { get_mac_platform(this) };
|
let platform = unsafe { get_mac_platform(this) };
|
||||||
let mut lock = platform.0.lock();
|
let mut lock = platform.0.lock();
|
||||||
|
let keyboard_layout = MacKeyboardLayout::new();
|
||||||
|
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||||
drop(lock);
|
drop(lock);
|
||||||
callback();
|
callback();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||||
|
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -237,6 +238,10 @@ impl Platform for TestPlatform {
|
||||||
Box::new(TestKeyboardLayout)
|
Box::new(TestKeyboardLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
Rc::new(DummyKeyboardMapper)
|
||||||
|
}
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||||
|
|
||||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||||
|
|
|
@ -1,22 +1,31 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
use windows::Win32::UI::{
|
use windows::Win32::UI::{
|
||||||
Input::KeyboardAndMouse::{
|
Input::KeyboardAndMouse::{
|
||||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||||
},
|
},
|
||||||
WindowsAndMessaging::KL_NAMELENGTH,
|
WindowsAndMessaging::KL_NAMELENGTH,
|
||||||
};
|
};
|
||||||
use windows_core::HSTRING;
|
use windows_core::HSTRING;
|
||||||
|
|
||||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
use crate::{
|
||||||
|
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) struct WindowsKeyboardLayout {
|
pub(crate) struct WindowsKeyboardLayout {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WindowsKeyboardMapper {
|
||||||
|
key_to_vkey: HashMap<String, (u16, bool)>,
|
||||||
|
vkey_to_key: HashMap<u16, String>,
|
||||||
|
vkey_to_shifted: HashMap<u16, String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||||
fn id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
|
@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||||
|
fn map_key_equivalent(
|
||||||
|
&self,
|
||||||
|
mut keystroke: Keystroke,
|
||||||
|
use_key_equivalents: bool,
|
||||||
|
) -> KeybindingKeystroke {
|
||||||
|
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
||||||
|
else {
|
||||||
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||||
|
};
|
||||||
|
if shifted_key && keystroke.modifiers.shift {
|
||||||
|
log::warn!(
|
||||||
|
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
||||||
|
keystroke.key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shift = shifted_key || keystroke.modifiers.shift;
|
||||||
|
keystroke.modifiers.shift = false;
|
||||||
|
|
||||||
|
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
||||||
|
log::error!(
|
||||||
|
"Failed to map key equivalent '{:?}' to a valid key",
|
||||||
|
keystroke
|
||||||
|
);
|
||||||
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||||
|
};
|
||||||
|
|
||||||
|
keystroke.key = if shift {
|
||||||
|
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
||||||
|
log::error!(
|
||||||
|
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
||||||
|
keystroke,
|
||||||
|
vkey
|
||||||
|
);
|
||||||
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||||
|
};
|
||||||
|
shifted_key
|
||||||
|
} else {
|
||||||
|
key.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let modifiers = Modifiers {
|
||||||
|
shift,
|
||||||
|
..keystroke.modifiers
|
||||||
|
};
|
||||||
|
|
||||||
|
KeybindingKeystroke {
|
||||||
|
inner: keystroke,
|
||||||
|
display_modifiers: modifiers,
|
||||||
|
display_key: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WindowsKeyboardLayout {
|
impl WindowsKeyboardLayout {
|
||||||
pub(crate) fn new() -> Result<Self> {
|
pub(crate) fn new() -> Result<Self> {
|
||||||
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
||||||
|
@ -48,6 +116,41 @@ impl WindowsKeyboardLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WindowsKeyboardMapper {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let mut key_to_vkey = HashMap::default();
|
||||||
|
let mut vkey_to_key = HashMap::default();
|
||||||
|
let mut vkey_to_shifted = HashMap::default();
|
||||||
|
for vkey in CANDIDATE_VKEYS {
|
||||||
|
if let Some(key) = get_key_from_vkey(*vkey) {
|
||||||
|
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
||||||
|
vkey_to_key.insert(vkey.0, key);
|
||||||
|
}
|
||||||
|
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
||||||
|
if scan_code == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
||||||
|
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
||||||
|
vkey_to_shifted.insert(vkey.0, shifted_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
key_to_vkey,
|
||||||
|
vkey_to_key,
|
||||||
|
vkey_to_shifted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
||||||
|
if use_key_equivalents {
|
||||||
|
get_vkey_from_key_with_us_layout(key)
|
||||||
|
} else {
|
||||||
|
self.key_to_vkey.get(key).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_keystroke_key(
|
pub(crate) fn get_keystroke_key(
|
||||||
vkey: VIRTUAL_KEY,
|
vkey: VIRTUAL_KEY,
|
||||||
scan_code: u32,
|
scan_code: u32,
|
||||||
|
@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||||
|
match key {
|
||||||
|
// ` => VK_OEM_3
|
||||||
|
"`" => Some((VK_OEM_3.0, false)),
|
||||||
|
"~" => Some((VK_OEM_3.0, true)),
|
||||||
|
"1" => Some((VK_1.0, false)),
|
||||||
|
"!" => Some((VK_1.0, true)),
|
||||||
|
"2" => Some((VK_2.0, false)),
|
||||||
|
"@" => Some((VK_2.0, true)),
|
||||||
|
"3" => Some((VK_3.0, false)),
|
||||||
|
"#" => Some((VK_3.0, true)),
|
||||||
|
"4" => Some((VK_4.0, false)),
|
||||||
|
"$" => Some((VK_4.0, true)),
|
||||||
|
"5" => Some((VK_5.0, false)),
|
||||||
|
"%" => Some((VK_5.0, true)),
|
||||||
|
"6" => Some((VK_6.0, false)),
|
||||||
|
"^" => Some((VK_6.0, true)),
|
||||||
|
"7" => Some((VK_7.0, false)),
|
||||||
|
"&" => Some((VK_7.0, true)),
|
||||||
|
"8" => Some((VK_8.0, false)),
|
||||||
|
"*" => Some((VK_8.0, true)),
|
||||||
|
"9" => Some((VK_9.0, false)),
|
||||||
|
"(" => Some((VK_9.0, true)),
|
||||||
|
"0" => Some((VK_0.0, false)),
|
||||||
|
")" => Some((VK_0.0, true)),
|
||||||
|
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||||
|
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||||
|
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||||
|
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||||
|
"[" => Some((VK_OEM_4.0, false)),
|
||||||
|
"{" => Some((VK_OEM_4.0, true)),
|
||||||
|
"]" => Some((VK_OEM_6.0, false)),
|
||||||
|
"}" => Some((VK_OEM_6.0, true)),
|
||||||
|
"\\" => Some((VK_OEM_5.0, false)),
|
||||||
|
"|" => Some((VK_OEM_5.0, true)),
|
||||||
|
";" => Some((VK_OEM_1.0, false)),
|
||||||
|
":" => Some((VK_OEM_1.0, true)),
|
||||||
|
"'" => Some((VK_OEM_7.0, false)),
|
||||||
|
"\"" => Some((VK_OEM_7.0, true)),
|
||||||
|
"," => Some((VK_OEM_COMMA.0, false)),
|
||||||
|
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||||
|
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||||
|
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||||
|
"/" => Some((VK_OEM_2.0, false)),
|
||||||
|
"?" => Some((VK_OEM_2.0, true)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||||
|
VK_OEM_3,
|
||||||
|
VK_OEM_MINUS,
|
||||||
|
VK_OEM_PLUS,
|
||||||
|
VK_OEM_4,
|
||||||
|
VK_OEM_5,
|
||||||
|
VK_OEM_6,
|
||||||
|
VK_OEM_1,
|
||||||
|
VK_OEM_7,
|
||||||
|
VK_OEM_COMMA,
|
||||||
|
VK_OEM_PERIOD,
|
||||||
|
VK_OEM_2,
|
||||||
|
VK_OEM_102,
|
||||||
|
VK_OEM_8,
|
||||||
|
VK_ABNT_C1,
|
||||||
|
VK_0,
|
||||||
|
VK_1,
|
||||||
|
VK_2,
|
||||||
|
VK_3,
|
||||||
|
VK_4,
|
||||||
|
VK_5,
|
||||||
|
VK_6,
|
||||||
|
VK_7,
|
||||||
|
VK_8,
|
||||||
|
VK_9,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keyboard_mapper() {
|
||||||
|
let mapper = WindowsKeyboardMapper::new();
|
||||||
|
|
||||||
|
// Normal case
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control(),
|
||||||
|
key: "a".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||||
|
assert_eq!(mapped.inner, keystroke);
|
||||||
|
assert_eq!(mapped.display_key, "a");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
||||||
|
|
||||||
|
// Shifted case, ctrl-$
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control(),
|
||||||
|
key: "$".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||||
|
assert_eq!(mapped.inner, keystroke);
|
||||||
|
assert_eq!(mapped.display_key, "4");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||||
|
|
||||||
|
// Shifted case, but shift is true
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control_shift(),
|
||||||
|
key: "$".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||||
|
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||||
|
assert_eq!(mapped.display_key, "4");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||||
|
|
||||||
|
// Windows style
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control_shift(),
|
||||||
|
key: "4".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||||
|
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||||
|
assert_eq!(mapped.inner.key, "$");
|
||||||
|
assert_eq!(mapped.display_key, "4");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -351,6 +351,10 @@ impl Platform for WindowsPlatform {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
Rc::new(WindowsKeyboardMapper::new())
|
||||||
|
}
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::get_key_equivalents;
|
|
||||||
use ui::{Button, ButtonStyle};
|
use ui::{Button, ButtonStyle};
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||||
|
@ -169,7 +168,8 @@ impl Item for KeyContextView {
|
||||||
impl Render for KeyContextView {
|
impl Render for KeyContextView {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
|
||||||
|
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("key-context-view")
|
.id("key-context-view")
|
||||||
.overflow_scroll()
|
.overflow_scroll()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
||||||
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
|
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
|
||||||
|
NoAction, SharedString,
|
||||||
};
|
};
|
||||||
use schemars::{JsonSchema, json_schema};
|
use schemars::{JsonSchema, json_schema};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -211,9 +212,6 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
|
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
|
||||||
let key_equivalents =
|
|
||||||
crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
|
|
||||||
|
|
||||||
if content.is_empty() {
|
if content.is_empty() {
|
||||||
return KeymapFileLoadResult::Success {
|
return KeymapFileLoadResult::Success {
|
||||||
key_bindings: Vec::new(),
|
key_bindings: Vec::new(),
|
||||||
|
@ -255,12 +253,6 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let key_equivalents = if *use_key_equivalents {
|
|
||||||
key_equivalents.as_ref()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut section_errors = String::new();
|
let mut section_errors = String::new();
|
||||||
|
|
||||||
if !unrecognized_fields.is_empty() {
|
if !unrecognized_fields.is_empty() {
|
||||||
|
@ -278,7 +270,7 @@ impl KeymapFile {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
context_predicate.clone(),
|
context_predicate.clone(),
|
||||||
key_equivalents,
|
*use_key_equivalents,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
match result {
|
match result {
|
||||||
|
@ -336,7 +328,7 @@ impl KeymapFile {
|
||||||
keystrokes: &str,
|
keystrokes: &str,
|
||||||
action: &KeymapAction,
|
action: &KeymapAction,
|
||||||
context: Option<Rc<KeyBindingContextPredicate>>,
|
context: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
key_equivalents: Option<&HashMap<char, char>>,
|
use_key_equivalents: bool,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> std::result::Result<KeyBinding, String> {
|
) -> std::result::Result<KeyBinding, String> {
|
||||||
let (build_result, action_input_string) = match &action.0 {
|
let (build_result, action_input_string) = match &action.0 {
|
||||||
|
@ -404,8 +396,9 @@ impl KeymapFile {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
context,
|
context,
|
||||||
key_equivalents,
|
use_key_equivalents,
|
||||||
action_input_string.map(SharedString::from),
|
action_input_string.map(SharedString::from),
|
||||||
|
cx.keyboard_mapper().as_ref(),
|
||||||
) {
|
) {
|
||||||
Ok(key_binding) => key_binding,
|
Ok(key_binding) => key_binding,
|
||||||
Err(InvalidKeystrokeError { keystroke }) => {
|
Err(InvalidKeystrokeError { keystroke }) => {
|
||||||
|
@ -607,6 +600,7 @@ impl KeymapFile {
|
||||||
mut operation: KeybindUpdateOperation<'a>,
|
mut operation: KeybindUpdateOperation<'a>,
|
||||||
mut keymap_contents: String,
|
mut keymap_contents: String,
|
||||||
tab_size: usize,
|
tab_size: usize,
|
||||||
|
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
match operation {
|
match operation {
|
||||||
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
|
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
|
||||||
|
@ -646,7 +640,7 @@ impl KeymapFile {
|
||||||
.action_value()
|
.action_value()
|
||||||
.context("Failed to generate target action JSON value")?;
|
.context("Failed to generate target action JSON value")?;
|
||||||
let Some((index, keystrokes_str)) =
|
let Some((index, keystrokes_str)) =
|
||||||
find_binding(&keymap, &target, &target_action_value)
|
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||||
else {
|
else {
|
||||||
anyhow::bail!("Failed to find keybinding to remove");
|
anyhow::bail!("Failed to find keybinding to remove");
|
||||||
};
|
};
|
||||||
|
@ -681,7 +675,7 @@ impl KeymapFile {
|
||||||
.context("Failed to generate source action JSON value")?;
|
.context("Failed to generate source action JSON value")?;
|
||||||
|
|
||||||
if let Some((index, keystrokes_str)) =
|
if let Some((index, keystrokes_str)) =
|
||||||
find_binding(&keymap, &target, &target_action_value)
|
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||||
{
|
{
|
||||||
if target.context == source.context {
|
if target.context == source.context {
|
||||||
// if we are only changing the keybinding (common case)
|
// if we are only changing the keybinding (common case)
|
||||||
|
@ -781,7 +775,7 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
let use_key_equivalents = from.and_then(|from| {
|
let use_key_equivalents = from.and_then(|from| {
|
||||||
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
|
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
|
||||||
let (index, _) = find_binding(&keymap, &from, &action_value)?;
|
let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
|
||||||
Some(keymap.0[index].use_key_equivalents)
|
Some(keymap.0[index].use_key_equivalents)
|
||||||
}).unwrap_or(false);
|
}).unwrap_or(false);
|
||||||
if use_key_equivalents {
|
if use_key_equivalents {
|
||||||
|
@ -808,6 +802,7 @@ impl KeymapFile {
|
||||||
keymap: &'b KeymapFile,
|
keymap: &'b KeymapFile,
|
||||||
target: &KeybindUpdateTarget<'a>,
|
target: &KeybindUpdateTarget<'a>,
|
||||||
target_action_value: &Value,
|
target_action_value: &Value,
|
||||||
|
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||||
) -> Option<(usize, &'b str)> {
|
) -> Option<(usize, &'b str)> {
|
||||||
let target_context_parsed =
|
let target_context_parsed =
|
||||||
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
||||||
|
@ -823,8 +818,11 @@ impl KeymapFile {
|
||||||
for (keystrokes_str, action) in bindings {
|
for (keystrokes_str, action) in bindings {
|
||||||
let Ok(keystrokes) = keystrokes_str
|
let Ok(keystrokes) = keystrokes_str
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(Keystroke::parse)
|
.map(|source| {
|
||||||
.collect::<Result<Vec<_>, _>>()
|
let keystroke = Keystroke::parse(source)?;
|
||||||
|
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
@ -832,7 +830,7 @@ impl KeymapFile {
|
||||||
|| !keystrokes
|
|| !keystrokes
|
||||||
.iter()
|
.iter()
|
||||||
.zip(target.keystrokes)
|
.zip(target.keystrokes)
|
||||||
.all(|(a, b)| a.should_match(b))
|
.all(|(a, b)| a.inner.should_match(b))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -847,7 +845,7 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum KeybindUpdateOperation<'a> {
|
pub enum KeybindUpdateOperation<'a> {
|
||||||
Replace {
|
Replace {
|
||||||
/// Describes the keybind to create
|
/// Describes the keybind to create
|
||||||
|
@ -916,7 +914,7 @@ impl<'a> KeybindUpdateOperation<'a> {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct KeybindUpdateTarget<'a> {
|
pub struct KeybindUpdateTarget<'a> {
|
||||||
pub context: Option<&'a str>,
|
pub context: Option<&'a str>,
|
||||||
pub keystrokes: &'a [Keystroke],
|
pub keystrokes: &'a [KeybindingKeystroke],
|
||||||
pub action_name: &'a str,
|
pub action_name: &'a str,
|
||||||
pub action_arguments: Option<&'a str>,
|
pub action_arguments: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
@ -941,6 +939,9 @@ impl<'a> KeybindUpdateTarget<'a> {
|
||||||
fn keystrokes_unparsed(&self) -> String {
|
fn keystrokes_unparsed(&self) -> String {
|
||||||
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
|
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
|
||||||
for keystroke in self.keystrokes {
|
for keystroke in self.keystrokes {
|
||||||
|
// The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
|
||||||
|
// here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
|
||||||
|
// by default on Windows.
|
||||||
keystrokes.push_str(&keystroke.unparse());
|
keystrokes.push_str(&keystroke.unparse());
|
||||||
keystrokes.push(' ');
|
keystrokes.push(' ');
|
||||||
}
|
}
|
||||||
|
@ -959,7 +960,7 @@ impl<'a> KeybindUpdateTarget<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||||
pub enum KeybindSource {
|
pub enum KeybindSource {
|
||||||
User,
|
User,
|
||||||
Vim,
|
Vim,
|
||||||
|
@ -1020,7 +1021,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::Keystroke;
|
use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -1049,16 +1050,27 @@ mod tests {
|
||||||
operation: KeybindUpdateOperation,
|
operation: KeybindUpdateOperation,
|
||||||
expected: impl ToString,
|
expected: impl ToString,
|
||||||
) {
|
) {
|
||||||
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
|
let result = KeymapFile::update_keybinding(
|
||||||
.expect("Update succeeded");
|
operation,
|
||||||
|
input.to_string(),
|
||||||
|
4,
|
||||||
|
&gpui::DummyKeyboardMapper,
|
||||||
|
)
|
||||||
|
.expect("Update succeeded");
|
||||||
pretty_assertions::assert_eq!(expected.to_string(), result);
|
pretty_assertions::assert_eq!(expected.to_string(), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
|
fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
|
||||||
keystrokes
|
keystrokes
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
|
.map(|s| {
|
||||||
|
KeybindingKeystroke::new(
|
||||||
|
Keystroke::parse(s).expect("Keystrokes valid"),
|
||||||
|
false,
|
||||||
|
&DummyKeyboardMapper,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
mod base_keymap_setting;
|
mod base_keymap_setting;
|
||||||
mod editable_setting_control;
|
mod editable_setting_control;
|
||||||
mod key_equivalents;
|
|
||||||
mod keymap_file;
|
mod keymap_file;
|
||||||
mod settings_file;
|
mod settings_file;
|
||||||
mod settings_json;
|
mod settings_json;
|
||||||
|
@ -14,7 +13,6 @@ use util::asset_str;
|
||||||
|
|
||||||
pub use base_keymap_setting::*;
|
pub use base_keymap_setting::*;
|
||||||
pub use editable_setting_control::*;
|
pub use editable_setting_control::*;
|
||||||
pub use key_equivalents::*;
|
|
||||||
pub use keymap_file::{
|
pub use keymap_file::{
|
||||||
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
|
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
|
||||||
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
|
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
|
||||||
|
@ -89,7 +87,10 @@ pub fn default_settings() -> Cow<'static, str> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
|
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(target_os = "windows")]
|
||||||
|
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
|
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
|
||||||
|
|
||||||
pub fn default_keymap() -> Cow<'static, str> {
|
pub fn default_keymap() -> Cow<'static, str> {
|
||||||
|
|
|
@ -14,9 +14,9 @@ use gpui::{
|
||||||
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
||||||
EventEmitter, FocusHandle, Focusable, Global, IsZero,
|
EventEmitter, FocusHandle, Focusable, Global, IsZero,
|
||||||
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
|
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
|
||||||
KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
|
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
|
||||||
StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
|
ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
|
||||||
div,
|
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
|
||||||
};
|
};
|
||||||
use language::{Language, LanguageConfig, ToOffset as _};
|
use language::{Language, LanguageConfig, ToOffset as _};
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
|
@ -174,7 +174,7 @@ impl FilterState {
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
|
||||||
struct ActionMapping {
|
struct ActionMapping {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: Vec<KeybindingKeystroke>,
|
||||||
context: Option<SharedString>,
|
context: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ struct ConflictState {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConflictKeybindMapping = HashMap<
|
type ConflictKeybindMapping = HashMap<
|
||||||
Vec<Keystroke>,
|
Vec<KeybindingKeystroke>,
|
||||||
Vec<(
|
Vec<(
|
||||||
Option<gpui::KeyBindingContextPredicate>,
|
Option<gpui::KeyBindingContextPredicate>,
|
||||||
Vec<ConflictOrigin>,
|
Vec<ConflictOrigin>,
|
||||||
|
@ -414,12 +414,14 @@ impl Focusable for KeymapEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Helper function to check if two keystroke sequences match exactly
|
/// Helper function to check if two keystroke sequences match exactly
|
||||||
fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
|
fn keystrokes_match_exactly(
|
||||||
|
keystrokes1: &[KeybindingKeystroke],
|
||||||
|
keystrokes2: &[KeybindingKeystroke],
|
||||||
|
) -> bool {
|
||||||
keystrokes1.len() == keystrokes2.len()
|
keystrokes1.len() == keystrokes2.len()
|
||||||
&& keystrokes1
|
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
|
||||||
.iter()
|
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
|
||||||
.zip(keystrokes2)
|
})
|
||||||
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeymapEditor {
|
impl KeymapEditor {
|
||||||
|
@ -509,7 +511,7 @@ impl KeymapEditor {
|
||||||
self.filter_editor.read(cx).text(cx)
|
self.filter_editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
|
fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
|
||||||
match self.search_mode {
|
match self.search_mode {
|
||||||
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
|
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
|
||||||
SearchMode::Normal => Default::default(),
|
SearchMode::Normal => Default::default(),
|
||||||
|
@ -530,7 +532,7 @@ impl KeymapEditor {
|
||||||
|
|
||||||
let keystroke_query = keystroke_query
|
let keystroke_query = keystroke_query
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|keystroke| keystroke.unparse())
|
.map(|keystroke| keystroke.inner.unparse())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
@ -554,7 +556,7 @@ impl KeymapEditor {
|
||||||
async fn update_matches(
|
async fn update_matches(
|
||||||
this: WeakEntity<Self>,
|
this: WeakEntity<Self>,
|
||||||
action_query: String,
|
action_query: String,
|
||||||
keystroke_query: Vec<Keystroke>,
|
keystroke_query: Vec<KeybindingKeystroke>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let action_query = command_palette::normalize_action_query(&action_query);
|
let action_query = command_palette::normalize_action_query(&action_query);
|
||||||
|
@ -603,13 +605,15 @@ impl KeymapEditor {
|
||||||
{
|
{
|
||||||
let query = &keystroke_query[query_cursor];
|
let query = &keystroke_query[query_cursor];
|
||||||
let keystroke = &keystrokes[keystroke_cursor];
|
let keystroke = &keystrokes[keystroke_cursor];
|
||||||
let matches =
|
let matches = query
|
||||||
query.modifiers.is_subset_of(&keystroke.modifiers)
|
.inner
|
||||||
&& ((query.key.is_empty()
|
.modifiers
|
||||||
|| query.key == keystroke.key)
|
.is_subset_of(&keystroke.inner.modifiers)
|
||||||
&& query.key_char.as_ref().is_none_or(
|
&& ((query.inner.key.is_empty()
|
||||||
|q_kc| q_kc == &keystroke.key,
|
|| query.inner.key == keystroke.inner.key)
|
||||||
));
|
&& query.inner.key_char.as_ref().is_none_or(
|
||||||
|
|q_kc| q_kc == &keystroke.inner.key,
|
||||||
|
));
|
||||||
if matches {
|
if matches {
|
||||||
found_count += 1;
|
found_count += 1;
|
||||||
query_cursor += 1;
|
query_cursor += 1;
|
||||||
|
@ -678,7 +682,7 @@ impl KeymapEditor {
|
||||||
.map(KeybindSource::from_meta)
|
.map(KeybindSource::from_meta)
|
||||||
.unwrap_or(KeybindSource::Unknown);
|
.unwrap_or(KeybindSource::Unknown);
|
||||||
|
|
||||||
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
|
let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
|
||||||
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
|
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
|
||||||
.vim_mode(source == KeybindSource::Vim);
|
.vim_mode(source == KeybindSource::Vim);
|
||||||
|
|
||||||
|
@ -1202,8 +1206,11 @@ impl KeymapEditor {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.get_scrollbar_offset(Axis::Vertical),
|
.get_scrollbar_offset(Axis::Vertical),
|
||||||
));
|
));
|
||||||
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
|
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||||
.detach_and_notify_err(window, cx);
|
cx.spawn(async move |_, _| {
|
||||||
|
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
|
||||||
|
})
|
||||||
|
.detach_and_notify_err(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_context_to_clipboard(
|
fn copy_context_to_clipboard(
|
||||||
|
@ -1422,7 +1429,7 @@ impl ProcessedBinding {
|
||||||
.map(|keybind| keybind.get_action_mapping())
|
.map(|keybind| keybind.get_action_mapping())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keystrokes(&self) -> Option<&[Keystroke]> {
|
fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
|
||||||
self.ui_key_binding()
|
self.ui_key_binding()
|
||||||
.map(|binding| binding.keystrokes.as_slice())
|
.map(|binding| binding.keystrokes.as_slice())
|
||||||
}
|
}
|
||||||
|
@ -2220,7 +2227,7 @@ impl KeybindingEditorModal {
|
||||||
Ok(action_arguments)
|
Ok(action_arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
|
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
|
||||||
let new_keystrokes = self
|
let new_keystrokes = self
|
||||||
.keybind_editor
|
.keybind_editor
|
||||||
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
|
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
|
||||||
|
@ -2316,6 +2323,7 @@ impl KeybindingEditorModal {
|
||||||
}).unwrap_or(Ok(()))?;
|
}).unwrap_or(Ok(()))?;
|
||||||
|
|
||||||
let create = self.creating;
|
let create = self.creating;
|
||||||
|
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let action_name = existing_keybind.action().name;
|
let action_name = existing_keybind.action().name;
|
||||||
|
@ -2328,6 +2336,7 @@ impl KeybindingEditorModal {
|
||||||
new_action_args.as_deref(),
|
new_action_args.as_deref(),
|
||||||
&fs,
|
&fs,
|
||||||
tab_size,
|
tab_size,
|
||||||
|
keyboard_mapper.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -2445,11 +2454,21 @@ impl KeybindingEditorModal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
|
fn remove_key_char(
|
||||||
Keystroke {
|
KeybindingKeystroke {
|
||||||
modifiers,
|
inner,
|
||||||
key,
|
display_modifiers,
|
||||||
..Default::default()
|
display_key,
|
||||||
|
}: KeybindingKeystroke,
|
||||||
|
) -> KeybindingKeystroke {
|
||||||
|
KeybindingKeystroke {
|
||||||
|
inner: Keystroke {
|
||||||
|
modifiers: inner.modifiers,
|
||||||
|
key: inner.key,
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
|
display_modifiers,
|
||||||
|
display_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2992,6 +3011,7 @@ async fn save_keybinding_update(
|
||||||
new_args: Option<&str>,
|
new_args: Option<&str>,
|
||||||
fs: &Arc<dyn Fs>,
|
fs: &Arc<dyn Fs>,
|
||||||
tab_size: usize,
|
tab_size: usize,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||||
.await
|
.await
|
||||||
|
@ -3034,9 +3054,13 @@ async fn save_keybinding_update(
|
||||||
|
|
||||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||||
|
|
||||||
let updated_keymap_contents =
|
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
operation,
|
||||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
keymap_contents,
|
||||||
|
tab_size,
|
||||||
|
keyboard_mapper,
|
||||||
|
)
|
||||||
|
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||||
fs.write(
|
fs.write(
|
||||||
paths::keymap_file().as_path(),
|
paths::keymap_file().as_path(),
|
||||||
updated_keymap_contents.as_bytes(),
|
updated_keymap_contents.as_bytes(),
|
||||||
|
@ -3057,6 +3081,7 @@ async fn remove_keybinding(
|
||||||
existing: ProcessedBinding,
|
existing: ProcessedBinding,
|
||||||
fs: &Arc<dyn Fs>,
|
fs: &Arc<dyn Fs>,
|
||||||
tab_size: usize,
|
tab_size: usize,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let Some(keystrokes) = existing.keystrokes() else {
|
let Some(keystrokes) = existing.keystrokes() else {
|
||||||
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
||||||
|
@ -3080,9 +3105,13 @@ async fn remove_keybinding(
|
||||||
};
|
};
|
||||||
|
|
||||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||||
let updated_keymap_contents =
|
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
operation,
|
||||||
.context("Failed to update keybinding")?;
|
keymap_contents,
|
||||||
|
tab_size,
|
||||||
|
keyboard_mapper,
|
||||||
|
)
|
||||||
|
.context("Failed to update keybinding")?;
|
||||||
fs.write(
|
fs.write(
|
||||||
paths::keymap_file().as_path(),
|
paths::keymap_file().as_path(),
|
||||||
updated_keymap_contents.as_bytes(),
|
updated_keymap_contents.as_bytes(),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
||||||
Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
|
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
|
||||||
|
@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KeystrokeInput {
|
pub struct KeystrokeInput {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: Vec<KeybindingKeystroke>,
|
||||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||||
outer_focus_handle: FocusHandle,
|
outer_focus_handle: FocusHandle,
|
||||||
inner_focus_handle: FocusHandle,
|
inner_focus_handle: FocusHandle,
|
||||||
intercept_subscription: Option<Subscription>,
|
intercept_subscription: Option<Subscription>,
|
||||||
|
@ -70,7 +70,7 @@ impl KeystrokeInput {
|
||||||
const KEYSTROKE_COUNT_MAX: usize = 3;
|
const KEYSTROKE_COUNT_MAX: usize = 3;
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -97,7 +97,7 @@ impl KeystrokeInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
|
pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
|
||||||
self.keystrokes = keystrokes;
|
self.keystrokes = keystrokes;
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,7 @@ impl KeystrokeInput {
|
||||||
self.search = search;
|
self.search = search;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||||
&& self.keystrokes.is_empty()
|
&& self.keystrokes.is_empty()
|
||||||
{
|
{
|
||||||
|
@ -116,18 +116,22 @@ impl KeystrokeInput {
|
||||||
&& self
|
&& self
|
||||||
.keystrokes
|
.keystrokes
|
||||||
.last()
|
.last()
|
||||||
.is_some_and(|last| last.key.is_empty())
|
.is_some_and(|last| last.display_key.is_empty())
|
||||||
{
|
{
|
||||||
return &self.keystrokes[..self.keystrokes.len() - 1];
|
return &self.keystrokes[..self.keystrokes.len() - 1];
|
||||||
}
|
}
|
||||||
&self.keystrokes
|
&self.keystrokes
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy(modifiers: Modifiers) -> Keystroke {
|
fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
|
||||||
Keystroke {
|
KeybindingKeystroke {
|
||||||
modifiers,
|
inner: Keystroke {
|
||||||
key: "".to_string(),
|
modifiers,
|
||||||
key_char: None,
|
key: "".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
|
display_modifiers: modifiers,
|
||||||
|
display_key: "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +258,7 @@ impl KeystrokeInput {
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
|
|
||||||
if let Some(last) = self.keystrokes.last_mut()
|
if let Some(last) = self.keystrokes.last_mut()
|
||||||
&& last.key.is_empty()
|
&& last.display_key.is_empty()
|
||||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||||
{
|
{
|
||||||
if !self.search && !event.modifiers.modified() {
|
if !self.search && !event.modifiers.modified() {
|
||||||
|
@ -263,13 +267,15 @@ impl KeystrokeInput {
|
||||||
}
|
}
|
||||||
if self.search {
|
if self.search {
|
||||||
if self.previous_modifiers.modified() {
|
if self.previous_modifiers.modified() {
|
||||||
last.modifiers |= event.modifiers;
|
last.display_modifiers |= event.modifiers;
|
||||||
|
last.inner.modifiers |= event.modifiers;
|
||||||
} else {
|
} else {
|
||||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||||
}
|
}
|
||||||
self.previous_modifiers |= event.modifiers;
|
self.previous_modifiers |= event.modifiers;
|
||||||
} else {
|
} else {
|
||||||
last.modifiers = event.modifiers;
|
last.display_modifiers = event.modifiers;
|
||||||
|
last.inner.modifiers = event.modifiers;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||||
|
@ -297,14 +303,17 @@ impl KeystrokeInput {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut keystroke = keystroke.clone();
|
let mut keystroke =
|
||||||
|
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
|
||||||
if let Some(last) = self.keystrokes.last()
|
if let Some(last) = self.keystrokes.last()
|
||||||
&& last.key.is_empty()
|
&& last.display_key.is_empty()
|
||||||
&& (!self.search || self.previous_modifiers.modified())
|
&& (!self.search || self.previous_modifiers.modified())
|
||||||
{
|
{
|
||||||
let key = keystroke.key.clone();
|
let display_key = keystroke.display_key.clone();
|
||||||
|
let inner_key = keystroke.inner.key.clone();
|
||||||
keystroke = last.clone();
|
keystroke = last.clone();
|
||||||
keystroke.key = key;
|
keystroke.display_key = display_key;
|
||||||
|
keystroke.inner.key = inner_key;
|
||||||
self.keystrokes.pop();
|
self.keystrokes.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,11 +333,14 @@ impl KeystrokeInput {
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
|
|
||||||
if self.search {
|
if self.search {
|
||||||
self.previous_modifiers = keystroke.modifiers;
|
self.previous_modifiers = keystroke.display_modifiers;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
|
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
|
||||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
&& keystroke.display_modifiers.modified()
|
||||||
|
{
|
||||||
|
self.keystrokes
|
||||||
|
.push(Self::dummy(keystroke.display_modifiers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,7 +376,7 @@ impl KeystrokeInput {
|
||||||
&self.keystrokes
|
&self.keystrokes
|
||||||
};
|
};
|
||||||
keystrokes.iter().map(move |keystroke| {
|
keystrokes.iter().map(move |keystroke| {
|
||||||
h_flex().children(ui::render_keystroke(
|
h_flex().children(ui::render_keybinding_keystroke(
|
||||||
keystroke,
|
keystroke,
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
Some(rems(0.875).into()),
|
Some(rems(0.875).into()),
|
||||||
|
@ -809,9 +821,13 @@ mod tests {
|
||||||
/// Verifies that the keystrokes match the expected strings
|
/// Verifies that the keystrokes match the expected strings
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
|
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
|
||||||
let actual = self
|
let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
|
||||||
.input
|
input
|
||||||
.read_with(&self.cx, |input, _| input.keystrokes.clone());
|
.keystrokes
|
||||||
|
.iter()
|
||||||
|
.map(|keystroke| keystroke.inner.clone())
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
Self::expect_keystrokes_equal(&actual, expected);
|
Self::expect_keystrokes_equal(&actual, expected);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -939,7 +955,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct KeystrokeUpdateTracker {
|
struct KeystrokeUpdateTracker {
|
||||||
initial_keystrokes: Vec<Keystroke>,
|
initial_keystrokes: Vec<KeybindingKeystroke>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
input: Entity<KeystrokeInput>,
|
input: Entity<KeystrokeInput>,
|
||||||
received_keystrokes_updated: bool,
|
received_keystrokes_updated: bool,
|
||||||
|
@ -983,8 +999,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keystrokes_str(ks: &[Keystroke]) -> String {
|
fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
|
||||||
ks.iter().map(|ks| ks.unparse()).join(" ")
|
ks.iter().map(|ks| ks.inner.unparse()).join(" ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::PlatformStyle;
|
use crate::PlatformStyle;
|
||||||
use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
|
use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
|
Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
|
||||||
relative,
|
Modifiers, Window, relative,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ pub struct KeyBinding {
|
||||||
/// More than one keystroke produces a chord.
|
/// More than one keystroke produces a chord.
|
||||||
///
|
///
|
||||||
/// This should always contain at least one keystroke.
|
/// This should always contain at least one keystroke.
|
||||||
pub keystrokes: Vec<Keystroke>,
|
pub keystrokes: Vec<KeybindingKeystroke>,
|
||||||
|
|
||||||
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
|
@ -59,7 +59,7 @@ impl KeyBinding {
|
||||||
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
|
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
|
pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
|
||||||
Self {
|
Self {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
platform_style: PlatformStyle::platform(),
|
platform_style: PlatformStyle::platform(),
|
||||||
|
@ -99,16 +99,16 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_key(
|
fn render_key(
|
||||||
keystroke: &Keystroke,
|
key: &str,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
size: impl Into<Option<AbsoluteLength>>,
|
size: impl Into<Option<AbsoluteLength>>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let key_icon = icon_for_key(keystroke, platform_style);
|
let key_icon = icon_for_key(key, platform_style);
|
||||||
match key_icon {
|
match key_icon {
|
||||||
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
||||||
None => {
|
None => {
|
||||||
let key = util::capitalize(&keystroke.key);
|
let key = util::capitalize(key);
|
||||||
Key::new(&key, color).size(size).into_any_element()
|
Key::new(&key, color).size(size).into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
|
||||||
"KEY_BINDING-{}",
|
"KEY_BINDING-{}",
|
||||||
self.keystrokes
|
self.keystrokes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|k| k.key.to_string())
|
.map(|k| k.display_key.to_string())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ")
|
.join(" ")
|
||||||
)
|
)
|
||||||
|
@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding {
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.rounded_xs()
|
.rounded_xs()
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.children(render_keystroke(
|
.children(render_keybinding_keystroke(
|
||||||
keystroke,
|
keystroke,
|
||||||
color,
|
color,
|
||||||
self.size,
|
self.size,
|
||||||
|
@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_keystroke(
|
pub fn render_keybinding_keystroke(
|
||||||
keystroke: &Keystroke,
|
keystroke: &KeybindingKeystroke,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
size: impl Into<Option<AbsoluteLength>>,
|
size: impl Into<Option<AbsoluteLength>>,
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
|
@ -163,26 +163,39 @@ pub fn render_keystroke(
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
|
|
||||||
if use_text {
|
if use_text {
|
||||||
let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color)
|
let element = Key::new(
|
||||||
.size(size)
|
keystroke_text(
|
||||||
.into_any_element();
|
&keystroke.display_modifiers,
|
||||||
|
&keystroke.display_key,
|
||||||
|
platform_style,
|
||||||
|
vim_mode,
|
||||||
|
),
|
||||||
|
color,
|
||||||
|
)
|
||||||
|
.size(size)
|
||||||
|
.into_any_element();
|
||||||
vec![element]
|
vec![element]
|
||||||
} else {
|
} else {
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
elements.extend(render_modifiers(
|
elements.extend(render_modifiers(
|
||||||
&keystroke.modifiers,
|
&keystroke.display_modifiers,
|
||||||
platform_style,
|
platform_style,
|
||||||
color,
|
color,
|
||||||
size,
|
size,
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
elements.push(render_key(keystroke, color, platform_style, size));
|
elements.push(render_key(
|
||||||
|
&keystroke.display_key,
|
||||||
|
color,
|
||||||
|
platform_style,
|
||||||
|
size,
|
||||||
|
));
|
||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
|
fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
|
||||||
match keystroke.key.as_str() {
|
match key {
|
||||||
"left" => Some(IconName::ArrowLeft),
|
"left" => Some(IconName::ArrowLeft),
|
||||||
"right" => Some(IconName::ArrowRight),
|
"right" => Some(IconName::ArrowRight),
|
||||||
"up" => Some(IconName::ArrowUp),
|
"up" => Some(IconName::ArrowUp),
|
||||||
|
@ -379,7 +392,7 @@ 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, cx: &App) -> Option<String> {
|
pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
|
||||||
let key_binding = window.highest_precedence_binding_for_action(action)?;
|
let key_binding = window.highest_precedence_binding_for_action(action)?;
|
||||||
Some(text_for_keystrokes(key_binding.keystrokes(), cx))
|
Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
||||||
|
@ -387,22 +400,50 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
||||||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||||
keystrokes
|
keystrokes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
|
.map(|keystroke| {
|
||||||
|
keystroke_text(
|
||||||
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
|
platform_style,
|
||||||
|
vim_enabled,
|
||||||
|
)
|
||||||
|
})
|
||||||
.join(" ")
|
.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
|
pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
|
||||||
let platform_style = PlatformStyle::platform();
|
let platform_style = PlatformStyle::platform();
|
||||||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||||
keystroke_text(keystroke, platform_style, vim_enabled)
|
keystrokes
|
||||||
|
.iter()
|
||||||
|
.map(|keystroke| {
|
||||||
|
keystroke_text(
|
||||||
|
&keystroke.display_modifiers,
|
||||||
|
&keystroke.display_key,
|
||||||
|
platform_style,
|
||||||
|
vim_enabled,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
|
||||||
|
let platform_style = PlatformStyle::platform();
|
||||||
|
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||||
|
keystroke_text(modifiers, key, platform_style, vim_enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a textual representation of the given [`Keystroke`].
|
/// Returns a textual representation of the given [`Keystroke`].
|
||||||
fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
|
fn keystroke_text(
|
||||||
|
modifiers: &Modifiers,
|
||||||
|
key: &str,
|
||||||
|
platform_style: PlatformStyle,
|
||||||
|
vim_mode: bool,
|
||||||
|
) -> String {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let delimiter = '-';
|
let delimiter = '-';
|
||||||
|
|
||||||
if keystroke.modifiers.function {
|
if modifiers.function {
|
||||||
match vim_mode {
|
match vim_mode {
|
||||||
false => text.push_str("Fn"),
|
false => text.push_str("Fn"),
|
||||||
true => text.push_str("fn"),
|
true => text.push_str("fn"),
|
||||||
|
@ -411,7 +452,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.control {
|
if modifiers.control {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(PlatformStyle::Mac, false) => text.push_str("Control"),
|
(PlatformStyle::Mac, false) => text.push_str("Control"),
|
||||||
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
|
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
|
||||||
|
@ -421,7 +462,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.platform {
|
if modifiers.platform {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(PlatformStyle::Mac, false) => text.push_str("Command"),
|
(PlatformStyle::Mac, false) => text.push_str("Command"),
|
||||||
(PlatformStyle::Mac, true) => text.push_str("cmd"),
|
(PlatformStyle::Mac, true) => text.push_str("cmd"),
|
||||||
|
@ -434,7 +475,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.alt {
|
if modifiers.alt {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(PlatformStyle::Mac, false) => text.push_str("Option"),
|
(PlatformStyle::Mac, false) => text.push_str("Option"),
|
||||||
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
|
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
|
||||||
|
@ -444,7 +485,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.shift {
|
if modifiers.shift {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(_, false) => text.push_str("Shift"),
|
(_, false) => text.push_str("Shift"),
|
||||||
(_, true) => text.push_str("shift"),
|
(_, true) => text.push_str("shift"),
|
||||||
|
@ -453,9 +494,9 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
if vim_mode {
|
if vim_mode {
|
||||||
text.push_str(&keystroke.key)
|
text.push_str(key)
|
||||||
} else {
|
} else {
|
||||||
let key = match keystroke.key.as_str() {
|
let key = match key {
|
||||||
"pageup" => "PageUp",
|
"pageup" => "PageUp",
|
||||||
"pagedown" => "PageDown",
|
"pagedown" => "PageDown",
|
||||||
key => &util::capitalize(key),
|
key => &util::capitalize(key),
|
||||||
|
@ -562,9 +603,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_for_keystroke() {
|
fn test_text_for_keystroke() {
|
||||||
|
let keystroke = Keystroke::parse("cmd-c").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Mac,
|
PlatformStyle::Mac,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -572,7 +615,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Linux,
|
PlatformStyle::Linux,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -580,16 +624,19 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Windows,
|
PlatformStyle::Windows,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
"Win-C".to_string()
|
"Win-C".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Mac,
|
PlatformStyle::Mac,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -597,7 +644,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Linux,
|
PlatformStyle::Linux,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -605,16 +653,19 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Windows,
|
PlatformStyle::Windows,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
"Ctrl-Alt-Delete".to_string()
|
"Ctrl-Alt-Delete".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let keystroke = Keystroke::parse("shift-pageup").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Mac,
|
PlatformStyle::Mac,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -622,7 +673,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Linux,
|
PlatformStyle::Linux,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
@ -630,7 +682,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Windows,
|
PlatformStyle::Windows,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
|
|
@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
let mut current_layout_id = cx.keyboard_layout().id().to_string();
|
||||||
cx.on_keyboard_layout_change(move |cx| {
|
cx.on_keyboard_layout_change(move |cx| {
|
||||||
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
let next_layout_id = cx.keyboard_layout().id();
|
||||||
if next_mapping != current_mapping {
|
if next_layout_id != current_layout_id {
|
||||||
current_mapping = next_mapping;
|
current_layout_id = next_layout_id.to_string();
|
||||||
keyboard_layout_tx.unbounded_send(()).ok();
|
keyboard_layout_tx.unbounded_send(()).ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -4729,7 +4729,7 @@ mod tests {
|
||||||
// and key strokes contain the given key
|
// and key strokes contain the given key
|
||||||
bindings
|
bindings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
|
.any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
|
||||||
"On {} Failed to find {} with key binding {}",
|
"On {} Failed to find {} with key binding {}",
|
||||||
line,
|
line,
|
||||||
action.name(),
|
action.name(),
|
||||||
|
|
|
@ -72,7 +72,10 @@ impl QuickActionBar {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
tooltip_text,
|
tooltip_text,
|
||||||
Some(open_action_for_tooltip),
|
Some(open_action_for_tooltip),
|
||||||
format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
|
format!(
|
||||||
|
"{} to open in a split",
|
||||||
|
text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
|
||||||
|
),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue