Compare commits
112 Commits
v0.204.2
...
vscode-sho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d103a3ecb2 | ||
|
|
c921212c59 | ||
|
|
5bf3f5be50 | ||
|
|
10ce1111f2 | ||
|
|
3c84069df2 | ||
|
|
ea35b55ff5 | ||
|
|
33bb07125f | ||
|
|
109471a768 | ||
|
|
78e5134b7d | ||
|
|
6211ad1e61 | ||
|
|
9d203ba0d8 | ||
|
|
2a1ec794eb | ||
|
|
44e9f7f0a0 | ||
|
|
1002a04583 | ||
|
|
1bd09837c8 | ||
|
|
616f932145 | ||
|
|
c8d1de87d6 | ||
|
|
17f35e88ac | ||
|
|
b96b6cf5ef | ||
|
|
2714fc6d5b | ||
|
|
6064799099 | ||
|
|
fa54137c05 | ||
|
|
2d531fab6a | ||
|
|
f4a24167fd | ||
|
|
4e46aa5057 | ||
|
|
b23efbf62f | ||
|
|
e54780b2da | ||
|
|
e3bd1a0e41 | ||
|
|
2b0b9b337d | ||
|
|
c65c6f20e1 | ||
|
|
5e4959728e | ||
|
|
cb893ccd84 | ||
|
|
d37b7f5d21 | ||
|
|
7c9c2363ab | ||
|
|
c00ce9f327 | ||
|
|
a1fb04b49f | ||
|
|
bd4557503f | ||
|
|
8af8e69870 | ||
|
|
91d56b815a | ||
|
|
7f06102e7e | ||
|
|
ba6dc2040e | ||
|
|
0f8327f662 | ||
|
|
7a84135aac | ||
|
|
1393fefb8f | ||
|
|
8f74bdd091 | ||
|
|
010f345963 | ||
|
|
4bbc56d7bd | ||
|
|
e5c6ef8361 | ||
|
|
7f11dea9f2 | ||
|
|
36fc38cf87 | ||
|
|
7d1f5d4718 | ||
|
|
3027225767 | ||
|
|
0c6f50032b | ||
|
|
b26332f514 | ||
|
|
1c5fd70a04 | ||
|
|
451e7bed60 | ||
|
|
854f55b4d8 | ||
|
|
c8b6020804 | ||
|
|
622f6ffe2d | ||
|
|
49a85e59ac | ||
|
|
43f0e6dedd | ||
|
|
091d2cfcb3 | ||
|
|
de217bef18 | ||
|
|
991922ce54 | ||
|
|
68ca64a310 | ||
|
|
8ef6c77934 | ||
|
|
13811202cb | ||
|
|
595be0135c | ||
|
|
2f1f231f0b | ||
|
|
4de395f933 | ||
|
|
f34e6f127d | ||
|
|
90bbc49b0c | ||
|
|
76ffcb3c77 | ||
|
|
6a929b7dc5 | ||
|
|
94c78851c3 | ||
|
|
23f68c9ffc | ||
|
|
1ddb4126c8 | ||
|
|
92440054ce | ||
|
|
493c3a6084 | ||
|
|
16c92944df | ||
|
|
8b569d7dc2 | ||
|
|
8a7ab5c3d4 | ||
|
|
fc1947b97c | ||
|
|
6fc96e6b7f | ||
|
|
fb89852586 | ||
|
|
2c04d7d118 | ||
|
|
424ce07c35 | ||
|
|
2489e4cea4 | ||
|
|
eaa1821673 | ||
|
|
b11688a5e5 | ||
|
|
8510cea452 | ||
|
|
77ce5a7a7c | ||
|
|
9a6c7d5c41 | ||
|
|
126ba040e8 | ||
|
|
d49f75ab3f | ||
|
|
2fbb5e5db4 | ||
|
|
f4c0d52530 | ||
|
|
1d7b61bdd0 | ||
|
|
b4b0c58822 | ||
|
|
932d776efd | ||
|
|
b0ae7e16f6 | ||
|
|
b44e7a82c1 | ||
|
|
9730a36dd2 | ||
|
|
3a7d186726 | ||
|
|
567af9455d | ||
|
|
8537b72597 | ||
|
|
b60f185bcf | ||
|
|
46b17ef148 | ||
|
|
8f7d5ecf81 | ||
|
|
fec7620eee | ||
|
|
97319c4fe3 | ||
|
|
da422e7dcd |
13
.github/actions/run_tests/action.yml
vendored
13
.github/actions/run_tests/action.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -295,6 +295,8 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
with:
|
||||
use-xvfb: true
|
||||
|
||||
- name: Build other binaries and features
|
||||
run: |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, _, _| {
|
||||
|
||||
@@ -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()];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
464
crates/gpui/src/platform/keycodes.rs
Normal file
464
crates/gpui/src/platform/keycodes.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user