Add support for applying theme after extension is installed (#9529)

Release Notes:

- Added support for opening the theme selector with installed themes
after installing an extension containing themes.
([#9228](https://github.com/zed-industries/zed/issues/9228)).

<img width="1315" alt="Screenshot 2024-03-20 at 11 00 35 AM"
src="https://github.com/zed-industries/zed/assets/1486634/593389b3-eade-4bce-ae17-25c02a074f21">

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Jason Lee 2024-03-20 23:13:58 +08:00 committed by GitHub
parent 6cec389125
commit 269d2513ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 102 additions and 16 deletions

2
Cargo.lock generated
View File

@ -3528,6 +3528,7 @@ dependencies = [
"settings", "settings",
"smallvec", "smallvec",
"theme", "theme",
"theme_selector",
"ui", "ui",
"util", "util",
"workspace", "workspace",
@ -9619,6 +9620,7 @@ dependencies = [
"gpui", "gpui",
"log", "log",
"picker", "picker",
"serde",
"settings", "settings",
"theme", "theme",
"ui", "ui",

View File

@ -689,7 +689,7 @@ impl CollabTitlebarItem {
ContextMenu::build(cx, |menu, _| { ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone()) .action("Extensions", extensions_ui::Extensions.boxed_clone())
.action("Themes...", theme_selector::Toggle.boxed_clone()) .action("Themes...", theme_selector::Toggle::default().boxed_clone())
.separator() .separator()
.action("Sign Out", client::SignOut.boxed_clone()) .action("Sign Out", client::SignOut.boxed_clone())
}) })
@ -713,7 +713,7 @@ impl CollabTitlebarItem {
ContextMenu::build(cx, |menu, _| { ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone()) .action("Extensions", extensions_ui::Extensions.boxed_clone())
.action("Themes...", theme_selector::Toggle.boxed_clone()) .action("Themes...", theme_selector::Toggle::default().boxed_clone())
}) })
.into() .into()
}) })

View File

@ -92,16 +92,18 @@ pub enum ExtensionStatus {
Removing, Removing,
} }
#[derive(Clone, Copy)]
enum ExtensionOperation { enum ExtensionOperation {
Upgrade, Upgrade,
Install, Install,
Remove, Remove,
} }
#[derive(Copy, Clone)] #[derive(Clone)]
pub enum Event { pub enum Event {
ExtensionsUpdated, ExtensionsUpdated,
StartedReloading, StartedReloading,
ExtensionInstalled(Arc<str>),
} }
impl EventEmitter<Event> for ExtensionStore {} impl EventEmitter<Event> for ExtensionStore {}
@ -330,6 +332,7 @@ impl ExtensionStore {
.unbounded_send(modified_extension) .unbounded_send(modified_extension)
.expect("reload task exited"); .expect("reload task exited");
cx.emit(Event::StartedReloading); cx.emit(Event::StartedReloading);
async move { async move {
rx.await.ok(); rx.await.ok();
} }
@ -358,6 +361,17 @@ impl ExtensionStore {
.filter_map(|extension| extension.dev.then_some(&extension.manifest)) .filter_map(|extension| extension.dev.then_some(&extension.manifest))
} }
/// Returns the names of themes provided by extensions.
pub fn extension_themes<'a>(
&'a self,
extension_id: &'a str,
) -> impl Iterator<Item = &'a Arc<str>> {
self.extension_index
.themes
.iter()
.filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name))
}
pub fn fetch_extensions( pub fn fetch_extensions(
&self, &self,
search: Option<&str>, search: Option<&str>,
@ -441,8 +455,21 @@ impl ExtensionStore {
archive archive
.unpack(extensions_dir.join(extension_id.as_ref())) .unpack(extensions_dir.join(extension_id.as_ref()))
.await?; .await?;
this.update(&mut cx, |this, cx| this.reload(Some(extension_id), cx))? this.update(&mut cx, |this, cx| {
this.reload(Some(extension_id.clone()), cx)
})?
.await; .await;
match operation {
ExtensionOperation::Install => {
this.update(&mut cx, |_, cx| {
cx.emit(Event::ExtensionInstalled(extension_id));
})
.ok();
}
_ => {}
}
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);

View File

@ -28,6 +28,7 @@ serde.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true smallvec.workspace = true
theme.workspace = true theme.workspace = true
theme_selector.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
workspace.workspace = true workspace.workspace = true

View File

@ -9,7 +9,7 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle, actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle, FontWeight, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WhiteSpace, WindowContext, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
}; };
use settings::Settings; use settings::Settings;
use std::ops::DerefMut; use std::ops::DerefMut;
@ -100,10 +100,14 @@ impl ExtensionsPage {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> { pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
cx.new_view(|cx: &mut ViewContext<Self>| { cx.new_view(|cx: &mut ViewContext<Self>| {
let store = ExtensionStore::global(cx); let store = ExtensionStore::global(cx);
let workspace_handle = workspace.weak_handle();
let subscriptions = [ let subscriptions = [
cx.observe(&store, |_, _, cx| cx.notify()), cx.observe(&store, |_, _, cx| cx.notify()),
cx.subscribe(&store, |this, _, event, cx| match event { cx.subscribe(&store, move |this, _, event, cx| match event {
extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx), extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),
extension::Event::ExtensionInstalled(extension_id) => {
this.on_extension_installed(workspace_handle.clone(), extension_id, cx)
}
_ => {} _ => {}
}), }),
]; ];
@ -133,6 +137,32 @@ impl ExtensionsPage {
}) })
} }
fn on_extension_installed(
&mut self,
workspace: WeakView<Workspace>,
extension_id: &str,
cx: &mut ViewContext<Self>,
) {
let extension_store = ExtensionStore::global(cx).read(cx);
let themes = extension_store
.extension_themes(extension_id)
.map(|name| name.to_string())
.collect::<Vec<_>>();
if !themes.is_empty() {
workspace
.update(cx, |workspace, cx| {
theme_selector::toggle(
workspace,
&theme_selector::Toggle {
themes_filter: Some(themes),
},
cx,
)
})
.ok();
}
}
fn filter_extension_entries(&mut self, cx: &mut ViewContext<Self>) { fn filter_extension_entries(&mut self, cx: &mut ViewContext<Self>) {
let extension_store = ExtensionStore::global(cx).read(cx); let extension_store = ExtensionStore::global(cx).read(cx);

View File

@ -20,6 +20,7 @@ fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
log.workspace = true log.workspace = true
picker.workspace = true picker.workspace = true
serde.workspace = true
settings.workspace = true settings.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true

View File

@ -3,10 +3,11 @@ use feature_flags::FeatureFlagAppExt;
use fs::Fs; use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, View, ViewContext, actions, impl_actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, View,
VisualContext, WeakView, ViewContext, VisualContext, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use serde::Deserialize;
use settings::{update_settings_file, SettingsStore}; use settings::{update_settings_file, SettingsStore};
use std::sync::Arc; use std::sync::Arc;
use theme::{ use theme::{
@ -16,7 +17,14 @@ use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
use util::ResultExt; use util::ResultExt;
use workspace::{ui::HighlightedLabel, ModalView, Workspace}; use workspace::{ui::HighlightedLabel, ModalView, Workspace};
actions!(theme_selector, [Toggle, Reload]); #[derive(PartialEq, Clone, Default, Debug, Deserialize)]
pub struct Toggle {
/// A list of theme names to filter the theme selector down to.
pub themes_filter: Option<Vec<String>>,
}
impl_actions!(theme_selector, [Toggle]);
actions!(theme_selector, [Reload]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views( cx.observe_new_views(
@ -27,14 +35,18 @@ pub fn init(cx: &mut AppContext) {
.detach(); .detach();
} }
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { pub fn toggle(workspace: &mut Workspace, toggle: &Toggle, cx: &mut ViewContext<Workspace>) {
let fs = workspace.app_state().fs.clone(); let fs = workspace.app_state().fs.clone();
let telemetry = workspace.client().telemetry().clone(); let telemetry = workspace.client().telemetry().clone();
workspace.toggle_modal(cx, |cx| { workspace.toggle_modal(cx, |cx| {
ThemeSelector::new( let delegate = ThemeSelectorDelegate::new(
ThemeSelectorDelegate::new(cx.view().downgrade(), fs, telemetry, cx), cx.view().downgrade(),
fs,
telemetry,
toggle.themes_filter.as_ref(),
cx, cx,
) );
ThemeSelector::new(delegate, cx)
}); });
} }
@ -81,13 +93,25 @@ impl ThemeSelectorDelegate {
weak_view: WeakView<ThemeSelector>, weak_view: WeakView<ThemeSelector>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
telemetry: Arc<Telemetry>, telemetry: Arc<Telemetry>,
themes_filter: Option<&Vec<String>>,
cx: &mut ViewContext<ThemeSelector>, cx: &mut ViewContext<ThemeSelector>,
) -> Self { ) -> Self {
let original_theme = cx.theme().clone(); let original_theme = cx.theme().clone();
let staff_mode = cx.is_staff(); let staff_mode = cx.is_staff();
let registry = ThemeRegistry::global(cx); let registry = ThemeRegistry::global(cx);
let mut themes = registry.list(staff_mode); let mut themes = registry
.list(staff_mode)
.into_iter()
.filter(|meta| {
if let Some(theme_filter) = themes_filter {
theme_filter.contains(&meta.name.to_string())
} else {
true
}
})
.collect::<Vec<_>>();
themes.sort_unstable_by(|a, b| { themes.sort_unstable_by(|a, b| {
a.appearance a.appearance
.is_light() .is_light()
@ -113,6 +137,7 @@ impl ThemeSelectorDelegate {
telemetry, telemetry,
view: weak_view, view: weak_view,
}; };
this.select_if_matching(&original_theme.name); this.select_if_matching(&original_theme.name);
this this
} }

View File

@ -20,7 +20,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
MenuItem::action("Open Local Settings", super::OpenLocalSettings), MenuItem::action("Open Local Settings", super::OpenLocalSettings),
MenuItem::action("Select Theme...", theme_selector::Toggle), MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
], ],
}), }),
MenuItem::action("Extensions", extensions_ui::Extensions), MenuItem::action("Extensions", extensions_ui::Extensions),