diff --git a/Cargo.lock b/Cargo.lock index fca454f1f8..53f8aae9c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2009,6 +2009,7 @@ dependencies = [ "serde", "serde_json", "smol", + "tempfile", "util", ] @@ -5065,6 +5066,7 @@ dependencies = [ "gpui", "json_comments", "postage", + "rope", "schemars", "serde", "serde_json", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 182d13894d..5b9082d114 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -15,6 +15,7 @@ util = { path = "../util" } anyhow = "1.0.57" async-trait = "0.1" futures = "0.3" +tempfile = "3" fsevent = { path = "../fsevent" } lazy_static = "1.4.0" parking_lot = "0.11.1" diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 26045f2776..2061d3734b 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -12,6 +12,7 @@ use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::borrow::Cow; use std::cmp; +use std::io::Write; use std::sync::Arc; use std::{ io, @@ -20,6 +21,7 @@ use std::{ pin::Pin, time::{Duration, SystemTime}, }; +use tempfile::NamedTempFile; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] @@ -100,6 +102,7 @@ pub trait Fs: Send + Sync { async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn open_sync(&self, path: &Path) -> Result>; async fn load(&self, path: &Path) -> Result; + async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; async fn is_file(&self, path: &Path) -> bool; @@ -260,6 +263,18 @@ impl Fs for RealFs { Ok(text) } + async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { + smol::unblock(move || { + let mut tmp_file = NamedTempFile::new()?; + tmp_file.write_all(data.as_bytes())?; + tmp_file.persist(path)?; + Ok::<(), anyhow::Error>(()) + }) + .await?; + + Ok(()) + } + async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); let file = smol::fs::File::create(path).await?; @@ -880,6 +895,14 @@ impl Fs for FakeFs { entry.file_content(&path).cloned() } + async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { + self.simulate_random_delay().await; + let path = normalize_path(path.as_path()); + self.insert_file(path, data.to_string()).await; + + Ok(()) + } + async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path); diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 64c906a833..1cc73fabc4 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1.0.38" futures = "0.3" theme = { path = "../theme" } util = { path = "../util" } +rope = { path = "../rope" } json_comments = "0.2" postage = { version = "0.4.1", features = ["futures-traits"] } schemars = "0.8" @@ -32,3 +33,4 @@ tree-sitter-json = "*" [dev-dependencies] unindent = "0.1" gpui = { path = "../gpui", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 883d7694c7..2e7dc08d16 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -503,7 +503,11 @@ pub fn settings_file_json_schema( serde_json::to_value(root_schema).unwrap() } -pub fn write_theme(mut settings_content: String, new_val: &str) -> String { +pub fn write_top_level_setting( + mut settings_content: String, + top_level_key: &str, + new_val: &str, +) -> String { let mut parser = tree_sitter::Parser::new(); parser.set_language(tree_sitter_json::language()).unwrap(); let tree = parser.parse(&settings_content, None).unwrap(); @@ -536,7 +540,7 @@ pub fn write_theme(mut settings_content: String, new_val: &str) -> String { first_key_start.get_or_insert_with(|| key.node.start_byte()); if let Some(key_text) = settings_content.get(key.node.byte_range()) { - if key_text == "\"theme\"" { + if key_text == format!("\"{top_level_key}\"") { existing_value_range = Some(value.node.byte_range()); break; } @@ -547,7 +551,12 @@ pub fn write_theme(mut settings_content: String, new_val: &str) -> String { (None, None) => { // No document, create a new object and overwrite settings_content.clear(); - write!(settings_content, "{{\n \"theme\": \"{new_val}\"\n}}\n").unwrap(); + write!( + settings_content, + "{{\n \"{}\": \"{new_val}\"\n}}\n", + top_level_key + ) + .unwrap(); } (_, Some(existing_value_range)) => { @@ -572,7 +581,7 @@ pub fn write_theme(mut settings_content: String, new_val: &str) -> String { } } - let content = format!(r#""theme": "{new_val}","#); + let content = format!(r#""{top_level_key}": "{new_val}","#); settings_content.insert_str(first_key_start, &content); if row > 0 { @@ -603,7 +612,7 @@ pub fn parse_json_with_comments(content: &str) -> Result #[cfg(test)] mod tests { - use crate::write_theme; + use crate::write_top_level_setting; use unindent::Unindent; #[test] @@ -622,7 +631,7 @@ mod tests { "# .unindent(); - let settings_after_theme = write_theme(settings, "summerfruit-light"); + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); assert_eq!(settings_after_theme, new_settings) } @@ -642,7 +651,7 @@ mod tests { "# .unindent(); - let settings_after_theme = write_theme(settings, "summerfruit-light"); + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); assert_eq!(settings_after_theme, new_settings) } @@ -658,7 +667,7 @@ mod tests { "# .unindent(); - let settings_after_theme = write_theme(settings, "summerfruit-light"); + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); assert_eq!(settings_after_theme, new_settings) } @@ -668,7 +677,7 @@ mod tests { let settings = r#"{ "a": "", "ok": true }"#.to_string(); let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#; - let settings_after_theme = write_theme(settings, "summerfruit-light"); + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); assert_eq!(settings_after_theme, new_settings) } @@ -678,7 +687,7 @@ mod tests { let settings = r#" { "a": "", "ok": true }"#.to_string(); let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#; - let settings_after_theme = write_theme(settings, "summerfruit-light"); + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); assert_eq!(settings_after_theme, new_settings) } @@ -700,7 +709,7 @@ mod tests { "# .unindent(); - let settings_after_theme = write_theme(settings, "summerfruit-light"); + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); assert_eq!(settings_after_theme, new_settings) } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index f12f191041..6a7c96fd81 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -9,15 +9,54 @@ use std::{path::Path, sync::Arc, time::Duration}; use theme::ThemeRegistry; use util::ResultExt; -use crate::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent}; +use crate::{ + parse_json_with_comments, write_top_level_setting, KeymapFileContent, Settings, + SettingsFileContent, +}; + +// 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, + fs: Arc, +} + +impl SettingsFile { + pub fn new(path: &'static Path, fs: Arc) -> Self { + SettingsFile { path, fs } + } + + pub async fn rewrite_settings_file(&self, f: F) -> anyhow::Result<()> + where + F: Fn(String) -> String, + { + let content = self.fs.load(self.path).await?; + + let new_settings = f(content); + + self.fs + .atomic_write(self.path.to_path_buf(), new_settings) + .await?; + + Ok(()) + } +} + +pub fn write_setting(key: &'static str, val: String, cx: &mut MutableAppContext) { + let settings_file = cx.global::().clone(); + cx.background() + .spawn(async move { + settings_file + .rewrite_settings_file(|settings| write_top_level_setting(settings, key, &val)) + .await + }) + .detach_and_log_err(cx); +} #[derive(Clone)] pub struct WatchedJsonFile(pub watch::Receiver); -// 1) Do the refactoring to pull WatchedJSON and fs out and into everything else -// 2) Scaffold this by making the basic structs we'll need SettingsFile::atomic_write_theme() -// 3) Fix the overeager settings writing, if that works, and there's no data loss, call it? - impl WatchedJsonFile where T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync, diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 1cd7b3f926..f3ca38b78b 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -153,6 +153,10 @@ impl PickerDelegate for ThemeSelector { fn confirm(&mut self, cx: &mut ViewContext) { self.selection_completed = true; + + let theme_name = cx.global::().theme.meta.name.clone(); + settings::settings_file::write_setting("theme", theme_name, cx); + cx.emit(Event::Dismissed); } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 7e626c60a9..30607afdff 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -35,16 +35,23 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Dock::move_dock); cx.add_action( |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext| { + settings::settings_file::write_setting("default_dock_anchor", "right".to_string(), cx); Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx) }, ); cx.add_action( |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext| { + settings::settings_file::write_setting("default_dock_anchor", "bottom".to_string(), cx); Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx) }, ); cx.add_action( |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext| { + settings::settings_file::write_setting( + "default_dock_anchor", + "expanded".to_string(), + cx, + ); Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx) }, ); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7dc30d34f5..1c6a818ef3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -25,7 +25,10 @@ use log::LevelFilter; use parking_lot::Mutex; use project::{Fs, ProjectStore}; use serde_json::json; -use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory}; +use settings::{ + self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent, + WorkingDirectory, +}; use smol::process::Command; use std::fs::OpenOptions; use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration}; @@ -62,6 +65,7 @@ fn main() { let themes = ThemeRegistry::new(Assets, app.font_cache()); let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes); + let settings_file = SettingsFile::new(&*zed::paths::SETTINGS, fs.clone()); let config_files = load_config_files(&app, fs.clone()); let login_shell_env_loaded = if stdout_is_a_pty() { @@ -94,10 +98,11 @@ fn main() { .spawn(languages::init(languages.clone(), cx.background().clone())); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); + let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap(); //Setup settings global before binding actions - watch_settings_file(default_settings, settings_file, themes.clone(), cx); + cx.set_global(settings_file); + watch_settings_file(default_settings, settings_file_content, themes.clone(), cx); watch_keymap_file(keymap_file, cx); context_menu::init(cx);