Skip to content

Commit fff0ece

Browse files
JunkuiZhanglocalcc
andauthored
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 <[email protected]>
1 parent b1b60bb commit fff0ece

File tree

25 files changed

+3515
-1721
lines changed

25 files changed

+3515
-1721
lines changed

assets/keymaps/default-windows.json

Lines changed: 1260 additions & 0 deletions
Large diffs are not rendered by default.

crates/docs_preprocessor/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
1919
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
2020
});
2121

22+
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
23+
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
24+
});
25+
2226
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
2327

2428
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
@@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
216220
let keymap = match os {
217221
"macos" => &KEYMAP_MACOS,
218222
"linux" | "freebsd" => &KEYMAP_LINUX,
223+
"windows" => &KEYMAP_WINDOWS,
219224
_ => unreachable!("Not a valid OS: {}", os),
220225
};
221226

crates/editor/src/editor.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2588,7 +2588,7 @@ impl Editor {
25882588
|| binding
25892589
.keystrokes()
25902590
.first()
2591-
.is_some_and(|keystroke| keystroke.modifiers.modified())
2591+
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
25922592
}))
25932593
}
25942594

@@ -7686,16 +7686,16 @@ impl Editor {
76867686
.keystroke()
76877687
{
76887688
modifiers_held = modifiers_held
7689-
|| (&accept_keystroke.modifiers == modifiers
7690-
&& accept_keystroke.modifiers.modified());
7689+
|| (&accept_keystroke.display_modifiers == modifiers
7690+
&& accept_keystroke.display_modifiers.modified());
76917691
};
76927692
if let Some(accept_partial_keystroke) = self
76937693
.accept_edit_prediction_keybind(true, window, cx)
76947694
.keystroke()
76957695
{
76967696
modifiers_held = modifiers_held
7697-
|| (&accept_partial_keystroke.modifiers == modifiers
7698-
&& accept_partial_keystroke.modifiers.modified());
7697+
|| (&accept_partial_keystroke.display_modifiers == modifiers
7698+
&& accept_partial_keystroke.display_modifiers.modified());
76997699
}
77007700

77017701
if modifiers_held {
@@ -9044,7 +9044,7 @@ impl Editor {
90449044

90459045
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
90469046

9047-
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
9047+
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
90489048
Color::Accent
90499049
} else {
90509050
Color::Muted
@@ -9056,19 +9056,19 @@ impl Editor {
90569056
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
90579057
.text_size(TextSize::XSmall.rems(cx))
90589058
.child(h_flex().children(ui::render_modifiers(
9059-
&accept_keystroke.modifiers,
9059+
&accept_keystroke.display_modifiers,
90609060
PlatformStyle::platform(),
90619061
Some(modifiers_color),
90629062
Some(IconSize::XSmall.rems().into()),
90639063
true,
90649064
)))
90659065
.when(is_platform_style_mac, |parent| {
9066-
parent.child(accept_keystroke.key.clone())
9066+
parent.child(accept_keystroke.display_key.clone())
90679067
})
90689068
.when(!is_platform_style_mac, |parent| {
90699069
parent.child(
90709070
Key::new(
9071-
util::capitalize(&accept_keystroke.key),
9071+
util::capitalize(&accept_keystroke.display_key),
90729072
Some(Color::Default),
90739073
)
90749074
.size(Some(IconSize::XSmall.rems().into())),
@@ -9171,7 +9171,7 @@ impl Editor {
91719171
max_width: Pixels,
91729172
cursor_point: Point,
91739173
style: &EditorStyle,
9174-
accept_keystroke: Option<&gpui::Keystroke>,
9174+
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
91759175
_window: &Window,
91769176
cx: &mut Context<Editor>,
91779177
) -> Option<AnyElement> {
@@ -9249,7 +9249,7 @@ impl Editor {
92499249
accept_keystroke.as_ref(),
92509250
|el, accept_keystroke| {
92519251
el.child(h_flex().children(ui::render_modifiers(
9252-
&accept_keystroke.modifiers,
9252+
&accept_keystroke.display_modifiers,
92539253
PlatformStyle::platform(),
92549254
Some(Color::Default),
92559255
Some(IconSize::XSmall.rems().into()),
@@ -9319,7 +9319,7 @@ impl Editor {
93199319
.child(completion),
93209320
)
93219321
.when_some(accept_keystroke, |el, accept_keystroke| {
9322-
if !accept_keystroke.modifiers.modified() {
9322+
if !accept_keystroke.display_modifiers.modified() {
93239323
return el;
93249324
}
93259325

@@ -9338,7 +9338,7 @@ impl Editor {
93389338
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
93399339
.when(is_platform_style_mac, |parent| parent.gap_1())
93409340
.child(h_flex().children(ui::render_modifiers(
9341-
&accept_keystroke.modifiers,
9341+
&accept_keystroke.display_modifiers,
93429342
PlatformStyle::platform(),
93439343
Some(if !has_completion {
93449344
Color::Muted

crates/editor/src/element.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ use gpui::{
4343
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
4444
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
4545
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
46-
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
47-
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
48-
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
49-
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
46+
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
47+
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
48+
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
49+
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
5050
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
5151
transparent_black,
5252
};
@@ -7150,7 +7150,7 @@ fn header_jump_data(
71507150
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
71517151

71527152
impl AcceptEditPredictionBinding {
7153-
pub fn keystroke(&self) -> Option<&Keystroke> {
7153+
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
71547154
if let Some(binding) = self.0.as_ref() {
71557155
match &binding.keystrokes() {
71567156
[keystroke, ..] => Some(keystroke),

crates/gpui/src/app.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ use crate::{
3737
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
3838
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
3939
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
40-
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
41-
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
42-
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
43-
WindowHandle, WindowId, WindowInvalidator,
40+
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
41+
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
42+
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
43+
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
4444
colors::{Colors, GlobalColors},
4545
current_platform, hash, init_app_menus,
4646
};
@@ -263,6 +263,7 @@ pub struct App {
263263
pub(crate) focus_handles: Arc<FocusMap>,
264264
pub(crate) keymap: Rc<RefCell<Keymap>>,
265265
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
266+
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
266267
pub(crate) global_action_listeners:
267268
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
268269
pending_effects: VecDeque<Effect>,
@@ -312,6 +313,7 @@ impl App {
312313
let text_system = Arc::new(TextSystem::new(platform.text_system()));
313314
let entities = EntityMap::new();
314315
let keyboard_layout = platform.keyboard_layout();
316+
let keyboard_mapper = platform.keyboard_mapper();
315317

316318
let app = Rc::new_cyclic(|this| AppCell {
317319
app: RefCell::new(App {
@@ -337,6 +339,7 @@ impl App {
337339
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
338340
keymap: Rc::new(RefCell::new(Keymap::default())),
339341
keyboard_layout,
342+
keyboard_mapper,
340343
global_action_listeners: FxHashMap::default(),
341344
pending_effects: VecDeque::new(),
342345
pending_notifications: FxHashSet::default(),
@@ -376,6 +379,7 @@ impl App {
376379
if let Some(app) = app.upgrade() {
377380
let cx = &mut app.borrow_mut();
378381
cx.keyboard_layout = cx.platform.keyboard_layout();
382+
cx.keyboard_mapper = cx.platform.keyboard_mapper();
379383
cx.keyboard_layout_observers
380384
.clone()
381385
.retain(&(), move |callback| (callback)(cx));
@@ -424,6 +428,11 @@ impl App {
424428
self.keyboard_layout.as_ref()
425429
}
426430

431+
/// Get the current keyboard mapper.
432+
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
433+
&self.keyboard_mapper
434+
}
435+
427436
/// Invokes a handler when the current keyboard layout changes
428437
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
429438
where

crates/gpui/src/keymap.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod context;
44
pub use binding::*;
55
pub use context::*;
66

7-
use crate::{Action, Keystroke, is_no_action};
7+
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
88
use collections::{HashMap, HashSet};
99
use smallvec::SmallVec;
1010
use std::any::TypeId;
@@ -141,7 +141,7 @@ impl Keymap {
141141
/// only.
142142
pub fn bindings_for_input(
143143
&self,
144-
input: &[Keystroke],
144+
input: &[impl AsKeystroke],
145145
context_stack: &[KeyContext],
146146
) -> (SmallVec<[KeyBinding; 1]>, bool) {
147147
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
@@ -192,7 +192,6 @@ impl Keymap {
192192

193193
(bindings, !pending.is_empty())
194194
}
195-
196195
/// Check if the given binding is enabled, given a certain key context.
197196
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
198197
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
@@ -639,7 +638,7 @@ mod tests {
639638
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
640639
let actual = keymap
641640
.bindings_for_action(action)
642-
.map(|binding| binding.keystrokes[0].unparse())
641+
.map(|binding| binding.keystrokes[0].inner.unparse())
643642
.collect::<Vec<_>>();
644643
assert_eq!(actual, expected, "{:?}", action);
645644
}

crates/gpui/src/keymap/binding.rs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use std::rc::Rc;
22

3-
use collections::HashMap;
4-
5-
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
3+
use crate::{
4+
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
5+
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
6+
};
67
use smallvec::SmallVec;
78

89
/// A keybinding and its associated metadata, from the keymap.
910
pub struct KeyBinding {
1011
pub(crate) action: Box<dyn Action>,
11-
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
12+
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
1213
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
1314
pub(crate) meta: Option<KeyBindingMetaIndex>,
1415
/// The json input string used when building the keybinding, if any
@@ -32,32 +33,38 @@ impl KeyBinding {
3233
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
3334
let context_predicate =
3435
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
35-
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
36+
Self::load(
37+
keystrokes,
38+
Box::new(action),
39+
context_predicate,
40+
false,
41+
None,
42+
&DummyKeyboardMapper,
43+
)
44+
.unwrap()
3645
}
3746

3847
/// Load a keybinding from the given raw data.
3948
pub fn load(
4049
keystrokes: &str,
4150
action: Box<dyn Action>,
4251
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
43-
key_equivalents: Option<&HashMap<char, char>>,
52+
use_key_equivalents: bool,
4453
action_input: Option<SharedString>,
54+
keyboard_mapper: &dyn PlatformKeyboardMapper,
4555
) -> std::result::Result<Self, InvalidKeystrokeError> {
46-
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
56+
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
4757
.split_whitespace()
48-
.map(Keystroke::parse)
58+
.map(|source| {
59+
let keystroke = Keystroke::parse(source)?;
60+
Ok(KeybindingKeystroke::new(
61+
keystroke,
62+
use_key_equivalents,
63+
keyboard_mapper,
64+
))
65+
})
4966
.collect::<std::result::Result<_, _>>()?;
5067

51-
if let Some(equivalents) = key_equivalents {
52-
for keystroke in keystrokes.iter_mut() {
53-
if keystroke.key.chars().count() == 1
54-
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
55-
{
56-
keystroke.key = key.to_string();
57-
}
58-
}
59-
}
60-
6168
Ok(Self {
6269
keystrokes,
6370
action,
@@ -79,13 +86,13 @@ impl KeyBinding {
7986
}
8087

8188
/// Check if the given keystrokes match this binding.
82-
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
89+
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
8390
if self.keystrokes.len() < typed.len() {
8491
return None;
8592
}
8693

8794
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
88-
if !typed.should_match(target) {
95+
if !typed.as_keystroke().should_match(target) {
8996
return None;
9097
}
9198
}
@@ -94,7 +101,7 @@ impl KeyBinding {
94101
}
95102

96103
/// Get the keystrokes associated with this binding
97-
pub fn keystrokes(&self) -> &[Keystroke] {
104+
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
98105
self.keystrokes.as_slice()
99106
}
100107

crates/gpui/src/platform.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
231231

232232
fn on_quit(&self, callback: Box<dyn FnMut()>);
233233
fn on_reopen(&self, callback: Box<dyn FnMut()>);
234-
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
235234

236235
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
237236
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
251250
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
252251
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
253252
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
254-
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
255253

256254
fn compositor_name(&self) -> &'static str {
257255
""
@@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
272270
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
273271
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
274272
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
273+
274+
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
275+
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
276+
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
275277
}
276278

277279
/// A handle to a platform's display, e.g. a monitor or laptop screen.

0 commit comments

Comments
 (0)