From 354bc35974c0584725a9b5b87ca6d3ac840a7c8b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 27 Mar 2026 09:17:30 +0100 Subject: [PATCH] Cut `fs` dependency from `theme` (#52482) Trying to clean up the deps here for potential use of the ui crate in web Release Notes: - N/A or Added/Fixed/Improved ... --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index 9da2e0144da52421bb2e2d044899d2c881ce3ad4..c183dcadf823de0017b321216616341e595c9865 100644 --- a/Cargo.lock +++ b/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", ] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1786013a4a4d746c0580813c3e9b9962b1baa72d..3c35175002fe17995db478f01eed06585c0ebe88 100644 --- a/crates/editor/src/editor.rs +++ b/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() } diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 4d290992f318dc8fec78dad0e40d347d4826ed65..4d16cd4f1bba1df53c621bad2c15c01a1db4a533 100644 --- a/crates/extension_cli/src/main.rs +++ b/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 { diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index ef193c500d461201e8746ad3ec0f33b01e423b18..b1c689bc7c451b92e9fff86cbacebc60c9a31b58 100644 --- a/crates/theme/Cargo.toml +++ b/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"] } diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index c362b62704257fefde125e81ca1c056490263b0b..5b8e47439cd1baf47172bd17f3fae2ca56635b29 100644 --- a/crates/theme/src/registry.rs +++ b/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) -> 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) -> 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, ) -> 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()) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3449e039a9e3f4135a0f8471b8346f6b6e6b9fcc..8134461dfa9dd0aac1ae685a5be9861fb78ba4a1 100644 --- a/crates/theme/src/theme.rs +++ b/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) -> Result { - 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 { + 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) -> Result, -) -> Result { - 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 { + let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_slice(bytes)?; Ok(icon_theme_family) } diff --git a/crates/theme_extension/src/theme_extension.rs b/crates/theme_extension/src/theme_extension.rs index 10df2349c86decbadaa010778a95d04af36a6aab..7dcb43aa74c5520399e7d3a37f30ed3f6b74d410 100644 --- a/crates/theme_extension/src/theme_extension.rs +++ b/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, @@ -30,7 +30,7 @@ impl ExtensionThemeProxy for ThemeRegistryProxy { fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { 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) -> Task> { 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, ) -> Task>> { 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> { 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) }) } diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 5eb58bf1da1f25cc273a9fc5d7c08b920d3471e9..6ea1b6d26f700c9c44a8dda5e510d0505d7e7db8 100644 --- a/crates/ui/Cargo.toml +++ b/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 diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index e22669995db416a3ec6884a79860e76610dd7d03..6c7efa4e49ee93fd13407c03cf383ff3385bacc7 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/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); } diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index d0c720d5081d3ab7ad700df798b931933e03db28..795c5174fb42d3caeec3052d3c636b0408ac7ed6 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/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; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index 2f2a148e1985d026371c96297eb92cc4ec079a3b..d88bf4a45e0b54536b6f5ca5ad4ae7c7fe936937 100644 --- a/crates/ui/src/utils.rs +++ b/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::() + chars.as_str(), + } +} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 4f129ef6d529aff0991b86882e5e60b6ad837d5c..bd8ab4e2d4d99864c5e0dc228410904f3338d7c6 100644 --- a/crates/util/src/util.rs +++ b/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::() + chars.as_str(), - } -} - fn emoji_regex() -> &'static Regex { static EMOJI_REGEX: LazyLock = LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap()); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0a55953931ff4527851f9c9e7d6ac5f451eea0fd..18f7e98f001a83fbdd527f98ee8f4b22c7e91bcc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1781,7 +1781,23 @@ fn load_user_themes_in_background(fs: Arc, 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, 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); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d8438eb1b85aaa5191a178adc6b61865ebd94590..e4ccedcf4143d194406307e84b2ff03ef375609f 100644 --- a/crates/zed/src/zed.rs +++ b/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, 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); }