Closes #16343
Closes #10972
Release Notes:
- (breaking change) On macOS when using a keyboard that supports an
extended Latin character set (e.g. French, German, ...) keyboard
shortcuts are automatically updated so that they can be typed without
`option`. This fixes several long-standing problems where some keyboards
could not type some shortcuts.
- This mapping works the same way as
[macOS](https://developer.apple.com/documentation/swiftui/view/keyboardshortcut(_:modifiers:localization:)).
For example on a German keyboard shortcuts like `cmd->` become `cmd-:`,
`cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`. This mapping happens at
the time keyboard layout files are read so the keybindings are visible
in the command palette. To opt out of this behavior for your custom
keyboard shortcuts, set `"use_layout_keys": true` in your binding
section. For the mappings used for each layout [see
here](a890df1863/crates/settings/src/key_equivalents.rs (L7)).
---------
Co-authored-by: Will <will@zed.dev>
211 lines
7.0 KiB
Rust
211 lines
7.0 KiB
Rust
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
|
use anyhow::{anyhow, Context, Result};
|
|
use collections::BTreeMap;
|
|
use gpui::{Action, AppContext, KeyBinding, SharedString};
|
|
use schemars::{
|
|
gen::{SchemaGenerator, SchemaSettings},
|
|
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
|
JsonSchema,
|
|
};
|
|
use serde::Deserialize;
|
|
use serde_json::Value;
|
|
use util::{asset_str, ResultExt};
|
|
|
|
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
|
#[serde(transparent)]
|
|
pub struct KeymapFile(Vec<KeymapBlock>);
|
|
|
|
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
|
pub struct KeymapBlock {
|
|
#[serde(default)]
|
|
context: Option<String>,
|
|
#[serde(default)]
|
|
use_layout_keys: Option<bool>,
|
|
bindings: BTreeMap<String, KeymapAction>,
|
|
}
|
|
|
|
impl KeymapBlock {
|
|
pub fn context(&self) -> Option<&str> {
|
|
self.context.as_deref()
|
|
}
|
|
|
|
pub fn bindings(&self) -> &BTreeMap<String, KeymapAction> {
|
|
&self.bindings
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Default, Clone)]
|
|
#[serde(transparent)]
|
|
pub struct KeymapAction(Value);
|
|
|
|
impl std::fmt::Display for KeymapAction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match &self.0 {
|
|
Value::String(s) => write!(f, "{}", s),
|
|
Value::Array(arr) => {
|
|
let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
|
|
write!(f, "{}", strings.join(", "))
|
|
}
|
|
_ => write!(f, "{}", self.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl JsonSchema for KeymapAction {
|
|
fn schema_name() -> String {
|
|
"KeymapAction".into()
|
|
}
|
|
|
|
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
|
Schema::Bool(true)
|
|
}
|
|
}
|
|
|
|
impl KeymapFile {
|
|
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
|
|
let content = asset_str::<SettingsAssets>(asset_path);
|
|
|
|
Self::parse(content.as_ref())?.add_to_cx(cx)
|
|
}
|
|
|
|
pub fn parse(content: &str) -> Result<Self> {
|
|
if content.is_empty() {
|
|
return Ok(Self::default());
|
|
}
|
|
parse_json_with_comments::<Self>(content)
|
|
}
|
|
|
|
pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
|
|
let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
|
|
|
|
for KeymapBlock {
|
|
context,
|
|
use_layout_keys,
|
|
bindings,
|
|
} in self.0
|
|
{
|
|
let bindings = bindings
|
|
.into_iter()
|
|
.filter_map(|(keystroke, action)| {
|
|
let action = action.0;
|
|
|
|
// This is a workaround for a limitation in serde: serde-rs/json#497
|
|
// We want to deserialize the action data as a `RawValue` so that we can
|
|
// deserialize the action itself dynamically directly from the JSON
|
|
// string. But `RawValue` currently does not work inside of an untagged enum.
|
|
match action {
|
|
Value::Array(items) => {
|
|
let Ok([name, data]): Result<[serde_json::Value; 2], _> =
|
|
items.try_into()
|
|
else {
|
|
return Some(Err(anyhow!("Expected array of length 2")));
|
|
};
|
|
let serde_json::Value::String(name) = name else {
|
|
return Some(Err(anyhow!(
|
|
"Expected first item in array to be a string."
|
|
)));
|
|
};
|
|
cx.build_action(&name, Some(data))
|
|
}
|
|
Value::String(name) => cx.build_action(&name, None),
|
|
Value::Null => Ok(no_action()),
|
|
_ => {
|
|
return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
|
|
}
|
|
}
|
|
.with_context(|| {
|
|
format!(
|
|
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
|
)
|
|
})
|
|
.log_err()
|
|
.map(|action| {
|
|
KeyBinding::load(
|
|
&keystroke,
|
|
action,
|
|
context.as_deref(),
|
|
if use_layout_keys.unwrap_or_default() {
|
|
None
|
|
} else {
|
|
key_equivalents.as_ref()
|
|
},
|
|
)
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
cx.bind_keys(bindings);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn generate_json_schema(action_names: &[SharedString]) -> serde_json::Value {
|
|
let mut root_schema = SchemaSettings::draft07()
|
|
.with(|settings| settings.option_add_null_type = false)
|
|
.into_generator()
|
|
.into_root_schema_for::<KeymapFile>();
|
|
|
|
let action_schema = Schema::Object(SchemaObject {
|
|
subschemas: Some(Box::new(SubschemaValidation {
|
|
one_of: Some(vec![
|
|
Schema::Object(SchemaObject {
|
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
|
enum_values: Some(
|
|
action_names
|
|
.iter()
|
|
.map(|name| Value::String(name.to_string()))
|
|
.collect(),
|
|
),
|
|
..Default::default()
|
|
}),
|
|
Schema::Object(SchemaObject {
|
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
|
..Default::default()
|
|
}),
|
|
Schema::Object(SchemaObject {
|
|
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
|
|
..Default::default()
|
|
}),
|
|
]),
|
|
..Default::default()
|
|
})),
|
|
..Default::default()
|
|
});
|
|
|
|
root_schema
|
|
.definitions
|
|
.insert("KeymapAction".to_owned(), action_schema);
|
|
|
|
serde_json::to_value(root_schema).unwrap()
|
|
}
|
|
|
|
pub fn blocks(&self) -> &[KeymapBlock] {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
fn no_action() -> Box<dyn gpui::Action> {
|
|
gpui::NoAction.boxed_clone()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::KeymapFile;
|
|
|
|
#[test]
|
|
fn can_deserialize_keymap_with_trailing_comma() {
|
|
let json = indoc::indoc! {"[
|
|
// Standard macOS bindings
|
|
{
|
|
\"bindings\": {
|
|
\"up\": \"menu::SelectPrev\",
|
|
},
|
|
},
|
|
]
|
|
"
|
|
|
|
};
|
|
KeymapFile::parse(json).unwrap();
|
|
}
|
|
}
|