Added theme and dock anchor saving :D

Mikayla Maki created

Change summary

Cargo.lock                                  |  2 
crates/fs/Cargo.toml                        |  1 
crates/fs/src/fs.rs                         | 23 ++++++++++
crates/settings/Cargo.toml                  |  2 
crates/settings/src/settings.rs             | 31 +++++++++-----
crates/settings/src/settings_file.rs        | 49 ++++++++++++++++++++--
crates/theme_selector/src/theme_selector.rs |  4 +
crates/workspace/src/dock.rs                |  7 +++
crates/zed/src/main.rs                      | 11 +++-
9 files changed, 111 insertions(+), 19 deletions(-)

Detailed changes

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",

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"

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<Box<dyn io::Read>>;
     async fn load(&self, path: &Path) -> Result<String>;
+    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<PathBuf>;
     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);

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"] }

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<T: DeserializeOwned>(content: &str) -> Result<T>
 
 #[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)
     }

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<T>(pub watch::Receiver<T>);
+pub struct SettingsFile {
+    path: &'static Path,
+    fs: Arc<dyn Fs>,
+}
+
+impl SettingsFile {
+    pub fn new(path: &'static Path, fs: Arc<dyn Fs>) -> 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<F>(&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::<SettingsFile>().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<T>(pub watch::Receiver<T>);
 
 impl<T> WatchedJsonFile<T>
 where

crates/theme_selector/src/theme_selector.rs 🔗

@@ -153,6 +153,10 @@ impl PickerDelegate for ThemeSelector {
 
     fn confirm(&mut self, cx: &mut ViewContext<Self>) {
         self.selection_completed = true;
+
+        let theme_name = cx.global::<Settings>().theme.meta.name.clone();
+        settings::settings_file::write_setting("theme", theme_name, cx);
+
         cx.emit(Event::Dismissed);
     }
 

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<Workspace>| {
+            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<Workspace>| {
+            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<Workspace>| {
+            settings::settings_file::write_setting(
+                "default_dock_anchor",
+                "expanded".to_string(),
+                cx,
+            );
             Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
         },
     );

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);