Start using the SettingsStore in the app

This commit is contained in:
Max Brunsfeld 2023-05-09 18:14:42 -07:00
parent 316f791a77
commit 9a6a2d9d27
17 changed files with 530 additions and 1049 deletions

View file

@ -1,88 +1,181 @@
use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
use crate::{
settings_store::parse_json_with_comments, settings_store::SettingsStore, KeymapFileContent,
Settings, SettingsFileContent, DEFAULT_SETTINGS_ASSET_PATH,
};
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use gpui::AppContext;
use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
use futures::{channel::mpsc, StreamExt};
use gpui::{executor::Background, AppContext, AssetSource};
use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
use util::{paths, ResultExt};
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
// And instant updates in the Zed editor
#[derive(Clone)]
pub struct SettingsFile {
path: &'static Path,
settings_file_content: WatchedJsonFile<SettingsFileContent>,
fs: Arc<dyn Fs>,
pub fn default_settings() -> Cow<'static, str> {
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
}
}
impl SettingsFile {
pub fn new(
path: &'static Path,
settings_file_content: WatchedJsonFile<SettingsFileContent>,
fs: Arc<dyn Fs>,
) -> Self {
SettingsFile {
path,
settings_file_content,
fs,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String {
let mut value =
parse_json_with_comments::<serde_json::Value>(default_settings().as_ref()).unwrap();
util::merge_non_null_json_value_into(
serde_json::json!({
"buffer_font_family": "Courier",
"buffer_font_features": {},
"default_buffer_font_size": 14,
"preferred_line_length": 80,
"theme": theme::EMPTY_THEME_NAME,
}),
&mut value,
);
serde_json::to_string(&value).unwrap()
}
async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
match fs.load(path).await {
result @ Ok(_) => result,
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == ErrorKind::NotFound {
return Ok(Settings::initial_user_settings_content(&Assets).to_string());
pub fn watch_config_file(
executor: Arc<Background>,
fs: Arc<dyn Fs>,
path: PathBuf,
) -> mpsc::UnboundedReceiver<String> {
let (tx, rx) = mpsc::unbounded();
executor
.spawn(async move {
let events = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
loop {
if let Ok(contents) = fs.load(&path).await {
if !tx.unbounded_send(contents).is_ok() {
break;
}
}
return Err(err);
if events.next().await.is_none() {
break;
}
}
})
.detach();
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 update_unsaved(
text: &str,
cx: &AppContext,
update: impl FnOnce(&mut SettingsFileContent),
) -> Vec<(Range<usize>, String)> {
let this = cx.global::<SettingsFile>();
let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
let current_file_content = this.settings_file_content.current();
update_settings_file(&text, current_file_content, tab_size, update)
}
pub fn handle_settings_file_changes(
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
cx: &mut AppContext,
) {
let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store
.set_user_settings(&user_settings_content, cx)
.log_err();
pub fn update(
cx: &mut AppContext,
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
) {
let this = cx.global::<SettingsFile>();
let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
let current_file_content = this.settings_file_content.current();
let fs = this.fs.clone();
let path = this.path.clone();
// TODO - remove the Settings global, use the SettingsStore instead.
store.register_setting::<Settings>(cx);
cx.set_global(store.get::<Settings>(None).clone());
});
cx.spawn(move |mut cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await {
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store
.set_user_settings(&user_settings_content, cx)
.log_err();
// TODO - remove the Settings global, use the SettingsStore instead.
cx.set_global(store.get::<Settings>(None).clone());
});
});
}
})
.detach();
}
async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
match fs.load(&paths::SETTINGS).await {
result @ Ok(_) => result,
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == ErrorKind::NotFound {
return Ok(Settings::initial_user_settings_content(&Assets).to_string());
}
}
return Err(err);
}
}
}
pub fn update_settings_file(
fs: Arc<dyn Fs>,
cx: &mut AppContext,
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
) {
cx.spawn(|cx| async move {
let old_text = cx
.background()
.spawn({
let fs = fs.clone();
async move { load_settings(&fs).await }
})
.await?;
let edits = cx.read(|cx| {
cx.global::<SettingsStore>()
.update::<Settings>(&old_text, update)
});
let mut new_text = old_text;
for (range, replacement) in edits.into_iter().rev() {
new_text.replace_range(range, &replacement);
}
cx.background()
.spawn(async move {
let old_text = SettingsFile::load_settings(path, &fs).await?;
let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
let mut new_text = old_text;
for (range, replacement) in edits.into_iter().rev() {
new_text.replace_range(range, &replacement);
}
fs.atomic_write(path.to_path_buf(), new_text).await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
.spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await })
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
};
use fs::FakeFs;
use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
use theme::ThemeRegistry;
@ -107,7 +200,6 @@ mod tests {
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
let executor = cx.background();
let fs = FakeFs::new(executor.clone());
let font_cache = cx.font_cache();
actions!(test, [A, B]);
// From the Atom keymap
@ -145,25 +237,26 @@ mod tests {
.await
.unwrap();
let settings_file =
WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
let keymaps_file =
WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
let default_settings = cx.read(Settings::test);
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| {});
watch_files(
default_settings,
settings_file,
ThemeRegistry::new((), font_cache),
keymaps_file,
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();
@ -255,113 +348,4 @@ mod tests {
);
}
}
#[gpui::test]
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
let executor = cx.background();
let fs = FakeFs::new(executor.clone());
let font_cache = cx.font_cache();
fs.save(
"/settings.json".as_ref(),
&r#"
{
"buffer_font_size": 24,
"soft_wrap": "editor_width",
"tab_size": 8,
"language_overrides": {
"Markdown": {
"tab_size": 2,
"preferred_line_length": 100,
"soft_wrap": "preferred_line_length"
}
}
}
"#
.into(),
Default::default(),
)
.await
.unwrap();
let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
let default_settings = cx.read(Settings::test).with_language_defaults(
"JavaScript",
EditorSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
);
cx.update(|cx| {
watch_settings_file(
default_settings.clone(),
source,
ThemeRegistry::new((), font_cache),
cx,
)
});
cx.foreground().run_until_parked();
let settings = cx.read(|cx| cx.global::<Settings>().clone());
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
assert_eq!(
settings.soft_wrap(Some("Markdown")),
SoftWrap::PreferredLineLength
);
assert_eq!(
settings.soft_wrap(Some("JavaScript")),
SoftWrap::EditorWidth
);
assert_eq!(settings.preferred_line_length(None), 80);
assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
assert_eq!(settings.tab_size(None).get(), 8);
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
fs.save(
"/settings.json".as_ref(),
&"(garbage)".into(),
Default::default(),
)
.await
.unwrap();
// fs.remove_file("/settings.json".as_ref(), Default::default())
// .await
// .unwrap();
cx.foreground().run_until_parked();
let settings = cx.read(|cx| cx.global::<Settings>().clone());
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
assert_eq!(
settings.soft_wrap(Some("Markdown")),
SoftWrap::PreferredLineLength
);
assert_eq!(
settings.soft_wrap(Some("JavaScript")),
SoftWrap::EditorWidth
);
assert_eq!(settings.preferred_line_length(None), 80);
assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
assert_eq!(settings.tab_size(None).get(), 8);
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
fs.remove_file("/settings.json".as_ref(), Default::default())
.await
.unwrap();
cx.foreground().run_until_parked();
let settings = cx.read(|cx| cx.global::<Settings>().clone());
assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
}
}