diff --git a/Cargo.lock b/Cargo.lock index c2e0790cf4..e5922f9e66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8353,6 +8353,8 @@ dependencies = [ "log", "picker", "project", + "schemars", + "serde", "settings", "theme", "theme_selector", diff --git a/assets/settings/default.json b/assets/settings/default.json index 20d07acae0..3a80a5d5af 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1,6 +1,15 @@ { // The name of the Zed theme to use for the UI "theme": "One Dark", + // The name of a base set of key bindings to use. + // This setting can take four values, each named after another + // text editor: + // + // 1. "VSCode" + // 2. "JetBrains" + // 3. "SublimeText" + // 4. "Atom" + "base_keymap": "VSCode", // Features that can be globally enabled or disabled "features": { // Show Copilot icon in status bar diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index e0b3f547f9..0b638da924 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,4 +1,4 @@ -use crate::{settings_store::parse_json_with_comments, Settings}; +use crate::settings_store::parse_json_with_comments; use anyhow::{Context, Result}; use assets::Assets; use collections::BTreeMap; @@ -41,20 +41,14 @@ impl JsonSchema for KeymapAction { struct ActionWithData(Box, Box); impl KeymapFileContent { - pub fn load_defaults(cx: &mut AppContext) { - for path in ["keymaps/default.json", "keymaps/vim.json"] { - Self::load(path, cx).unwrap(); - } - - if let Some(asset_path) = cx.global::().base_keymap.asset_path() { - Self::load(asset_path, cx).log_err(); - } - } - - pub fn load(asset_path: &str, cx: &mut AppContext) -> Result<()> { + pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { let content = Assets::get(asset_path).unwrap().data; let content_str = std::str::from_utf8(content.as_ref()).unwrap(); - parse_json_with_comments::(content_str)?.add_to_cx(cx) + Self::parse(content_str)?.add_to_cx(cx) + } + + pub fn parse(content: &str) -> Result { + parse_json_with_comments::(content) } pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 0e4cd4d922..bd1104105f 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -34,7 +34,6 @@ pub struct Settings { pub buffer_font_family: FamilyId, pub buffer_font_size: f32, pub theme: Arc, - pub base_keymap: BaseKeymap, } impl Setting for Settings { @@ -62,7 +61,6 @@ impl Setting for Settings { buffer_font_features, buffer_font_size: defaults.buffer_font_size.unwrap(), theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(), - base_keymap: Default::default(), }; for value in user_values.into_iter().copied().cloned() { @@ -111,48 +109,6 @@ impl Setting for Settings { } } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] -pub enum BaseKeymap { - #[default] - VSCode, - JetBrains, - SublimeText, - Atom, - TextMate, -} - -impl BaseKeymap { - pub const OPTIONS: [(&'static str, Self); 5] = [ - ("VSCode (Default)", Self::VSCode), - ("Atom", Self::Atom), - ("JetBrains", Self::JetBrains), - ("Sublime Text", Self::SublimeText), - ("TextMate", Self::TextMate), - ]; - - pub fn asset_path(&self) -> Option<&'static str> { - match self { - BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), - BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), - BaseKeymap::Atom => Some("keymaps/atom.json"), - BaseKeymap::TextMate => Some("keymaps/textmate.json"), - BaseKeymap::VSCode => None, - } - } - - pub fn names() -> impl Iterator { - Self::OPTIONS.iter().map(|(name, _)| *name) - } - - pub fn from_names(option: &str) -> BaseKeymap { - Self::OPTIONS - .iter() - .copied() - .find_map(|(name, value)| (name == option).then(|| value)) - .unwrap_or_default() - } -} - #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct SettingsFileContent { #[serde(default)] @@ -163,8 +119,6 @@ pub struct SettingsFileContent { pub buffer_font_features: Option, #[serde(default)] pub theme: Option, - #[serde(default)] - pub base_keymap: Option, } impl Settings { @@ -198,7 +152,6 @@ impl Settings { buffer_font_features, buffer_font_size: defaults.buffer_font_size.unwrap(), theme: themes.get(&defaults.theme.unwrap()).unwrap(), - base_keymap: Default::default(), } } @@ -234,7 +187,6 @@ impl Settings { } merge(&mut self.buffer_font_size, data.buffer_font_size); - merge(&mut self.base_keymap, data.base_keymap); } #[cfg(any(test, feature = "test-support"))] @@ -248,7 +200,6 @@ impl Settings { .unwrap(), buffer_font_size: 14., theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default), - base_keymap: Default::default(), } } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 070da5d1ea..cf1787d7c0 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,6 +1,6 @@ use crate::{ - settings_store::parse_json_with_comments, settings_store::SettingsStore, KeymapFileContent, - Setting, Settings, DEFAULT_SETTINGS_ASSET_PATH, + settings_store::parse_json_with_comments, settings_store::SettingsStore, Setting, Settings, + DEFAULT_SETTINGS_ASSET_PATH, }; use anyhow::Result; use assets::Assets; @@ -76,43 +76,6 @@ pub fn watch_config_file( rx } -pub fn handle_keymap_file_changes( - mut user_keymap_file_rx: mpsc::UnboundedReceiver, - cx: &mut AppContext, -) { - cx.spawn(move |mut cx| async move { - let mut settings_subscription = None; - while let Some(user_keymap_content) = user_keymap_file_rx.next().await { - if let Ok(keymap_content) = - parse_json_with_comments::(&user_keymap_content) - { - cx.update(|cx| { - cx.clear_bindings(); - KeymapFileContent::load_defaults(cx); - keymap_content.clone().add_to_cx(cx).log_err(); - }); - - let mut old_base_keymap = cx.read(|cx| cx.global::().base_keymap.clone()); - drop(settings_subscription); - settings_subscription = Some(cx.update(|cx| { - cx.observe_global::(move |cx| { - let settings = cx.global::(); - if settings.base_keymap != old_base_keymap { - old_base_keymap = settings.base_keymap.clone(); - - cx.clear_bindings(); - KeymapFileContent::load_defaults(cx); - keymap_content.clone().add_to_cx(cx).log_err(); - } - }) - .detach(); - })); - } - } - }) - .detach(); -} - pub fn handle_settings_file_changes( mut user_settings_file_rx: mpsc::UnboundedReceiver, cx: &mut AppContext, @@ -184,180 +147,3 @@ pub fn update_settings_file( }) .detach_and_log_err(cx); } - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext}; - use theme::ThemeRegistry; - - struct TestView; - - impl Entity for TestView { - type Event = (); - } - - impl View for TestView { - fn ui_name() -> &'static str { - "TestView" - } - - fn render(&mut self, _: &mut ViewContext) -> AnyElement { - Empty::new().into_any() - } - } - - #[gpui::test] - async fn test_base_keymap(cx: &mut gpui::TestAppContext) { - let executor = cx.background(); - let fs = FakeFs::new(executor.clone()); - - actions!(test, [A, B]); - // From the Atom keymap - actions!(workspace, [ActivatePreviousPane]); - // From the JetBrains keymap - actions!(pane, [ActivatePrevItem]); - - fs.save( - "/settings.json".as_ref(), - &r#" - { - "base_keymap": "Atom" - } - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - fs.save( - "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test::A" - } - } - ] - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - cx.update(|cx| { - let mut store = SettingsStore::default(); - store.set_default_settings(&test_settings(), cx).unwrap(); - cx.set_global(store); - cx.set_global(ThemeRegistry::new(Assets, cx.font_cache().clone())); - cx.add_global_action(|_: &A, _cx| {}); - cx.add_global_action(|_: &B, _cx| {}); - cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); - cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); - - let settings_rx = watch_config_file( - executor.clone(), - fs.clone(), - PathBuf::from("/settings.json"), - ); - let keymap_rx = - watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); - - handle_keymap_file_changes(keymap_rx, cx); - handle_settings_file_changes(settings_rx, cx); - }); - - cx.foreground().run_until_parked(); - - let (window_id, _view) = cx.add_window(|_| TestView); - - // Test loading the keymap base at all - assert_key_bindings_for( - window_id, - cx, - vec![("backspace", &A), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the users keymap, while retaining the base keymap - fs.save( - "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test::B" - } - } - ] - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - cx.foreground().run_until_parked(); - - assert_key_bindings_for( - window_id, - cx, - vec![("backspace", &B), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the base, while retaining the users keymap - fs.save( - "/settings.json".as_ref(), - &r#" - { - "base_keymap": "JetBrains" - } - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - cx.foreground().run_until_parked(); - - assert_key_bindings_for( - window_id, - cx, - vec![("backspace", &B), ("[", &ActivatePrevItem)], - line!(), - ); - } - - fn assert_key_bindings_for<'a>( - window_id: usize, - cx: &TestAppContext, - actions: Vec<(&'static str, &'a dyn Action)>, - line: u32, - ) { - for (key, action) in actions { - // assert that... - assert!( - cx.available_actions(window_id, 0) - .into_iter() - .any(|(_, bound_action, b)| { - // action names match... - bound_action.name() == action.name() - && bound_action.namespace() == action.namespace() - // and key strokes contain the given key - && b.iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) - }), - "On {} Failed to find {} with key binding {}", - line, - action.name(), - key - ); - } - } -} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 9ac40831d1..531fbf0bba 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); - settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); + settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap(); }); // Setup search toolbars and keypress hook diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 82cd4ca35a..65f5151584 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -11,8 +11,6 @@ path = "src/welcome.rs" test-support = [] [dependencies] -anyhow.workspace = true -log.workspace = true client = { path = "../client" } editor = { path = "../editor" } fs = { path = "../fs" } @@ -27,3 +25,8 @@ theme_selector = { path = "../theme_selector" } util = { path = "../util" } picker = { path = "../picker" } workspace = { path = "../workspace" } + +anyhow.workspace = true +log.workspace = true +schemars.workspace = true +serde.workspace = true diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 24600d5b82..815de5e6ae 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -1,3 +1,4 @@ +use super::base_keymap_setting::BaseKeymap; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ actions, @@ -6,7 +7,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate, PickerEvent}; use project::Fs; -use settings::{update_settings_file, BaseKeymap, Settings}; +use settings::{update_settings_file, Settings}; use std::sync::Arc; use util::ResultExt; use workspace::Workspace; @@ -39,10 +40,10 @@ pub struct BaseKeymapSelectorDelegate { impl BaseKeymapSelectorDelegate { fn new(fs: Arc, cx: &mut ViewContext) -> Self { - let base = cx.global::().base_keymap; + let base = settings::get_setting::(None, cx); let selected_index = BaseKeymap::OPTIONS .iter() - .position(|(_, value)| *value == base) + .position(|(_, value)| value == base) .unwrap_or(0); Self { matches: Vec::new(), @@ -122,8 +123,8 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { fn confirm(&mut self, cx: &mut ViewContext) { if let Some(selection) = self.matches.get(self.selected_index) { let base_keymap = BaseKeymap::from_names(&selection.string); - update_settings_file::(self.fs.clone(), cx, move |settings| { - settings.base_keymap = Some(base_keymap) + update_settings_file::(self.fs.clone(), cx, move |setting| { + *setting = Some(base_keymap) }); } cx.emit(PickerEvent::Dismiss); diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs new file mode 100644 index 0000000000..c5b6171f9b --- /dev/null +++ b/crates/welcome/src/base_keymap_setting.rs @@ -0,0 +1,65 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum BaseKeymap { + #[default] + VSCode, + JetBrains, + SublimeText, + Atom, + TextMate, +} + +impl BaseKeymap { + pub const OPTIONS: [(&'static str, Self); 5] = [ + ("VSCode (Default)", Self::VSCode), + ("Atom", Self::Atom), + ("JetBrains", Self::JetBrains), + ("Sublime Text", Self::SublimeText), + ("TextMate", Self::TextMate), + ]; + + pub fn asset_path(&self) -> Option<&'static str> { + match self { + BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), + BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), + BaseKeymap::Atom => Some("keymaps/atom.json"), + BaseKeymap::TextMate => Some("keymaps/textmate.json"), + BaseKeymap::VSCode => None, + } + } + + pub fn names() -> impl Iterator { + Self::OPTIONS.iter().map(|(name, _)| *name) + } + + pub fn from_names(option: &str) -> BaseKeymap { + Self::OPTIONS + .iter() + .copied() + .find_map(|(name, value)| (name == option).then(|| value)) + .unwrap_or_default() + } +} + +impl Setting for BaseKeymap { + const KEY: Option<&'static str> = Some("base_keymap"); + + type FileContent = Option; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result + where + Self: Sized, + { + Ok(user_values + .first() + .and_then(|v| **v) + .unwrap_or(default_value.unwrap())) + } +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 6b8fe7312d..ca04d5f6dc 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,7 +1,7 @@ mod base_keymap_picker; +mod base_keymap_setting; -use std::{borrow::Cow, sync::Arc}; - +use crate::base_keymap_picker::ToggleBaseKeymapSelector; use client::TelemetrySettings; use db::kvp::KEY_VALUE_STORE; use gpui::{ @@ -9,17 +9,19 @@ use gpui::{ AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle, }; use settings::{update_settings_file, Settings}; - +use std::{borrow::Cow, sync::Arc}; use workspace::{ item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace, WorkspaceId, }; -use crate::base_keymap_picker::ToggleBaseKeymapSelector; +pub use base_keymap_setting::BaseKeymap; pub const FIRST_OPEN: &str = "first_open"; pub fn init(cx: &mut AppContext) { + settings::register_setting::(cx); + cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); workspace.add_item(Box::new(welcome_page), cx) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b290460cca..def5049633 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -24,8 +24,7 @@ use parking_lot::Mutex; use project::Fs; use serde::{Deserialize, Serialize}; use settings::{ - default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file, - Settings, SettingsStore, + default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, }; use simplelog::ConfigBuilder; use smol::process::Command; @@ -63,7 +62,9 @@ use workspace::{ dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace, }; -use zed::{self, build_window_options, initialize_workspace, languages, menus}; +use zed::{ + self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, +}; fn main() { let http = http::client(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 923a3d6d9c..d6e4f26b67 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -15,7 +15,7 @@ use anyhow::anyhow; use feedback::{ feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton, }; -use futures::StreamExt; +use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, geometry::vector::vec2f, @@ -29,11 +29,14 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{adjust_font_size_delta, Settings, DEFAULT_SETTINGS_ASSET_PATH}; +use settings::{ + adjust_font_size_delta, KeymapFileContent, Settings, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH, +}; use std::{borrow::Cow, str, sync::Arc}; use terminal_view::terminal_button::TerminalButton; use util::{channel::ReleaseChannel, paths, ResultExt}; use uuid::Uuid; +use welcome::BaseKeymap; pub use workspace; use workspace::{ create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow, @@ -258,7 +261,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { activity_indicator::init(cx); lsp_log::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - settings::KeymapFileContent::load_defaults(cx); + load_default_keymap(cx); } pub fn initialize_workspace( @@ -478,6 +481,52 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { .detach(); } +pub fn load_default_keymap(cx: &mut AppContext) { + for path in ["keymaps/default.json", "keymaps/vim.json"] { + KeymapFileContent::load_asset(path, cx).unwrap(); + } + + if let Some(asset_path) = settings::get_setting::(None, cx).asset_path() { + KeymapFileContent::load_asset(asset_path, cx).unwrap(); + } +} + +pub fn handle_keymap_file_changes( + mut user_keymap_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + cx.spawn(move |mut cx| async move { + let mut settings_subscription = None; + while let Some(user_keymap_content) = user_keymap_file_rx.next().await { + if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) { + cx.update(|cx| { + cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + }); + + let mut old_base_keymap = + cx.read(|cx| *settings::get_setting::(None, cx)); + drop(settings_subscription); + settings_subscription = Some(cx.update(|cx| { + cx.observe_global::(move |cx| { + let new_base_keymap = *settings::get_setting::(None, cx); + if new_base_keymap != old_base_keymap { + old_base_keymap = new_base_keymap.clone(); + + cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + } + }) + .detach(); + })); + } + } + }) + .detach(); +} + fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { workspace.with_local_workspace(cx, move |workspace, cx| { let app_state = workspace.app_state().clone(); @@ -579,11 +628,16 @@ mod tests { use super::*; use assets::Assets; use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; - use gpui::{executor::Deterministic, AppContext, AssetSource, TestAppContext, ViewHandle}; + use fs::{FakeFs, Fs}; + use gpui::{ + elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource, + Element, Entity, TestAppContext, View, ViewHandle, + }; use language::LanguageRegistry; use node_runtime::NodeRuntime; use project::{Project, ProjectPath}; use serde_json::json; + use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; use std::{ collections::HashSet, path::{Path, PathBuf}, @@ -1797,6 +1851,175 @@ mod tests { } } + #[gpui::test] + async fn test_base_keymap(cx: &mut gpui::TestAppContext) { + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } + } + + let executor = cx.background(); + let fs = FakeFs::new(executor.clone()); + + actions!(test, [A, B]); + // From the Atom keymap + actions!(workspace, [ActivatePreviousPane]); + // From the JetBrains keymap + actions!(pane, [ActivatePrevItem]); + + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(ThemeRegistry::new(Assets, cx.font_cache().clone())); + welcome::init(cx); + + cx.add_global_action(|_: &A, _cx| {}); + cx.add_global_action(|_: &B, _cx| {}); + cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); + cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + + let settings_rx = watch_config_file( + executor.clone(), + fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = + watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + + handle_keymap_file_changes(keymap_rx, cx); + handle_settings_file_changes(settings_rx, cx); + }); + + cx.foreground().run_until_parked(); + + let (window_id, _view) = cx.add_window(|_| TestView); + + // Test loading the keymap base at all + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the users keymap, while retaining the base keymap + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::B" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &B), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the base, while retaining the users keymap + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &B), ("[", &ActivatePrevItem)], + line!(), + ); + + fn assert_key_bindings_for<'a>( + window_id: usize, + cx: &TestAppContext, + actions: Vec<(&'static str, &'a dyn Action)>, + line: u32, + ) { + for (key, action) in actions { + // assert that... + assert!( + cx.available_actions(window_id, 0) + .into_iter() + .any(|(_, bound_action, b)| { + // action names match... + bound_action.name() == action.name() + && bound_action.namespace() == action.namespace() + // and key strokes contain the given key + && b.iter() + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) + }), + "On {} Failed to find {} with key binding {}", + line, + action.name(), + key + ); + } + } + } + #[gpui::test] fn test_bundled_settings_and_themes(cx: &mut AppContext) { cx.platform()