Start using the SettingsStore in the app
This commit is contained in:
parent
316f791a77
commit
9a6a2d9d27
17 changed files with 530 additions and 1049 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue