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

Mikayla Maki created

Internal themes

Change summary

.gitignore                                  |  2 
assets/keymaps/experiments/.gitkeep         |  0 
assets/keymaps/internal.json                |  1 
assets/themes/experiments/.gitkeep          |  0 
assets/themes/internal/.gitkeep             |  0 
crates/settings/src/keymap_file.rs          |  9 +++
crates/settings/src/settings.rs             | 52 ++++++++++++----------
crates/theme/src/theme.rs                   |  8 +++
crates/theme/src/theme_registry.rs          | 27 +++++++++--
crates/theme_selector/src/theme_selector.rs | 41 +++++++++++-------
crates/zed/src/main.rs                      |  1 
crates/zed/src/paths.rs                     |  1 
crates/zed/src/zed.rs                       | 17 +++++-
styles/src/buildThemes.ts                   | 47 +++++++++++++-------
styles/src/styleTree/app.ts                 |  4 +
styles/src/themes.ts                        | 31 +++++++++---
styles/src/themes/experiments/.gitkeep      |  0 
styles/src/themes/internal/.gitkeep         |  0 
18 files changed, 166 insertions(+), 75 deletions(-)

Detailed changes

.gitignore 🔗

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

crates/settings/src/keymap_file.rs 🔗

@@ -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();
         }

crates/settings/src/settings.rs 🔗

@@ -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()
 }

crates/theme/src/theme.rs 🔗

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

crates/theme/src/theme_registry.rs 🔗

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

crates/theme_selector/src/theme_selector.rs 🔗

@@ -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<_>>();
 

crates/zed/src/main.rs 🔗

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

crates/zed/src/paths.rs 🔗

@@ -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");
 }

crates/zed/src/zed.rs 🔗

@@ -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);
     }

styles/src/buildThemes.ts 🔗

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

styles/src/styleTree/app.ts 🔗

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

styles/src/themes.ts 🔗

@@ -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);
-
-  if (fs.statSync(filePath).isFile()) {
-    const theme = require(filePath);
-    if (theme.dark) themes.push(theme.dark);
-    if (theme.light) themes.push(theme.light);
+const internalThemes: Theme[] = [];
+export { internalThemes }
+
+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)
+