Introduce settings profiles (#35339)
Settings Profiles - [X] Allow profiles to be defined, where each profile can be any of Zed's settings - [X] Autocompletion of all settings - [X] Errors on invalid keys - [X] Action brings up modal that shows user-defined profiles - [X] Alphabetize profiles - [X] Ability to filter down via keyboard, and navigate via arrow up and down - [X] Auto select Disabled option by default (first in list, after alphabetizing user-defined profiles) - [X] Automatically select active profile on next picker summoning - [X] Persist settings until toggled off - [X] Show live preview as you select from the profile picker - [X] Tweaking a setting, while in a profile, updates the profile live - [X] Make sure actions that live update Zed, such as `cmd-0`, `cmd-+`, and `cmd--`, work while in a profile - [X] Add a test to track state Release Notes: - Added the ability to configure settings profiles, via the "profiles" key. Example: ```json { "profiles": { "Streaming": { "agent_font_size": 20, "buffer_font_size": 20, "theme": "One Light", "ui_font_size": 20 } } } ``` To set a profile, use `settings profile selector: toggle`
This commit is contained in:
parent
2d4afd2119
commit
5ef5f3c5ca
13 changed files with 698 additions and 18 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -14755,6 +14755,25 @@ dependencies = [
|
||||||
"zlog",
|
"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]]
|
[[package]]
|
||||||
name = "settings_ui"
|
name = "settings_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -20321,6 +20340,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"session",
|
"session",
|
||||||
"settings",
|
"settings",
|
||||||
|
"settings_profile_selector",
|
||||||
"settings_ui",
|
"settings_ui",
|
||||||
"shellexpand 2.1.2",
|
"shellexpand 2.1.2",
|
||||||
"smol",
|
"smol",
|
||||||
|
|
|
@ -119,6 +119,7 @@ members = [
|
||||||
"crates/paths",
|
"crates/paths",
|
||||||
"crates/picker",
|
"crates/picker",
|
||||||
"crates/prettier",
|
"crates/prettier",
|
||||||
|
"crates/settings_profile_selector",
|
||||||
"crates/project",
|
"crates/project",
|
||||||
"crates/project_panel",
|
"crates/project_panel",
|
||||||
"crates/project_symbols",
|
"crates/project_symbols",
|
||||||
|
@ -210,7 +211,7 @@ members = [
|
||||||
#
|
#
|
||||||
|
|
||||||
"tooling/workspace-hack",
|
"tooling/workspace-hack",
|
||||||
"tooling/xtask",
|
"tooling/xtask", "crates/settings_profile_selector",
|
||||||
]
|
]
|
||||||
default-members = ["crates/zed"]
|
default-members = ["crates/zed"]
|
||||||
|
|
||||||
|
@ -342,6 +343,7 @@ picker = { path = "crates/picker" }
|
||||||
plugin = { path = "crates/plugin" }
|
plugin = { path = "crates/plugin" }
|
||||||
plugin_macros = { path = "crates/plugin_macros" }
|
plugin_macros = { path = "crates/plugin_macros" }
|
||||||
prettier = { path = "crates/prettier" }
|
prettier = { path = "crates/prettier" }
|
||||||
|
settings_profile_selector = { path = "crates/settings_profile_selector" }
|
||||||
project = { path = "crates/project" }
|
project = { path = "crates/project" }
|
||||||
project_panel = { path = "crates/project_panel" }
|
project_panel = { path = "crates/project_panel" }
|
||||||
project_symbols = { path = "crates/project_symbols" }
|
project_symbols = { path = "crates/project_symbols" }
|
||||||
|
|
|
@ -1877,5 +1877,8 @@
|
||||||
"save_breakpoints": true,
|
"save_breakpoints": true,
|
||||||
"dock": "bottom",
|
"dock": "bottom",
|
||||||
"button": true
|
"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 settings_store;
|
||||||
mod vscode_import;
|
mod vscode_import;
|
||||||
|
|
||||||
use gpui::App;
|
use gpui::{App, Global};
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use std::{borrow::Cow, fmt, str};
|
use std::{borrow::Cow, fmt, str};
|
||||||
use util::asset_str;
|
use util::asset_str;
|
||||||
|
@ -27,6 +27,11 @@ pub use settings_store::{
|
||||||
};
|
};
|
||||||
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
|
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)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||||
pub struct WorktreeId(usize);
|
pub struct WorktreeId(usize);
|
||||||
|
|
||||||
|
@ -74,6 +79,7 @@ pub fn init(cx: &mut App) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
BaseKeymap::register(cx);
|
BaseKeymap::register(cx);
|
||||||
|
SettingsStore::observe_active_settings_profile_name(cx).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_settings() -> Cow<'static, str> {
|
pub fn default_settings() -> Cow<'static, str> {
|
||||||
|
|
|
@ -26,8 +26,8 @@ use util::{
|
||||||
pub type EditorconfigProperties = ec4rs::Properties;
|
pub type EditorconfigProperties = ec4rs::Properties;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
|
ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
|
||||||
parse_json_with_comments, update_value_in_json_text,
|
WorktreeId, parse_json_with_comments, update_value_in_json_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A value that can be defined as a user setting.
|
/// A value that can be defined as a user setting.
|
||||||
|
@ -122,6 +122,8 @@ pub struct SettingsSources<'a, T> {
|
||||||
pub user: Option<&'a T>,
|
pub user: Option<&'a T>,
|
||||||
/// The user settings for the current release channel.
|
/// The user settings for the current release channel.
|
||||||
pub release_channel: Option<&'a T>,
|
pub release_channel: Option<&'a T>,
|
||||||
|
/// The settings associated with an enabled settings profile
|
||||||
|
pub profile: Option<&'a T>,
|
||||||
/// The server's settings.
|
/// The server's settings.
|
||||||
pub server: Option<&'a T>,
|
pub server: Option<&'a T>,
|
||||||
/// The project settings, ordered from least specific to most specific.
|
/// 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.extensions)
|
||||||
.chain(self.user)
|
.chain(self.user)
|
||||||
.chain(self.release_channel)
|
.chain(self.release_channel)
|
||||||
|
.chain(self.profile)
|
||||||
.chain(self.server)
|
.chain(self.server)
|
||||||
.chain(self.project.iter().copied())
|
.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
|
pub fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
|
||||||
where
|
where
|
||||||
C: BorrowAppContext,
|
C: BorrowAppContext,
|
||||||
|
@ -321,6 +332,17 @@ impl SettingsStore {
|
||||||
.log_err();
|
.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
|
let server_value = self
|
||||||
.raw_server_settings
|
.raw_server_settings
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -340,6 +362,7 @@ impl SettingsStore {
|
||||||
extensions: extension_value.as_ref(),
|
extensions: extension_value.as_ref(),
|
||||||
user: user_value.as_ref(),
|
user: user_value.as_ref(),
|
||||||
release_channel: release_channel_value.as_ref(),
|
release_channel: release_channel_value.as_ref(),
|
||||||
|
profile: profile_value.as_ref(),
|
||||||
server: server_value.as_ref(),
|
server: server_value.as_ref(),
|
||||||
project: &[],
|
project: &[],
|
||||||
},
|
},
|
||||||
|
@ -402,6 +425,16 @@ impl SettingsStore {
|
||||||
&self.raw_user_settings
|
&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.
|
/// Access the raw JSON value of the global settings.
|
||||||
pub fn raw_global_settings(&self) -> Option<&Value> {
|
pub fn raw_global_settings(&self) -> Option<&Value> {
|
||||||
self.raw_global_settings.as_ref()
|
self.raw_global_settings.as_ref()
|
||||||
|
@ -1003,18 +1036,18 @@ impl SettingsStore {
|
||||||
const ZED_SETTINGS: &str = "ZedSettings";
|
const ZED_SETTINGS: &str = "ZedSettings";
|
||||||
let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
|
let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
|
||||||
|
|
||||||
// add `ZedReleaseStageSettings` which is the same as `ZedSettings` except that unknown
|
// add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown
|
||||||
// fields are rejected.
|
// fields are rejected. This is used for release stage settings and profiles.
|
||||||
let mut zed_release_stage_settings = zed_settings_ref.clone();
|
let mut zed_settings_override = zed_settings_ref.clone();
|
||||||
zed_release_stage_settings.insert("unevaluatedProperties".to_string(), false.into());
|
zed_settings_override.insert("unevaluatedProperties".to_string(), false.into());
|
||||||
let zed_release_stage_settings_ref = add_new_subschema(
|
let zed_settings_override_ref = add_new_subschema(
|
||||||
&mut generator,
|
&mut generator,
|
||||||
"ZedReleaseStageSettings",
|
"ZedSettingsOverride",
|
||||||
zed_release_stage_settings.to_value(),
|
zed_settings_override.to_value(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
|
// 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);
|
let mut definitions = generator.take_definitions(true);
|
||||||
definitions
|
definitions
|
||||||
.get_mut(ZED_SETTINGS)
|
.get_mut(ZED_SETTINGS)
|
||||||
|
@ -1034,15 +1067,20 @@ impl SettingsStore {
|
||||||
"$schema": meta_schema,
|
"$schema": meta_schema,
|
||||||
"title": "Zed Settings",
|
"title": "Zed Settings",
|
||||||
"unevaluatedProperties": false,
|
"unevaluatedProperties": false,
|
||||||
// ZedSettings + settings overrides for each release stage
|
// ZedSettings + settings overrides for each release stage / profiles
|
||||||
"allOf": [
|
"allOf": [
|
||||||
zed_settings_ref,
|
zed_settings_ref,
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"dev": zed_release_stage_settings_ref,
|
"dev": zed_settings_override_ref,
|
||||||
"nightly": zed_release_stage_settings_ref,
|
"nightly": zed_settings_override_ref,
|
||||||
"stable": zed_release_stage_settings_ref,
|
"stable": zed_settings_override_ref,
|
||||||
"preview": zed_release_stage_settings_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 the global settings file changed, reload the global value for the field.
|
||||||
if changed_local_path.is_none() {
|
if changed_local_path.is_none() {
|
||||||
if let Some(value) = setting_value
|
if let Some(value) = setting_value
|
||||||
|
@ -1111,6 +1159,7 @@ impl SettingsStore {
|
||||||
extensions: extension_settings.as_ref(),
|
extensions: extension_settings.as_ref(),
|
||||||
user: user_settings.as_ref(),
|
user: user_settings.as_ref(),
|
||||||
release_channel: release_channel_settings.as_ref(),
|
release_channel: release_channel_settings.as_ref(),
|
||||||
|
profile: profile_settings.as_ref(),
|
||||||
server: server_settings.as_ref(),
|
server: server_settings.as_ref(),
|
||||||
project: &[],
|
project: &[],
|
||||||
},
|
},
|
||||||
|
@ -1163,6 +1212,7 @@ impl SettingsStore {
|
||||||
extensions: extension_settings.as_ref(),
|
extensions: extension_settings.as_ref(),
|
||||||
user: user_settings.as_ref(),
|
user: user_settings.as_ref(),
|
||||||
release_channel: release_channel_settings.as_ref(),
|
release_channel: release_channel_settings.as_ref(),
|
||||||
|
profile: profile_settings.as_ref(),
|
||||||
server: server_settings.as_ref(),
|
server: server_settings.as_ref(),
|
||||||
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
||||||
},
|
},
|
||||||
|
@ -1288,6 +1338,9 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
|
||||||
release_channel: values
|
release_channel: values
|
||||||
.release_channel
|
.release_channel
|
||||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
|
profile: values
|
||||||
|
.profile
|
||||||
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
server: values
|
server: values
|
||||||
.server
|
.server
|
||||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
|
|
33
crates/settings_profile_selector/Cargo.toml
Normal file
33
crates/settings_profile_selector/Cargo.toml
Normal file
|
@ -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"] }
|
1
crates/settings_profile_selector/LICENSE-GPL
Symbolic link
1
crates/settings_profile_selector/LICENSE-GPL
Symbolic link
|
@ -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
|
.user
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(sources.release_channel)
|
.chain(sources.release_channel)
|
||||||
|
.chain(sources.profile)
|
||||||
.chain(sources.server)
|
.chain(sources.server)
|
||||||
{
|
{
|
||||||
if let Some(value) = value.ui_density {
|
if let Some(value) = value.ui_density {
|
||||||
|
|
|
@ -106,6 +106,7 @@ outline_panel.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
settings_profile_selector.workspace = true
|
||||||
profiling.workspace = true
|
profiling.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
project_panel.workspace = true
|
project_panel.workspace = true
|
||||||
|
|
|
@ -613,6 +613,7 @@ pub fn main() {
|
||||||
language_selector::init(cx);
|
language_selector::init(cx);
|
||||||
toolchain_selector::init(cx);
|
toolchain_selector::init(cx);
|
||||||
theme_selector::init(cx);
|
theme_selector::init(cx);
|
||||||
|
settings_profile_selector::init(cx);
|
||||||
language_tools::init(cx);
|
language_tools::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), 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);
|
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
|
|
|
@ -4366,6 +4366,7 @@ mod tests {
|
||||||
"repl",
|
"repl",
|
||||||
"rules_library",
|
"rules_library",
|
||||||
"search",
|
"search",
|
||||||
|
"settings_profile_selector",
|
||||||
"snippets",
|
"snippets",
|
||||||
"supermaven",
|
"supermaven",
|
||||||
"svg",
|
"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 {
|
pub mod agent {
|
||||||
use gpui::actions;
|
use gpui::actions;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue