diff --git a/Cargo.lock b/Cargo.lock index fca454f1f8dc497e0195c60bedf80dbfb51c625e..53f8aae9c5a92c3225a0a13467e9ad0389020570 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 182d13894d821ca0b3639b5002f8cf92026bce5d..5b9082d1147800d3db75b76ae29a41210a00236c 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 26045f27763e30d11cc636ee524ec0aa0370efbc..2061d3734bd386901a97441fa34ea71c5b33f5a6 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 64c906a833eed00a0152b1ed38a97c9113686d6f..1cc73fabc4dea7d77886f236ee0b4aef8ca94e48 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 883d7694c75403993d67d687f9d75843c97d7ba8..2e7dc08d167995c68f41478024bfdfe9057b6e4d 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 f12f191041d8b6f681d116c672f09e6fefee2d5e..6a7c96fd81ddb1d81b69dcda625417b27c7a66fe 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -9,14 +9,53 @@ 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 WatchedJsonFile(pub watch::Receiver); +pub struct SettingsFile { + path: &'static Path, + fs: Arc, +} + +impl SettingsFile { + pub fn new(path: &'static Path, fs: Arc) -> Self { + SettingsFile { path, fs } + } -// 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? + 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); impl WatchedJsonFile where diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 1cd7b3f9260a91dbf94e2e7cf8aee9b70427d230..f3ca38b78b2db22cca284b9d06fb253569c7c1c0 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 7e626c60a90d0396873a20332fc636d3cd661044..30607afdff3d499e41e86703138ff0966fd9e120 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 7dc30d34f54f518a6906973847389016c317d501..1c6a818ef38b7cd580b1e2d9a0689ed109f50053 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);