Reload themes defined in extensions (#7520)

Marshall Bowers and Max created

This PR extends the extension directory watcher to also watch and reload
themes defined in extensions.

Release Notes:

- N/A

Co-authored-by: Max <max@zed.dev>

Change summary

crates/extension/src/extension_store.rs | 73 +++++++++++++++++++++-----
crates/theme/src/settings.rs            | 14 +++++
2 files changed, 71 insertions(+), 16 deletions(-)

Detailed changes

crates/extension/src/extension_store.rs 🔗

@@ -14,7 +14,7 @@ use std::{
     sync::Arc,
     time::Duration,
 };
-use theme::ThemeRegistry;
+use theme::{ThemeRegistry, ThemeSettings};
 use util::{paths::EXTENSIONS_DIR, ResultExt};
 
 #[cfg(test)]
@@ -27,7 +27,7 @@ pub struct ExtensionStore {
     manifest_path: PathBuf,
     language_registry: Arc<LanguageRegistry>,
     theme_registry: Arc<ThemeRegistry>,
-    _watch_extensions_dir: Task<()>,
+    _watch_extensions_dir: [Task<()>; 2],
 }
 
 struct GlobalExtensionStore(Model<ExtensionStore>);
@@ -103,7 +103,7 @@ impl ExtensionStore {
             fs,
             language_registry,
             theme_registry,
-            _watch_extensions_dir: Task::ready(()),
+            _watch_extensions_dir: [Task::ready(()), Task::ready(())],
         };
         this._watch_extensions_dir = this.watch_extensions_dir(cx);
         this.load(cx);
@@ -176,30 +176,71 @@ impl ExtensionStore {
         *self.manifest.write() = manifest;
     }
 
-    fn watch_extensions_dir(&self, cx: &mut ModelContext<Self>) -> Task<()> {
+    fn watch_extensions_dir(&self, cx: &mut ModelContext<Self>) -> [Task<()>; 2] {
         let manifest = self.manifest.clone();
         let fs = self.fs.clone();
         let language_registry = self.language_registry.clone();
+        let theme_registry = self.theme_registry.clone();
         let extensions_dir = self.extensions_dir.clone();
-        cx.background_executor().spawn(async move {
-            let mut changed_languages = HashSet::default();
+
+        let (reload_theme_tx, mut reload_theme_rx) = futures::channel::mpsc::unbounded();
+
+        let events_task = cx.background_executor().spawn(async move {
             let mut events = fs.watch(&extensions_dir, Duration::from_millis(250)).await;
             while let Some(events) = events.next().await {
-                changed_languages.clear();
-                let manifest = manifest.read();
-                for event in events {
-                    for (language_name, language) in &manifest.languages {
-                        let mut language_path = extensions_dir.clone();
-                        language_path
-                            .extend([language.extension.as_ref(), language.path.as_path()]);
-                        if event.path.starts_with(&language_path) || event.path == language_path {
-                            changed_languages.insert(language_name.clone());
+                let mut changed_languages = HashSet::default();
+                let mut changed_themes = HashSet::default();
+
+                {
+                    let manifest = manifest.read();
+                    for event in events {
+                        for (language_name, language) in &manifest.languages {
+                            let mut language_path = extensions_dir.clone();
+                            language_path
+                                .extend([language.extension.as_ref(), language.path.as_path()]);
+                            if event.path.starts_with(&language_path) || event.path == language_path
+                            {
+                                changed_languages.insert(language_name.clone());
+                            }
+                        }
+
+                        for (_theme_name, theme) in &manifest.themes {
+                            let mut theme_path = extensions_dir.clone();
+                            theme_path.extend([theme.extension.as_ref(), theme.path.as_path()]);
+                            if event.path.starts_with(&theme_path) || event.path == theme_path {
+                                changed_themes.insert(theme_path.clone());
+                            }
                         }
                     }
                 }
+
                 language_registry.reload_languages(&changed_languages);
+
+                for theme_path in &changed_themes {
+                    theme_registry
+                        .load_user_theme(&theme_path, fs.clone())
+                        .await
+                        .log_err();
+                }
+
+                if !changed_themes.is_empty() {
+                    reload_theme_tx.unbounded_send(()).ok();
+                }
             }
-        })
+        });
+
+        let reload_theme_task = cx.spawn(|_, cx| async move {
+            while let Some(_) = reload_theme_rx.next().await {
+                if cx
+                    .update(|cx| ThemeSettings::reload_current_theme(cx))
+                    .is_err()
+                {
+                    break;
+                }
+            }
+        });
+
+        [events_task, reload_theme_task]
     }
 
     pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {

crates/theme/src/settings.rs 🔗

@@ -33,6 +33,20 @@ pub struct ThemeSettings {
     pub theme_overrides: Option<ThemeStyleContent>,
 }
 
+impl ThemeSettings {
+    pub fn reload_current_theme(cx: &mut AppContext) {
+        let mut theme_settings = ThemeSettings::get_global(cx).clone();
+
+        if let Some(theme_selection) = theme_settings.theme_selection.clone() {
+            let theme_name = theme_selection.theme(*SystemAppearance::global(cx));
+
+            if let Some(_theme) = theme_settings.switch_theme(&theme_name, cx) {
+                ThemeSettings::override_global(theme_settings, cx);
+            }
+        }
+    }
+}
+
 /// The appearance of the system.
 #[derive(Debug, Clone, Copy, Deref)]
 pub struct SystemAppearance(pub Appearance);