Merge pull request #1619 from zed-industries/experimental-themes

Internal themes
This commit is contained in:
Mikayla Maki 2022-09-08 16:45:47 -07:00 committed by GitHub
commit c7df17b9af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 165 additions and 74 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
/assets/themes/internal/*.json
/assets/themes/experiments/*.json

View file

View file

@ -0,0 +1 @@
[]

View file

View file

View file

@ -42,8 +42,15 @@ struct ActionWithData(Box<str>, Box<RawValue>);
impl KeymapFileContent {
pub fn load_defaults(cx: &mut MutableAppContext) {
let settings = cx.global::<Settings>();
let mut paths = vec!["keymaps/default.json", "keymaps/vim.json"];
paths.extend(cx.global::<Settings>().experiments.keymap_files());
if settings.staff_mode {
paths.push("keymaps/internal.json")
}
paths.extend(settings.experiments.keymap_files());
for path in paths {
Self::load(path, cx).unwrap();
}

View file

@ -37,10 +37,13 @@ pub struct Settings {
pub language_overrides: HashMap<Arc<str>, EditorSettings>,
pub lsp: HashMap<Arc<str>, LspSettings>,
pub theme: Arc<Theme>,
pub staff_mode: bool,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct FeatureFlags {}
pub struct FeatureFlags {
pub experimental_themes: bool,
}
impl FeatureFlags {
pub fn keymap_files(&self) -> Vec<&'static str> {
@ -175,6 +178,8 @@ pub struct SettingsFileContent {
pub lsp: HashMap<Arc<str>, LspSettings>,
#[serde(default)]
pub theme: Option<String>,
#[serde(default)]
pub staff_mode: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
@ -226,6 +231,8 @@ impl Settings {
language_overrides: Default::default(),
lsp: defaults.lsp.clone(),
theme: themes.get(&defaults.theme.unwrap()).unwrap(),
staff_mode: false,
}
}
@ -260,7 +267,7 @@ impl Settings {
merge(&mut self.vim_mode, data.vim_mode);
merge(&mut self.autosave, data.autosave);
merge(&mut self.experiments, data.experiments);
merge(&mut self.staff_mode, data.staff_mode);
// Ensure terminal font is loaded, so we can request it in terminal_element layout
if let Some(terminal_font) = &data.terminal.font_family {
font_cache.load_family(&[terminal_font]).log_err();
@ -345,6 +352,7 @@ impl Settings {
lsp: Default::default(),
projects_online_by_default: true,
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
staff_mode: false,
}
}
@ -400,27 +408,25 @@ pub fn settings_file_json_schema(
("ThemeName".into(), theme_name_schema.into()),
("Languages".into(), languages_object_schema.into()),
]);
root_schema
.schema
.object
.as_mut()
.unwrap()
.properties
.extend([
(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
),
(
"languages".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
// For backward compatibility
(
"language_overrides".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
]);
let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
// Avoid automcomplete for non-user facing settings
root_schema_object.properties.remove("staff_mode");
root_schema_object.properties.extend([
(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
),
(
"languages".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
// For backward compatibility
(
"language_overrides".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
]);
serde_json::to_value(root_schema).unwrap()
}

View file

@ -15,7 +15,7 @@ pub use theme_registry::*;
#[derive(Deserialize, Default)]
pub struct Theme {
#[serde(default)]
pub name: String,
pub meta: ThemeMeta,
pub workspace: Workspace,
pub context_menu: ContextMenu,
pub chat_panel: ChatPanel,
@ -34,6 +34,12 @@ pub struct Theme {
pub terminal: TerminalStyle,
}
#[derive(Deserialize, Default, Clone)]
pub struct ThemeMeta {
pub name: String,
pub is_light: bool,
}
#[derive(Deserialize, Default)]
pub struct Workspace {
pub background: Color,

View file

@ -1,4 +1,4 @@
use crate::Theme;
use crate::{Theme, ThemeMeta};
use anyhow::{Context, Result};
use gpui::{fonts, AssetSource, FontCache};
use parking_lot::Mutex;
@ -22,11 +22,27 @@ impl ThemeRegistry {
})
}
pub fn list(&self) -> impl Iterator<Item = String> {
self.assets.list("themes/").into_iter().filter_map(|path| {
pub fn list(&self, internal: bool, experiments: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
let mut dirs = self.assets.list("themes/");
if !internal {
dirs = dirs
.into_iter()
.filter(|path| !path.starts_with("themes/internal"))
.collect()
}
if !experiments {
dirs = dirs
.into_iter()
.filter(|path| !path.starts_with("themes/experiments"))
.collect()
}
dirs.into_iter().filter_map(|path| {
let filename = path.strip_prefix("themes/")?;
let theme_name = filename.strip_suffix(".json")?;
Some(theme_name.to_string())
self.get(theme_name).ok().map(|theme| theme.meta.clone())
})
}
@ -50,7 +66,8 @@ impl ThemeRegistry {
serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json))
})?;
theme.name = name.into();
// Reset name to be the file path, so that we can use it to access the stored themes
theme.meta.name = name.into();
let theme = Arc::new(theme);
self.themes.lock().insert(name.to_string(), theme.clone());
Ok(theme)

View file

@ -6,12 +6,12 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use theme::{Theme, ThemeRegistry};
use theme::{Theme, ThemeMeta, ThemeRegistry};
use workspace::{AppState, Workspace};
pub struct ThemeSelector {
registry: Arc<ThemeRegistry>,
theme_names: Vec<String>,
theme_data: Vec<ThemeMeta>,
matches: Vec<StringMatch>,
original_theme: Arc<Theme>,
picker: ViewHandle<Picker<Self>>,
@ -39,32 +39,41 @@ impl ThemeSelector {
fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
let picker = cx.add_view(|cx| Picker::new(handle, cx));
let original_theme = cx.global::<Settings>().theme.clone();
let mut theme_names = registry.list().collect::<Vec<_>>();
let settings = cx.global::<Settings>();
let original_theme = settings.theme.clone();
let mut theme_names = registry
.list(
settings.staff_mode,
settings.experiments.experimental_themes,
)
.collect::<Vec<_>>();
theme_names.sort_unstable_by(|a, b| {
a.ends_with("dark")
.cmp(&b.ends_with("dark"))
.then_with(|| a.cmp(b))
a.is_light
.cmp(&b.is_light)
.reverse()
.then(a.name.cmp(&b.name))
});
let matches = theme_names
.iter()
.map(|name| StringMatch {
.map(|meta| StringMatch {
candidate_id: 0,
score: 0.0,
positions: Default::default(),
string: name.clone(),
string: meta.name.clone(),
})
.collect();
let mut this = Self {
registry,
theme_names,
theme_data: theme_names,
matches,
picker,
original_theme: original_theme.clone(),
selected_index: 0,
selection_completed: false,
};
this.select_if_matching(&original_theme.name);
this.select_if_matching(&original_theme.meta.name);
this
}
@ -82,7 +91,7 @@ impl ThemeSelector {
#[cfg(debug_assertions)]
pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
let current_theme_name = cx.global::<Settings>().theme.name.clone();
let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
themes.clear();
match themes.get(&current_theme_name) {
Ok(theme) => {
@ -165,13 +174,13 @@ impl PickerDelegate for ThemeSelector {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
let background = cx.background().clone();
let candidates = self
.theme_names
.theme_data
.iter()
.enumerate()
.map(|(id, name)| StringMatchCandidate {
.map(|(id, meta)| StringMatchCandidate {
id,
char_bag: name.as_str().into(),
string: name.clone(),
char_bag: meta.name.as_str().into(),
string: meta.name.clone(),
})
.collect::<Vec<_>>();

View file

@ -60,6 +60,7 @@ fn main() {
load_embedded_fonts(&app);
let fs = Arc::new(RealFs);
let themes = ThemeRegistry::new(Assets, app.font_cache());
let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);

View file

@ -9,6 +9,7 @@ lazy_static::lazy_static! {
pub static ref DB: PathBuf = DB_DIR.join("zed.db");
pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
}

View file

@ -244,7 +244,16 @@ pub fn initialize_workspace(
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
let theme_names = app_state.themes.list().collect();
let settings = cx.global::<Settings>();
let theme_names = app_state
.themes
.list(
settings.staff_mode,
settings.experiments.experimental_themes,
)
.map(|meta| meta.name)
.collect();
let language_names = &languages::LANGUAGE_NAMES;
workspace.project().update(cx, |project, cx| {
@ -1668,12 +1677,12 @@ mod tests {
let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
let mut has_default_theme = false;
for theme_name in themes.list() {
for theme_name in themes.list(false, false).map(|meta| meta.name) {
let theme = themes.get(&theme_name).unwrap();
if theme.name == settings.theme.name {
if theme.meta.name == settings.theme.meta.name {
has_default_theme = true;
}
assert_eq!(theme.name, theme_name);
assert_eq!(theme.meta.name, theme_name);
}
assert!(has_default_theme);
}

View file

@ -2,29 +2,44 @@ import * as fs from "fs";
import * as path from "path";
import { tmpdir } from "os";
import app from "./styleTree/app";
import themes from "./themes";
import themes, { internalThemes, experimentalThemes } from "./themes";
import snakeCase from "./utils/snakeCase";
import Theme from "./themes/common/theme";
const themeDirectory = `${__dirname}/../../assets/themes/`;
const themeDirectory = `${__dirname}/../../assets/themes`;
const internalDirectory = `${themeDirectory}/internal`;
const experimentsDirectory = `${themeDirectory}/experiments`;
const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"));
// Clear existing themes
for (const file of fs.readdirSync(themeDirectory)) {
if (file.endsWith(".json")) {
const name = file.replace(/\.json$/, "");
if (!themes.find((theme) => theme.name === name)) {
fs.unlinkSync(path.join(themeDirectory, file));
function clearThemes(themeDirectory: string) {
for (const file of fs.readdirSync(themeDirectory)) {
if (file.endsWith(".json")) {
const name = file.replace(/\.json$/, "");
if (!themes.find((theme) => theme.name === name)) {
fs.unlinkSync(path.join(themeDirectory, file));
}
}
}
}
// Write new themes to theme directory
for (let theme of themes) {
let styleTree = snakeCase(app(theme));
let styleTreeJSON = JSON.stringify(styleTree, null, 2);
let tempPath = path.join(tempDirectory, `${theme.name}.json`);
let outPath = path.join(themeDirectory, `${theme.name}.json`);
fs.writeFileSync(tempPath, styleTreeJSON);
fs.renameSync(tempPath, outPath);
console.log(`- ${outPath} created`);
clearThemes(themeDirectory);
clearThemes(internalDirectory);
clearThemes(experimentsDirectory);
function writeThemes(themes: Theme[], outputDirectory: string) {
for (let theme of themes) {
let styleTree = snakeCase(app(theme));
let styleTreeJSON = JSON.stringify(styleTree, null, 2);
let tempPath = path.join(tempDirectory, `${theme.name}.json`);
let outPath = path.join(outputDirectory, `${theme.name}.json`);
fs.writeFileSync(tempPath, styleTreeJSON);
fs.renameSync(tempPath, outPath);
console.log(`- ${outPath} created`);
}
}
// Write new themes to theme directory
writeThemes(themes, themeDirectory);
writeThemes(internalThemes, internalDirectory);
writeThemes(experimentalThemes, experimentsDirectory);

View file

@ -22,6 +22,10 @@ export const panel = {
export default function app(theme: Theme): Object {
return {
meta: {
name: theme.name,
isLight: theme.isLight
},
picker: picker(theme),
workspace: workspace(theme),
contextMenu: contextMenu(theme),

View file

@ -5,14 +5,27 @@ import Theme from "./themes/common/theme";
const themes: Theme[] = [];
export default themes;
const themesPath = path.resolve(`${__dirname}/themes`);
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue;
const filePath = path.join(themesPath, fileName);
const internalThemes: Theme[] = [];
export { internalThemes }
if (fs.statSync(filePath).isFile()) {
const theme = require(filePath);
if (theme.dark) themes.push(theme.dark);
if (theme.light) themes.push(theme.light);
const experimentalThemes: Theme[] = [];
export { experimentalThemes }
function fillThemes(themesPath: string, themes: Theme[]) {
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue;
const filePath = path.join(themesPath, fileName);
if (fs.statSync(filePath).isFile()) {
const theme = require(filePath);
if (theme.dark) themes.push(theme.dark);
if (theme.light) themes.push(theme.light);
}
}
}
fillThemes(path.resolve(`${__dirname}/themes`), themes)
fillThemes(path.resolve(`${__dirname}/themes/internal`), internalThemes)
fillThemes(path.resolve(`${__dirname}/themes/experiments`), experimentalThemes)

View file

View file