diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 962343a28a..f0d33252e4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1358,6 +1358,10 @@ impl MutableAppContext { self.keystroke_matcher.add_bindings(bindings); } + pub fn clear_bindings(&mut self) { + self.keystroke_matcher.clear_bindings(); + } + pub fn dispatch_keystroke( &mut self, window_id: usize, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 1b9e920ff2..723403f160 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -106,6 +106,11 @@ impl Matcher { self.keymap.add_bindings(bindings); } + pub fn clear_bindings(&mut self) { + self.pending.clear(); + self.keymap.clear(); + } + pub fn clear_pending(&mut self) { self.pending.clear(); } @@ -164,6 +169,10 @@ impl Keymap { fn add_bindings>(&mut self, bindings: T) { self.0.extend(bindings.into_iter()); } + + fn clear(&mut self) { + self.0.clear(); + } } impl Binding { diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 621310c220..9ecebfd5d2 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -5,50 +5,57 @@ use gpui::{keymap::Binding, MutableAppContext}; use serde::Deserialize; use serde_json::value::RawValue; +#[derive(Deserialize, Default, Clone)] +#[serde(transparent)] +pub struct KeyMapFile(BTreeMap); + +type ActionsByKeystroke = BTreeMap>; + #[derive(Deserialize)] 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) { - for path in ["keymaps/default.json", "keymaps/vim.json"] { - load_keymap( - cx, - std::str::from_utf8(Assets::get(path).unwrap().data.as_ref()).unwrap(), - ) - .unwrap(); +impl KeyMapFile { + pub fn load_defaults(cx: &mut MutableAppContext) { + for path in ["keymaps/default.json", "keymaps/vim.json"] { + Self::load(path, cx).unwrap(); + } } -} -pub fn load_keymap(cx: &mut MutableAppContext, content: &str) -> Result<()> { - let actions: ActionSetsByContext = serde_json::from_str(content)?; - for (context, actions) in actions { - let context = if context.is_empty() { - None - } else { - Some(context) - }; - cx.add_bindings( - actions - .into_iter() - .map(|(keystroke, action)| { - let action = action.get(); - let action = if action.starts_with('[') { - let ActionWithData(name, data) = serde_json::from_str(action)?; - cx.deserialize_action(name, Some(data.get())) - } else { - let name = serde_json::from_str(action)?; - cx.deserialize_action(name, None) - } - .with_context(|| { - format!( + pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> { + let content = Assets::get(asset_path).unwrap().data; + let content_str = std::str::from_utf8(content.as_ref()).unwrap(); + Ok(serde_json::from_str::(content_str)?.add(cx)?) + } + + pub fn add(self, cx: &mut MutableAppContext) -> Result<()> { + for (context, actions) in self.0 { + let context = if context.is_empty() { + None + } else { + Some(context) + }; + cx.add_bindings( + actions + .into_iter() + .map(|(keystroke, action)| { + let action = action.get(); + let action = if action.starts_with('[') { + let ActionWithData(name, data) = serde_json::from_str(action)?; + cx.deserialize_action(name, Some(data.get())) + } else { + let name = serde_json::from_str(action)?; + cx.deserialize_action(name, None) + } + .with_context(|| { + format!( "invalid binding value for keystroke {keystroke}, context {context:?}" ) - })?; - Binding::load(keystroke, action, context) - }) - .collect::>>()?, - ) + })?; + Binding::load(&keystroke, action, context.as_deref()) + }) + .collect::>>()?, + ) + } + Ok(()) } - Ok(()) } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index d9a86037a2..eb4120bb52 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,4 +1,4 @@ -pub mod keymap_file; +mod keymap_file; use anyhow::Result; use gpui::font_cache::{FamilyId, FontCache}; @@ -15,6 +15,8 @@ use std::{collections::HashMap, sync::Arc}; use theme::{Theme, ThemeRegistry}; use util::ResultExt as _; +pub use keymap_file::KeyMapFile; + #[derive(Clone)] pub struct Settings { pub buffer_font_family: FamilyId, diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 5818bdad65..2e9b0f60aa 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -24,7 +24,7 @@ impl<'a> VimTestContext<'a> { editor::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); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 112de01898..d7b43bd721 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -2,6 +2,7 @@ #![allow(non_snake_case)] use anyhow::{anyhow, Context, Result}; +use assets::Assets; use client::{self, http, ChannelList, UserStore}; use fs::OpenOptions; use futures::{channel::oneshot, StreamExt}; @@ -9,19 +10,17 @@ use gpui::{App, AssetSource, Task}; use log::LevelFilter; use parking_lot::Mutex; use project::Fs; -use settings::{self, Settings}; +use settings::{self, KeyMapFile, Settings, SettingsFileContent}; use smol::process::Command; use std::{env, fs, path::PathBuf, sync::Arc}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use util::ResultExt; use workspace::{self, AppState, OpenNew, OpenPaths}; -use assets::Assets; use zed::{ - self, - build_window_options, build_workspace, + self, build_window_options, build_workspace, fs::RealFs, languages, menus, - settings_file::{settings_from_files, SettingsFile}, + settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile}, }; fn main() { @@ -63,7 +62,8 @@ fn main() { ..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() { Task::ready(()) @@ -112,13 +112,16 @@ fn main() { }) .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( default_settings, vec![settings_file], themes.clone(), cx.font_cache().clone(), ); + + cx.spawn(|cx| watch_keymap_file(bindings_file, cx)).detach(); + let settings = cx.background().block(settings_rx.next()).unwrap(); cx.spawn(|mut cx| async move { while let Some(settings) = settings_rx.next().await { @@ -254,14 +257,23 @@ fn load_embedded_fonts(app: &App) { .unwrap(); } -fn load_settings_file(app: &App, fs: Arc) -> oneshot::Receiver { +fn load_config_files( + app: &App, + fs: Arc, +) -> oneshot::Receiver<( + WatchedJsonFile, + WatchedJsonFile, +)> { let executor = app.background(); let (tx, rx) = oneshot::channel(); executor .clone() .spawn(async move { - let file = SettingsFile::new(fs, &executor, zed::SETTINGS_PATH.clone()).await; - tx.send(file).ok() + let settings_file = + 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(); rx diff --git a/crates/zed/src/settings_file.rs b/crates/zed/src/settings_file.rs index 46a3d41eb1..024a49447f 100644 --- a/crates/zed/src/settings_file.rs +++ b/crates/zed/src/settings_file.rs @@ -1,17 +1,22 @@ use futures::{stream, StreamExt}; -use gpui::{executor, FontCache}; +use gpui::{executor, AsyncAppContext, FontCache}; use postage::sink::Sink as _; use postage::{prelude::Stream, watch}; use project::Fs; +use serde::Deserialize; +use settings::KeyMapFile; use settings::{Settings, SettingsFileContent}; use std::{path::Path, sync::Arc, time::Duration}; use theme::ThemeRegistry; use util::ResultExt; #[derive(Clone)] -pub struct SettingsFile(watch::Receiver); +pub struct WatchedJsonFile(watch::Receiver); -impl SettingsFile { +impl WatchedJsonFile +where + T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync, +{ pub async fn new( fs: Arc, executor: &executor::Background, @@ -35,21 +40,21 @@ impl SettingsFile { Self(rx) } - async fn load(fs: Arc, path: &Path) -> Option { + async fn load(fs: Arc, path: &Path) -> Option { if fs.is_file(&path).await { fs.load(&path) .await .log_err() .and_then(|data| serde_json::from_str(&data).log_err()) } else { - Some(SettingsFileContent::default()) + Some(T::default()) } } } pub fn settings_from_files( defaults: Settings, - sources: Vec, + sources: Vec>, theme_registry: Arc, font_cache: Arc, ) -> impl futures::stream::Stream { @@ -72,6 +77,16 @@ pub fn settings_from_files( }) } +pub async fn watch_keymap_file(mut file: WatchedJsonFile, 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)] mod tests { use super::*; @@ -102,9 +117,9 @@ mod tests { .await .unwrap(); - let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await; - let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await; - let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await; + let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await; + let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await; + let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await; let mut settings_rx = settings_from_files( cx.read(Settings::test), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5632c61038..6deecc9b0c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -45,6 +45,7 @@ lazy_static! { .expect("failed to determine home directory") .join(".zed"); 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, cx: &mut gpui::MutableAppContext) { @@ -102,7 +103,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { workspace::lsp_status::init(cx); - settings::keymap_file::load_built_in_keymaps(cx); + settings::KeyMapFile::load_defaults(cx); } pub fn build_workspace(