From dc64411ccab069d9fd52d8d2868238467c8169a1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Dec 2023 13:47:30 -0500 Subject: [PATCH] Extend `theme_importer` in preparation for importing Zed1 themes (#3791) This PR extends the `theme_importer` with the overall structure required to support importing themes from Zed1. Release Notes: - N/A --- Cargo.lock | 2 + crates/gpui/src/platform/mac.rs | 2 +- crates/gpui2/Cargo.toml | 6 + crates/theme2/src/themes/mod.rs | 16 +- crates/theme_importer/Cargo.toml | 6 +- crates/theme_importer/src/assets.rs | 26 +++ crates/theme_importer/src/main.rs | 118 ++++++++++++- crates/theme_importer/src/zed1.rs | 3 + crates/theme_importer/src/zed1/converter.rs | 176 ++++++++++++++++++++ crates/util/Cargo.toml | 6 + crates/util/src/util.rs | 7 +- 11 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 crates/theme_importer/src/assets.rs create mode 100644 crates/theme_importer/src/zed1.rs create mode 100644 crates/theme_importer/src/zed1/converter.rs diff --git a/Cargo.lock b/Cargo.lock index 1bcbd3be38..01230cd805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9834,6 +9834,7 @@ dependencies = [ "anyhow", "clap 4.4.4", "convert_case 0.6.0", + "gpui", "gpui2", "indexmap 1.9.3", "json_comments", @@ -9843,6 +9844,7 @@ dependencies = [ "serde", "simplelog", "strum", + "theme", "theme2", "uuid 1.4.1", ] diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 92ab53f15e..193651c087 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -25,7 +25,7 @@ use window::MacWindow; use crate::executor; -pub(crate) fn platform() -> Arc { +pub fn platform() -> Arc { Arc::new(MacPlatform::new()) } diff --git a/crates/gpui2/Cargo.toml b/crates/gpui2/Cargo.toml index bf0ac955a5..cb3e1af70b 100644 --- a/crates/gpui2/Cargo.toml +++ b/crates/gpui2/Cargo.toml @@ -9,6 +9,12 @@ publish = false [features] test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"] +# Suppress a panic when both GPUI1 and GPUI2 are loaded. +# +# This is used in the `theme_importer` where we need to depend on both +# GPUI1 and GPUI2 in order to convert Zed1 themes to Zed2 themes. +allow-multiple-gpui-versions = ["util/allow-multiple-gpui-versions"] + [lib] path = "src/gpui2.rs" doctest = false diff --git a/crates/theme2/src/themes/mod.rs b/crates/theme2/src/themes/mod.rs index 4059d3751e..aaf111703f 100644 --- a/crates/theme2/src/themes/mod.rs +++ b/crates/theme2/src/themes/mod.rs @@ -29,16 +29,16 @@ use crate::UserThemeFamily; pub(crate) fn all_user_themes() -> Vec { vec![ - rose_pine(), - night_owl(), andromeda(), - synthwave_84(), - palenight(), - dracula(), - solarized(), - nord(), - noctis(), ayu(), + dracula(), gruvbox(), + night_owl(), + noctis(), + nord(), + palenight(), + rose_pine(), + solarized(), + synthwave_84(), ] } diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index 23a53053ce..2841839c09 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -4,14 +4,13 @@ version = "0.1.0" edition = "2021" publish = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] any_ascii = "0.3.2" anyhow.workspace = true clap = { version = "4.4", features = ["derive"] } convert_case = "0.6.0" -gpui = { package = "gpui2", path = "../gpui2" } +gpui = { package = "gpui2", path = "../gpui2", features = ["allow-multiple-gpui-versions"] } +gpui1 = { package = "gpui", path = "../gpui" } indexmap = { version = "1.6.2", features = ["serde"] } json_comments = "0.2.2" log.workspace = true @@ -21,4 +20,5 @@ serde.workspace = true simplelog = "0.9" strum = { version = "0.25.0", features = ["derive"] } theme = { package = "theme2", path = "../theme2", features = ["importing-themes"] } +theme1 = { package = "theme", path = "../theme" } uuid.workspace = true diff --git a/crates/theme_importer/src/assets.rs b/crates/theme_importer/src/assets.rs new file mode 100644 index 0000000000..9009b4c144 --- /dev/null +++ b/crates/theme_importer/src/assets.rs @@ -0,0 +1,26 @@ +use std::borrow::Cow; + +use anyhow::{anyhow, Result}; +use gpui::{AssetSource, SharedString}; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "fonts/**/*"] +#[exclude = "*.DS_Store"] +pub struct Assets; + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result> { + Self::get(path) + .map(|f| f.data) + .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) + } + + fn list(&self, path: &str) -> Result> { + Ok(Self::iter() + .filter(|p| p.starts_with(path)) + .map(SharedString::from) + .collect()) + } +} diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index 01ab191571..e6cf8952d1 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -1,29 +1,36 @@ +mod assets; mod color; mod theme_printer; mod util; mod vscode; +mod zed1; +use std::collections::HashMap; use std::fs::{self, File}; use std::io::Write; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; +use std::sync::Arc; use any_ascii::any_ascii; use anyhow::{anyhow, Context, Result}; use clap::Parser; use convert_case::{Case, Casing}; -use gpui::serde_json; +use gpui::{serde_json, AssetSource}; use indexmap::IndexMap; use json_comments::StripComments; use log::LevelFilter; use serde::Deserialize; use simplelog::{TermLogger, TerminalMode}; -use theme::{Appearance, UserThemeFamily}; +use theme::{Appearance, UserTheme, UserThemeFamily}; +use theme1::Theme as Zed1Theme; +use crate::assets::Assets; use crate::theme_printer::UserThemeFamilyPrinter; use crate::vscode::VsCodeTheme; use crate::vscode::VsCodeThemeConverter; +use crate::zed1::Zed1ThemeConverter; #[derive(Debug, Deserialize)] struct FamilyMetadata { @@ -66,6 +73,10 @@ pub struct ThemeMetadata { #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { + /// Whether to import Zed1 themes. + #[arg(long)] + zed1: bool, + /// Whether to warn when values are missing from the theme. #[arg(long)] warn_on_missing: bool, @@ -176,6 +187,102 @@ fn main() -> Result<()> { theme_families.push(theme_family); } + if args.zed1 { + let zed1_themes_path = PathBuf::from_str("assets/themes")?; + + let zed1_theme_familes = [ + "Andromeda", + "Atelier", + "Ayu", + "Gruvbox", + "One", + "Rosé Pine", + "Sandcastle", + "Solarized", + "Summercamp", + ]; + + let mut zed1_themes_by_family: HashMap> = HashMap::from_iter( + zed1_theme_familes + .into_iter() + .map(|family| (family.to_string(), Vec::new())), + ); + + let platform = gpui1::platform::current::platform(); + let zed1_font_cache = Arc::new(gpui1::FontCache::new(platform.fonts())); + + let mut embedded_fonts = Vec::new(); + for font_path in Assets.list("fonts")? { + if font_path.ends_with(".ttf") { + let font_bytes = Assets.load(&font_path)?.to_vec(); + embedded_fonts.push(Arc::from(font_bytes)); + } + } + + platform.fonts().add_fonts(&embedded_fonts)?; + + for entry in fs::read_dir(&zed1_themes_path)? { + let entry = entry?; + + if entry.file_type()?.is_dir() { + continue; + } + + match entry.path().extension() { + None => continue, + Some(extension) => { + if extension != "json" { + continue; + } + } + } + + let theme_file_path = entry.path(); + + let theme_file = match File::open(&theme_file_path) { + Ok(file) => file, + Err(_) => { + log::info!("Failed to open file at path: {:?}", theme_file_path); + continue; + } + }; + + let theme_without_comments = StripComments::new(theme_file); + + let zed1_theme: Zed1Theme = + gpui1::fonts::with_font_cache(zed1_font_cache.clone(), || { + serde_json::from_reader(theme_without_comments) + .context(format!("failed to parse theme {theme_file_path:?}")) + })?; + + let theme_name = zed1_theme.meta.name.clone(); + + let converter = Zed1ThemeConverter::new(zed1_theme); + + let theme = converter.convert()?; + + let Some((_, themes_for_family)) = zed1_themes_by_family + .iter_mut() + .find(|(family, _)| theme_name.starts_with(*family)) + else { + log::warn!("No theme family found for '{}'.", theme_name); + continue; + }; + + themes_for_family.push(theme); + } + + for (family, themes) in zed1_themes_by_family { + let theme_family = UserThemeFamily { + name: format!("{family} (Zed1)"), + author: "Zed Industries".to_string(), + themes, + }; + + theme_families.push(theme_family); + } + } + let themes_output_path = PathBuf::from_str(OUT_PATH)?; if !themes_output_path.exists() { @@ -188,7 +295,10 @@ fn main() -> Result<()> { let mut theme_modules = Vec::new(); for theme_family in theme_families { - let theme_family_slug = any_ascii(&theme_family.name).to_case(Case::Snake); + let theme_family_slug = any_ascii(&theme_family.name) + .replace("(", "") + .replace(")", "") + .to_case(Case::Snake); let mut output_file = File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?; @@ -222,6 +332,8 @@ fn main() -> Result<()> { theme_modules.push(theme_family_slug); } + theme_modules.sort(); + let themes_vector_contents = format!( r#" use crate::UserThemeFamily; diff --git a/crates/theme_importer/src/zed1.rs b/crates/theme_importer/src/zed1.rs new file mode 100644 index 0000000000..11bc94497f --- /dev/null +++ b/crates/theme_importer/src/zed1.rs @@ -0,0 +1,3 @@ +mod converter; + +pub use converter::*; diff --git a/crates/theme_importer/src/zed1/converter.rs b/crates/theme_importer/src/zed1/converter.rs new file mode 100644 index 0000000000..1a5f7af8b7 --- /dev/null +++ b/crates/theme_importer/src/zed1/converter.rs @@ -0,0 +1,176 @@ +use anyhow::Result; +use gpui::{Hsla, Rgba}; +use gpui1::color::Color as Zed1Color; +use gpui1::fonts::HighlightStyle as Zed1HighlightStyle; +use theme::{ + Appearance, StatusColorsRefinement, ThemeColorsRefinement, UserFontStyle, UserFontWeight, + UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeStylesRefinement, +}; +use theme1::Theme as Zed1Theme; + +fn zed1_color_to_hsla(color: Zed1Color) -> Hsla { + let r = color.r as f32 / 255.; + let g = color.g as f32 / 255.; + let b = color.b as f32 / 255.; + let a = color.a as f32 / 255.; + + Hsla::from(Rgba { r, g, b, a }) +} + +fn zed1_highlight_style_to_user_highlight_style( + highlight: Zed1HighlightStyle, +) -> UserHighlightStyle { + UserHighlightStyle { + color: highlight.color.map(zed1_color_to_hsla), + font_style: highlight.italic.map(|is_italic| { + if is_italic { + UserFontStyle::Italic + } else { + UserFontStyle::Normal + } + }), + font_weight: highlight.weight.map(|weight| UserFontWeight(weight.0)), + } +} + +pub struct Zed1ThemeConverter { + theme: Zed1Theme, +} + +impl Zed1ThemeConverter { + pub fn new(theme: Zed1Theme) -> Self { + Self { theme } + } + + pub fn convert(self) -> Result { + let appearance = match self.theme.meta.is_light { + true => Appearance::Light, + false => Appearance::Dark, + }; + + let status_colors_refinement = self.convert_status_colors()?; + let theme_colors_refinement = self.convert_theme_colors()?; + let syntax_theme = self.convert_syntax_theme()?; + + Ok(UserTheme { + name: format!("{} (Zed1)", self.theme.meta.name), + appearance, + styles: UserThemeStylesRefinement { + colors: theme_colors_refinement, + status: status_colors_refinement, + syntax: Some(syntax_theme), + }, + }) + } + + fn convert_status_colors(&self) -> Result { + fn convert(color: Zed1Color) -> Option { + Some(zed1_color_to_hsla(color)) + } + + let diff_style = self.theme.editor.diff.clone(); + + Ok(StatusColorsRefinement { + created: convert(diff_style.inserted), + modified: convert(diff_style.modified), + deleted: convert(diff_style.deleted), + ..Default::default() + }) + } + + fn convert_theme_colors(&self) -> Result { + fn convert(color: Zed1Color) -> Option { + Some(zed1_color_to_hsla(color)) + } + + let tab_bar = self.theme.workspace.tab_bar.clone(); + let active_tab = self.theme.workspace.tab_bar.tab_style(true, true).clone(); + let inactive_tab = self.theme.workspace.tab_bar.tab_style(true, false).clone(); + let toolbar = self.theme.workspace.toolbar.clone(); + let scrollbar = self.theme.editor.scrollbar.clone(); + + let zed1_titlebar_border = convert(self.theme.titlebar.container.border.color); + + Ok(ThemeColorsRefinement { + border: zed1_titlebar_border, + border_variant: zed1_titlebar_border, + background: convert(self.theme.workspace.background), + title_bar_background: self + .theme + .titlebar + .container + .background_color + .map(zed1_color_to_hsla), + status_bar_background: self + .theme + .workspace + .status_bar + .container + .background_color + .map(zed1_color_to_hsla), + text: convert(self.theme.editor.text_color), + tab_bar_background: tab_bar.container.background_color.map(zed1_color_to_hsla), + tab_active_background: active_tab + .container + .background_color + .map(zed1_color_to_hsla), + tab_inactive_background: inactive_tab + .container + .background_color + .map(zed1_color_to_hsla), + toolbar_background: toolbar.container.background_color.map(zed1_color_to_hsla), + editor_foreground: convert(self.theme.editor.text_color), + editor_background: convert(self.theme.editor.background), + editor_gutter_background: convert(self.theme.editor.gutter_background), + editor_line_number: convert(self.theme.editor.line_number), + editor_active_line_number: convert(self.theme.editor.line_number_active), + editor_wrap_guide: convert(self.theme.editor.wrap_guide), + editor_active_wrap_guide: convert(self.theme.editor.active_wrap_guide), + scrollbar_track_background: scrollbar.track.background_color.map(zed1_color_to_hsla), + scrollbar_track_border: convert(scrollbar.track.border.color), + scrollbar_thumb_background: scrollbar.thumb.background_color.map(zed1_color_to_hsla), + scrollbar_thumb_border: convert(scrollbar.thumb.border.color), + scrollbar_thumb_hover_background: scrollbar + .thumb + .background_color + .map(zed1_color_to_hsla), + terminal_background: convert(self.theme.terminal.background), + terminal_ansi_bright_black: convert(self.theme.terminal.bright_black), + terminal_ansi_bright_red: convert(self.theme.terminal.bright_red), + terminal_ansi_bright_green: convert(self.theme.terminal.bright_green), + terminal_ansi_bright_yellow: convert(self.theme.terminal.bright_yellow), + terminal_ansi_bright_blue: convert(self.theme.terminal.bright_blue), + terminal_ansi_bright_magenta: convert(self.theme.terminal.bright_magenta), + terminal_ansi_bright_cyan: convert(self.theme.terminal.bright_cyan), + terminal_ansi_bright_white: convert(self.theme.terminal.bright_white), + terminal_ansi_black: convert(self.theme.terminal.black), + terminal_ansi_red: convert(self.theme.terminal.red), + terminal_ansi_green: convert(self.theme.terminal.green), + terminal_ansi_yellow: convert(self.theme.terminal.yellow), + terminal_ansi_blue: convert(self.theme.terminal.blue), + terminal_ansi_magenta: convert(self.theme.terminal.magenta), + terminal_ansi_cyan: convert(self.theme.terminal.cyan), + terminal_ansi_white: convert(self.theme.terminal.white), + ..Default::default() + }) + } + + fn convert_syntax_theme(&self) -> Result { + Ok(UserSyntaxTheme { + highlights: self + .theme + .editor + .syntax + .highlights + .clone() + .into_iter() + .map(|(name, highlight_style)| { + ( + name, + zed1_highlight_style_to_user_highlight_style(highlight_style), + ) + }) + .collect(), + }) + } +} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index cfbd7551f9..4320983c2f 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -11,6 +11,12 @@ doctest = true [features] test-support = ["tempdir", "git2"] +# Suppress a panic when both GPUI1 and GPUI2 are loaded. +# +# This is used in the `theme_importer` where we need to depend on both +# GPUI1 and GPUI2 in order to convert Zed1 themes to Zed2 themes. +allow-multiple-gpui-versions = [] + [dependencies] anyhow.workspace = true backtrace = "0.3" diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 3f2371121c..ac96cc6652 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -13,10 +13,12 @@ use std::{ ops::{AddAssign, Range, RangeInclusive}, panic::Location, pin::Pin, - sync::atomic::AtomicU32, task::{Context, Poll}, }; +#[cfg(not(feature = "allow-multiple-gpui-versions"))] +use std::sync::atomic::AtomicU32; + pub use backtrace::Backtrace; use futures::Future; use rand::{seq::SliceRandom, Rng}; @@ -434,15 +436,18 @@ impl RangeExt for RangeInclusive { } } +#[cfg(not(feature = "allow-multiple-gpui-versions"))] static GPUI_LOADED: AtomicU32 = AtomicU32::new(0); pub fn gpui2_loaded() { + #[cfg(not(feature = "allow-multiple-gpui-versions"))] if GPUI_LOADED.fetch_add(2, std::sync::atomic::Ordering::SeqCst) != 0 { panic!("=========\nYou are loading both GPUI1 and GPUI2 in the same build!\nFix Your Dependencies with cargo tree!\n=========") } } pub fn gpui1_loaded() { + #[cfg(not(feature = "allow-multiple-gpui-versions"))] if GPUI_LOADED.fetch_add(1, std::sync::atomic::Ordering::SeqCst) != 0 { panic!("=========\nYou are loading both GPUI1 and GPUI2 in the same build!\nFix Your Dependencies with cargo tree!\n=========") }