Cut `fs` dependency from `theme` (#52482)

Lukas Wirth created

Trying to clean up the deps here for potential use of the ui crate in
web

Release Notes:

- N/A or Added/Fixed/Improved ...

Change summary

Cargo.lock                                    |  6 -
crates/editor/src/editor.rs                   |  4 
crates/extension_cli/src/main.rs              |  2 
crates/theme/Cargo.toml                       |  7 --
crates/theme/src/registry.rs                  | 43 ++++----------------
crates/theme/src/theme.rs                     | 19 ++------
crates/theme_extension/src/theme_extension.rs | 18 ++++---
crates/ui/Cargo.toml                          |  2 
crates/ui/src/components/keybinding.rs        |  5 +
crates/ui/src/components/scrollbar.rs         |  2 
crates/ui/src/utils.rs                        | 22 ++++++++++
crates/util/src/util.rs                       | 22 ----------
crates/zed/src/main.rs                        | 25 +++++++++--
crates/zed/src/zed.rs                         | 26 ++++++-----
14 files changed, 92 insertions(+), 111 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -17571,9 +17571,8 @@ dependencies = [
  "anyhow",
  "collections",
  "derive_more",
- "fs",
- "futures 0.3.31",
  "gpui",
+ "gpui_util",
  "log",
  "palette",
  "parking_lot",
@@ -17585,7 +17584,6 @@ dependencies = [
  "settings",
  "strum 0.27.2",
  "thiserror 2.0.17",
- "util",
  "uuid",
 ]
 
@@ -18771,6 +18769,7 @@ dependencies = [
  "documented",
  "gpui",
  "gpui_macros",
+ "gpui_util",
  "icons",
  "itertools 0.14.0",
  "menu",
@@ -18782,7 +18781,6 @@ dependencies = [
  "strum 0.27.2",
  "theme",
  "ui_macros",
- "util",
  "windows 0.61.3",
 ]
 

crates/editor/src/editor.rs 🔗

@@ -9950,7 +9950,7 @@ impl Editor {
             })
             .when(!is_platform_style_mac, |parent| {
                 parent.child(
-                    Key::new(util::capitalize(keystroke.key()), Some(Color::Default))
+                    Key::new(ui::utils::capitalize(keystroke.key()), Some(Color::Default))
                         .size(Some(IconSize::XSmall.rems().into())),
                 )
             })
@@ -9978,7 +9978,7 @@ impl Editor {
                 )))
                 .into_any()
         } else {
-            Key::new(util::capitalize(keystroke.key()), Some(color))
+            Key::new(ui::utils::capitalize(keystroke.key()), Some(color))
                 .size(Some(IconSize::XSmall.rems().into()))
                 .into_any_element()
         }

crates/extension_cli/src/main.rs 🔗

@@ -413,7 +413,7 @@ async fn test_themes(
 ) -> Result<()> {
     for relative_theme_path in &manifest.themes {
         let theme_path = extension_path.join(relative_theme_path);
-        let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?;
+        let theme_family = theme::deserialize_user_theme(&fs.load_bytes(&theme_path).await?)?;
         log::info!("loaded theme family {}", theme_family.name);
 
         for theme in &theme_family.themes {

crates/theme/Cargo.toml 🔗

@@ -10,7 +10,7 @@ workspace = true
 
 [features]
 default = []
-test-support = ["gpui/test-support", "fs/test-support", "settings/test-support"]
+test-support = ["gpui/test-support", "settings/test-support"]
 
 [lib]
 path = "src/theme.rs"
@@ -20,9 +20,8 @@ doctest = false
 anyhow.workspace = true
 collections.workspace = true
 derive_more.workspace = true
-fs.workspace = true
-futures.workspace = true
 gpui.workspace = true
+gpui_util.workspace = true
 log.workspace = true
 palette = { workspace = true, default-features = false, features = ["std"] }
 parking_lot.workspace = true
@@ -34,10 +33,8 @@ serde_json_lenient.workspace = true
 settings.workspace = true
 strum.workspace = true
 thiserror.workspace = true
-util.workspace = true
 uuid.workspace = true
 
 [dev-dependencies]
-fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }

crates/theme/src/registry.rs 🔗

@@ -4,17 +4,15 @@ use std::{fmt::Debug, path::Path};
 use anyhow::{Context as _, Result};
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
-use fs::Fs;
-use futures::StreamExt;
 use gpui::{App, AssetSource, Global, SharedString};
+use gpui_util::ResultExt;
 use parking_lot::RwLock;
 use thiserror::Error;
-use util::ResultExt;
 
 use crate::{
     Appearance, AppearanceContent, ChevronIcons, DEFAULT_ICON_THEME_NAME, DirectoryIcons,
-    IconDefinition, IconTheme, Theme, ThemeFamily, ThemeFamilyContent, default_icon_theme,
-    read_icon_theme, read_user_theme, refine_theme_family,
+    IconDefinition, IconTheme, IconThemeFamilyContent, Theme, ThemeFamily, ThemeFamilyContent,
+    default_icon_theme, deserialize_user_theme, refine_theme_family,
 };
 
 /// The metadata for a theme.
@@ -208,29 +206,9 @@ impl ThemeRegistry {
         }
     }
 
-    /// Loads the user themes from the specified directory and adds them to the registry.
-    pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
-        let mut theme_paths = fs
-            .read_dir(themes_path)
-            .await
-            .with_context(|| format!("reading themes from {themes_path:?}"))?;
-
-        while let Some(theme_path) = theme_paths.next().await {
-            let Some(theme_path) = theme_path.log_err() else {
-                continue;
-            };
-
-            self.load_user_theme(&theme_path, fs.clone())
-                .await
-                .log_err();
-        }
-
-        Ok(())
-    }
-
-    /// Loads the user theme from the specified path and adds it to the registry.
-    pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
-        let theme = read_user_theme(theme_path, fs).await?;
+    /// Loads the user theme from the specified data and adds it to the registry.
+    pub fn load_user_theme(&self, bytes: &[u8]) -> Result<()> {
+        let theme = deserialize_user_theme(bytes)?;
 
         self.insert_user_theme_families([theme]);
 
@@ -273,18 +251,15 @@ impl ThemeRegistry {
             .retain(|name, _| !icon_themes_to_remove.contains(name))
     }
 
-    /// Loads the icon theme from the specified path and adds it to the registry.
+    /// Loads the icon theme from the icon theme family and adds it to the registry.
     ///
     /// The `icons_root_dir` parameter indicates the root directory from which
     /// the relative paths to icons in the theme should be resolved against.
-    pub async fn load_icon_theme(
+    pub fn load_icon_theme(
         &self,
-        icon_theme_path: &Path,
+        icon_theme_family: IconThemeFamilyContent,
         icons_root_dir: &Path,
-        fs: Arc<dyn Fs>,
     ) -> Result<()> {
-        let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
-
         let resolve_icon_path = |path: SharedString| {
             icons_root_dir
                 .join(path.as_ref())

crates/theme/src/theme.rs 🔗

@@ -19,7 +19,6 @@ mod schema;
 mod settings;
 mod styles;
 
-use std::path::Path;
 use std::sync::Arc;
 
 use ::settings::DEFAULT_DARK_THEME;
@@ -28,7 +27,6 @@ use ::settings::Settings;
 use ::settings::SettingsStore;
 use anyhow::Result;
 use fallback_themes::apply_status_color_defaults;
-use fs::Fs;
 use gpui::BorrowAppContext;
 use gpui::Global;
 use gpui::{
@@ -405,10 +403,9 @@ impl Theme {
     }
 }
 
-/// Asynchronously reads the user theme from the specified path.
-pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
-    let bytes = fs.load_bytes(theme_path).await?;
-    let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(&bytes)?;
+/// Deserializes a user theme from the given bytes.
+pub fn deserialize_user_theme(bytes: &[u8]) -> Result<ThemeFamilyContent> {
+    let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
 
     for theme in &theme_family.themes {
         if theme
@@ -427,13 +424,9 @@ pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<Theme
     Ok(theme_family)
 }
 
-/// Asynchronously reads the icon theme from the specified path.
-pub async fn read_icon_theme(
-    icon_theme_path: &Path,
-    fs: Arc<dyn Fs>,
-) -> Result<IconThemeFamilyContent> {
-    let bytes = fs.load_bytes(icon_theme_path).await?;
-    let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_slice(&bytes)?;
+/// Deserializes a icon theme from the given bytes.
+pub fn deserialize_icon_theme(bytes: &[u8]) -> Result<IconThemeFamilyContent> {
+    let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_slice(bytes)?;
 
     Ok(icon_theme_family)
 }

crates/theme_extension/src/theme_extension.rs 🔗

@@ -5,7 +5,7 @@ use anyhow::Result;
 use extension::{ExtensionHostProxy, ExtensionThemeProxy};
 use fs::Fs;
 use gpui::{App, BackgroundExecutor, SharedString, Task};
-use theme::{GlobalTheme, ThemeRegistry};
+use theme::{GlobalTheme, ThemeRegistry, deserialize_icon_theme};
 
 pub fn init(
     extension_host_proxy: Arc<ExtensionHostProxy>,
@@ -30,7 +30,7 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
 
     fn list_theme_names(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
         self.executor.spawn(async move {
-            let themes = theme::read_user_theme(&theme_path, fs).await?;
+            let themes = theme::deserialize_user_theme(&fs.load_bytes(&theme_path).await?)?;
             Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
         })
     }
@@ -41,8 +41,9 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
 
     fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>> {
         let theme_registry = self.theme_registry.clone();
-        self.executor
-            .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
+        self.executor.spawn(async move {
+            theme_registry.load_user_theme(&fs.load_bytes(&theme_path).await?)
+        })
     }
 
     fn reload_current_theme(&self, cx: &mut App) {
@@ -55,7 +56,8 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
         fs: Arc<dyn Fs>,
     ) -> Task<Result<Vec<String>>> {
         self.executor.spawn(async move {
-            let icon_theme_family = theme::read_icon_theme(&icon_theme_path, fs).await?;
+            let icon_theme_family =
+                theme::deserialize_icon_theme(&fs.load_bytes(&icon_theme_path).await?)?;
             Ok(icon_theme_family
                 .themes
                 .into_iter()
@@ -76,9 +78,9 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
     ) -> Task<Result<()>> {
         let theme_registry = self.theme_registry.clone();
         self.executor.spawn(async move {
-            theme_registry
-                .load_icon_theme(&icon_theme_path, &icons_root_dir, fs)
-                .await
+            let icon_theme_family =
+                deserialize_icon_theme(&fs.load_bytes(&icon_theme_path).await?)?;
+            theme_registry.load_icon_theme(icon_theme_family, &icons_root_dir)
         })
     }
 

crates/ui/Cargo.toml 🔗

@@ -29,7 +29,7 @@ story = { workspace = true, optional = true }
 strum.workspace = true
 theme.workspace = true
 ui_macros.workspace = true
-util.workspace = true
+gpui_util.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 windows.workspace = true

crates/ui/src/components/keybinding.rs 🔗

@@ -1,6 +1,7 @@
 use std::rc::Rc;
 
 use crate::PlatformStyle;
+use crate::utils::capitalize;
 use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
 use gpui::{
     Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
@@ -142,7 +143,7 @@ fn render_key(
     match key_icon {
         Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
         None => {
-            let key = util::capitalize(key);
+            let key = capitalize(key);
             Key::new(&key, color).size(size).into_any_element()
         }
     }
@@ -546,7 +547,7 @@ fn keystroke_text(
         let key = match key {
             "pageup" => "PageUp",
             "pagedown" => "PageDown",
-            key => &util::capitalize(key),
+            key => &capitalize(key),
         };
         text.push_str(key);
     }

crates/ui/src/components/scrollbar.rs 🔗

@@ -15,10 +15,10 @@ use gpui::{
     UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative,
     size,
 };
+use gpui_util::ResultExt;
 use settings::SettingsStore;
 use smallvec::SmallVec;
 use theme::ActiveTheme as _;
-use util::ResultExt;
 
 use std::ops::Range;
 

crates/ui/src/utils.rs 🔗

@@ -34,3 +34,25 @@ pub fn reveal_in_file_manager_label(is_remote: bool) -> &'static str {
         "Reveal in File Manager"
     }
 }
+
+/// Capitalizes the first character of a string.
+///
+/// This function takes a string slice as input and returns a new `String` with the first character
+/// capitalized.
+///
+/// # Examples
+///
+/// ```
+/// use ui::utils::capitalize;
+///
+/// assert_eq!(capitalize("hello"), "Hello");
+/// assert_eq!(capitalize("WORLD"), "WORLD");
+/// assert_eq!(capitalize(""), "");
+/// ```
+pub fn capitalize(str: &str) -> String {
+    let mut chars = str.chars();
+    match chars.next() {
+        None => String::new(),
+        Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
+    }
+}

crates/util/src/util.rs 🔗

@@ -686,28 +686,6 @@ impl PartialOrd for NumericPrefixWithSuffix<'_> {
     }
 }
 
-/// Capitalizes the first character of a string.
-///
-/// This function takes a string slice as input and returns a new `String` with the first character
-/// capitalized.
-///
-/// # Examples
-///
-/// ```
-/// use util::capitalize;
-///
-/// assert_eq!(capitalize("hello"), "Hello");
-/// assert_eq!(capitalize("WORLD"), "WORLD");
-/// assert_eq!(capitalize(""), "");
-/// ```
-pub fn capitalize(str: &str) -> String {
-    let mut chars = str.chars();
-    match chars.next() {
-        None => String::new(),
-        Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
-    }
-}
-
 fn emoji_regex() -> &'static Regex {
     static EMOJI_REGEX: LazyLock<Regex> =
         LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap());

crates/zed/src/main.rs 🔗

@@ -1781,7 +1781,23 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) {
                     })?;
                 }
             }
-            theme_registry.load_user_themes(themes_dir, fs).await?;
+
+            let mut theme_paths = fs
+                .read_dir(themes_dir)
+                .await
+                .with_context(|| format!("reading themes from {themes_dir:?}"))?;
+
+            while let Some(theme_path) = theme_paths.next().await {
+                let Some(theme_path) = theme_path.log_err() else {
+                    continue;
+                };
+                let Some(bytes) = fs.load_bytes(&theme_path).await.log_err() else {
+                    continue;
+                };
+
+                theme_registry.load_user_theme(&bytes).log_err();
+            }
+
             cx.update(GlobalTheme::reload_theme);
             anyhow::Ok(())
         }
@@ -1801,11 +1817,8 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) {
             for event in paths {
                 if fs.metadata(&event.path).await.ok().flatten().is_some() {
                     let theme_registry = cx.update(|cx| ThemeRegistry::global(cx));
-                    if theme_registry
-                        .load_user_theme(&event.path, fs.clone())
-                        .await
-                        .log_err()
-                        .is_some()
+                    if let Some(bytes) = fs.load_bytes(&event.path).await.log_err()
+                        && theme_registry.load_user_theme(&bytes).log_err().is_some()
                     {
                         cx.update(GlobalTheme::reload_theme);
                     }

crates/zed/src/zed.rs 🔗

@@ -77,7 +77,10 @@ use std::{
     sync::atomic::{self, AtomicBool},
 };
 use terminal_view::terminal_panel::{self, TerminalPanel};
-use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
+use theme::{
+    ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings,
+    deserialize_icon_theme,
+};
 use ui::{PopoverMenuHandle, prelude::*};
 use util::markdown::MarkdownString;
 use util::rel_path::RelPath;
@@ -2221,24 +2224,23 @@ pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut A
             let reload_tasks = &reload_tasks;
             let fs = fs.clone();
 
-            scope.spawn(async {
+            scope.spawn(async move {
                 match load_target {
                     LoadTarget::Theme(theme_path) => {
-                        if theme_registry
-                            .load_user_theme(&theme_path, fs)
-                            .await
-                            .log_err()
-                            .is_some()
+                        if let Some(bytes) = fs.load_bytes(&theme_path).await.log_err()
+                            && theme_registry.load_user_theme(&bytes).log_err().is_some()
                         {
                             reload_tasks.lock().push(ReloadTarget::Theme);
                         }
                     }
                     LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
-                        if theme_registry
-                            .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
-                            .await
-                            .log_err()
-                            .is_some()
+                        if let Some(bytes) = fs.load_bytes(&icon_theme_path).await.log_err()
+                            && let Some(icon_theme_family) =
+                                deserialize_icon_theme(&bytes).log_err()
+                            && theme_registry
+                                .load_icon_theme(icon_theme_family, &icons_root_path)
+                                .log_err()
+                                .is_some()
                         {
                             reload_tasks.lock().push(ReloadTarget::IconTheme);
                         }