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
This commit is contained in:
Marshall Bowers 2023-12-22 13:47:30 -05:00 committed by GitHub
parent c34a81152f
commit dc64411cca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 352 additions and 16 deletions

2
Cargo.lock generated
View File

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

View File

@ -25,7 +25,7 @@ use window::MacWindow;
use crate::executor;
pub(crate) fn platform() -> Arc<dyn super::Platform> {
pub fn platform() -> Arc<dyn super::Platform> {
Arc::new(MacPlatform::new())
}

View File

@ -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

View File

@ -29,16 +29,16 @@ use crate::UserThemeFamily;
pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {
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(),
]
}

View File

@ -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

View File

@ -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<Cow<[u8]>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
Ok(Self::iter()
.filter(|p| p.starts_with(path))
.map(SharedString::from)
.collect())
}
}

View File

@ -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<String, Vec<UserTheme>> = 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;

View File

@ -0,0 +1,3 @@
mod converter;
pub use converter::*;

View File

@ -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<UserTheme> {
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<StatusColorsRefinement> {
fn convert(color: Zed1Color) -> Option<Hsla> {
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<ThemeColorsRefinement> {
fn convert(color: Zed1Color) -> Option<Hsla> {
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<UserSyntaxTheme> {
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(),
})
}
}

View File

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

View File

@ -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<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
}
}
#[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=========")
}