Watch ~/.zed/bindings.json file for custom key bindings
Co-authored-by: Keith Simmons <keith@zed.dev>
This commit is contained in:
parent
92a5c30389
commit
be11f63f1e
8 changed files with 110 additions and 60 deletions
|
@ -1358,6 +1358,10 @@ impl MutableAppContext {
|
||||||
self.keystroke_matcher.add_bindings(bindings);
|
self.keystroke_matcher.add_bindings(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_bindings(&mut self) {
|
||||||
|
self.keystroke_matcher.clear_bindings();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch_keystroke(
|
pub fn dispatch_keystroke(
|
||||||
&mut self,
|
&mut self,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
|
|
|
@ -106,6 +106,11 @@ impl Matcher {
|
||||||
self.keymap.add_bindings(bindings);
|
self.keymap.add_bindings(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_bindings(&mut self) {
|
||||||
|
self.pending.clear();
|
||||||
|
self.keymap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_pending(&mut self) {
|
pub fn clear_pending(&mut self) {
|
||||||
self.pending.clear();
|
self.pending.clear();
|
||||||
}
|
}
|
||||||
|
@ -164,6 +169,10 @@ impl Keymap {
|
||||||
fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
||||||
self.0.extend(bindings.into_iter());
|
self.0.extend(bindings.into_iter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.0.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Binding {
|
impl Binding {
|
||||||
|
|
|
@ -5,50 +5,57 @@ use gpui::{keymap::Binding, MutableAppContext};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, Clone)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct KeyMapFile(BTreeMap<String, ActionsByKeystroke>);
|
||||||
|
|
||||||
|
type ActionsByKeystroke = BTreeMap<String, Box<RawValue>>;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ActionWithData<'a>(#[serde(borrow)] &'a str, #[serde(borrow)] &'a RawValue);
|
struct ActionWithData<'a>(#[serde(borrow)] &'a str, #[serde(borrow)] &'a RawValue);
|
||||||
type ActionSetsByContext<'a> = BTreeMap<&'a str, ActionsByKeystroke<'a>>;
|
|
||||||
type ActionsByKeystroke<'a> = BTreeMap<&'a str, &'a RawValue>;
|
|
||||||
|
|
||||||
pub fn load_built_in_keymaps(cx: &mut MutableAppContext) {
|
impl KeyMapFile {
|
||||||
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
pub fn load_defaults(cx: &mut MutableAppContext) {
|
||||||
load_keymap(
|
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
||||||
cx,
|
Self::load(path, cx).unwrap();
|
||||||
std::str::from_utf8(Assets::get(path).unwrap().data.as_ref()).unwrap(),
|
}
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_keymap(cx: &mut MutableAppContext, content: &str) -> Result<()> {
|
pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> {
|
||||||
let actions: ActionSetsByContext = serde_json::from_str(content)?;
|
let content = Assets::get(asset_path).unwrap().data;
|
||||||
for (context, actions) in actions {
|
let content_str = std::str::from_utf8(content.as_ref()).unwrap();
|
||||||
let context = if context.is_empty() {
|
Ok(serde_json::from_str::<Self>(content_str)?.add(cx)?)
|
||||||
None
|
}
|
||||||
} else {
|
|
||||||
Some(context)
|
pub fn add(self, cx: &mut MutableAppContext) -> Result<()> {
|
||||||
};
|
for (context, actions) in self.0 {
|
||||||
cx.add_bindings(
|
let context = if context.is_empty() {
|
||||||
actions
|
None
|
||||||
.into_iter()
|
} else {
|
||||||
.map(|(keystroke, action)| {
|
Some(context)
|
||||||
let action = action.get();
|
};
|
||||||
let action = if action.starts_with('[') {
|
cx.add_bindings(
|
||||||
let ActionWithData(name, data) = serde_json::from_str(action)?;
|
actions
|
||||||
cx.deserialize_action(name, Some(data.get()))
|
.into_iter()
|
||||||
} else {
|
.map(|(keystroke, action)| {
|
||||||
let name = serde_json::from_str(action)?;
|
let action = action.get();
|
||||||
cx.deserialize_action(name, None)
|
let action = if action.starts_with('[') {
|
||||||
}
|
let ActionWithData(name, data) = serde_json::from_str(action)?;
|
||||||
.with_context(|| {
|
cx.deserialize_action(name, Some(data.get()))
|
||||||
format!(
|
} else {
|
||||||
|
let name = serde_json::from_str(action)?;
|
||||||
|
cx.deserialize_action(name, None)
|
||||||
|
}
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
Binding::load(keystroke, action, context)
|
Binding::load(&keystroke, action, context.as_deref())
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?,
|
.collect::<Result<Vec<_>>>()?,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
pub mod keymap_file;
|
mod keymap_file;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::font_cache::{FamilyId, FontCache};
|
use gpui::font_cache::{FamilyId, FontCache};
|
||||||
|
@ -15,6 +15,8 @@ use std::{collections::HashMap, sync::Arc};
|
||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
pub use keymap_file::KeyMapFile;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub buffer_font_family: FamilyId,
|
pub buffer_font_family: FamilyId,
|
||||||
|
|
|
@ -24,7 +24,7 @@ impl<'a> VimTestContext<'a> {
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
|
|
||||||
settings::keymap_file::load_built_in_keymaps(cx);
|
settings::KeyMapFile::load("keymaps/vim.json", cx).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let params = cx.update(WorkspaceParams::test);
|
let params = cx.update(WorkspaceParams::test);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use assets::Assets;
|
||||||
use client::{self, http, ChannelList, UserStore};
|
use client::{self, http, ChannelList, UserStore};
|
||||||
use fs::OpenOptions;
|
use fs::OpenOptions;
|
||||||
use futures::{channel::oneshot, StreamExt};
|
use futures::{channel::oneshot, StreamExt};
|
||||||
|
@ -9,19 +10,17 @@ use gpui::{App, AssetSource, Task};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
use settings::{self, Settings};
|
use settings::{self, KeyMapFile, Settings, SettingsFileContent};
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
use std::{env, fs, path::PathBuf, sync::Arc};
|
use std::{env, fs, path::PathBuf, sync::Arc};
|
||||||
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{self, AppState, OpenNew, OpenPaths};
|
use workspace::{self, AppState, OpenNew, OpenPaths};
|
||||||
use assets::Assets;
|
|
||||||
use zed::{
|
use zed::{
|
||||||
self,
|
self, build_window_options, build_workspace,
|
||||||
build_window_options, build_workspace,
|
|
||||||
fs::RealFs,
|
fs::RealFs,
|
||||||
languages, menus,
|
languages, menus,
|
||||||
settings_file::{settings_from_files, SettingsFile},
|
settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -63,7 +62,8 @@ fn main() {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let settings_file = load_settings_file(&app, fs.clone());
|
|
||||||
|
let config_files = load_config_files(&app, fs.clone());
|
||||||
|
|
||||||
let login_shell_env_loaded = if stdout_is_a_pty() {
|
let login_shell_env_loaded = if stdout_is_a_pty() {
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
|
@ -112,13 +112,16 @@ fn main() {
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
let settings_file = cx.background().block(settings_file).unwrap();
|
let (settings_file, bindings_file) = cx.background().block(config_files).unwrap();
|
||||||
let mut settings_rx = settings_from_files(
|
let mut settings_rx = settings_from_files(
|
||||||
default_settings,
|
default_settings,
|
||||||
vec![settings_file],
|
vec![settings_file],
|
||||||
themes.clone(),
|
themes.clone(),
|
||||||
cx.font_cache().clone(),
|
cx.font_cache().clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cx.spawn(|cx| watch_keymap_file(bindings_file, cx)).detach();
|
||||||
|
|
||||||
let settings = cx.background().block(settings_rx.next()).unwrap();
|
let settings = cx.background().block(settings_rx.next()).unwrap();
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
while let Some(settings) = settings_rx.next().await {
|
while let Some(settings) = settings_rx.next().await {
|
||||||
|
@ -254,14 +257,23 @@ fn load_embedded_fonts(app: &App) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_settings_file(app: &App, fs: Arc<dyn Fs>) -> oneshot::Receiver<SettingsFile> {
|
fn load_config_files(
|
||||||
|
app: &App,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
) -> oneshot::Receiver<(
|
||||||
|
WatchedJsonFile<SettingsFileContent>,
|
||||||
|
WatchedJsonFile<KeyMapFile>,
|
||||||
|
)> {
|
||||||
let executor = app.background();
|
let executor = app.background();
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
executor
|
executor
|
||||||
.clone()
|
.clone()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let file = SettingsFile::new(fs, &executor, zed::SETTINGS_PATH.clone()).await;
|
let settings_file =
|
||||||
tx.send(file).ok()
|
WatchedJsonFile::new(fs.clone(), &executor, zed::SETTINGS_PATH.clone()).await;
|
||||||
|
let bindings_file =
|
||||||
|
WatchedJsonFile::new(fs, &executor, zed::BINDINGS_PATH.clone()).await;
|
||||||
|
tx.send((settings_file, bindings_file)).ok()
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
rx
|
rx
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
use futures::{stream, StreamExt};
|
use futures::{stream, StreamExt};
|
||||||
use gpui::{executor, FontCache};
|
use gpui::{executor, AsyncAppContext, FontCache};
|
||||||
use postage::sink::Sink as _;
|
use postage::sink::Sink as _;
|
||||||
use postage::{prelude::Stream, watch};
|
use postage::{prelude::Stream, watch};
|
||||||
use project::Fs;
|
use project::Fs;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use settings::KeyMapFile;
|
||||||
use settings::{Settings, SettingsFileContent};
|
use settings::{Settings, SettingsFileContent};
|
||||||
use std::{path::Path, sync::Arc, time::Duration};
|
use std::{path::Path, sync::Arc, time::Duration};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
|
pub struct WatchedJsonFile<T>(watch::Receiver<T>);
|
||||||
|
|
||||||
impl SettingsFile {
|
impl<T> WatchedJsonFile<T>
|
||||||
|
where
|
||||||
|
T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
|
||||||
|
{
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
executor: &executor::Background,
|
executor: &executor::Background,
|
||||||
|
@ -35,21 +40,21 @@ impl SettingsFile {
|
||||||
Self(rx)
|
Self(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
|
async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
|
||||||
if fs.is_file(&path).await {
|
if fs.is_file(&path).await {
|
||||||
fs.load(&path)
|
fs.load(&path)
|
||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
.and_then(|data| serde_json::from_str(&data).log_err())
|
.and_then(|data| serde_json::from_str(&data).log_err())
|
||||||
} else {
|
} else {
|
||||||
Some(SettingsFileContent::default())
|
Some(T::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn settings_from_files(
|
pub fn settings_from_files(
|
||||||
defaults: Settings,
|
defaults: Settings,
|
||||||
sources: Vec<SettingsFile>,
|
sources: Vec<WatchedJsonFile<SettingsFileContent>>,
|
||||||
theme_registry: Arc<ThemeRegistry>,
|
theme_registry: Arc<ThemeRegistry>,
|
||||||
font_cache: Arc<FontCache>,
|
font_cache: Arc<FontCache>,
|
||||||
) -> impl futures::stream::Stream<Item = Settings> {
|
) -> impl futures::stream::Stream<Item = Settings> {
|
||||||
|
@ -72,6 +77,16 @@ pub fn settings_from_files(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn watch_keymap_file(mut file: WatchedJsonFile<KeyMapFile>, mut cx: AsyncAppContext) {
|
||||||
|
while let Some(content) = file.0.recv().await {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.clear_bindings();
|
||||||
|
settings::KeyMapFile::load_defaults(cx);
|
||||||
|
content.add(cx).log_err();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -102,9 +117,9 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
|
let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
|
||||||
let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
|
let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
|
||||||
let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
|
let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
|
||||||
|
|
||||||
let mut settings_rx = settings_from_files(
|
let mut settings_rx = settings_from_files(
|
||||||
cx.read(Settings::test),
|
cx.read(Settings::test),
|
||||||
|
|
|
@ -45,6 +45,7 @@ lazy_static! {
|
||||||
.expect("failed to determine home directory")
|
.expect("failed to determine home directory")
|
||||||
.join(".zed");
|
.join(".zed");
|
||||||
pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
|
pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
|
||||||
|
pub static ref BINDINGS_PATH: PathBuf = ROOT_PATH.join("bindings.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
|
@ -102,7 +103,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
|
|
||||||
workspace::lsp_status::init(cx);
|
workspace::lsp_status::init(cx);
|
||||||
|
|
||||||
settings::keymap_file::load_built_in_keymaps(cx);
|
settings::KeyMapFile::load_defaults(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_workspace(
|
pub fn build_workspace(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue