Detailed changes
@@ -14755,6 +14755,25 @@ dependencies = [
"zlog",
]
+[[package]]
+name = "settings_profile_selector"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "fuzzy",
+ "gpui",
+ "language",
+ "menu",
+ "picker",
+ "project",
+ "serde_json",
+ "settings",
+ "ui",
+ "workspace",
+ "workspace-hack",
+ "zed_actions",
+]
+
[[package]]
name = "settings_ui"
version = "0.1.0"
@@ -20321,6 +20340,7 @@ dependencies = [
"serde_json",
"session",
"settings",
+ "settings_profile_selector",
"settings_ui",
"shellexpand 2.1.2",
"smol",
@@ -119,6 +119,7 @@ members = [
"crates/paths",
"crates/picker",
"crates/prettier",
+ "crates/settings_profile_selector",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
@@ -210,7 +211,7 @@ members = [
#
"tooling/workspace-hack",
- "tooling/xtask",
+ "tooling/xtask", "crates/settings_profile_selector",
]
default-members = ["crates/zed"]
@@ -342,6 +343,7 @@ picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
+settings_profile_selector = { path = "crates/settings_profile_selector" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@@ -1877,5 +1877,8 @@
"save_breakpoints": true,
"dock": "bottom",
"button": true
- }
+ },
+ // Configures any number of settings profiles that are temporarily applied
+ // when selected from `settings profile selector: toggle`.
+ "profiles": []
}
@@ -7,7 +7,7 @@ mod settings_json;
mod settings_store;
mod vscode_import;
-use gpui::App;
+use gpui::{App, Global};
use rust_embed::RustEmbed;
use std::{borrow::Cow, fmt, str};
use util::asset_str;
@@ -27,6 +27,11 @@ pub use settings_store::{
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
+#[derive(Clone, Debug, PartialEq)]
+pub struct ActiveSettingsProfileName(pub String);
+
+impl Global for ActiveSettingsProfileName {}
+
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -74,6 +79,7 @@ pub fn init(cx: &mut App) {
.unwrap();
cx.set_global(settings);
BaseKeymap::register(cx);
+ SettingsStore::observe_active_settings_profile_name(cx).detach();
}
pub fn default_settings() -> Cow<'static, str> {
@@ -26,8 +26,8 @@ use util::{
pub type EditorconfigProperties = ec4rs::Properties;
use crate::{
- ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
- parse_json_with_comments, update_value_in_json_text,
+ ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
+ WorktreeId, parse_json_with_comments, update_value_in_json_text,
};
/// A value that can be defined as a user setting.
@@ -122,6 +122,8 @@ pub struct SettingsSources<'a, T> {
pub user: Option<&'a T>,
/// The user settings for the current release channel.
pub release_channel: Option<&'a T>,
+ /// The settings associated with an enabled settings profile
+ pub profile: Option<&'a T>,
/// The server's settings.
pub server: Option<&'a T>,
/// The project settings, ordered from least specific to most specific.
@@ -141,6 +143,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> {
.chain(self.extensions)
.chain(self.user)
.chain(self.release_channel)
+ .chain(self.profile)
.chain(self.server)
.chain(self.project.iter().copied())
}
@@ -282,6 +285,14 @@ impl SettingsStore {
}
}
+ pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription {
+ cx.observe_global::<ActiveSettingsProfileName>(|cx| {
+ Self::update_global(cx, |store, cx| {
+ store.recompute_values(None, cx).log_err();
+ });
+ })
+ }
+
pub fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
where
C: BorrowAppContext,
@@ -321,6 +332,17 @@ impl SettingsStore {
.log_err();
}
+ let mut profile_value = None;
+ if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
+ if let Some(profiles) = self.raw_user_settings.get("profiles") {
+ if let Some(profile_settings) = profiles.get(&active_profile.0) {
+ profile_value = setting_value
+ .deserialize_setting(profile_settings)
+ .log_err();
+ }
+ }
+ }
+
let server_value = self
.raw_server_settings
.as_ref()
@@ -340,6 +362,7 @@ impl SettingsStore {
extensions: extension_value.as_ref(),
user: user_value.as_ref(),
release_channel: release_channel_value.as_ref(),
+ profile: profile_value.as_ref(),
server: server_value.as_ref(),
project: &[],
},
@@ -402,6 +425,16 @@ impl SettingsStore {
&self.raw_user_settings
}
+ /// Get the configured settings profile names.
+ pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
+ self.raw_user_settings
+ .get("profiles")
+ .and_then(|v| v.as_object())
+ .into_iter()
+ .flat_map(|obj| obj.keys())
+ .map(|s| s.as_str())
+ }
+
/// Access the raw JSON value of the global settings.
pub fn raw_global_settings(&self) -> Option<&Value> {
self.raw_global_settings.as_ref()
@@ -1003,18 +1036,18 @@ impl SettingsStore {
const ZED_SETTINGS: &str = "ZedSettings";
let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
- // add `ZedReleaseStageSettings` which is the same as `ZedSettings` except that unknown
- // fields are rejected.
- let mut zed_release_stage_settings = zed_settings_ref.clone();
- zed_release_stage_settings.insert("unevaluatedProperties".to_string(), false.into());
- let zed_release_stage_settings_ref = add_new_subschema(
+ // add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown
+ // fields are rejected. This is used for release stage settings and profiles.
+ let mut zed_settings_override = zed_settings_ref.clone();
+ zed_settings_override.insert("unevaluatedProperties".to_string(), false.into());
+ let zed_settings_override_ref = add_new_subschema(
&mut generator,
- "ZedReleaseStageSettings",
- zed_release_stage_settings.to_value(),
+ "ZedSettingsOverride",
+ zed_settings_override.to_value(),
);
// Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
- // unknown fields can be handled by the root schema and `ZedReleaseStageSettings`.
+ // unknown fields can be handled by the root schema and `ZedSettingsOverride`.
let mut definitions = generator.take_definitions(true);
definitions
.get_mut(ZED_SETTINGS)
@@ -1034,15 +1067,20 @@ impl SettingsStore {
"$schema": meta_schema,
"title": "Zed Settings",
"unevaluatedProperties": false,
- // ZedSettings + settings overrides for each release stage
+ // ZedSettings + settings overrides for each release stage / profiles
"allOf": [
zed_settings_ref,
{
"properties": {
- "dev": zed_release_stage_settings_ref,
- "nightly": zed_release_stage_settings_ref,
- "stable": zed_release_stage_settings_ref,
- "preview": zed_release_stage_settings_ref,
+ "dev": zed_settings_override_ref,
+ "nightly": zed_settings_override_ref,
+ "stable": zed_settings_override_ref,
+ "preview": zed_settings_override_ref,
+ "profiles": {
+ "type": "object",
+ "description": "Configures any number of settings profiles that are temporarily applied when selected from `settings profile selector: toggle`.",
+ "additionalProperties": zed_settings_override_ref
+ }
}
}
],
@@ -1101,6 +1139,16 @@ impl SettingsStore {
}
}
+ let mut profile_settings = None;
+ if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
+ if let Some(profiles) = self.raw_user_settings.get("profiles") {
+ if let Some(profile_json) = profiles.get(&active_profile.0) {
+ profile_settings =
+ setting_value.deserialize_setting(profile_json).log_err();
+ }
+ }
+ }
+
// If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() {
if let Some(value) = setting_value
@@ -1111,6 +1159,7 @@ impl SettingsStore {
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
+ profile: profile_settings.as_ref(),
server: server_settings.as_ref(),
project: &[],
},
@@ -1163,6 +1212,7 @@ impl SettingsStore {
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
+ profile: profile_settings.as_ref(),
server: server_settings.as_ref(),
project: &project_settings_stack.iter().collect::<Vec<_>>(),
},
@@ -1288,6 +1338,9 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
release_channel: values
.release_channel
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
+ profile: values
+ .profile
+ .map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
server: values
.server
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
@@ -0,0 +1,33 @@
+[package]
+name = "settings_profile_selector"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/settings_profile_selector.rs"
+doctest = false
+
+[dependencies]
+fuzzy.workspace = true
+gpui.workspace = true
+picker.workspace = true
+settings.workspace = true
+ui.workspace = true
+workspace-hack.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
+
+[dev-dependencies]
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+menu.workspace = true
+project = { workspace = true, features = ["test-support"] }
+serde_json.workspace = true
+settings = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,548 @@
+use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
+use gpui::{
+ App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window,
+};
+use picker::{Picker, PickerDelegate};
+use settings::{ActiveSettingsProfileName, SettingsStore};
+use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
+use workspace::{ModalView, Workspace};
+
+pub fn init(cx: &mut App) {
+ cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| {
+ workspace::with_active_or_new_workspace(cx, |workspace, window, cx| {
+ toggle_settings_profile_selector(workspace, window, cx);
+ });
+ });
+}
+
+fn toggle_settings_profile_selector(
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+) {
+ workspace.toggle_modal(window, cx, |window, cx| {
+ let delegate = SettingsProfileSelectorDelegate::new(cx.entity().downgrade(), window, cx);
+ SettingsProfileSelector::new(delegate, window, cx)
+ });
+}
+
+pub struct SettingsProfileSelector {
+ picker: Entity<Picker<SettingsProfileSelectorDelegate>>,
+}
+
+impl ModalView for SettingsProfileSelector {}
+
+impl EventEmitter<DismissEvent> for SettingsProfileSelector {}
+
+impl Focusable for SettingsProfileSelector {
+ fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl Render for SettingsProfileSelector {
+ fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+ v_flex().w(rems(34.)).child(self.picker.clone())
+ }
+}
+
+impl SettingsProfileSelector {
+ pub fn new(
+ delegate: SettingsProfileSelectorDelegate,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+ Self { picker }
+ }
+}
+
+pub struct SettingsProfileSelectorDelegate {
+ matches: Vec<StringMatch>,
+ profile_names: Vec<Option<String>>,
+ original_profile_name: Option<String>,
+ selected_profile_name: Option<String>,
+ selected_index: usize,
+ selection_completed: bool,
+ selector: WeakEntity<SettingsProfileSelector>,
+}
+
+impl SettingsProfileSelectorDelegate {
+ fn new(
+ selector: WeakEntity<SettingsProfileSelector>,
+ _: &mut Window,
+ cx: &mut Context<SettingsProfileSelector>,
+ ) -> Self {
+ let settings_store = cx.global::<SettingsStore>();
+ let mut profile_names: Vec<String> = settings_store
+ .configured_settings_profiles()
+ .map(|s| s.to_string())
+ .collect();
+
+ profile_names.sort();
+ let mut profile_names: Vec<_> = profile_names.into_iter().map(Some).collect();
+ profile_names.insert(0, None);
+
+ let matches = profile_names
+ .iter()
+ .enumerate()
+ .map(|(ix, profile_name)| StringMatch {
+ candidate_id: ix,
+ score: 0.0,
+ positions: Default::default(),
+ string: display_name(profile_name),
+ })
+ .collect();
+
+ let profile_name = cx
+ .try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone());
+
+ let mut this = Self {
+ matches,
+ profile_names,
+ original_profile_name: profile_name.clone(),
+ selected_profile_name: None,
+ selected_index: 0,
+ selection_completed: false,
+ selector,
+ };
+
+ if let Some(profile_name) = profile_name {
+ this.select_if_matching(&profile_name);
+ }
+
+ this
+ }
+
+ fn select_if_matching(&mut self, profile_name: &str) {
+ self.selected_index = self
+ .matches
+ .iter()
+ .position(|mat| mat.string == profile_name)
+ .unwrap_or(self.selected_index);
+ }
+
+ fn set_selected_profile(
+ &self,
+ cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
+ ) -> Option<String> {
+ let mat = self.matches.get(self.selected_index)?;
+ let profile_name = self.profile_names.get(mat.candidate_id)?;
+ return Self::update_active_profile_name_global(profile_name.clone(), cx);
+ }
+
+ fn update_active_profile_name_global(
+ profile_name: Option<String>,
+ cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
+ ) -> Option<String> {
+ if let Some(profile_name) = profile_name {
+ cx.set_global(ActiveSettingsProfileName(profile_name.clone()));
+ return Some(profile_name.clone());
+ }
+
+ if cx.has_global::<ActiveSettingsProfileName>() {
+ cx.remove_global::<ActiveSettingsProfileName>();
+ }
+
+ None
+ }
+}
+
+impl PickerDelegate for SettingsProfileSelectorDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self, _: &mut Window, _: &mut App) -> std::sync::Arc<str> {
+ "Select a settings profile...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _: &mut Window,
+ cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
+ ) {
+ self.selected_index = ix;
+ self.selected_profile_name = self.set_selected_profile(cx);
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ window: &mut Window,
+ cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
+ ) -> Task<()> {
+ let background = cx.background_executor().clone();
+ let candidates = self
+ .profile_names
+ .iter()
+ .enumerate()
+ .map(|(id, profile_name)| StringMatchCandidate::new(id, &display_name(profile_name)))
+ .collect::<Vec<_>>();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ match_strings(
+ &candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &Default::default(),
+ background,
+ )
+ .await
+ };
+
+ this.update_in(cx, |this, _, cx| {
+ this.delegate.matches = matches;
+ this.delegate.selected_index = this
+ .delegate
+ .selected_index
+ .min(this.delegate.matches.len().saturating_sub(1));
+ this.delegate.selected_profile_name = this.delegate.set_selected_profile(cx);
+ })
+ .ok();
+ })
+ }
+
+ fn confirm(
+ &mut self,
+ _: bool,
+ _: &mut Window,
+ cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
+ ) {
+ self.selection_completed = true;
+ self.selector
+ .update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
+ }
+
+ fn dismissed(
+ &mut self,
+ _: &mut Window,
+ cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
+ ) {
+ if !self.selection_completed {
+ SettingsProfileSelectorDelegate::update_active_profile_name_global(
+ self.original_profile_name.clone(),
+ cx,
+ );
+ }
+ self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _: &mut Window,
+ _: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let mat = &self.matches[ix];
+ let profile_name = &self.profile_names[mat.candidate_id];
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(HighlightedLabel::new(
+ display_name(profile_name),
+ mat.positions.clone(),
+ )),
+ )
+ }
+}
+
+fn display_name(profile_name: &Option<String>) -> String {
+ profile_name.clone().unwrap_or("Disabled".into())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use editor;
+ use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
+ use language;
+ use menu::{Cancel, Confirm, SelectNext, SelectPrevious};
+ use project::{FakeFs, Project};
+ use serde_json::json;
+ use workspace::{self, AppState};
+ use zed_actions::settings_profile_selector;
+
+ async fn init_test(
+ profiles_json: serde_json::Value,
+ cx: &mut TestAppContext,
+ ) -> (Entity<Workspace>, &mut VisualTestContext) {
+ cx.update(|cx| {
+ let state = AppState::test(cx);
+ language::init(cx);
+ super::init(cx);
+ editor::init(cx);
+ workspace::init_settings(cx);
+ Project::init_settings(cx);
+ state
+ });
+
+ cx.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ let settings_json = json!({
+ "profiles": profiles_json
+ });
+
+ store
+ .set_user_settings(&settings_json.to_string(), cx)
+ .unwrap();
+ });
+ });
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, ["/test".as_ref()], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ cx.update(|_, cx| {
+ assert!(!cx.has_global::<ActiveSettingsProfileName>());
+ });
+
+ (workspace, cx)
+ }
+
+ #[track_caller]
+ fn active_settings_profile_picker(
+ workspace: &Entity<Workspace>,
+ cx: &mut VisualTestContext,
+ ) -> Entity<Picker<SettingsProfileSelectorDelegate>> {
+ workspace.update(cx, |workspace, cx| {
+ workspace
+ .active_modal::<SettingsProfileSelector>(cx)
+ .expect("settings profile selector is not open")
+ .read(cx)
+ .picker
+ .clone()
+ })
+ }
+
+ #[gpui::test]
+ async fn test_settings_profile_selector_state(cx: &mut TestAppContext) {
+ let profiles_json = json!({
+ "Demo Videos": {
+ "buffer_font_size": 14
+ },
+ "Classroom / Streaming": {
+ "buffer_font_size": 16,
+ "vim_mode": true
+ }
+ });
+ let (workspace, cx) = init_test(profiles_json.clone(), cx).await;
+
+ cx.dispatch_action(settings_profile_selector::Toggle);
+
+ let picker = active_settings_profile_picker(&workspace, cx);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.matches.len(), 3);
+ assert_eq!(picker.delegate.matches[0].string, "Disabled");
+ assert_eq!(picker.delegate.matches[1].string, "Classroom / Streaming");
+ assert_eq!(picker.delegate.matches[2].string, "Demo Videos");
+ assert_eq!(picker.delegate.matches.get(3), None);
+
+ assert_eq!(picker.delegate.selected_index, 0);
+ assert_eq!(picker.delegate.selected_profile_name, None);
+
+ assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
+ });
+
+ cx.dispatch_action(Confirm);
+
+ cx.update(|_, cx| {
+ assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
+ });
+
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 1);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Classroom / Streaming".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Classroom / Streaming".to_string())
+ );
+ });
+
+ cx.dispatch_action(Cancel);
+
+ cx.update(|_, cx| {
+ assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
+ });
+
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 1);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Classroom / Streaming".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Classroom / Streaming".to_string())
+ );
+ });
+
+ cx.dispatch_action(SelectNext);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 2);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Demo Videos".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Demo Videos".to_string())
+ );
+ });
+
+ cx.dispatch_action(Confirm);
+
+ cx.update(|_, cx| {
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Demo Videos".to_string())
+ );
+ });
+
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 2);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Demo Videos".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Demo Videos".to_string())
+ );
+ });
+
+ cx.dispatch_action(SelectPrevious);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 1);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Classroom / Streaming".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Classroom / Streaming".to_string())
+ );
+ });
+
+ cx.dispatch_action(Cancel);
+
+ cx.update(|_, cx| {
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Demo Videos".to_string())
+ );
+ });
+
+ cx.dispatch_action(settings_profile_selector::Toggle);
+ let picker = active_settings_profile_picker(&workspace, cx);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 2);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Demo Videos".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Demo Videos".to_string())
+ );
+ });
+
+ cx.dispatch_action(SelectPrevious);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 1);
+ assert_eq!(
+ picker.delegate.selected_profile_name,
+ Some("Classroom / Streaming".to_string())
+ );
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ Some("Classroom / Streaming".to_string())
+ );
+ });
+
+ cx.dispatch_action(SelectPrevious);
+
+ picker.read_with(cx, |picker, cx| {
+ assert_eq!(picker.delegate.selected_index, 0);
+ assert_eq!(picker.delegate.selected_profile_name, None);
+
+ assert_eq!(
+ cx.try_global::<ActiveSettingsProfileName>()
+ .map(|p| p.0.clone()),
+ None
+ );
+ });
+
+ cx.dispatch_action(Confirm);
+
+ cx.update(|_, cx| {
+ assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
+ });
+ }
+}
@@ -867,6 +867,7 @@ impl settings::Settings for ThemeSettings {
.user
.into_iter()
.chain(sources.release_channel)
+ .chain(sources.profile)
.chain(sources.server)
{
if let Some(value) = value.ui_density {
@@ -106,6 +106,7 @@ outline_panel.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
+settings_profile_selector.workspace = true
profiling.workspace = true
project.workspace = true
project_panel.workspace = true
@@ -613,6 +613,7 @@ pub fn main() {
language_selector::init(cx);
toolchain_selector::init(cx);
theme_selector::init(cx);
+ settings_profile_selector::init(cx);
language_tools::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
@@ -4366,6 +4366,7 @@ mod tests {
"repl",
"rules_library",
"search",
+ "settings_profile_selector",
"snippets",
"supermaven",
"svg",
@@ -260,6 +260,16 @@ pub mod icon_theme_selector {
}
}
+pub mod settings_profile_selector {
+ use gpui::Action;
+ use schemars::JsonSchema;
+ use serde::Deserialize;
+
+ #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
+ #[action(namespace = settings_profile_selector)]
+ pub struct Toggle;
+}
+
pub mod agent {
use gpui::actions;