Compare commits

...

112 Commits

Author SHA1 Message Date
Junkui Zhang
d103a3ecb2 refactor 2025-06-04 20:24:17 +08:00
Junkui Zhang
c921212c59 fix merge conflicts 2025-06-04 20:19:11 +08:00
Junkui Zhang
5bf3f5be50 clippy 2025-06-04 17:27:41 +08:00
Junkui Zhang
10ce1111f2 fix merge conflicts 2025-06-04 17:03:14 +08:00
Junkui Zhang
3c84069df2 clippy 2025-06-04 16:53:32 +08:00
Junkui Zhang
ea35b55ff5 remove debug print 2025-06-04 16:53:32 +08:00
Junkui Zhang
33bb07125f use atomic write 2025-06-04 16:53:32 +08:00
Junkui Zhang
109471a768 fix tests 2025-06-04 16:53:32 +08:00
Junkui Zhang
78e5134b7d fix ci 2025-06-04 16:53:32 +08:00
Junkui Zhang
6211ad1e61 fix all 2025-06-04 16:53:32 +08:00
Junkui Zhang
9d203ba0d8 rename static 2025-06-04 16:53:31 +08:00
Junkui Zhang
2a1ec794eb fix ci 2025-06-04 16:53:31 +08:00
Junkui Zhang
44e9f7f0a0 use xvfb 2025-06-04 16:53:31 +08:00
Junkui Zhang
1002a04583 test 2025-06-04 16:53:31 +08:00
Junkui Zhang
1bd09837c8 try 2025-06-04 16:53:31 +08:00
Junkui Zhang
616f932145 fix 2025-06-04 16:53:31 +08:00
Junkui Zhang
c8d1de87d6 fix 2025-06-04 16:53:31 +08:00
Junkui Zhang
17f35e88ac test 2025-06-04 16:53:31 +08:00
Junkui Zhang
b96b6cf5ef test 2025-06-04 16:53:31 +08:00
Junkui Zhang
2714fc6d5b test 2025-06-04 16:53:30 +08:00
Junkui Zhang
6064799099 test 2025-06-04 16:53:18 +08:00
Junkui Zhang
fa54137c05 test 2025-06-04 16:53:18 +08:00
Junkui Zhang
2d531fab6a test 2025-06-04 16:53:18 +08:00
Junkui Zhang
f4a24167fd fix 2025-06-04 16:53:18 +08:00
Junkui Zhang
4e46aa5057 trigger ci 2025-06-04 16:53:18 +08:00
Junkui Zhang
b23efbf62f test 2025-06-04 16:53:17 +08:00
Junkui Zhang
e54780b2da test 2025-06-04 16:53:17 +08:00
Junkui Zhang
e3bd1a0e41 test 2025-06-04 16:53:17 +08:00
Junkui Zhang
2b0b9b337d test 2025-06-04 16:53:17 +08:00
Junkui Zhang
c65c6f20e1 try fix linux 2025-06-04 16:53:17 +08:00
Junkui Zhang
5e4959728e fix Russian layout on macOS 2025-06-04 16:53:17 +08:00
Junkui Zhang
cb893ccd84 use is_immutable_key again 2025-06-04 16:53:17 +08:00
Junkui Zhang
d37b7f5d21 fix linux 2025-06-04 16:53:17 +08:00
Junkui Zhang
7c9c2363ab fix linux 2025-06-04 16:53:16 +08:00
Junkui Zhang
c00ce9f327 Fix all tests for linux 2025-06-04 16:53:16 +08:00
Junkui Zhang
a1fb04b49f fix linux 2025-06-04 16:53:16 +08:00
Junkui Zhang
bd4557503f use the new serializer 2025-06-04 16:53:16 +08:00
Junkui Zhang
8af8e69870 introduce KeymapSerializer 2025-06-04 16:53:16 +08:00
Junkui Zhang
91d56b815a remove is_immutable_keys 2025-06-04 16:53:16 +08:00
Junkui Zhang
7f06102e7e fix 2025-06-04 16:53:15 +08:00
Junkui Zhang
ba6dc2040e disblae tests for linux 2025-06-04 16:53:15 +08:00
Junkui Zhang
0f8327f662 disable for linux 2025-06-04 16:53:15 +08:00
Junkui Zhang
7a84135aac Fix macOS and Linux 2025-06-04 16:53:15 +08:00
Junkui Zhang
1393fefb8f use Result<Option<key>> instead 2025-06-04 16:53:15 +08:00
Junkui Zhang
8f74bdd091 fix 2025-06-04 16:53:15 +08:00
Junkui Zhang
010f345963 basic impl for linux 2025-06-04 16:53:15 +08:00
Junkui Zhang
4bbc56d7bd try fix all 2025-06-04 16:53:14 +08:00
Junkui Zhang
e5c6ef8361 fmt 2025-06-04 16:53:14 +08:00
Junkui Zhang
7f11dea9f2 try fix 2025-06-04 16:53:14 +08:00
Junkui Zhang
36fc38cf87 fix 2025-06-04 16:53:14 +08:00
Junkui Zhang
7d1f5d4718 fix tests 2025-06-04 16:53:14 +08:00
Junkui Zhang
3027225767 remove unused function 2025-06-04 16:53:14 +08:00
Junkui Zhang
0c6f50032b clippy 2025-06-04 16:53:14 +08:00
Junkui Zhang
b26332f514 trigger ci 2025-06-04 16:53:13 +08:00
Junkui Zhang
1c5fd70a04 fix all 2025-06-04 16:53:13 +08:00
Junkui Zhang
451e7bed60 fix tests 2025-06-04 16:53:01 +08:00
Junkui Zhang
854f55b4d8 manual serialize 2025-06-04 16:53:01 +08:00
Junkui Zhang
c8b6020804 refactor 2025-06-04 16:53:01 +08:00
Junkui Zhang
622f6ffe2d CHECKPOINT 2025-06-04 16:53:01 +08:00
Junkui Zhang
49a85e59ac rename some variables 2025-06-04 16:53:01 +08:00
Junkui Zhang
43f0e6dedd checkpoint 2025-06-04 16:53:01 +08:00
Junkui Zhang
091d2cfcb3 checkpoint 2025-06-04 16:53:01 +08:00
Junkui Zhang
de217bef18 wip 2025-06-04 16:53:01 +08:00
Junkui Zhang
991922ce54 wip 2025-06-04 16:53:00 +08:00
Junkui Zhang
68ca64a310 checkpoint 2025-06-04 16:53:00 +08:00
Junkui Zhang
8ef6c77934 add debugger 2025-06-04 16:53:00 +08:00
Junkui Zhang
13811202cb wip 2025-06-04 16:53:00 +08:00
Junkui Zhang
595be0135c wip 2025-06-04 16:53:00 +08:00
Junkui Zhang
2f1f231f0b add to_gpui_style 2025-06-04 16:53:00 +08:00
Junkui Zhang
4de395f933 rename some parameters 2025-06-04 16:52:59 +08:00
Junkui Zhang
f34e6f127d fix tests 2025-06-04 16:52:59 +08:00
Junkui Zhang
90bbc49b0c fix test 2025-06-04 16:52:59 +08:00
Junkui Zhang
76ffcb3c77 refactor 2025-06-04 16:52:59 +08:00
Junkui Zhang
6a929b7dc5 add more test 2025-06-04 16:52:59 +08:00
Junkui Zhang
94c78851c3 fix windows oem check 2025-06-04 16:52:59 +08:00
Junkui Zhang
23f68c9ffc simplify 2025-06-04 16:52:58 +08:00
Junkui Zhang
1ddb4126c8 fix macOS 2025-06-04 16:52:58 +08:00
Junkui Zhang
92440054ce fix scan code 2025-06-04 16:52:58 +08:00
Junkui Zhang
493c3a6084 fix macOS 2025-06-04 16:52:58 +08:00
Junkui Zhang
16c92944df test passes 2025-06-04 16:52:58 +08:00
Junkui Zhang
8b569d7dc2 fix tests 2025-06-04 16:52:58 +08:00
Junkui Zhang
8a7ab5c3d4 add test 2025-06-04 16:52:58 +08:00
Junkui Zhang
fc1947b97c fix all 2025-06-04 16:52:57 +08:00
Junkui Zhang
6fc96e6b7f parse ScanCode 2025-06-04 16:52:57 +08:00
Junkui Zhang
fb89852586 add ScanCode 2025-06-04 16:52:57 +08:00
Junkui Zhang
2c04d7d118 add keycodes 2025-06-04 16:52:57 +08:00
Junkui Zhang
424ce07c35 fix deserializing command 2025-06-04 16:52:57 +08:00
Junkui Zhang
2489e4cea4 fix 2025-06-04 16:52:57 +08:00
Junkui Zhang
eaa1821673 fix macOS 2025-06-04 16:52:57 +08:00
Junkui Zhang
b11688a5e5 fix macOS 2025-06-04 16:52:01 +08:00
Junkui Zhang
8510cea452 checkpoint 2025-06-04 16:52:00 +08:00
Junkui Zhang
77ce5a7a7c tests 2025-06-04 16:51:15 +08:00
Junkui Zhang
9a6c7d5c41 windows impl 2025-06-04 16:51:15 +08:00
Junkui Zhang
126ba040e8 rename method 2025-06-04 16:51:15 +08:00
Junkui Zhang
d49f75ab3f add tests 2025-06-04 16:51:14 +08:00
Junkui Zhang
2fbb5e5db4 fix 2025-06-04 16:51:14 +08:00
Junkui Zhang
f4c0d52530 macOS checkpoint 2025-06-04 16:51:14 +08:00
Junkui Zhang
1d7b61bdd0 refactor 2025-06-04 16:51:14 +08:00
Junkui Zhang
b4b0c58822 add scan codes 2025-06-04 16:51:14 +08:00
Junkui Zhang
932d776efd macOS impl 2025-06-04 16:51:14 +08:00
Junkui Zhang
b0ae7e16f6 fix 2025-06-04 16:51:05 +08:00
Junkui Zhang
b44e7a82c1 Allow setting other separators 2025-06-04 16:51:04 +08:00
Junkui Zhang
9730a36dd2 parse_shortcuts 2025-06-04 16:50:38 +08:00
Junkui Zhang
3a7d186726 update App 2025-06-04 16:50:37 +08:00
Junkui Zhang
567af9455d Fix windows and test 2025-06-04 16:50:18 +08:00
Junkui Zhang
8537b72597 init test 2025-06-04 16:49:39 +08:00
Junkui Zhang
b60f185bcf register action 2025-06-04 16:49:39 +08:00
Junkui Zhang
46b17ef148 basci impl 2025-06-04 16:47:29 +08:00
Junkui Zhang
8f7d5ecf81 add new trait 2025-06-04 16:47:05 +08:00
Junkui Zhang
fec7620eee Add docs 2025-06-04 16:47:05 +08:00
Junkui Zhang
97319c4fe3 add vscode_shortcuts_file 2025-06-04 16:47:05 +08:00
Junkui Zhang
da422e7dcd Fix vscode_settings_file for Windows 2025-06-04 16:39:10 +08:00
32 changed files with 3468 additions and 205 deletions

View File

@@ -1,6 +1,12 @@
name: "Run tests"
description: "Runs the tests"
inputs:
use-xvfb:
description: "Whether to run tests with xvfb"
required: false
default: "false"
runs:
using: "composite"
steps:
@@ -20,4 +26,9 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
run: |
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
else
cargo nextest run --workspace --no-fail-fast
fi

View File

@@ -295,6 +295,8 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |

View File

@@ -240,7 +240,8 @@ impl EditorTestContext {
// unlike cx.simulate_keystrokes(), this does not run_until_parked
// so you can use it to test detailed timing
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
let keyboard_mapper = self.keyboard_mapper();
let keystroke = Keystroke::parse(keystroke_text, keyboard_mapper.as_ref()).unwrap();
self.cx.dispatch_keystroke(self.window, keystroke);
}

View File

@@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -262,6 +262,7 @@ pub struct App {
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_mapper: Box<dyn PlatformKeyboardMapper>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
@@ -308,6 +309,7 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_mapper = platform.keyboard_mapper();
let keyboard_layout = platform.keyboard_layout();
let app = Rc::new_cyclic(|this| AppCell {
@@ -333,6 +335,7 @@ impl App {
window_handles: FxHashMap::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_mapper,
keyboard_layout,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
@@ -369,6 +372,7 @@ impl App {
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout = cx.platform.keyboard_layout();
cx.keyboard_layout_observers
.clone()
@@ -413,6 +417,11 @@ impl App {
self.quitting = false;
}
/// Get the keyboard mapper of current keyboard layout
pub fn keyboard_mapper(&self) -> &dyn PlatformKeyboardMapper {
self.keyboard_mapper.as_ref()
}
/// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
self.keyboard_layout.as_ref()

View File

@@ -3,9 +3,9 @@ use crate::{
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, DrawPhase, Drawable, Element,
Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
WindowHandle, WindowOptions,
Platform, PlatformKeyboardMapper, Point, Render, Result, Size, Task, TestDispatcher,
TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window,
WindowBounds, WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
@@ -397,14 +397,20 @@ impl TestAppContext {
self.background_executor.run_until_parked()
}
/// Returns the current keyboard mapper for this platform.
pub fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
self.test_platform.keyboard_mapper()
}
/// simulate_keystrokes takes a space-separated list of keys to type.
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
/// in Zed, this will run backspace on the current editor through the command palette.
/// This will also run the background executor until it's parked.
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
let keyboard_mapper = self.test_platform.keyboard_mapper();
for keystroke in keystrokes
.split(' ')
.map(Keystroke::parse)
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke);
@@ -418,7 +424,12 @@ impl TestAppContext {
/// will type abc into your current editor
/// This will also run the background executor until it's parked.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
let keyboard_mapper = self.test_platform.keyboard_mapper();
for keystroke in input
.split("")
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke);
}

View File

@@ -538,8 +538,15 @@ mod test {
})
.unwrap();
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
let keyboard_mapper = cx.keyboard_mapper();
cx.dispatch_keystroke(
*window,
Keystroke::parse("a", keyboard_mapper.as_ref()).unwrap(),
);
cx.dispatch_keystroke(
*window,
Keystroke::parse("ctrl-g", keyboard_mapper.as_ref()).unwrap(),
);
window
.update(cx, |test_view, _, _| {

View File

@@ -256,7 +256,7 @@ impl Keymap {
#[cfg(test)]
mod tests {
use super::*;
use crate as gpui;
use crate::{self as gpui, TestKeyboardMapper};
use gpui::{NoAction, actions};
actions!(
@@ -292,6 +292,7 @@ mod tests {
#[test]
fn test_keymap_disabled() {
let keyboard_mapper = TestKeyboardMapper::new();
let bindings = [
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")),
@@ -306,7 +307,7 @@ mod tests {
assert!(
keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[Keystroke::parse("ctrl-a", &keyboard_mapper).unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
@@ -315,7 +316,7 @@ mod tests {
assert!(
!keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[Keystroke::parse("ctrl-a", &keyboard_mapper).unwrap()],
&[KeyContext::parse("editor").unwrap()],
)
.0
@@ -326,7 +327,7 @@ mod tests {
assert!(
keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[Keystroke::parse("ctrl-a", &keyboard_mapper).unwrap()],
&[KeyContext::parse("editor mode=full").unwrap()],
)
.0
@@ -337,7 +338,7 @@ mod tests {
assert!(
keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-b").unwrap()],
&[Keystroke::parse("ctrl-b", &keyboard_mapper).unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
@@ -356,8 +357,9 @@ mod tests {
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
let space = || Keystroke::parse("space").unwrap();
let w = || Keystroke::parse("w").unwrap();
let keyboard_mapper = TestKeyboardMapper::new();
let space = || Keystroke::parse("space", &keyboard_mapper).unwrap();
let w = || Keystroke::parse("w", &keyboard_mapper).unwrap();
let space_w = [space(), w()];
let space_w_w = [space(), w(), w()];

View File

@@ -2,7 +2,9 @@ use std::rc::Rc;
use collections::HashMap;
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
use crate::{
Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, PlatformKeyboardMapper,
};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
@@ -30,7 +32,14 @@ impl KeyBinding {
} else {
None
};
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
Self::load(
keystrokes,
Box::new(action),
context_predicate,
None,
&crate::EmptyKeyboardMapper,
)
.unwrap()
}
/// Load a keybinding from the given raw data.
@@ -39,10 +48,11 @@ impl KeyBinding {
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.map(|source| Keystroke::parse(source, keyboard_mapper))
.collect::<std::result::Result<_, _>>()?;
if let Some(equivalents) = key_equivalents {

View File

@@ -1,5 +1,6 @@
mod app_menu;
mod keyboard;
mod keycodes;
mod keystroke;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -66,6 +67,7 @@ use uuid::Uuid;
pub use app_menu::*;
pub use keyboard::*;
pub use keycodes::*;
pub use keystroke::*;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -194,7 +196,6 @@ pub(crate) trait Platform: 'static {
fn on_quit(&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 get_menus(&self) -> Option<Vec<OwnedMenu>> {
@@ -214,7 +215,6 @@ pub(crate) trait Platform: 'static {
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_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 {
""
@@ -235,6 +235,10 @@ pub(crate) trait Platform: 'static {
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 delete_credentials(&self, url: &str) -> Task<Result<()>>;
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper>;
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
}
/// A handle to a platform's display, e.g. a monitor or laptop screen.

View File

@@ -1,3 +1,7 @@
use anyhow::Result;
use super::ScanCode;
/// A trait for platform-specific keyboard layouts
pub trait PlatformKeyboardLayout {
/// Get the keyboard layout ID, which should be unique to the layout
@@ -5,3 +9,150 @@ pub trait PlatformKeyboardLayout {
/// Get the keyboard layout display name
fn name(&self) -> &str;
}
/// TODO:
pub trait PlatformKeyboardMapper {
/// TODO:
fn scan_code_to_key(&self, scan_code: ScanCode) -> Result<String>;
/// TODO:
fn get_shifted_key(&self, key: &str) -> Result<Option<String>>;
}
/// TODO:
pub struct TestKeyboardMapper {
#[cfg(target_os = "windows")]
mapper: super::WindowsKeyboardMapper,
#[cfg(target_os = "macos")]
mapper: super::MacKeyboardMapper,
#[cfg(target_os = "linux")]
mapper: super::LinuxKeyboardMapper,
}
impl PlatformKeyboardMapper for TestKeyboardMapper {
fn scan_code_to_key(&self, scan_code: ScanCode) -> Result<String> {
self.mapper.scan_code_to_key(scan_code)
}
fn get_shifted_key(&self, key: &str) -> Result<Option<String>> {
self.mapper.get_shifted_key(key)
}
}
impl TestKeyboardMapper {
/// TODO:
pub fn new() -> Self {
Self {
#[cfg(target_os = "windows")]
mapper: super::WindowsKeyboardMapper::new(),
#[cfg(target_os = "macos")]
mapper: super::MacKeyboardMapper::new(),
#[cfg(target_os = "linux")]
mapper: super::LinuxKeyboardMapper::new(),
}
}
}
/// A dummy keyboard mapper that does not support any key mappings
pub struct EmptyKeyboardMapper;
impl PlatformKeyboardMapper for EmptyKeyboardMapper {
fn scan_code_to_key(&self, _scan_code: ScanCode) -> Result<String> {
anyhow::bail!("EmptyKeyboardMapper does not support scan codes")
}
fn get_shifted_key(&self, _key: &str) -> Result<Option<String>> {
anyhow::bail!("EmptyKeyboardMapper does not support shifted keys")
}
}
pub(crate) fn is_alphabetic_key(key: &str) -> bool {
matches!(
key,
"a" | "b"
| "c"
| "d"
| "e"
| "f"
| "g"
| "h"
| "i"
| "j"
| "k"
| "l"
| "m"
| "n"
| "o"
| "p"
| "q"
| "r"
| "s"
| "t"
| "u"
| "v"
| "w"
| "x"
| "y"
| "z"
)
}
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::ScanCode;
use super::{PlatformKeyboardMapper, TestKeyboardMapper};
#[test]
fn test_get_shifted_key() {
let mapper = TestKeyboardMapper::new();
for ch in 'a'..='z' {
let key = ch.to_string();
let shifted_key = key.to_uppercase();
assert_eq!(mapper.get_shifted_key(&key).unwrap().unwrap(), shifted_key);
}
let shift_pairs = [
("1", "!"),
("2", "@"),
("3", "#"),
("4", "$"),
("5", "%"),
("6", "^"),
("7", "&"),
("8", "*"),
("9", "("),
("0", ")"),
("`", "~"),
("-", "_"),
("=", "+"),
("[", "{"),
("]", "}"),
("\\", "|"),
(";", ":"),
("'", "\""),
(",", "<"),
(".", ">"),
("/", "?"),
];
for (key, shifted_key) in shift_pairs {
assert_eq!(mapper.get_shifted_key(key).unwrap().unwrap(), shifted_key);
}
let immutable_keys = ["backspace", "space", "tab", "enter", "f1"];
for key in immutable_keys {
assert_eq!(mapper.get_shifted_key(key).unwrap(), None);
}
}
#[test]
fn test_scan_code_to_key() {
let mapper = TestKeyboardMapper::new();
for scan_code in ScanCode::iter() {
let key = mapper.scan_code_to_key(scan_code).unwrap();
assert_eq!(key, scan_code.to_key());
}
}
}

View File

@@ -0,0 +1,464 @@
use strum::EnumIter;
/// Scan codes for the keyboard, which are used to identify keys in a keyboard layout-independent way.
/// Currently, we only support a limited set of scan codes here:
/// https://code.visualstudio.com/docs/configure/keybindings#_keyboard-layoutindependent-bindings
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum ScanCode {
/// F1 key
F1,
/// F1 key
F2,
/// F1 key
F3,
/// F1 key
F4,
/// F1 key
F5,
/// F1 key
F6,
/// F1 key
F7,
/// F1 key
F8,
/// F1 key
F9,
/// F1 key
F10,
/// F1 key
F11,
/// F1 key
F12,
/// F1 key
F13,
/// F1 key
F14,
/// F1 key
F15,
/// F1 key
F16,
/// F1 key
F17,
/// F1 key
F18,
/// F1 key
F19,
/// F20 key
F20,
/// F20 key
F21,
/// F20 key
F22,
/// F20 key
F23,
/// F20 key
F24,
/// A key on the main keyboard.
A,
/// B key on the main keyboard.
B,
/// C key on the main keyboard.
C,
/// D key on the main keyboard.
D,
/// E key on the main keyboard.
E,
/// F key on the main keyboard.
F,
/// G key on the main keyboard.
G,
/// H key on the main keyboard.
H,
/// I key on the main keyboard.
I,
/// J key on the main keyboard.
J,
/// K key on the main keyboard.
K,
/// L key on the main keyboard.
L,
/// M key on the main keyboard.
M,
/// N key on the main keyboard.
N,
/// O key on the main keyboard.
O,
/// P key on the main keyboard.
P,
/// Q key on the main keyboard.
Q,
/// R key on the main keyboard.
R,
/// S key on the main keyboard.
S,
/// T key on the main keyboard.
T,
/// U key on the main keyboard.
U,
/// V key on the main keyboard.
V,
/// W key on the main keyboard.
W,
/// X key on the main keyboard.
X,
/// Y key on the main keyboard.
Y,
/// Z key on the main keyboard.
Z,
/// 0 key on the main keyboard.
Digit0,
/// 1 key on the main keyboard.
Digit1,
/// 2 key on the main keyboard.
Digit2,
/// 3 key on the main keyboard.
Digit3,
/// 4 key on the main keyboard.
Digit4,
/// 5 key on the main keyboard.
Digit5,
/// 6 key on the main keyboard.
Digit6,
/// 7 key on the main keyboard.
Digit7,
/// 8 key on the main keyboard.
Digit8,
/// 9 key on the main keyboard.
Digit9,
/// Backquote key on the main keyboard: `
Backquote,
/// Minus key on the main keyboard: -
Minus,
/// Equal key on the main keyboard: =
Equal,
/// BracketLeft key on the main keyboard: [
BracketLeft,
/// BracketRight key on the main keyboard: ]
BracketRight,
/// Backslash key on the main keyboard: \
Backslash,
/// Semicolon key on the main keyboard: ;
Semicolon,
/// Quote key on the main keyboard: '
Quote,
/// Comma key on the main keyboard: ,
Comma,
/// Period key on the main keyboard: .
Period,
/// Slash key on the main keyboard: /
Slash,
/// Left arrow key
Left,
/// Up arrow key
Up,
/// Right arrow key
Right,
/// Down arrow key
Down,
/// PAGE UP key
PageUp,
/// PAGE DOWN key
PageDown,
/// END key
End,
/// HOME key
Home,
/// TAB key
Tab,
/// ENTER key, also known as RETURN key
/// This does not distinguish between the main Enter key and the numeric keypad Enter key.
Enter,
/// ESCAPE key
Escape,
/// SPACE key
Space,
/// BACKSPACE key
Backspace,
/// DELETE key
Delete,
// Pause, not supported yet
// CapsLock, not supported yet
/// INSERT key
Insert,
// The following keys are not supported yet:
// Numpad0,
// Numpad1,
// Numpad2,
// Numpad3,
// Numpad4,
// Numpad5,
// Numpad6,
// Numpad7,
// Numpad8,
// Numpad9,
// NumpadMultiply,
// NumpadAdd,
// NumpadComma,
// NumpadSubtract,
// NumpadDecimal,
// NumpadDivide,
}
impl ScanCode {
/// Parse a scan code from a string.
pub fn parse(source: &str) -> Option<Self> {
match source {
"[f1]" => Some(Self::F1),
"[f2]" => Some(Self::F2),
"[f3]" => Some(Self::F3),
"[f4]" => Some(Self::F4),
"[f5]" => Some(Self::F5),
"[f6]" => Some(Self::F6),
"[f7]" => Some(Self::F7),
"[f8]" => Some(Self::F8),
"[f9]" => Some(Self::F9),
"[f10]" => Some(Self::F10),
"[f11]" => Some(Self::F11),
"[f12]" => Some(Self::F12),
"[f13]" => Some(Self::F13),
"[f14]" => Some(Self::F14),
"[f15]" => Some(Self::F15),
"[f16]" => Some(Self::F16),
"[f17]" => Some(Self::F17),
"[f18]" => Some(Self::F18),
"[f19]" => Some(Self::F19),
"[f20]" => Some(Self::F20),
"[f21]" => Some(Self::F21),
"[f22]" => Some(Self::F22),
"[f23]" => Some(Self::F23),
"[f24]" => Some(Self::F24),
"[a]" | "[keya]" => Some(Self::A),
"[b]" | "[keyb]" => Some(Self::B),
"[c]" | "[keyc]" => Some(Self::C),
"[d]" | "[keyd]" => Some(Self::D),
"[e]" | "[keye]" => Some(Self::E),
"[f]" | "[keyf]" => Some(Self::F),
"[g]" | "[keyg]" => Some(Self::G),
"[h]" | "[keyh]" => Some(Self::H),
"[i]" | "[keyi]" => Some(Self::I),
"[j]" | "[keyj]" => Some(Self::J),
"[k]" | "[keyk]" => Some(Self::K),
"[l]" | "[keyl]" => Some(Self::L),
"[m]" | "[keym]" => Some(Self::M),
"[n]" | "[keyn]" => Some(Self::N),
"[o]" | "[keyo]" => Some(Self::O),
"[p]" | "[keyp]" => Some(Self::P),
"[q]" | "[keyq]" => Some(Self::Q),
"[r]" | "[keyr]" => Some(Self::R),
"[s]" | "[keys]" => Some(Self::S),
"[t]" | "[keyt]" => Some(Self::T),
"[u]" | "[keyu]" => Some(Self::U),
"[v]" | "[keyv]" => Some(Self::V),
"[w]" | "[keyw]" => Some(Self::W),
"[x]" | "[keyx]" => Some(Self::X),
"[y]" | "[keyy]" => Some(Self::Y),
"[z]" | "[keyz]" => Some(Self::Z),
"[0]" | "[digit0]" => Some(Self::Digit0),
"[1]" | "[digit1]" => Some(Self::Digit1),
"[2]" | "[digit2]" => Some(Self::Digit2),
"[3]" | "[digit3]" => Some(Self::Digit3),
"[4]" | "[digit4]" => Some(Self::Digit4),
"[5]" | "[digit5]" => Some(Self::Digit5),
"[6]" | "[digit6]" => Some(Self::Digit6),
"[7]" | "[digit7]" => Some(Self::Digit7),
"[8]" | "[digit8]" => Some(Self::Digit8),
"[9]" | "[digit9]" => Some(Self::Digit9),
"[backquote]" => Some(Self::Backquote),
"[minus]" => Some(Self::Minus),
"[equal]" => Some(Self::Equal),
"[bracketleft]" => Some(Self::BracketLeft),
"[bracketright]" => Some(Self::BracketRight),
"[backslash]" => Some(Self::Backslash),
"[semicolon]" => Some(Self::Semicolon),
"[quote]" => Some(Self::Quote),
"[comma]" => Some(Self::Comma),
"[period]" => Some(Self::Period),
"[slash]" => Some(Self::Slash),
"[left]" | "[arrowleft]" => Some(Self::Left),
"[up]" | "[arrowup]" => Some(Self::Up),
"[right]" | "[arrowright]" => Some(Self::Right),
"[down]" | "[arrowdown]" => Some(Self::Down),
"[pageup]" => Some(Self::PageUp),
"[pagedown]" => Some(Self::PageDown),
"[end]" => Some(Self::End),
"[home]" => Some(Self::Home),
"[tab]" => Some(Self::Tab),
"[enter]" => Some(Self::Enter),
"[escape]" => Some(Self::Escape),
"[space]" => Some(Self::Space),
"[backspace]" => Some(Self::Backspace),
"[delete]" => Some(Self::Delete),
// "[pause]" => Some(Self::Pause),
// "[capslock]" => Some(Self::CapsLock),
"[insert]" => Some(Self::Insert),
// "[numpad0]" => Some(Self::Numpad0),
// "[numpad1]" => Some(Self::Numpad1),
// "[numpad2]" => Some(Self::Numpad2),
// "[numpad3]" => Some(Self::Numpad3),
// "[numpad4]" => Some(Self::Numpad4),
// "[numpad5]" => Some(Self::Numpad5),
// "[numpad6]" => Some(Self::Numpad6),
// "[numpad7]" => Some(Self::Numpad7),
// "[numpad8]" => Some(Self::Numpad8),
// "[numpad9]" => Some(Self::Numpad9),
// "[numpadmultiply]" => Some(Self::NumpadMultiply),
// "[numpadadd]" => Some(Self::NumpadAdd),
// "[numpadcomma]" => Some(Self::NumpadComma),
// "[numpadsubtract]" => Some(Self::NumpadSubtract),
// "[numpaddecimal]" => Some(Self::NumpadDecimal),
// "[numpaddivide]" => Some(Self::NumpadDivide),
_ => None,
}
}
/// Convert the scan code to its key face for immutable keys.
pub fn try_to_key(&self) -> Option<String> {
Some(
match self {
ScanCode::F1 => "f1",
ScanCode::F2 => "f2",
ScanCode::F3 => "f3",
ScanCode::F4 => "f4",
ScanCode::F5 => "f5",
ScanCode::F6 => "f6",
ScanCode::F7 => "f7",
ScanCode::F8 => "f8",
ScanCode::F9 => "f9",
ScanCode::F10 => "f10",
ScanCode::F11 => "f11",
ScanCode::F12 => "f12",
ScanCode::F13 => "f13",
ScanCode::F14 => "f14",
ScanCode::F15 => "f15",
ScanCode::F16 => "f16",
ScanCode::F17 => "f17",
ScanCode::F18 => "f18",
ScanCode::F19 => "f19",
ScanCode::F20 => "f20",
ScanCode::F21 => "f21",
ScanCode::F22 => "f22",
ScanCode::F23 => "f23",
ScanCode::F24 => "f24",
ScanCode::Left => "left",
ScanCode::Up => "up",
ScanCode::Right => "right",
ScanCode::Down => "down",
ScanCode::PageUp => "pageup",
ScanCode::PageDown => "pagedown",
ScanCode::End => "end",
ScanCode::Home => "home",
ScanCode::Tab => "tab",
ScanCode::Enter => "enter",
ScanCode::Escape => "escape",
ScanCode::Space => "space",
ScanCode::Backspace => "backspace",
ScanCode::Delete => "delete",
ScanCode::Insert => "insert",
_ => return None,
}
.to_string(),
)
}
/// This function is used to convert the scan code to its key face on US keyboard layout.
/// Only used for tests and Linux.
pub fn to_key(&self) -> &str {
match self {
ScanCode::F1 => "f1",
ScanCode::F2 => "f2",
ScanCode::F3 => "f3",
ScanCode::F4 => "f4",
ScanCode::F5 => "f5",
ScanCode::F6 => "f6",
ScanCode::F7 => "f7",
ScanCode::F8 => "f8",
ScanCode::F9 => "f9",
ScanCode::F10 => "f10",
ScanCode::F11 => "f11",
ScanCode::F12 => "f12",
ScanCode::F13 => "f13",
ScanCode::F14 => "f14",
ScanCode::F15 => "f15",
ScanCode::F16 => "f16",
ScanCode::F17 => "f17",
ScanCode::F18 => "f18",
ScanCode::F19 => "f19",
ScanCode::F20 => "f20",
ScanCode::F21 => "f21",
ScanCode::F22 => "f22",
ScanCode::F23 => "f23",
ScanCode::F24 => "f24",
ScanCode::A => "a",
ScanCode::B => "b",
ScanCode::C => "c",
ScanCode::D => "d",
ScanCode::E => "e",
ScanCode::F => "f",
ScanCode::G => "g",
ScanCode::H => "h",
ScanCode::I => "i",
ScanCode::J => "j",
ScanCode::K => "k",
ScanCode::L => "l",
ScanCode::M => "m",
ScanCode::N => "n",
ScanCode::O => "o",
ScanCode::P => "p",
ScanCode::Q => "q",
ScanCode::R => "r",
ScanCode::S => "s",
ScanCode::T => "t",
ScanCode::U => "u",
ScanCode::V => "v",
ScanCode::W => "w",
ScanCode::X => "x",
ScanCode::Y => "y",
ScanCode::Z => "z",
ScanCode::Digit0 => "0",
ScanCode::Digit1 => "1",
ScanCode::Digit2 => "2",
ScanCode::Digit3 => "3",
ScanCode::Digit4 => "4",
ScanCode::Digit5 => "5",
ScanCode::Digit6 => "6",
ScanCode::Digit7 => "7",
ScanCode::Digit8 => "8",
ScanCode::Digit9 => "9",
ScanCode::Backquote => "`",
ScanCode::Minus => "-",
ScanCode::Equal => "=",
ScanCode::BracketLeft => "[",
ScanCode::BracketRight => "]",
ScanCode::Backslash => "\\",
ScanCode::Semicolon => ";",
ScanCode::Quote => "'",
ScanCode::Comma => ",",
ScanCode::Period => ".",
ScanCode::Slash => "/",
ScanCode::Left => "left",
ScanCode::Up => "up",
ScanCode::Right => "right",
ScanCode::Down => "down",
ScanCode::PageUp => "pageup",
ScanCode::PageDown => "pagedown",
ScanCode::End => "end",
ScanCode::Home => "home",
ScanCode::Tab => "tab",
ScanCode::Enter => "enter",
ScanCode::Escape => "escape",
ScanCode::Space => "space",
ScanCode::Backspace => "backspace",
ScanCode::Delete => "delete",
ScanCode::Insert => "insert",
}
}
}

View File

@@ -4,6 +4,9 @@ use std::{
error::Error,
fmt::{Display, Write},
};
use util::ResultExt;
use super::{PlatformKeyboardMapper, ScanCode, is_alphabetic_key};
/// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
@@ -93,12 +96,41 @@ impl Keystroke {
/// key_char syntax is only used for generating test events,
/// secondary means "cmd" on macOS and "ctrl" on other platforms
/// when matching a key with an key_char set will be matched without it.
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
pub fn parse(
source: &str,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystroke = Keystroke::parse_keystroke_components(source, '-')?;
// Create error once for reuse
let error = || InvalidKeystrokeError {
keystroke: source.to_owned(),
};
if keystroke.key.starts_with("oem") {
// The oem_key will be handled after https://github.com/zed-industries/zed/pull/29144
return Err(error());
}
if keystroke.key.starts_with('[') && keystroke.key.ends_with(']') {
let scan_code = ScanCode::parse(&keystroke.key).ok_or_else(error)?;
keystroke.key = keyboard_mapper
.scan_code_to_key(scan_code)
.map_err(|_| error())?;
}
Ok(keystroke.into_gpui_style(keyboard_mapper))
}
/// Parses a keystroke string representation into a `Keystroke` struct using a specified separator character.
/// This is the low-level parsing function that handles the basic string format without additional
/// platform-specific mapping or transformations.
pub fn parse_keystroke_components(
source: &str,
separator: char,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut modifiers = Modifiers::none();
let mut key = None;
let mut key_char = None;
let mut components = source.split('-').peekable();
let mut components = source.split(separator).peekable();
while let Some(component) = components.next() {
if component.eq_ignore_ascii_case("ctrl") {
modifiers.control = true;
@@ -137,8 +169,8 @@ impl Keystroke {
let mut key_str = component.to_string();
if let Some(next) = components.peek() {
if next.is_empty() && source.ends_with('-') {
key = Some(String::from("-"));
if next.is_empty() && source.ends_with(separator) {
key = Some(String::from(separator));
break;
} else if next.len() > 1 && next.starts_with('>') {
key = Some(key_str);
@@ -187,7 +219,6 @@ impl Keystroke {
let key = key.ok_or_else(|| InvalidKeystrokeError {
keystroke: source.to_owned(),
})?;
Ok(Keystroke {
modifiers,
key,
@@ -195,6 +226,22 @@ impl Keystroke {
})
}
/// Converts this keystroke to a GPUI style keystroke.
/// For example, `ctrl-shift-[` becomes `ctrl-{`, `ctrl-shift-=` becomes `ctrl-+`.
pub fn into_gpui_style(mut self, keyboard_mapper: &dyn PlatformKeyboardMapper) -> Keystroke {
if self.modifiers.shift && !is_alphabetic_key(&self.key) && self.key.len() == 1 {
if let Some(shifted_key) = keyboard_mapper
.get_shifted_key(&self.key)
.log_err()
.flatten()
{
self.modifiers.shift = false;
self.key = shifted_key;
}
}
self
}
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
let mut str = String::new();
@@ -538,3 +585,200 @@ impl Modifiers {
&& (other.function || !self.function)
}
}
#[cfg(test)]
mod tests {
use crate::{Keystroke, Modifiers, TestKeyboardMapper};
#[test]
fn test_different_separators() {
assert_eq!(
Keystroke::parse_keystroke_components("ctrl-alt--", '-').unwrap(),
Keystroke::parse_keystroke_components("ctrl+alt+-", '+').unwrap(),
);
assert_eq!(
Keystroke::parse_keystroke_components("ctrl-alt-+", '-').unwrap(),
Keystroke::parse_keystroke_components("ctrl+alt++", '+').unwrap(),
);
assert_eq!(
Keystroke::parse_keystroke_components("ctrl-alt-[Minus]", '-').unwrap(),
Keystroke::parse_keystroke_components("ctrl+alt+[Minus]", '+').unwrap(),
);
assert_eq!(
Keystroke::parse_keystroke_components("ctrl-alt-[张小白]", '-').unwrap(),
Keystroke::parse_keystroke_components("ctrl+alt+[张小白]", '+').unwrap(),
);
}
#[test]
fn test_parse_scan_code() {
let keyboard_mapper = TestKeyboardMapper::new();
for letter in 'a'..='z' {
let key1 = format!("[Key{}]", letter.to_uppercase());
let key2 = format!("[{}]", letter.to_uppercase());
let keystroke1 = Keystroke::parse(&key1, &keyboard_mapper).unwrap();
let keystroke2 = Keystroke::parse(&key2, &keyboard_mapper).unwrap();
assert_eq!(
keystroke1,
Keystroke {
modifiers: Modifiers::default(),
key: letter.to_string(),
key_char: None,
}
);
assert_eq!(keystroke1, keystroke2);
let source1 = format!("ctrl-{}", key1);
let source2 = format!("ctrl-{}", key2);
let keystroke1 = Keystroke::parse(&source1, &keyboard_mapper).unwrap();
let keystroke2 = Keystroke::parse(&source2, &keyboard_mapper).unwrap();
assert_eq!(
keystroke1,
Keystroke {
modifiers: Modifiers::control(),
key: letter.to_string(),
key_char: None,
}
);
assert_eq!(keystroke1, keystroke2);
let source1 = format!("ctrl-alt-{}", key1);
let source2 = format!("ctrl-alt-{}", key2);
let keystroke1 = Keystroke::parse(&source1, &keyboard_mapper).unwrap();
let keystroke2 = Keystroke::parse(&source2, &keyboard_mapper).unwrap();
assert_eq!(
keystroke1,
Keystroke {
modifiers: Modifiers {
control: true,
alt: true,
..Default::default()
},
key: letter.to_string(),
key_char: None,
}
);
assert_eq!(keystroke1, keystroke2);
let source1 = format!("ctrl-shift-{}", key1);
let source2 = format!("ctrl-shift-{}", key2);
let keystroke1 = Keystroke::parse(&source1, &keyboard_mapper).unwrap();
let keystroke2 = Keystroke::parse(&source2, &keyboard_mapper).unwrap();
assert_eq!(
keystroke1,
Keystroke {
modifiers: Modifiers::control_shift(),
key: letter.to_string(),
key_char: None,
}
);
assert_eq!(keystroke1, keystroke2);
}
let other_keys = [
("[Backquote]", "`", "~"),
("[Minus]", "-", "_"),
("[Equal]", "=", "+"),
("[BracketLeft]", "[", "{"),
("[BracketRight]", "]", "}"),
("[Backslash]", "\\", "|"),
("[Semicolon]", ";", ":"),
("[Quote]", "'", "\""),
("[Comma]", ",", "<"),
("[Period]", ".", ">"),
("[Slash]", "/", "?"),
];
for (code, key, shifted_key) in other_keys {
assert_eq!(
Keystroke::parse(code, &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers::default(),
key: key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("shift-{}", code), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers::default(),
key: shifted_key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("shift-{}", key), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers::default(),
key: shifted_key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("ctrl-{}", code), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers::control(),
key: key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("ctrl-alt-{}", code), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers {
control: true,
alt: true,
..Default::default()
},
key: key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("ctrl-alt-{}", key), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers {
control: true,
alt: true,
..Default::default()
},
key: key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("ctrl-shift-{}", code), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers::control(),
key: shifted_key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("ctrl-alt-shift-{}", code), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers {
control: true,
alt: true,
..Default::default()
},
key: shifted_key.to_string(),
key_char: None,
}
);
assert_eq!(
Keystroke::parse(&format!("ctrl-alt-shift-{}", key), &keyboard_mapper).unwrap(),
Keystroke {
modifiers: Modifiers {
control: true,
alt: true,
..Default::default()
},
key: shifted_key.to_string(),
key_char: None,
}
);
}
}
}

View File

@@ -1,4 +1,20 @@
use crate::PlatformKeyboardLayout;
#[cfg(any(feature = "wayland", feature = "x11"))]
use std::sync::LazyLock;
#[cfg(any(feature = "wayland", feature = "x11"))]
use collections::HashMap;
#[cfg(any(feature = "wayland", feature = "x11"))]
use x11rb::{protocol::xkb::ConnectionExt, xcb_ffi::XCBConnection};
#[cfg(any(feature = "wayland", feature = "x11"))]
use xkbcommon::xkb::{
Keycode,
x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION},
};
use crate::{PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
#[cfg(any(feature = "wayland", feature = "x11"))]
use crate::is_alphabetic_key;
pub(crate) struct LinuxKeyboardLayout {
id: String,
@@ -19,3 +35,332 @@ impl LinuxKeyboardLayout {
Self { id }
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) struct LinuxKeyboardMapper {
code_to_key: HashMap<Keycode, String>,
key_to_code: HashMap<String, Keycode>,
code_to_shifted_key: HashMap<Keycode, String>,
}
#[cfg(any(feature = "wayland", feature = "x11"))]
impl PlatformKeyboardMapper for LinuxKeyboardMapper {
fn scan_code_to_key(&self, gpui_scan_code: ScanCode) -> anyhow::Result<String> {
if let Some(key) = gpui_scan_code.try_to_key() {
return Ok(key);
}
let Some(scan_code) = get_scan_code(gpui_scan_code) else {
return Err(anyhow::anyhow!("Scan code not found: {:?}", gpui_scan_code));
};
if let Some(key) = self.code_to_key.get(&Keycode::new(scan_code)) {
Ok(key.clone())
} else {
Err(anyhow::anyhow!(
"Key not found for input scan code: {:?}, scan code: {}",
gpui_scan_code,
scan_code
))
}
}
fn get_shifted_key(&self, key: &str) -> anyhow::Result<Option<String>> {
if is_immutable_key(key) {
return Ok(None);
}
if is_alphabetic_key(key) {
return Ok(Some(key.to_uppercase()));
}
let Some(scan_code) = self.key_to_code.get(key) else {
return Err(anyhow::anyhow!("Key not found: {}", key));
};
if let Some(shifted_key) = self.code_to_shifted_key.get(scan_code) {
Ok(Some(shifted_key.clone()))
} else {
Err(anyhow::anyhow!(
"Shifted key not found for key {} with scan code: {:?}",
key,
scan_code
))
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
static XCB_CONNECTION: LazyLock<XCBConnection> =
LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
#[cfg(any(feature = "wayland", feature = "x11"))]
impl LinuxKeyboardMapper {
pub(crate) fn new() -> Self {
let _ = XCB_CONNECTION
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let xkb_context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
let xkb_state = {
let xkb_keymap = xkbcommon::xkb::x11::keymap_new_from_device(
&xkb_context,
&*XCB_CONNECTION,
xkb_device_id,
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
);
xkbcommon::xkb::x11::state_new_from_device(&xkb_keymap, &*XCB_CONNECTION, xkb_device_id)
};
let mut code_to_key = HashMap::default();
let mut key_to_code = HashMap::default();
let mut code_to_shifted_key = HashMap::default();
let keymap = xkb_state.get_keymap();
let mut shifted_state = xkbcommon::xkb::State::new(&keymap);
let shift_mod = keymap.mod_get_index(xkbcommon::xkb::MOD_NAME_SHIFT);
let shift_mask = 1 << shift_mod;
shifted_state.update_mask(shift_mask, 0, 0, 0, 0, 0);
for &scan_code in TYPEABLE_CODES {
let keycode = Keycode::new(scan_code);
let key = xkb_state.key_get_utf8(keycode);
code_to_key.insert(keycode, key.clone());
key_to_code.insert(key, keycode);
let shifted_key = shifted_state.key_get_utf8(keycode);
code_to_shifted_key.insert(keycode, shifted_key);
}
Self {
code_to_key,
key_to_code,
code_to_shifted_key,
}
}
}
// All typeable scan codes for the standard US keyboard layout, ANSI104
#[cfg(any(feature = "wayland", feature = "x11"))]
const TYPEABLE_CODES: &[u32] = &[
0x0026, // a
0x0038, // b
0x0036, // c
0x0028, // d
0x001a, // e
0x0029, // f
0x002a, // g
0x002b, // h
0x001f, // i
0x002c, // j
0x002d, // k
0x002e, // l
0x003a, // m
0x0039, // n
0x0020, // o
0x0021, // p
0x0018, // q
0x001b, // r
0x0027, // s
0x001c, // t
0x001e, // u
0x0037, // v
0x0019, // w
0x0035, // x
0x001d, // y
0x0034, // z
0x0013, // Digit 0
0x000a, // Digit 1
0x000b, // Digit 2
0x000c, // Digit 3
0x000d, // Digit 4
0x000e, // Digit 5
0x000f, // Digit 6
0x0010, // Digit 7
0x0011, // Digit 8
0x0012, // Digit 9
0x0031, // ` Backquote
0x0014, // - Minus
0x0015, // = Equal
0x0022, // [ Left bracket
0x0023, // ] Right bracket
0x0033, // \ Backslash
0x002f, // ; Semicolon
0x0030, // ' Quote
0x003b, // , Comma
0x003c, // . Period
0x003d, // / Slash
];
#[cfg(any(feature = "wayland", feature = "x11"))]
fn is_immutable_key(key: &str) -> bool {
matches!(
key,
"f1" | "f2"
| "f3"
| "f4"
| "f5"
| "f6"
| "f7"
| "f8"
| "f9"
| "f10"
| "f11"
| "f12"
| "f13"
| "f14"
| "f15"
| "f16"
| "f17"
| "f18"
| "f19"
| "f20"
| "f21"
| "f22"
| "f23"
| "f24"
| "backspace"
| "delete"
| "left"
| "right"
| "up"
| "down"
| "pageup"
| "pagedown"
| "insert"
| "home"
| "end"
| "back"
| "forward"
| "escape"
| "space"
| "tab"
| "enter"
| "shift"
| "control"
| "alt"
| "platform"
| "cmd"
| "super"
| "win"
| "fn"
| "menu"
| "copy"
| "paste"
| "cut"
| "find"
| "open"
| "save"
)
}
#[cfg(any(feature = "wayland", feature = "x11"))]
fn get_scan_code(scan_code: ScanCode) -> Option<u32> {
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
Some(match scan_code {
ScanCode::F1 => 0x0043,
ScanCode::F2 => 0x0044,
ScanCode::F3 => 0x0045,
ScanCode::F4 => 0x0046,
ScanCode::F5 => 0x0047,
ScanCode::F6 => 0x0048,
ScanCode::F7 => 0x0049,
ScanCode::F8 => 0x004a,
ScanCode::F9 => 0x004b,
ScanCode::F10 => 0x004c,
ScanCode::F11 => 0x005f,
ScanCode::F12 => 0x0060,
ScanCode::F13 => 0x00bf,
ScanCode::F14 => 0x00c0,
ScanCode::F15 => 0x00c1,
ScanCode::F16 => 0x00c2,
ScanCode::F17 => 0x00c3,
ScanCode::F18 => 0x00c4,
ScanCode::F19 => 0x00c5,
ScanCode::F20 => 0x00c6,
ScanCode::F21 => 0x00c7,
ScanCode::F22 => 0x00c8,
ScanCode::F23 => 0x00c9,
ScanCode::F24 => 0x00ca,
ScanCode::A => 0x0026,
ScanCode::B => 0x0038,
ScanCode::C => 0x0036,
ScanCode::D => 0x0028,
ScanCode::E => 0x001a,
ScanCode::F => 0x0029,
ScanCode::G => 0x002a,
ScanCode::H => 0x002b,
ScanCode::I => 0x001f,
ScanCode::J => 0x002c,
ScanCode::K => 0x002d,
ScanCode::L => 0x002e,
ScanCode::M => 0x003a,
ScanCode::N => 0x0039,
ScanCode::O => 0x0020,
ScanCode::P => 0x0021,
ScanCode::Q => 0x0018,
ScanCode::R => 0x001b,
ScanCode::S => 0x0027,
ScanCode::T => 0x001c,
ScanCode::U => 0x001e,
ScanCode::V => 0x0037,
ScanCode::W => 0x0019,
ScanCode::X => 0x0035,
ScanCode::Y => 0x001d,
ScanCode::Z => 0x0034,
ScanCode::Digit0 => 0x0013,
ScanCode::Digit1 => 0x000a,
ScanCode::Digit2 => 0x000b,
ScanCode::Digit3 => 0x000c,
ScanCode::Digit4 => 0x000d,
ScanCode::Digit5 => 0x000e,
ScanCode::Digit6 => 0x000f,
ScanCode::Digit7 => 0x0010,
ScanCode::Digit8 => 0x0011,
ScanCode::Digit9 => 0x0012,
ScanCode::Backquote => 0x0031,
ScanCode::Minus => 0x0014,
ScanCode::Equal => 0x0015,
ScanCode::BracketLeft => 0x0022,
ScanCode::BracketRight => 0x0023,
ScanCode::Backslash => 0x0033,
ScanCode::Semicolon => 0x002f,
ScanCode::Quote => 0x0030,
ScanCode::Comma => 0x003b,
ScanCode::Period => 0x003c,
ScanCode::Slash => 0x003d,
ScanCode::Left => 0x0071,
ScanCode::Up => 0x006f,
ScanCode::Right => 0x0072,
ScanCode::Down => 0x0074,
ScanCode::PageUp => 0x0070,
ScanCode::PageDown => 0x0075,
ScanCode::End => 0x0073,
ScanCode::Home => 0x006e,
ScanCode::Tab => 0x0017,
ScanCode::Enter => 0x0024,
ScanCode::Escape => 0x0009,
ScanCode::Space => 0x0041,
ScanCode::Backspace => 0x0016,
ScanCode::Delete => 0x0077,
ScanCode::Insert => 0x0076,
})
}
#[cfg(not(any(feature = "wayland", feature = "x11")))]
pub(crate) struct LinuxKeyboardMapper;
#[cfg(not(any(feature = "wayland", feature = "x11")))]
impl PlatformKeyboardMapper for LinuxKeyboardMapper {
fn scan_code_to_key(&self, _scan_code: ScanCode) -> anyhow::Result<String> {
Err(anyhow::anyhow!("LinuxKeyboardMapper not supported"))
}
fn get_shifted_key(&self, _key: &str) -> anyhow::Result<Option<String>> {
Err(anyhow::anyhow!("LinuxKeyboardMapper not supported"))
}
}
#[cfg(not(any(feature = "wayland", feature = "x11")))]
impl LinuxKeyboardMapper {
pub(crate) fn new() -> Self {
Self
}
}

View File

@@ -25,8 +25,9 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
PlatformTextSystem, PlatformWindow, Point, Result, ScreenCaptureSource, Task, WindowAppearance,
WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@@ -138,6 +139,10 @@ impl<P: LinuxClient + 'static> Platform for P {
self.with_common(|common| common.text_system.clone())
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(super::LinuxKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
self.keyboard_layout()
}

View File

@@ -1,21 +1,14 @@
use crate::{
KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
platform::mac::{
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
},
point, px,
CMD_MOD, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NO_MOD, NavigationDirection,
OPTION_MOD, Pixels, PlatformInput, SHIFT_MOD, ScrollDelta, ScrollWheelEvent, TouchPhase,
always_use_command_layout, chars_for_modified_key, platform::mac::NSStringExt, point, px,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{YES, id},
};
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::c_void};
use std::borrow::Cow;
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@@ -452,80 +445,3 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
}
}
}
fn always_use_command_layout() -> bool {
if chars_for_modified_key(0, NO_MOD).is_ascii() {
return false;
}
chars_for_modified_key(0, CMD_MOD).is_ascii()
}
const NO_MOD: u32 = 0;
const CMD_MOD: u32 = 1;
const SHIFT_MOD: u32 = 2;
const OPTION_MOD: u32 = 8;
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
unsafe {
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}

View File

@@ -1,8 +1,14 @@
use std::ffi::{CStr, c_void};
use collections::HashMap;
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, runtime::Object, sel, sel_impl};
use crate::PlatformKeyboardLayout;
use crate::{
PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode, is_alphabetic_key,
platform::mac::{LMGetKbdType, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData},
};
use super::{
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
@@ -26,24 +32,338 @@ impl PlatformKeyboardLayout for MacKeyboardLayout {
impl MacKeyboardLayout {
pub(crate) fn new() -> Self {
unsafe {
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
let id: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyInputSourceID as *const c_void,
);
let id: *const std::os::raw::c_char = msg_send![id, UTF8String];
let id = CStr::from_ptr(id).to_str().unwrap().to_string();
let name: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyLocalizedName as *const c_void,
);
let (keyboard, id) = get_keyboard_layout_id();
let name = unsafe {
let name: *mut Object =
TISGetInputSourceProperty(keyboard, kTISPropertyLocalizedName as *const c_void);
let name: *const std::os::raw::c_char = msg_send![name, UTF8String];
let name = CStr::from_ptr(name).to_str().unwrap().to_string();
CStr::from_ptr(name).to_str().unwrap().to_string()
};
Self { id, name }
Self { id, name }
}
}
fn get_keyboard_layout_id() -> (*mut Object, String) {
unsafe {
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
let id: *mut Object =
TISGetInputSourceProperty(current_keyboard, kTISPropertyInputSourceID as *const c_void);
let id: *const std::os::raw::c_char = msg_send![id, UTF8String];
(
current_keyboard,
CStr::from_ptr(id).to_str().unwrap().to_string(),
)
}
}
pub(crate) struct MacKeyboardMapper {
code_to_key: HashMap<u16, String>,
key_to_code: HashMap<String, u16>,
code_to_shifted_key: HashMap<u16, String>,
}
impl MacKeyboardMapper {
pub(crate) fn new() -> Self {
let mut code_to_key = HashMap::default();
let mut key_to_code = HashMap::default();
let mut code_to_shifted_key = HashMap::default();
let always_use_cmd_layout = always_use_command_layout();
for &scan_code in TYPEABLE_CODES.iter() {
let (key, shifted_key) = generate_key_pairs(scan_code, always_use_cmd_layout);
code_to_key.insert(scan_code, key.clone());
key_to_code.insert(key, scan_code);
code_to_shifted_key.insert(scan_code, shifted_key);
}
Self {
code_to_key,
key_to_code,
code_to_shifted_key,
}
}
}
impl PlatformKeyboardMapper for MacKeyboardMapper {
fn scan_code_to_key(&self, gpui_scan_code: ScanCode) -> anyhow::Result<String> {
if let Some(key) = gpui_scan_code.try_to_key() {
return Ok(key);
}
let Some(scan_code) = get_scan_code(gpui_scan_code) else {
return Err(anyhow::anyhow!("Scan code not found: {:?}", gpui_scan_code));
};
if let Some(key) = self.code_to_key.get(&scan_code) {
Ok(key.clone())
} else {
Err(anyhow::anyhow!(
"Key not found for input scan code: {:?}, scan code: {}",
gpui_scan_code,
scan_code
))
}
}
fn get_shifted_key(&self, key: &str) -> anyhow::Result<Option<String>> {
if key.chars().count() != 1 {
return Ok(None);
}
if is_alphabetic_key(key) {
return Ok(Some(key.to_uppercase()));
}
let Some(scan_code) = self.key_to_code.get(key) else {
return Err(anyhow::anyhow!("Key not found: {}", key));
};
if let Some(shifted_key) = self.code_to_shifted_key.get(scan_code) {
Ok(Some(shifted_key.clone()))
} else {
Err(anyhow::anyhow!(
"Shifted key not found for key {} with scan code: {}",
key,
scan_code
))
}
}
}
pub(crate) const NO_MOD: u32 = 0;
pub(crate) const CMD_MOD: u32 = 1;
pub(crate) const SHIFT_MOD: u32 = 2;
pub(crate) const OPTION_MOD: u32 = 8;
pub(crate) fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
unsafe {
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}
pub(crate) fn always_use_command_layout() -> bool {
if chars_for_modified_key(0, NO_MOD).is_ascii() {
return false;
}
chars_for_modified_key(0, CMD_MOD).is_ascii()
}
fn generate_key_pairs(scan_code: u16, always_use_cmd_layout: bool) -> (String, String) {
let mut chars_ignoring_modifiers = chars_for_modified_key(scan_code, NO_MOD);
let mut chars_with_shift = chars_for_modified_key(scan_code, SHIFT_MOD);
// Handle Dvorak+QWERTY / Russian / Armenian
if always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(scan_code, CMD_MOD);
let chars_with_both = chars_for_modified_key(scan_code, CMD_MOD | SHIFT_MOD);
// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
if chars_with_both != chars_with_cmd {
chars_with_shift = chars_with_both;
// Handle edge-case where cmd-shift-s reports cmd-s instead of
// cmd-shift-s (Ukrainian, etc.)
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
}
chars_ignoring_modifiers = chars_with_cmd;
}
(chars_ignoring_modifiers, chars_with_shift)
}
// All typeable scan codes for the standard US keyboard layout, ANSI104
const TYPEABLE_CODES: &[u16] = &[
0x0000, // a
0x000b, // b
0x0008, // c
0x0002, // d
0x000e, // e
0x0003, // f
0x0005, // g
0x0004, // h
0x0022, // i
0x0026, // j
0x0028, // k
0x0025, // l
0x002e, // m
0x002d, // n
0x001f, // o
0x0023, // p
0x000c, // q
0x000f, // r
0x0001, // s
0x0011, // t
0x0020, // u
0x0009, // v
0x000d, // w
0x0007, // x
0x0010, // y
0x0006, // z
0x001d, // Digit 0
0x0012, // Digit 1
0x0013, // Digit 2
0x0014, // Digit 3
0x0015, // Digit 4
0x0017, // Digit 5
0x0016, // Digit 6
0x001a, // Digit 7
0x001c, // Digit 8
0x0019, // Digit 9
0x0032, // ` Tilde
0x001b, // - Minus
0x0018, // = Equal
0x0021, // [ Left bracket
0x001e, // ] Right bracket
0x002a, // \ Backslash
0x0029, // ; Semicolon
0x0027, // ' Quote
0x002b, // , Comma
0x002f, // . Period
0x002c, // / Slash
];
fn get_scan_code(scan_code: ScanCode) -> Option<u16> {
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
Some(match scan_code {
ScanCode::F1 => 0x007a,
ScanCode::F2 => 0x0078,
ScanCode::F3 => 0x0063,
ScanCode::F4 => 0x0076,
ScanCode::F5 => 0x0060,
ScanCode::F6 => 0x0061,
ScanCode::F7 => 0x0062,
ScanCode::F8 => 0x0064,
ScanCode::F9 => 0x0065,
ScanCode::F10 => 0x006d,
ScanCode::F11 => 0x0067,
ScanCode::F12 => 0x006f,
ScanCode::F13 => 0x0069,
ScanCode::F14 => 0x006b,
ScanCode::F15 => 0x0071,
ScanCode::F16 => 0x006a,
ScanCode::F17 => 0x0040,
ScanCode::F18 => 0x004f,
ScanCode::F19 => 0x0050,
ScanCode::F20 => 0x005a,
ScanCode::F21 | ScanCode::F22 | ScanCode::F23 | ScanCode::F24 => return None,
ScanCode::A => 0x0000,
ScanCode::B => 0x000b,
ScanCode::C => 0x0008,
ScanCode::D => 0x0002,
ScanCode::E => 0x000e,
ScanCode::F => 0x0003,
ScanCode::G => 0x0005,
ScanCode::H => 0x0004,
ScanCode::I => 0x0022,
ScanCode::J => 0x0026,
ScanCode::K => 0x0028,
ScanCode::L => 0x0025,
ScanCode::M => 0x002e,
ScanCode::N => 0x002d,
ScanCode::O => 0x001f,
ScanCode::P => 0x0023,
ScanCode::Q => 0x000c,
ScanCode::R => 0x000f,
ScanCode::S => 0x0001,
ScanCode::T => 0x0011,
ScanCode::U => 0x0020,
ScanCode::V => 0x0009,
ScanCode::W => 0x000d,
ScanCode::X => 0x0007,
ScanCode::Y => 0x0010,
ScanCode::Z => 0x0006,
ScanCode::Digit0 => 0x001d,
ScanCode::Digit1 => 0x0012,
ScanCode::Digit2 => 0x0013,
ScanCode::Digit3 => 0x0014,
ScanCode::Digit4 => 0x0015,
ScanCode::Digit5 => 0x0017,
ScanCode::Digit6 => 0x0016,
ScanCode::Digit7 => 0x001a,
ScanCode::Digit8 => 0x001c,
ScanCode::Digit9 => 0x0019,
ScanCode::Backquote => 0x0032,
ScanCode::Minus => 0x001b,
ScanCode::Equal => 0x0018,
ScanCode::BracketLeft => 0x0021,
ScanCode::BracketRight => 0x001e,
ScanCode::Backslash => 0x002a,
ScanCode::Semicolon => 0x0029,
ScanCode::Quote => 0x0027,
ScanCode::Comma => 0x002b,
ScanCode::Period => 0x002f,
ScanCode::Slash => 0x002c,
ScanCode::Left => 0x007b,
ScanCode::Up => 0x007e,
ScanCode::Right => 0x007c,
ScanCode::Down => 0x007d,
ScanCode::PageUp => 0x0074,
ScanCode::PageDown => 0x0079,
ScanCode::End => 0x0077,
ScanCode::Home => 0x0073,
ScanCode::Tab => 0x0030,
ScanCode::Enter => 0x0024,
ScanCode::Escape => 0x0035,
ScanCode::Space => 0x0031,
ScanCode::Backspace => 0x0033,
ScanCode::Delete => 0x0075,
ScanCode::Insert => 0x0072,
})
}

View File

@@ -1,5 +1,5 @@
use super::{
BoolExt, MacKeyboardLayout,
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
attributed_string::{NSAttributedString, NSMutableAttributedString},
events::key_to_native,
is_macos_version_at_least, renderer, screen_capture,
@@ -8,8 +8,8 @@ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
SemanticVersion, Task, WindowAppearance, WindowParams, hash,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Result,
ScreenCaptureSource, SemanticVersion, Task, WindowAppearance, WindowParams, hash,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
@@ -843,6 +843,10 @@ impl Platform for MacPlatform {
self.0.lock().validate_menu_command = Some(callback);
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(MacKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(MacKeyboardLayout::new())
}

View File

@@ -1,8 +1,9 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay, TestKeyboardMapper,
TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -223,6 +224,10 @@ impl Platform for TestPlatform {
self.text_system.clone()
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(TestKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(TestKeyboardLayout)
}

View File

@@ -1,16 +1,19 @@
use anyhow::Result;
use anyhow::{Context, Result};
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, 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_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_8, VK_OEM_102,
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MAPVK_VSC_TO_VK, MapVirtualKeyW,
ToUnicode, 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_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_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD,
VK_OEM_PLUS, VK_SHIFT, VkKeyScanW,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use windows_core::HSTRING;
use crate::{Modifiers, PlatformKeyboardLayout};
use crate::{
Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode, is_alphabetic_key,
};
pub(crate) struct WindowsKeyboardLayout {
id: String,
@@ -48,6 +51,38 @@ impl WindowsKeyboardLayout {
}
}
pub(crate) struct WindowsKeyboardMapper;
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
fn scan_code_to_key(&self, scan_code: ScanCode) -> Result<String> {
if let Some(key) = scan_code.try_to_key() {
return Ok(key);
}
let vkey = get_virtual_key_from_scan_code(scan_code)?;
let (key, _) = vkey_to_key(vkey).context(format!(
"Failed to get key from scan code: {:?}, vkey: {:?}",
scan_code, vkey
))?;
Ok(key)
}
fn get_shifted_key(&self, key: &str) -> Result<Option<String>> {
if key.chars().count() != 1 {
return Ok(None);
}
if is_alphabetic_key(key) {
return Ok(Some(key.to_uppercase()));
}
Ok(Some(get_shifted_character(key)?))
}
}
impl WindowsKeyboardMapper {
pub(crate) fn new() -> Self {
Self
}
}
pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY,
scan_code: u32,
@@ -138,3 +173,200 @@ pub(crate) fn generate_key_char(
}
None
}
fn get_vkey_from_char(key: &str, modifiers: &mut Modifiers) -> Result<VIRTUAL_KEY> {
if key.chars().count() != 1 {
anyhow::bail!("Key must be a single character, but got: {}", key);
}
let key_char = key
.encode_utf16()
.next()
.context("Empty key in keystorke")?;
let result = unsafe { VkKeyScanW(key_char) };
if result == -1 {
anyhow::bail!("Failed to get vkey from char: {}", key);
}
let high = (result >> 8) as i8;
let low = result as u8;
let (shift, ctrl, alt) = get_modifiers(high);
if ctrl {
if modifiers.control {
anyhow::bail!(
"Error parsing: {}, Ctrl modifier already set, but ctrl is required for this key: {}, you may be unable to use this shortcut.",
display_keystroke(key, modifiers),
key
);
}
modifiers.control = true;
}
if alt {
if modifiers.alt {
anyhow::bail!(
"Error parsing: {}, Alt modifier already set, but alt is required for this key: {}, you may be unable to use this shortcut.",
display_keystroke(key, modifiers),
key
);
}
modifiers.alt = true;
}
if shift {
if modifiers.shift {
anyhow::bail!(
"Error parsing: {}, Shift modifier already set, but shift is required for this key: {}, you may be unable to use this shortcut.",
display_keystroke(key, modifiers),
key
);
}
modifiers.shift = true;
}
Ok(VIRTUAL_KEY(low as u16))
}
fn get_modifiers(high: i8) -> (bool, bool, bool) {
let shift = high & 1;
let ctrl = (high >> 1) & 1;
let alt = (high >> 2) & 1;
(shift != 0, ctrl != 0, alt != 0)
}
fn get_shifted_character(key: &str) -> Result<String> {
let mut modifiers = Modifiers::default();
let virtual_key = get_vkey_from_char(key, &mut modifiers).context(format!(
"Failed to get virtual key from char while key_to_shifted: {}",
key
))?;
if modifiers != Modifiers::default() {
return Err(anyhow::anyhow!(
"Key is not a single character or has modifiers: {}",
key
));
}
let mut state = [0; 256];
state[VK_SHIFT.0 as usize] = 0x80;
let scan_code = unsafe { MapVirtualKeyW(virtual_key.0 as u32, MAPVK_VK_TO_VSC) };
let mut buffer = [0; 4];
let len = unsafe {
ToUnicode(
virtual_key.0 as u32,
scan_code,
Some(&state),
&mut buffer,
0,
)
};
if len > 0 {
let candidate = String::from_utf16_lossy(&buffer[..len as usize]);
if !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() {
return Ok(candidate);
}
}
Err(anyhow::anyhow!("Failed to get shifted key for: {}", key))
}
/// Converts a Windows virtual key code to its corresponding character and dead key status.
///
/// # Parameters
/// * `vkey` - The virtual key code to convert
///
/// # Returns
/// * `Some((String, bool))` - The character as a string and a boolean indicating if it's a dead key.
/// A dead key is a key that doesn't produce a character by itself but modifies the next key pressed
/// (e.g., accent keys like ^ or `).
/// * `None` - If the virtual key code doesn't map to a character
pub fn vkey_to_key(vkey: VIRTUAL_KEY) -> Option<(String, bool)> {
let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) };
if key_data == 0 {
return None;
}
// The high word contains dead key flag, the low word contains the character
let is_dead_key = (key_data >> 16) > 0;
let key = char::from_u32(key_data & 0xFFFF)?;
Some((key.to_ascii_lowercase().to_string(), is_dead_key))
}
fn display_keystroke(key: &str, modifiers: &Modifiers) -> String {
let mut display = String::new();
if modifiers.platform {
display.push_str("win-");
}
if modifiers.control {
display.push_str("ctrl-");
}
if modifiers.shift {
display.push_str("shift-");
}
if modifiers.alt {
display.push_str("alt-");
}
display.push_str(key);
display
}
fn get_virtual_key_from_scan_code(gpui_scan_code: ScanCode) -> Result<VIRTUAL_KEY> {
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
let scan_code = match gpui_scan_code {
ScanCode::A => 0x001e,
ScanCode::B => 0x0030,
ScanCode::C => 0x002e,
ScanCode::D => 0x0020,
ScanCode::E => 0x0012,
ScanCode::F => 0x0021,
ScanCode::G => 0x0022,
ScanCode::H => 0x0023,
ScanCode::I => 0x0017,
ScanCode::J => 0x0024,
ScanCode::K => 0x0025,
ScanCode::L => 0x0026,
ScanCode::M => 0x0032,
ScanCode::N => 0x0031,
ScanCode::O => 0x0018,
ScanCode::P => 0x0019,
ScanCode::Q => 0x0010,
ScanCode::R => 0x0013,
ScanCode::S => 0x001f,
ScanCode::T => 0x0014,
ScanCode::U => 0x0016,
ScanCode::V => 0x002f,
ScanCode::W => 0x0011,
ScanCode::X => 0x002d,
ScanCode::Y => 0x0015,
ScanCode::Z => 0x002c,
ScanCode::Digit0 => 0x000b,
ScanCode::Digit1 => 0x0002,
ScanCode::Digit2 => 0x0003,
ScanCode::Digit3 => 0x0004,
ScanCode::Digit4 => 0x0005,
ScanCode::Digit5 => 0x0006,
ScanCode::Digit6 => 0x0007,
ScanCode::Digit7 => 0x0008,
ScanCode::Digit8 => 0x0009,
ScanCode::Digit9 => 0x000a,
ScanCode::Backquote => 0x0029,
ScanCode::Minus => 0x000c,
ScanCode::Equal => 0x000d,
ScanCode::BracketLeft => 0x001a,
ScanCode::BracketRight => 0x001b,
ScanCode::Backslash => 0x002b,
ScanCode::Semicolon => 0x0027,
ScanCode::Quote => 0x0028,
ScanCode::Comma => 0x0033,
ScanCode::Period => 0x0034,
ScanCode::Slash => 0x0035,
_ => anyhow::bail!("Unsupported scan code: {:?}", gpui_scan_code),
};
let virtual_key = unsafe { MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK) };
if virtual_key == 0 {
anyhow::bail!(
"Failed to get virtual key from scan code: {:?}, {}",
gpui_scan_code,
scan_code
);
}
Ok(VIRTUAL_KEY(virtual_key as u16))
}

View File

@@ -313,6 +313,10 @@ impl Platform for WindowsPlatform {
self.text_system.clone()
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(WindowsKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(
WindowsKeyboardLayout::new()

View File

@@ -426,19 +426,58 @@ pub fn global_ssh_config_file() -> &'static Path {
Path::new("/etc/ssh/ssh_config")
}
/// Returns the path to the vscode user settings file
/// Returns the path to the vscode user settings file.
/// Note: This returns the `Default` profile settings file.
pub fn vscode_settings_file() -> &'static PathBuf {
static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
let rel_path = "Code/User/settings.json";
LOGS_DIR.get_or_init(|| {
if cfg!(target_os = "macos") {
#[cfg(target_os = "macos")]
{
LOGS_DIR.get_or_init(|| {
home_dir()
.join("Library/Application Support")
.join(rel_path)
} else {
home_dir().join(".config").join(rel_path)
}
})
})
}
#[cfg(target_os = "windows")]
{
LOGS_DIR.get_or_init(|| {
dirs::config_dir()
.expect("failed to determine RoamingAppData directory")
.join(rel_path)
})
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
LOGS_DIR.get_or_init(|| home_dir().join(".config").join(rel_path))
}
}
/// Returns the path to the vscode user keymap file.
/// Note: This returns the `Default` profile keymap file.
pub fn vscode_shortcuts_file() -> &'static PathBuf {
static RESULT: OnceLock<PathBuf> = OnceLock::new();
let rel_path = "Code/User/keybindings.json";
#[cfg(target_os = "macos")]
{
RESULT.get_or_init(|| {
home_dir()
.join("Library/Application Support")
.join(rel_path)
})
}
#[cfg(target_os = "windows")]
{
RESULT.get_or_init(|| {
dirs::config_dir()
.expect("failed to determine RoamingAppData directory")
.join(rel_path)
})
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
RESULT.get_or_init(|| home_dir().join(".config").join(rel_path))
}
}
/// Returns the path to the cursor user settings file

View File

@@ -387,7 +387,13 @@ impl KeymapFile {
},
};
let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
let key_binding = match KeyBinding::load(
keystrokes,
action,
context,
key_equivalents,
cx.keyboard_mapper(),
) {
Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => {
return Err(format!(

View File

@@ -22,7 +22,7 @@ pub use settings_store::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
SettingsStore, parse_json_with_comments,
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource, VsCodeShortcuts};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ pub struct ImportCursorSettings {
}
impl_actions!(zed, [ImportVsCodeSettings, ImportCursorSettings]);
actions!(zed, [OpenSettingsEditor]);
actions!(zed, [OpenSettingsEditor, ImportVsCodeShortcuts]);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
@@ -96,6 +96,15 @@ pub fn init(cx: &mut App) {
.detach();
});
workspace.register_action(|_, _: &ImportVsCodeShortcuts, window, cx| {
let fs = <dyn Fs>::global(cx);
window
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
handle_import_vscode_shortcuts(fs, cx).await;
})
.detach();
});
let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -171,6 +180,92 @@ async fn handle_import_vscode_settings(
.ok();
}
async fn handle_import_vscode_shortcuts(fs: Arc<dyn Fs>, cx: &mut AsyncWindowContext) {
let vscode = match settings::VsCodeShortcuts::load_user_shortcuts(fs.clone()).await {
Ok(vscode) => vscode,
Err(err) => {
println!(
"Failed to load VsCode shortcuts: {}\nLoading VsCode settings from path: {:?}",
err,
paths::vscode_shortcuts_file(),
);
let _ = cx.prompt(
gpui::PromptLevel::Info,
"Could not find or load a VsCode shortcuts file",
None,
&["Ok"],
);
return;
}
};
let Some(new_content) = cx
.update(|_, cx| {
let keyboard_mapper = cx.keyboard_mapper();
vscode.parse_shortcuts(keyboard_mapper)
})
.inspect_err(|err| log::error!("Failed to call cx.update while parsing shortcuts: {}", err))
.ok()
else {
return;
};
let prompt = {
let prompt = cx.prompt(
gpui::PromptLevel::Warning,
"Importing settings may overwrite your existing settings",
None,
&["Ok", "Cancel"],
);
cx.spawn(async move |_| prompt.await.ok())
};
if prompt.await != Some(0) {
return;
}
cx.background_executor()
.spawn(async move {
let keymap_file = paths::keymap_file().as_path();
if fs.is_file(keymap_file).await {
if let Some(resolved_path) = fs
.canonicalize(keymap_file)
.await
.inspect_err(|err| {
log::error!(
"Failed to canonicalize uer keymap path {:?}, error: {}",
keymap_file,
err
)
})
.ok()
{
fs.atomic_write(resolved_path.clone(), new_content)
.await
.inspect_err(|err| {
log::error!(
"Failed to write user keymap file {:?}, error: {}",
resolved_path,
err
)
})
.ok();
}
} else {
fs.atomic_write(keymap_file.to_path_buf(), new_content)
.await
.inspect_err(|err| {
log::error!(
"Failed to write user keymap file {:?}, error: {}",
keymap_file,
err
)
})
.ok();
}
})
.detach();
}
pub struct SettingsPage {
focus_handle: FocusHandle,
}

View File

@@ -262,18 +262,19 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
#[cfg(test)]
mod test {
use gpui::Modifiers;
use gpui::{Modifiers, TestKeyboardMapper};
use super::*;
#[test]
fn test_scroll_keys() {
let mapper = TestKeyboardMapper::new();
//These keys should be handled by the scrolling element directly
//Need to signify this by returning 'None'
let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
let shift_home = Keystroke::parse("shift-home").unwrap();
let shift_end = Keystroke::parse("shift-end").unwrap();
let shift_pageup = Keystroke::parse("shift-pageup", &mapper).unwrap();
let shift_pagedown = Keystroke::parse("shift-pagedown", &mapper).unwrap();
let shift_home = Keystroke::parse("shift-home", &mapper).unwrap();
let shift_end = Keystroke::parse("shift-end", &mapper).unwrap();
let none = TermMode::NONE;
assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
@@ -299,8 +300,8 @@ mod test {
Some("\x1b[1;2F".into())
);
let pageup = Keystroke::parse("pageup").unwrap();
let pagedown = Keystroke::parse("pagedown").unwrap();
let pageup = Keystroke::parse("pageup", &mapper).unwrap();
let pagedown = Keystroke::parse("pagedown", &mapper).unwrap();
let any = TermMode::ANY;
assert_eq!(to_esc_str(&pageup, &any, false), Some("\x1b[5~".into()));
@@ -325,13 +326,14 @@ mod test {
#[test]
fn test_application_mode() {
let mapper = TestKeyboardMapper::new();
let app_cursor = TermMode::APP_CURSOR;
let none = TermMode::NONE;
let up = Keystroke::parse("up").unwrap();
let down = Keystroke::parse("down").unwrap();
let left = Keystroke::parse("left").unwrap();
let right = Keystroke::parse("right").unwrap();
let up = Keystroke::parse("up", &mapper).unwrap();
let down = Keystroke::parse("down", &mapper).unwrap();
let left = Keystroke::parse("left", &mapper).unwrap();
let right = Keystroke::parse("right", &mapper).unwrap();
assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into()));
assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into()));
@@ -349,6 +351,7 @@ mod test {
#[test]
fn test_ctrl_codes() {
let mapper = TestKeyboardMapper::new();
let letters_lower = 'a'..='z';
let letters_upper = 'A'..='Z';
let mode = TermMode::ANY;
@@ -356,12 +359,12 @@ mod test {
for (lower, upper) in letters_lower.zip(letters_upper) {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(),
&Keystroke::parse(&format!("ctrl-shift-{}", lower), &mapper).unwrap(),
&mode,
false
),
to_esc_str(
&Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(),
&Keystroke::parse(&format!("ctrl-{}", upper), &mapper).unwrap(),
&mode,
false
),
@@ -374,11 +377,12 @@ mod test {
#[test]
fn alt_is_meta() {
let mapper = TestKeyboardMapper::new();
let ascii_printable = ' '..='~';
for character in ascii_printable {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("alt-{}", character)).unwrap(),
&Keystroke::parse(&format!("alt-{}", character), &mapper).unwrap(),
&TermMode::NONE,
true
)
@@ -396,7 +400,7 @@ mod test {
for key in gpui_keys {
assert_ne!(
to_esc_str(
&Keystroke::parse(&format!("alt-{}", key)).unwrap(),
&Keystroke::parse(&format!("alt-{}", key), &mapper).unwrap(),
&TermMode::NONE,
true
)
@@ -408,6 +412,7 @@ mod test {
#[test]
fn test_modifier_code_calc() {
let mapper = TestKeyboardMapper::new();
// Code Modifiers
// ---------+---------------------------
// 2 | Shift
@@ -419,15 +424,33 @@ mod test {
// 8 | Shift + Alt + Control
// ---------+---------------------------
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap()));
assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap()));
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap()));
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap()));
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap()));
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap()));
assert_eq!(
2,
modifier_code(&Keystroke::parse("shift-a", &mapper).unwrap())
);
assert_eq!(
3,
modifier_code(&Keystroke::parse("alt-a", &mapper).unwrap())
);
assert_eq!(
4,
modifier_code(&Keystroke::parse("shift-alt-a", &mapper).unwrap())
);
assert_eq!(
5,
modifier_code(&Keystroke::parse("ctrl-a", &mapper).unwrap())
);
assert_eq!(
6,
modifier_code(&Keystroke::parse("shift-ctrl-a", &mapper).unwrap())
);
assert_eq!(
7,
modifier_code(&Keystroke::parse("alt-ctrl-a", &mapper).unwrap())
);
assert_eq!(
8,
modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap())
modifier_code(&Keystroke::parse("shift-ctrl-alt-a", &mapper).unwrap())
);
}
}

View File

@@ -9,9 +9,9 @@ use assistant_slash_command::SlashCommandRegistry;
use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
use gpui::{
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent,
Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
impl_actions,
KeyDownEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, Pixels, Render,
ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored,
deferred, div, impl_actions,
};
use itertools::Itertools;
use persistence::TERMINAL_DB;
@@ -393,11 +393,17 @@ impl TerminalView {
.mode
.contains(TermMode::ALT_SCREEN)
{
let keystroke = Keystroke {
modifiers: Modifiers {
control: true,
platform: true,
..Default::default()
},
key: "space".to_string(),
key_char: None,
};
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&Keystroke::parse("ctrl-cmd-space").unwrap(),
TerminalSettings::get_global(cx).option_as_meta,
)
term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta)
});
} else {
window.show_character_palette();
@@ -671,7 +677,7 @@ impl TerminalView {
}
fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context<Self>) {
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
if let Some(keystroke) = Keystroke::parse(&text.0, cx.keyboard_mapper()).log_err() {
self.clear_bell(cx);
self.terminal.update(cx, |term, cx| {
let processed =

View File

@@ -533,13 +533,16 @@ impl Component for KeyBinding {
#[cfg(test)]
mod tests {
use gpui::TestKeyboardMapper;
use super::*;
#[test]
fn test_text_for_keystroke() {
let mapper = TestKeyboardMapper::new();
assert_eq!(
keystroke_text(
&Keystroke::parse("cmd-c").unwrap(),
&Keystroke::parse("cmd-c", &mapper).unwrap(),
PlatformStyle::Mac,
false
),
@@ -547,7 +550,7 @@ mod tests {
);
assert_eq!(
keystroke_text(
&Keystroke::parse("cmd-c").unwrap(),
&Keystroke::parse("cmd-c", &mapper).unwrap(),
PlatformStyle::Linux,
false
),
@@ -555,7 +558,7 @@ mod tests {
);
assert_eq!(
keystroke_text(
&Keystroke::parse("cmd-c").unwrap(),
&Keystroke::parse("cmd-c", &mapper).unwrap(),
PlatformStyle::Windows,
false
),
@@ -564,7 +567,7 @@ mod tests {
assert_eq!(
keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
&Keystroke::parse("ctrl-alt-delete", &mapper).unwrap(),
PlatformStyle::Mac,
false
),
@@ -572,7 +575,7 @@ mod tests {
);
assert_eq!(
keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
&Keystroke::parse("ctrl-alt-delete", &mapper).unwrap(),
PlatformStyle::Linux,
false
),
@@ -580,7 +583,7 @@ mod tests {
);
assert_eq!(
keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
&Keystroke::parse("ctrl-alt-delete", &mapper).unwrap(),
PlatformStyle::Windows,
false
),
@@ -589,7 +592,7 @@ mod tests {
assert_eq!(
keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(),
&Keystroke::parse("shift-pageup", &mapper).unwrap(),
PlatformStyle::Mac,
false
),
@@ -597,7 +600,7 @@ mod tests {
);
assert_eq!(
keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(),
&Keystroke::parse("shift-pageup", &mapper).unwrap(),
PlatformStyle::Linux,
false,
),
@@ -605,7 +608,7 @@ mod tests {
);
assert_eq!(
keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(),
&Keystroke::parse("shift-pageup", &mapper).unwrap(),
PlatformStyle::Windows,
false
),

View File

@@ -67,7 +67,7 @@ impl Vim {
fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) {
if let Some(Operator::Literal { prefix }) = self.active_operator() {
if let Some(prefix) = prefix {
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
if let Some(keystroke) = Keystroke::parse(&action.0, cx.keyboard_mapper()).ok() {
window.defer(cx, |window, cx| {
window.dispatch_keystroke(keystroke, cx);
});

View File

@@ -196,9 +196,12 @@ impl NeovimBackedTestContext {
}
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
let keyboard_mapper = self.cx.keyboard_mapper();
for keystroke_text in keystroke_texts.split(' ') {
self.recent_keystrokes.push(keystroke_text.to_string());
self.neovim.send_keystroke(keystroke_text).await;
self.neovim
.send_keystroke(keystroke_text, keyboard_mapper.as_ref())
.await;
}
self.simulate_keystrokes(keystroke_texts);
}

View File

@@ -5,6 +5,8 @@ use std::{
ops::{Deref, DerefMut, Range},
};
use gpui::PlatformKeyboardMapper;
#[cfg(feature = "neovim")]
use async_compat::Compat;
#[cfg(feature = "neovim")]
@@ -110,8 +112,12 @@ impl NeovimConnection {
// Sends a keystroke to the neovim process.
#[cfg(feature = "neovim")]
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
pub async fn send_keystroke(
&mut self,
keystroke_text: &str,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) {
let mut keystroke = Keystroke::parse(keystroke_text, keyboard_mapper).unwrap();
if keystroke.key == "<" {
keystroke.key = "lt".to_string()
@@ -148,7 +154,11 @@ impl NeovimConnection {
}
#[cfg(not(feature = "neovim"))]
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
pub async fn send_keystroke(
&mut self,
keystroke_text: &str,
_keyboard_mapper: &dyn PlatformKeyboardMapper,
) {
if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
self.data.pop_front();
}

View File

@@ -2162,7 +2162,7 @@ impl Workspace {
let mut keystrokes: Vec<Keystroke> = action
.0
.split(' ')
.flat_map(|k| Keystroke::parse(k).log_err())
.flat_map(|k| Keystroke::parse(k, cx.keyboard_mapper()).log_err())
.collect();
keystrokes.reverse();