Define base keymap setting in welcome crate

Max Brunsfeld created

Change summary

Cargo.lock                                |   2 
assets/settings/default.json              |   9 
crates/settings/src/keymap_file.rs        |  20 -
crates/settings/src/settings.rs           |  49 -----
crates/settings/src/settings_file.rs      | 218 -----------------------
crates/vim/src/test/vim_test_context.rs   |   2 
crates/welcome/Cargo.toml                 |   7 
crates/welcome/src/base_keymap_picker.rs  |  11 
crates/welcome/src/base_keymap_setting.rs |  65 +++++++
crates/welcome/src/welcome.rs             |  10 
crates/zed/src/main.rs                    |   7 
crates/zed/src/zed.rs                     | 231 ++++++++++++++++++++++++
12 files changed, 334 insertions(+), 297 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8353,6 +8353,8 @@ dependencies = [
  "log",
  "picker",
  "project",
+ "schemars",
+ "serde",
  "settings",
  "theme",
  "theme_selector",

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

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<str>, Box<RawValue>);
 
 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::<Settings>().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::<Self>(content_str)?.add_to_cx(cx)
+        Self::parse(content_str)?.add_to_cx(cx)
+    }
+
+    pub fn parse(content: &str) -> Result<Self> {
+        parse_json_with_comments::<Self>(content)
     }
 
     pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {

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<Theme>,
-    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<Item = &'static str> {
-        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<fonts::Features>,
     #[serde(default)]
     pub theme: Option<String>,
-    #[serde(default)]
-    pub base_keymap: Option<BaseKeymap>,
 }
 
 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(),
         }
     }
 

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<String>,
-    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::<KeymapFileContent>(&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::<Settings>().base_keymap.clone());
-                drop(settings_subscription);
-                settings_subscription = Some(cx.update(|cx| {
-                    cx.observe_global::<Settings, _>(move |cx| {
-                        let settings = cx.global::<Settings>();
-                        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<String>,
     cx: &mut AppContext,
@@ -184,180 +147,3 @@ pub fn update_settings_file<T: Setting>(
     })
     .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<Self>) -> AnyElement<Self> {
-            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
-            );
-        }
-    }
-}

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::<VimModeSetting>(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

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

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<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
-        let base = cx.global::<Settings>().base_keymap;
+        let base = settings::get_setting::<BaseKeymap>(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<BaseKeymapSelector>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
-            update_settings_file::<Settings>(self.fs.clone(), cx, move |settings| {
-                settings.base_keymap = Some(base_keymap)
+            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
+                *setting = Some(base_keymap)
             });
         }
         cx.emit(PickerEvent::Dismiss);

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<Item = &'static str> {
+        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<Self>;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Ok(user_values
+            .first()
+            .and_then(|v| **v)
+            .unwrap_or(default_value.unwrap()))
+    }
+}

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::<BaseKeymap>(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)

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

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<AppState>, 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<Workspace>) {
         .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::<BaseKeymap>(None, cx).asset_path() {
+        KeymapFileContent::load_asset(asset_path, cx).unwrap();
+    }
+}
+
+pub fn handle_keymap_file_changes(
+    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
+    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::<BaseKeymap>(None, cx));
+                drop(settings_subscription);
+                settings_subscription = Some(cx.update(|cx| {
+                    cx.observe_global::<SettingsStore, _>(move |cx| {
+                        let new_base_keymap = *settings::get_setting::<BaseKeymap>(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>) {
     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<Self>) -> AnyElement<Self> {
+                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()