Watch ~/.zed/bindings.json file for custom key bindings

Co-authored-by: Keith Simmons <keith@zed.dev>
This commit is contained in:
Max Brunsfeld 2022-04-11 16:50:44 -07:00
parent 92a5c30389
commit be11f63f1e
8 changed files with 110 additions and 60 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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(())
} }

View file

@ -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,

View file

@ -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);

View file

@ -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

View file

@ -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),

View file

@ -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(