Define base keymap setting in welcome crate

This commit is contained in:
Max Brunsfeld 2023-05-17 11:23:09 -07:00
parent 89204e85c0
commit 5c729c0e56
12 changed files with 334 additions and 297 deletions

2
Cargo.lock generated
View file

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

View file

@ -1,6 +1,15 @@
{ {
// The name of the Zed theme to use for the UI // The name of the Zed theme to use for the UI
"theme": "One Dark", "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 that can be globally enabled or disabled
"features": { "features": {
// Show Copilot icon in status bar // Show Copilot icon in status bar

View file

@ -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 anyhow::{Context, Result};
use assets::Assets; use assets::Assets;
use collections::BTreeMap; use collections::BTreeMap;
@ -41,20 +41,14 @@ impl JsonSchema for KeymapAction {
struct ActionWithData(Box<str>, Box<RawValue>); struct ActionWithData(Box<str>, Box<RawValue>);
impl KeymapFileContent { impl KeymapFileContent {
pub fn load_defaults(cx: &mut AppContext) { pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
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<()> {
let content = Assets::get(asset_path).unwrap().data; let content = Assets::get(asset_path).unwrap().data;
let content_str = std::str::from_utf8(content.as_ref()).unwrap(); 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<()> { pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {

View file

@ -34,7 +34,6 @@ pub struct Settings {
pub buffer_font_family: FamilyId, pub buffer_font_family: FamilyId,
pub buffer_font_size: f32, pub buffer_font_size: f32,
pub theme: Arc<Theme>, pub theme: Arc<Theme>,
pub base_keymap: BaseKeymap,
} }
impl Setting for Settings { impl Setting for Settings {
@ -62,7 +61,6 @@ impl Setting for Settings {
buffer_font_features, buffer_font_features,
buffer_font_size: defaults.buffer_font_size.unwrap(), buffer_font_size: defaults.buffer_font_size.unwrap(),
theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(), theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
base_keymap: Default::default(),
}; };
for value in user_values.into_iter().copied().cloned() { 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)] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct SettingsFileContent { pub struct SettingsFileContent {
#[serde(default)] #[serde(default)]
@ -163,8 +119,6 @@ pub struct SettingsFileContent {
pub buffer_font_features: Option<fonts::Features>, pub buffer_font_features: Option<fonts::Features>,
#[serde(default)] #[serde(default)]
pub theme: Option<String>, pub theme: Option<String>,
#[serde(default)]
pub base_keymap: Option<BaseKeymap>,
} }
impl Settings { impl Settings {
@ -198,7 +152,6 @@ impl Settings {
buffer_font_features, buffer_font_features,
buffer_font_size: defaults.buffer_font_size.unwrap(), buffer_font_size: defaults.buffer_font_size.unwrap(),
theme: themes.get(&defaults.theme.unwrap()).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.buffer_font_size, data.buffer_font_size);
merge(&mut self.base_keymap, data.base_keymap);
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -248,7 +200,6 @@ impl Settings {
.unwrap(), .unwrap(),
buffer_font_size: 14., buffer_font_size: 14.,
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default), theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
base_keymap: Default::default(),
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
settings_store::parse_json_with_comments, settings_store::SettingsStore, KeymapFileContent, settings_store::parse_json_with_comments, settings_store::SettingsStore, Setting, Settings,
Setting, Settings, DEFAULT_SETTINGS_ASSET_PATH, DEFAULT_SETTINGS_ASSET_PATH,
}; };
use anyhow::Result; use anyhow::Result;
use assets::Assets; use assets::Assets;
@ -76,43 +76,6 @@ pub fn watch_config_file(
rx 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( pub fn handle_settings_file_changes(
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>, mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
cx: &mut AppContext, cx: &mut AppContext,
@ -184,180 +147,3 @@ pub fn update_settings_file<T: Setting>(
}) })
.detach_and_log_err(cx); .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
);
}
}
}

View file

@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> {
cx.update_global(|store: &mut SettingsStore, cx| { cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled)); 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 // Setup search toolbars and keypress hook

View file

@ -11,8 +11,6 @@ path = "src/welcome.rs"
test-support = [] test-support = []
[dependencies] [dependencies]
anyhow.workspace = true
log.workspace = true
client = { path = "../client" } client = { path = "../client" }
editor = { path = "../editor" } editor = { path = "../editor" }
fs = { path = "../fs" } fs = { path = "../fs" }
@ -27,3 +25,8 @@ theme_selector = { path = "../theme_selector" }
util = { path = "../util" } util = { path = "../util" }
picker = { path = "../picker" } picker = { path = "../picker" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
anyhow.workspace = true
log.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -1,3 +1,4 @@
use super::base_keymap_setting::BaseKeymap;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, actions,
@ -6,7 +7,7 @@ use gpui::{
}; };
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use project::Fs; use project::Fs;
use settings::{update_settings_file, BaseKeymap, Settings}; use settings::{update_settings_file, Settings};
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -39,10 +40,10 @@ pub struct BaseKeymapSelectorDelegate {
impl BaseKeymapSelectorDelegate { impl BaseKeymapSelectorDelegate {
fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self { 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 let selected_index = BaseKeymap::OPTIONS
.iter() .iter()
.position(|(_, value)| *value == base) .position(|(_, value)| value == base)
.unwrap_or(0); .unwrap_or(0);
Self { Self {
matches: Vec::new(), matches: Vec::new(),
@ -122,8 +123,8 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) { fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
if let Some(selection) = self.matches.get(self.selected_index) { if let Some(selection) = self.matches.get(self.selected_index) {
let base_keymap = BaseKeymap::from_names(&selection.string); let base_keymap = BaseKeymap::from_names(&selection.string);
update_settings_file::<Settings>(self.fs.clone(), cx, move |settings| { update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
settings.base_keymap = Some(base_keymap) *setting = Some(base_keymap)
}); });
} }
cx.emit(PickerEvent::Dismiss); cx.emit(PickerEvent::Dismiss);

View file

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

View file

@ -1,7 +1,7 @@
mod base_keymap_picker; mod base_keymap_picker;
mod base_keymap_setting;
use std::{borrow::Cow, sync::Arc}; use crate::base_keymap_picker::ToggleBaseKeymapSelector;
use client::TelemetrySettings; use client::TelemetrySettings;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
@ -9,17 +9,19 @@ use gpui::{
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle, AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
}; };
use settings::{update_settings_file, Settings}; use settings::{update_settings_file, Settings};
use std::{borrow::Cow, sync::Arc};
use workspace::{ use workspace::{
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace, item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
WorkspaceId, WorkspaceId,
}; };
use crate::base_keymap_picker::ToggleBaseKeymapSelector; pub use base_keymap_setting::BaseKeymap;
pub const FIRST_OPEN: &str = "first_open"; pub const FIRST_OPEN: &str = "first_open";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
settings::register_setting::<BaseKeymap>(cx);
cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item(Box::new(welcome_page), cx) workspace.add_item(Box::new(welcome_page), cx)

View file

@ -24,8 +24,7 @@ use parking_lot::Mutex;
use project::Fs; use project::Fs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{ use settings::{
default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file, default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore,
Settings, SettingsStore,
}; };
use simplelog::ConfigBuilder; use simplelog::ConfigBuilder;
use smol::process::Command; use smol::process::Command;
@ -63,7 +62,9 @@ use workspace::{
dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
Workspace, 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() { fn main() {
let http = http::client(); let http = http::client();

View file

@ -15,7 +15,7 @@ use anyhow::anyhow;
use feedback::{ use feedback::{
feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton, feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
}; };
use futures::StreamExt; use futures::{channel::mpsc, StreamExt};
use gpui::{ use gpui::{
actions, actions,
geometry::vector::vec2f, geometry::vector::vec2f,
@ -29,11 +29,14 @@ use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar}; use search::{BufferSearchBar, ProjectSearchBar};
use serde::Deserialize; use serde::Deserialize;
use serde_json::to_string_pretty; 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 std::{borrow::Cow, str, sync::Arc};
use terminal_view::terminal_button::TerminalButton; use terminal_view::terminal_button::TerminalButton;
use util::{channel::ReleaseChannel, paths, ResultExt}; use util::{channel::ReleaseChannel, paths, ResultExt};
use uuid::Uuid; use uuid::Uuid;
use welcome::BaseKeymap;
pub use workspace; pub use workspace;
use workspace::{ use workspace::{
create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow, 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); activity_indicator::init(cx);
lsp_log::init(cx); lsp_log::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), 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( pub fn initialize_workspace(
@ -478,6 +481,52 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
.detach(); .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>) { fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
workspace.with_local_workspace(cx, move |workspace, cx| { workspace.with_local_workspace(cx, move |workspace, cx| {
let app_state = workspace.app_state().clone(); let app_state = workspace.app_state().clone();
@ -579,11 +628,16 @@ mod tests {
use super::*; use super::*;
use assets::Assets; use assets::Assets;
use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; 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 language::LanguageRegistry;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use serde_json::json; use serde_json::json;
use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
use std::{ use std::{
collections::HashSet, collections::HashSet,
path::{Path, PathBuf}, 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] #[gpui::test]
fn test_bundled_settings_and_themes(cx: &mut AppContext) { fn test_bundled_settings_and_themes(cx: &mut AppContext) {
cx.platform() cx.platform()