This PR adds the initial support for loading extensions in Zed.
### Extensions Directory
Extensions are loaded from the extensions directory.
The extensions directory has the following structure:
```
extensions/
installed/
extension-a/
grammars/
languages/
extension-b/
themes/
manifest.json
```
The `manifest.json` file is used internally by Zed to keep track of
which extensions are installed. This file should be maintained
automatically, and shouldn't require any direct interaction with it.
Extensions can provide Tree-sitter grammars, languages, and themes.
Release Notes:
- N/A
---------
Co-authored-by: Marshall <marshall@zed.dev>
290 lines
9.8 KiB
Rust
290 lines
9.8 KiB
Rust
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{anyhow, Context, Result};
|
|
use collections::HashMap;
|
|
use derive_more::{Deref, DerefMut};
|
|
use fs::Fs;
|
|
use futures::StreamExt;
|
|
use gpui::{AppContext, AssetSource, Global, HighlightStyle, SharedString};
|
|
use parking_lot::RwLock;
|
|
use refineable::Refineable;
|
|
use util::ResultExt;
|
|
|
|
use crate::{
|
|
try_parse_color, Appearance, AppearanceContent, PlayerColor, PlayerColors, StatusColors,
|
|
SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
|
|
ThemeStyles,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ThemeMeta {
|
|
pub name: SharedString,
|
|
pub appearance: Appearance,
|
|
}
|
|
|
|
/// The global [`ThemeRegistry`].
|
|
///
|
|
/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when
|
|
/// inserting the [`ThemeRegistry`] into the context as a global.
|
|
///
|
|
/// This should not be exposed outside of this module.
|
|
#[derive(Default, Deref, DerefMut)]
|
|
struct GlobalThemeRegistry(Arc<ThemeRegistry>);
|
|
|
|
impl Global for GlobalThemeRegistry {}
|
|
|
|
struct ThemeRegistryState {
|
|
themes: HashMap<SharedString, Arc<Theme>>,
|
|
}
|
|
|
|
pub struct ThemeRegistry {
|
|
state: RwLock<ThemeRegistryState>,
|
|
assets: Box<dyn AssetSource>,
|
|
}
|
|
|
|
impl ThemeRegistry {
|
|
/// Returns the global [`ThemeRegistry`].
|
|
pub fn global(cx: &AppContext) -> Arc<Self> {
|
|
cx.global::<GlobalThemeRegistry>().0.clone()
|
|
}
|
|
|
|
/// Returns the global [`ThemeRegistry`].
|
|
///
|
|
/// Inserts a default [`ThemeRegistry`] if one does not yet exist.
|
|
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
|
|
cx.default_global::<GlobalThemeRegistry>().0.clone()
|
|
}
|
|
|
|
/// Sets the global [`ThemeRegistry`].
|
|
pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
|
|
cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
|
|
}
|
|
|
|
pub fn new(assets: Box<dyn AssetSource>) -> Self {
|
|
let registry = Self {
|
|
state: RwLock::new(ThemeRegistryState {
|
|
themes: HashMap::default(),
|
|
}),
|
|
assets,
|
|
};
|
|
|
|
// We're loading our new versions of the One themes by default, as
|
|
// we need them to be loaded for tests.
|
|
//
|
|
// These themes will get overwritten when `load_user_themes` is called
|
|
// when Zed starts, so the One variants used will be the ones ported from Zed1.
|
|
registry.insert_theme_families([crate::one_themes::one_family()]);
|
|
|
|
registry
|
|
}
|
|
|
|
fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
|
|
for family in families.into_iter() {
|
|
self.insert_themes(family.themes);
|
|
}
|
|
}
|
|
|
|
fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
|
|
let mut state = self.state.write();
|
|
for theme in themes.into_iter() {
|
|
state.themes.insert(theme.name.clone(), Arc::new(theme));
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
|
|
for family in families.into_iter() {
|
|
self.insert_user_themes(family.themes);
|
|
}
|
|
}
|
|
|
|
pub fn insert_user_themes(&self, themes: impl IntoIterator<Item = ThemeContent>) {
|
|
self.insert_themes(themes.into_iter().map(|user_theme| {
|
|
let mut theme_colors = match user_theme.appearance {
|
|
AppearanceContent::Light => ThemeColors::light(),
|
|
AppearanceContent::Dark => ThemeColors::dark(),
|
|
};
|
|
theme_colors.refine(&user_theme.style.theme_colors_refinement());
|
|
|
|
let mut status_colors = match user_theme.appearance {
|
|
AppearanceContent::Light => StatusColors::light(),
|
|
AppearanceContent::Dark => StatusColors::dark(),
|
|
};
|
|
status_colors.refine(&user_theme.style.status_colors_refinement());
|
|
|
|
let mut player_colors = match user_theme.appearance {
|
|
AppearanceContent::Light => PlayerColors::light(),
|
|
AppearanceContent::Dark => PlayerColors::dark(),
|
|
};
|
|
if !user_theme.style.players.is_empty() {
|
|
player_colors = PlayerColors(
|
|
user_theme
|
|
.style
|
|
.players
|
|
.into_iter()
|
|
.map(|player| PlayerColor {
|
|
cursor: player
|
|
.cursor
|
|
.as_ref()
|
|
.and_then(|color| try_parse_color(&color).ok())
|
|
.unwrap_or_default(),
|
|
background: player
|
|
.background
|
|
.as_ref()
|
|
.and_then(|color| try_parse_color(&color).ok())
|
|
.unwrap_or_default(),
|
|
selection: player
|
|
.selection
|
|
.as_ref()
|
|
.and_then(|color| try_parse_color(&color).ok())
|
|
.unwrap_or_default(),
|
|
})
|
|
.collect(),
|
|
);
|
|
}
|
|
|
|
let mut syntax_colors = match user_theme.appearance {
|
|
AppearanceContent::Light => SyntaxTheme::light(),
|
|
AppearanceContent::Dark => SyntaxTheme::dark(),
|
|
};
|
|
if !user_theme.style.syntax.is_empty() {
|
|
syntax_colors.highlights = user_theme
|
|
.style
|
|
.syntax
|
|
.iter()
|
|
.map(|(syntax_token, highlight)| {
|
|
(
|
|
syntax_token.clone(),
|
|
HighlightStyle {
|
|
color: highlight
|
|
.color
|
|
.as_ref()
|
|
.and_then(|color| try_parse_color(&color).ok()),
|
|
font_style: highlight.font_style.map(Into::into),
|
|
font_weight: highlight.font_weight.map(Into::into),
|
|
..Default::default()
|
|
},
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
}
|
|
|
|
Theme {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
name: user_theme.name.into(),
|
|
appearance: match user_theme.appearance {
|
|
AppearanceContent::Light => Appearance::Light,
|
|
AppearanceContent::Dark => Appearance::Dark,
|
|
},
|
|
styles: ThemeStyles {
|
|
system: SystemColors::default(),
|
|
colors: theme_colors,
|
|
status: status_colors,
|
|
player: player_colors,
|
|
syntax: Arc::new(syntax_colors),
|
|
accents: Vec::new(),
|
|
},
|
|
}
|
|
}));
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.state.write().themes.clear();
|
|
}
|
|
|
|
pub fn list_names(&self, _staff: bool) -> Vec<SharedString> {
|
|
let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
|
|
names.sort();
|
|
names
|
|
}
|
|
|
|
pub fn list(&self, _staff: bool) -> Vec<ThemeMeta> {
|
|
self.state
|
|
.read()
|
|
.themes
|
|
.values()
|
|
.map(|theme| ThemeMeta {
|
|
name: theme.name.clone(),
|
|
appearance: theme.appearance(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
|
self.state
|
|
.read()
|
|
.themes
|
|
.get(name)
|
|
.ok_or_else(|| anyhow!("theme not found: {}", name))
|
|
.cloned()
|
|
}
|
|
|
|
/// Loads the themes bundled with the Zed binary and adds them to the registry.
|
|
pub fn load_bundled_themes(&self) {
|
|
let theme_paths = self
|
|
.assets
|
|
.list("themes/")
|
|
.expect("failed to list theme assets")
|
|
.into_iter()
|
|
.filter(|path| path.ends_with(".json"));
|
|
|
|
for path in theme_paths {
|
|
let Some(theme) = self.assets.load(&path).log_err() else {
|
|
continue;
|
|
};
|
|
|
|
let Some(theme_family) = serde_json::from_slice(&theme)
|
|
.with_context(|| format!("failed to parse theme at path \"{path}\""))
|
|
.log_err()
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
self.insert_user_theme_families([theme_family]);
|
|
}
|
|
}
|
|
|
|
/// 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<dyn Fs>) -> 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(())
|
|
}
|
|
|
|
pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
|
|
let reader = fs.open_sync(&theme_path).await?;
|
|
let theme = serde_json_lenient::from_reader(reader)?;
|
|
|
|
Ok(theme)
|
|
}
|
|
|
|
/// 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<dyn Fs>) -> Result<()> {
|
|
let theme = Self::read_user_theme(theme_path, fs).await?;
|
|
|
|
self.insert_user_theme_families([theme]);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Default for ThemeRegistry {
|
|
fn default() -> Self {
|
|
Self::new(Box::new(()))
|
|
}
|
|
}
|