settings_ui: Add Basic Implementation of Language Settings (#37803)
Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
@@ -317,7 +317,6 @@ pub struct AllLanguageSettingsContent {
|
||||
pub defaults: LanguageSettingsContent,
|
||||
/// The settings for individual languages.
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub languages: LanguageToSettingsMap,
|
||||
/// Settings for associating file extensions and filenames
|
||||
/// with languages.
|
||||
@@ -331,6 +330,37 @@ pub struct AllLanguageSettingsContent {
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
|
||||
|
||||
impl SettingsUi for LanguageToSettingsMap {
|
||||
fn settings_ui_item() -> settings::SettingsUiItem {
|
||||
settings::SettingsUiItem::DynamicMap(settings::SettingsUiItemDynamicMap {
|
||||
item: LanguageSettingsContent::settings_ui_item,
|
||||
defaults_path: &[],
|
||||
determine_items: |settings_value, cx| {
|
||||
use settings::SettingsUiEntryMetaData;
|
||||
|
||||
// todo(settings_ui): We should be using a global LanguageRegistry, but it's not implemented yet
|
||||
_ = cx;
|
||||
|
||||
let Some(settings_language_map) = settings_value.as_object() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut languages = Vec::with_capacity(settings_language_map.len());
|
||||
|
||||
for language_name in settings_language_map.keys().map(gpui::SharedString::from) {
|
||||
languages.push(SettingsUiEntryMetaData {
|
||||
title: language_name.clone(),
|
||||
path: language_name,
|
||||
// todo(settings_ui): Implement documentation for each language
|
||||
// ideally based on the language's official docs from extension or builtin info
|
||||
documentation: None,
|
||||
});
|
||||
}
|
||||
return languages;
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit! {
|
||||
ParameterizedJsonSchema {
|
||||
add_and_get_ref: |generator, params, _cx| {
|
||||
@@ -431,11 +461,13 @@ fn default_3() -> usize {
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
#[settings_ui(group = "Default")]
|
||||
pub struct LanguageSettingsContent {
|
||||
/// How many columns a tab should occupy.
|
||||
///
|
||||
/// Default: 4
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub tab_size: Option<NonZeroU32>,
|
||||
/// Whether to indent lines using tab characters, as opposed to multiple
|
||||
/// spaces.
|
||||
@@ -466,6 +498,7 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub wrap_guides: Option<Vec<usize>>,
|
||||
/// Indent guide related settings.
|
||||
#[serde(default)]
|
||||
@@ -491,6 +524,7 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: auto
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub formatter: Option<SelectedFormatter>,
|
||||
/// Zed's Prettier integration settings.
|
||||
/// Allows to enable/disable formatting with Prettier
|
||||
@@ -516,6 +550,7 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: ["..."]
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub language_servers: Option<Vec<String>>,
|
||||
/// Controls where the `editor::Rewrap` action is allowed for this language.
|
||||
///
|
||||
@@ -538,6 +573,7 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub edit_predictions_disabled_in: Option<Vec<String>>,
|
||||
/// Whether to show tabs and spaces in the editor.
|
||||
#[serde(default)]
|
||||
@@ -577,6 +613,7 @@ pub struct LanguageSettingsContent {
|
||||
/// These are not run if formatting is off.
|
||||
///
|
||||
/// Default: {} (or {"source.organizeImports": true} for Go).
|
||||
#[settings_ui(skip)]
|
||||
pub code_actions_on_format: Option<HashMap<String, bool>>,
|
||||
/// Whether to perform linked edits of associated ranges, if the language server supports it.
|
||||
/// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
|
||||
@@ -610,11 +647,14 @@ pub struct LanguageSettingsContent {
|
||||
/// Preferred debuggers for this language.
|
||||
///
|
||||
/// Default: []
|
||||
#[settings_ui(skip)]
|
||||
pub debuggers: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// The behavior of `editor::Rewrap`.
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(
|
||||
Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, SettingsUi,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RewrapBehavior {
|
||||
/// Only rewrap within comments.
|
||||
@@ -696,7 +736,7 @@ pub enum SoftWrap {
|
||||
}
|
||||
|
||||
/// Controls the behavior of formatting files when they are saved.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, SettingsUi)]
|
||||
pub enum FormatOnSave {
|
||||
/// Files should be formatted on save.
|
||||
On,
|
||||
@@ -795,7 +835,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
|
||||
}
|
||||
|
||||
/// Controls how whitespace should be displayedin the editor.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShowWhitespaceSetting {
|
||||
/// Draw whitespace only for the selected text.
|
||||
@@ -816,7 +856,7 @@ pub enum ShowWhitespaceSetting {
|
||||
}
|
||||
|
||||
/// Controls which formatter should be used when formatting code.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, SettingsUi)]
|
||||
pub enum SelectedFormatter {
|
||||
/// Format files using Zed's Prettier integration (if applicable),
|
||||
/// or falling back to formatting via language server.
|
||||
@@ -1012,7 +1052,7 @@ pub enum IndentGuideBackgroundColoring {
|
||||
}
|
||||
|
||||
/// The settings for inlay hints.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
pub struct InlayHintSettings {
|
||||
/// Global switch to toggle hints on and off.
|
||||
///
|
||||
@@ -1079,7 +1119,7 @@ fn scroll_debounce_ms() -> u64 {
|
||||
}
|
||||
|
||||
/// The task settings for a particular language.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, SettingsUi)]
|
||||
pub struct LanguageTaskConfig {
|
||||
/// Extra task variables to set for a particular language.
|
||||
#[serde(default)]
|
||||
@@ -1622,7 +1662,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
/// Allows to enable/disable formatting with Prettier
|
||||
/// and configure default Prettier, used when no project-level Prettier installation is found.
|
||||
/// Prettier formatting is disabled by default.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
pub struct PrettierSettings {
|
||||
/// Enables or disables formatting with Prettier for a given language.
|
||||
#[serde(default)]
|
||||
@@ -1635,15 +1675,17 @@ pub struct PrettierSettings {
|
||||
/// Forces Prettier integration to use specific plugins when formatting files with the language.
|
||||
/// The default Prettier will be installed with these plugins.
|
||||
#[serde(default)]
|
||||
#[settings_ui(skip)]
|
||||
pub plugins: HashSet<String>,
|
||||
|
||||
/// Default Prettier options, in the format as in package.json section for Prettier.
|
||||
/// If project installs Prettier via its package.json, these options will be ignored.
|
||||
#[serde(flatten)]
|
||||
#[settings_ui(skip)]
|
||||
pub options: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
pub struct JsxTagAutoCloseSettings {
|
||||
/// Enables or disables auto-closing of JSX tags.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -601,12 +601,12 @@ impl SettingsStore {
|
||||
pub fn update_settings_file_at_path(
|
||||
&self,
|
||||
fs: Arc<dyn Fs>,
|
||||
path: &[&str],
|
||||
path: &[impl AsRef<str>],
|
||||
new_value: serde_json::Value,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let key_path = path
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.map(AsRef::as_ref)
|
||||
.map(SharedString::new)
|
||||
.collect::<Vec<_>>();
|
||||
let update = move |mut old_text: String, cx: AsyncApp| {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use std::any::TypeId;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
num::{NonZeroU32, NonZeroUsize},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use fs::Fs;
|
||||
use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window};
|
||||
use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, SharedString, Window};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::SettingsStore;
|
||||
@@ -24,6 +28,7 @@ pub trait SettingsUi {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsUiEntry {
|
||||
/// The path in the settings JSON file for this setting. Relative to parent
|
||||
/// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings
|
||||
@@ -35,6 +40,7 @@ pub struct SettingsUiEntry {
|
||||
pub item: SettingsUiItem,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SettingsUiItemSingle {
|
||||
SwitchField,
|
||||
/// A numeric stepper for a specific type of number
|
||||
@@ -52,13 +58,13 @@ pub enum SettingsUiItemSingle {
|
||||
/// Must be the same length as `variants`
|
||||
labels: &'static [&'static str],
|
||||
},
|
||||
Custom(Box<dyn Fn(SettingsValue<serde_json::Value>, &mut Window, &mut App) -> AnyElement>),
|
||||
Custom(Rc<dyn Fn(SettingsValue<serde_json::Value>, &mut Window, &mut App) -> AnyElement>),
|
||||
}
|
||||
|
||||
pub struct SettingsValue<T> {
|
||||
pub title: &'static str,
|
||||
pub documentation: Option<&'static str>,
|
||||
pub path: SmallVec<[&'static str; 1]>,
|
||||
pub title: SharedString,
|
||||
pub documentation: Option<SharedString>,
|
||||
pub path: SmallVec<[SharedString; 1]>,
|
||||
pub value: Option<T>,
|
||||
pub default_value: T,
|
||||
}
|
||||
@@ -73,7 +79,7 @@ impl<T> SettingsValue<T> {
|
||||
}
|
||||
|
||||
impl SettingsValue<serde_json::Value> {
|
||||
pub fn write_value(path: &SmallVec<[&'static str; 1]>, value: serde_json::Value, cx: &mut App) {
|
||||
pub fn write_value(path: &SmallVec<[SharedString; 1]>, value: serde_json::Value, cx: &mut App) {
|
||||
let settings_store = SettingsStore::global(cx);
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
@@ -90,7 +96,7 @@ impl SettingsValue<serde_json::Value> {
|
||||
|
||||
impl<T: serde::Serialize> SettingsValue<T> {
|
||||
pub fn write(
|
||||
path: &SmallVec<[&'static str; 1]>,
|
||||
path: &SmallVec<[SharedString; 1]>,
|
||||
value: T,
|
||||
cx: &mut App,
|
||||
) -> Result<(), serde_json::Error> {
|
||||
@@ -99,19 +105,36 @@ impl<T: serde::Serialize> SettingsValue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsUiItemDynamic {
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsUiItemUnion {
|
||||
pub options: Vec<SettingsUiEntry>,
|
||||
pub determine_option: fn(&serde_json::Value, &App) -> usize,
|
||||
}
|
||||
|
||||
pub struct SettingsUiEntryMetaData {
|
||||
pub title: SharedString,
|
||||
pub path: SharedString,
|
||||
pub documentation: Option<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsUiItemDynamicMap {
|
||||
pub item: fn() -> SettingsUiItem,
|
||||
pub determine_items: fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
|
||||
pub defaults_path: &'static [&'static str],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsUiItemGroup {
|
||||
pub items: Vec<SettingsUiEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SettingsUiItem {
|
||||
Group(SettingsUiItemGroup),
|
||||
Single(SettingsUiItemSingle),
|
||||
Dynamic(SettingsUiItemDynamic),
|
||||
Union(SettingsUiItemUnion),
|
||||
DynamicMap(SettingsUiItemDynamicMap),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -134,6 +157,7 @@ pub enum NumType {
|
||||
U32 = 1,
|
||||
F32 = 2,
|
||||
USIZE = 3,
|
||||
U32NONZERO = 4,
|
||||
}
|
||||
|
||||
pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> =
|
||||
@@ -151,6 +175,7 @@ impl NumType {
|
||||
NumType::U32 => TypeId::of::<u32>(),
|
||||
NumType::F32 => TypeId::of::<f32>(),
|
||||
NumType::USIZE => TypeId::of::<usize>(),
|
||||
NumType::U32NONZERO => TypeId::of::<NonZeroU32>(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +185,7 @@ impl NumType {
|
||||
NumType::U32 => std::any::type_name::<u32>(),
|
||||
NumType::F32 => std::any::type_name::<f32>(),
|
||||
NumType::USIZE => std::any::type_name::<usize>(),
|
||||
NumType::U32NONZERO => std::any::type_name::<NonZeroU32>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,3 +211,4 @@ numeric_stepper_for_num_type!(u32, U32);
|
||||
// todo(settings_ui) is there a better ui for f32?
|
||||
numeric_stepper_for_num_type!(f32, F32);
|
||||
numeric_stepper_for_num_type!(usize, USIZE);
|
||||
numeric_stepper_for_num_type!(NonZeroUsize, U32NONZERO);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod appearance_settings_controls;
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::num::NonZeroU32;
|
||||
use std::ops::{Not, Range};
|
||||
|
||||
use anyhow::Context as _;
|
||||
@@ -9,8 +10,9 @@ use editor::EditorSettingsControls;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
|
||||
use settings::{
|
||||
NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic,
|
||||
SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue,
|
||||
NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem,
|
||||
SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion,
|
||||
SettingsValue,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*};
|
||||
@@ -135,9 +137,9 @@ impl Item for SettingsPage {
|
||||
// - Do we want to show the parent groups when a item is matched?
|
||||
|
||||
struct UiEntry {
|
||||
title: &'static str,
|
||||
path: Option<&'static str>,
|
||||
documentation: Option<&'static str>,
|
||||
title: SharedString,
|
||||
path: Option<SharedString>,
|
||||
documentation: Option<SharedString>,
|
||||
_depth: usize,
|
||||
// a
|
||||
// b < a descendant range < a total descendant range
|
||||
@@ -154,6 +156,11 @@ struct UiEntry {
|
||||
/// For dynamic items this is a way to select a value from a list of values
|
||||
/// this is always none for non-dynamic items
|
||||
select_descendant: Option<fn(&serde_json::Value, &App) -> usize>,
|
||||
generate_items: Option<(
|
||||
SettingsUiItem,
|
||||
fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
|
||||
SmallVec<[SharedString; 1]>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl UiEntry {
|
||||
@@ -193,15 +200,16 @@ fn build_tree_item(
|
||||
) {
|
||||
let index = tree.len();
|
||||
tree.push(UiEntry {
|
||||
title: entry.title,
|
||||
path: entry.path,
|
||||
documentation: entry.documentation,
|
||||
title: entry.title.into(),
|
||||
path: entry.path.map(SharedString::new_static),
|
||||
documentation: entry.documentation.map(SharedString::new_static),
|
||||
_depth: depth,
|
||||
descendant_range: index + 1..index + 1,
|
||||
total_descendant_range: index + 1..index + 1,
|
||||
render: None,
|
||||
next_sibling: None,
|
||||
select_descendant: None,
|
||||
generate_items: None,
|
||||
});
|
||||
if let Some(prev_index) = prev_index {
|
||||
tree[prev_index].next_sibling = Some(index);
|
||||
@@ -222,7 +230,7 @@ fn build_tree_item(
|
||||
SettingsUiItem::Single(item) => {
|
||||
tree[index].render = Some(item);
|
||||
}
|
||||
SettingsUiItem::Dynamic(SettingsUiItemDynamic {
|
||||
SettingsUiItem::Union(SettingsUiItemUnion {
|
||||
options,
|
||||
determine_option,
|
||||
}) => {
|
||||
@@ -238,6 +246,21 @@ fn build_tree_item(
|
||||
tree[index].total_descendant_range.end = tree.len();
|
||||
}
|
||||
}
|
||||
SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap {
|
||||
item: generate_settings_ui_item,
|
||||
determine_items,
|
||||
defaults_path,
|
||||
}) => {
|
||||
tree[index].generate_items = Some((
|
||||
generate_settings_ui_item(),
|
||||
determine_items,
|
||||
defaults_path
|
||||
.into_iter()
|
||||
.copied()
|
||||
.map(SharedString::new_static)
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
SettingsUiItem::None => {
|
||||
return;
|
||||
}
|
||||
@@ -263,7 +286,7 @@ impl SettingsUiTree {
|
||||
build_tree_item(&mut tree, item, 0, prev_root_entry_index);
|
||||
}
|
||||
|
||||
root_entry_indices.sort_by_key(|i| tree[*i].title);
|
||||
root_entry_indices.sort_by_key(|i| &tree[*i].title);
|
||||
|
||||
let active_entry_index = root_entry_indices[0];
|
||||
Self {
|
||||
@@ -276,18 +299,18 @@ impl SettingsUiTree {
|
||||
// todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
|
||||
// so that we can keep none/skip and still test in CI that all settings have
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn all_paths(&self, cx: &App) -> Vec<Vec<&'static str>> {
|
||||
pub fn all_paths(&self, cx: &App) -> Vec<Vec<SharedString>> {
|
||||
fn all_paths_rec(
|
||||
tree: &[UiEntry],
|
||||
paths: &mut Vec<Vec<&'static str>>,
|
||||
current_path: &mut Vec<&'static str>,
|
||||
paths: &mut Vec<Vec<SharedString>>,
|
||||
current_path: &mut Vec<SharedString>,
|
||||
idx: usize,
|
||||
cx: &App,
|
||||
) {
|
||||
let child = &tree[idx];
|
||||
let mut pushed_path = false;
|
||||
if let Some(path) = child.path.as_ref() {
|
||||
current_path.push(path);
|
||||
current_path.push(path.clone());
|
||||
paths.push(current_path.clone());
|
||||
pushed_path = true;
|
||||
}
|
||||
@@ -340,7 +363,7 @@ fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<Sett
|
||||
settings.settings_tree.active_entry_index = index;
|
||||
}))
|
||||
.child(
|
||||
Label::new(SharedString::new_static(tree.entries[index].title))
|
||||
Label::new(tree.entries[index].title.clone())
|
||||
.size(LabelSize::Large)
|
||||
.when(tree.active_entry_index == index, |this| {
|
||||
this.color(Color::Selected)
|
||||
@@ -361,45 +384,102 @@ fn render_content(
|
||||
let mut path = smallvec::smallvec![];
|
||||
|
||||
fn render_recursive(
|
||||
tree: &SettingsUiTree,
|
||||
tree: &[UiEntry],
|
||||
index: usize,
|
||||
path: &mut SmallVec<[&'static str; 1]>,
|
||||
path: &mut SmallVec<[SharedString; 1]>,
|
||||
mut element: Div,
|
||||
// todo(settings_ui): can this be a ref without cx borrow issues?
|
||||
fallback_path: &mut Option<SmallVec<[SharedString; 1]>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let Some(child) = tree.entries.get(index) else {
|
||||
let Some(child) = tree.get(index) else {
|
||||
return element.child(
|
||||
Label::new(SharedString::new_static("No settings found")).color(Color::Error),
|
||||
);
|
||||
};
|
||||
|
||||
element =
|
||||
element.child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large));
|
||||
element = element.child(Label::new(child.title.clone()).size(LabelSize::Large));
|
||||
|
||||
// todo(settings_ui): subgroups?
|
||||
let mut pushed_path = false;
|
||||
if let Some(child_path) = child.path {
|
||||
path.push(child_path);
|
||||
if let Some(child_path) = child.path.as_ref() {
|
||||
path.push(child_path.clone());
|
||||
if let Some(fallback_path) = fallback_path.as_mut() {
|
||||
fallback_path.push(child_path.clone());
|
||||
}
|
||||
pushed_path = true;
|
||||
}
|
||||
// let fallback_path_copy = fallback_path.cloned();
|
||||
let settings_value = settings_value_from_settings_and_path(
|
||||
path.clone(),
|
||||
child.title,
|
||||
child.documentation,
|
||||
fallback_path.as_ref().map(|path| path.as_slice()),
|
||||
child.title.clone(),
|
||||
child.documentation.clone(),
|
||||
// PERF: how to structure this better? There feels like there's a way to avoid the clone
|
||||
// and every value lookup
|
||||
SettingsStore::global(cx).raw_user_settings(),
|
||||
SettingsStore::global(cx).raw_default_settings(),
|
||||
);
|
||||
if let Some(select_descendant) = child.select_descendant {
|
||||
let selected_descendant = child
|
||||
.nth_descendant_index(&tree.entries, select_descendant(settings_value.read(), cx));
|
||||
let selected_descendant =
|
||||
child.nth_descendant_index(tree, select_descendant(settings_value.read(), cx));
|
||||
if let Some(descendant_index) = selected_descendant {
|
||||
element = render_recursive(&tree, descendant_index, path, element, window, cx);
|
||||
element = render_recursive(
|
||||
tree,
|
||||
descendant_index,
|
||||
path,
|
||||
element,
|
||||
fallback_path,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(child_render) = child.render.as_ref() {
|
||||
} else if let Some((settings_ui_item, generate_items, defaults_path)) =
|
||||
child.generate_items.as_ref()
|
||||
{
|
||||
let generated_items = generate_items(settings_value.read(), cx);
|
||||
let mut ui_items = Vec::with_capacity(generated_items.len());
|
||||
for item in generated_items {
|
||||
let settings_ui_entry = SettingsUiEntry {
|
||||
path: None,
|
||||
title: "",
|
||||
documentation: None,
|
||||
item: settings_ui_item.clone(),
|
||||
};
|
||||
let prev_index = if ui_items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ui_items.len() - 1)
|
||||
};
|
||||
let item_index = ui_items.len();
|
||||
build_tree_item(
|
||||
&mut ui_items,
|
||||
settings_ui_entry,
|
||||
child._depth + 1,
|
||||
prev_index,
|
||||
);
|
||||
if item_index < ui_items.len() {
|
||||
ui_items[item_index].path = None;
|
||||
ui_items[item_index].title = item.title.clone();
|
||||
ui_items[item_index].documentation = item.documentation.clone();
|
||||
|
||||
// push path instead of setting path on ui item so that the path isn't pushed to default_path as well
|
||||
// when we recurse
|
||||
path.push(item.path.clone());
|
||||
element = render_recursive(
|
||||
&ui_items,
|
||||
item_index,
|
||||
path,
|
||||
element,
|
||||
&mut Some(defaults_path.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
} else if let Some(child_render) = child.render.as_ref() {
|
||||
element = element.child(div().child(render_item_single(
|
||||
settings_value,
|
||||
child_render,
|
||||
@@ -409,8 +489,16 @@ fn render_content(
|
||||
} else if let Some(child_index) = child.first_descendant_index() {
|
||||
let mut index = Some(child_index);
|
||||
while let Some(sub_child_index) = index {
|
||||
element = render_recursive(tree, sub_child_index, path, element, window, cx);
|
||||
index = tree.entries[sub_child_index].next_sibling;
|
||||
element = render_recursive(
|
||||
tree,
|
||||
sub_child_index,
|
||||
path,
|
||||
element,
|
||||
fallback_path,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
index = tree[sub_child_index].next_sibling;
|
||||
}
|
||||
} else {
|
||||
element =
|
||||
@@ -419,15 +507,19 @@ fn render_content(
|
||||
|
||||
if pushed_path {
|
||||
path.pop();
|
||||
if let Some(fallback_path) = fallback_path.as_mut() {
|
||||
fallback_path.pop();
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
return render_recursive(
|
||||
tree,
|
||||
&tree.entries,
|
||||
tree.active_entry_index,
|
||||
&mut path,
|
||||
content,
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -484,15 +576,15 @@ fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
|
||||
)
|
||||
}
|
||||
|
||||
fn element_id_from_path(path: &[&'static str]) -> ElementId {
|
||||
fn element_id_from_path(path: &[SharedString]) -> ElementId {
|
||||
if path.len() == 0 {
|
||||
panic!("Path length must not be zero");
|
||||
} else if path.len() == 1 {
|
||||
ElementId::Name(SharedString::new_static(path[0]))
|
||||
ElementId::Name(path[0].clone())
|
||||
} else {
|
||||
ElementId::from((
|
||||
ElementId::from(SharedString::new_static(path[path.len() - 2])),
|
||||
SharedString::new_static(path[path.len() - 1]),
|
||||
ElementId::from(path[path.len() - 2].clone()),
|
||||
path[path.len() - 1].clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -525,13 +617,13 @@ fn render_item_single(
|
||||
|
||||
pub fn read_settings_value_from_path<'a>(
|
||||
settings_contents: &'a serde_json::Value,
|
||||
path: &[&str],
|
||||
path: &[impl AsRef<str>],
|
||||
) -> Option<&'a serde_json::Value> {
|
||||
// todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
|
||||
let Some((key, remaining)) = path.split_first() else {
|
||||
return Some(settings_contents);
|
||||
};
|
||||
let Some(value) = settings_contents.get(key) else {
|
||||
let Some(value) = settings_contents.get(key.as_ref()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -541,13 +633,17 @@ pub fn read_settings_value_from_path<'a>(
|
||||
fn downcast_any_item<T: serde::de::DeserializeOwned>(
|
||||
settings_value: SettingsValue<serde_json::Value>,
|
||||
) -> SettingsValue<T> {
|
||||
let value = settings_value
|
||||
.value
|
||||
.map(|value| serde_json::from_value::<T>(value).expect("value is not a T"));
|
||||
let value = settings_value.value.map(|value| {
|
||||
serde_json::from_value::<T>(value.clone())
|
||||
.with_context(|| format!("path: {:?}", settings_value.path.join(".")))
|
||||
.with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), value))
|
||||
.unwrap()
|
||||
});
|
||||
// todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
|
||||
let default_value = serde_json::from_value::<T>(settings_value.default_value)
|
||||
.with_context(|| format!("path: {:?}", settings_value.path.join(".")))
|
||||
.expect("default value is not an Option<T>");
|
||||
.with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
|
||||
.unwrap();
|
||||
let deserialized_setting_value = SettingsValue {
|
||||
title: settings_value.title,
|
||||
path: settings_value.path,
|
||||
@@ -577,8 +673,8 @@ fn render_any_numeric_stepper(
|
||||
match num_type {
|
||||
NumType::U64 => render_numeric_stepper::<u64>(
|
||||
downcast_any_item(settings_value),
|
||||
u64::saturating_sub,
|
||||
u64::saturating_add,
|
||||
|n| u64::saturating_sub(n, 1),
|
||||
|n| u64::saturating_add(n, 1),
|
||||
|n| {
|
||||
serde_json::Number::try_from(n)
|
||||
.context("Failed to convert u64 to serde_json::Number")
|
||||
@@ -588,8 +684,8 @@ fn render_any_numeric_stepper(
|
||||
),
|
||||
NumType::U32 => render_numeric_stepper::<u32>(
|
||||
downcast_any_item(settings_value),
|
||||
u32::saturating_sub,
|
||||
u32::saturating_add,
|
||||
|n| u32::saturating_sub(n, 1),
|
||||
|n| u32::saturating_add(n, 1),
|
||||
|n| {
|
||||
serde_json::Number::try_from(n)
|
||||
.context("Failed to convert u32 to serde_json::Number")
|
||||
@@ -599,8 +695,8 @@ fn render_any_numeric_stepper(
|
||||
),
|
||||
NumType::F32 => render_numeric_stepper::<f32>(
|
||||
downcast_any_item(settings_value),
|
||||
|a, b| a - b,
|
||||
|a, b| a + b,
|
||||
|a| a - 1.0,
|
||||
|a| a + 1.0,
|
||||
|n| {
|
||||
serde_json::Number::from_f64(n as f64)
|
||||
.context("Failed to convert f32 to serde_json::Number")
|
||||
@@ -610,8 +706,8 @@ fn render_any_numeric_stepper(
|
||||
),
|
||||
NumType::USIZE => render_numeric_stepper::<usize>(
|
||||
downcast_any_item(settings_value),
|
||||
usize::saturating_sub,
|
||||
usize::saturating_add,
|
||||
|n| usize::saturating_sub(n, 1),
|
||||
|n| usize::saturating_add(n, 1),
|
||||
|n| {
|
||||
serde_json::Number::try_from(n)
|
||||
.context("Failed to convert usize to serde_json::Number")
|
||||
@@ -619,15 +715,24 @@ fn render_any_numeric_stepper(
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
NumType::U32NONZERO => render_numeric_stepper::<NonZeroU32>(
|
||||
downcast_any_item(settings_value),
|
||||
|a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN),
|
||||
|a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX),
|
||||
|n| {
|
||||
serde_json::Number::try_from(n.get())
|
||||
.context("Failed to convert usize to serde_json::Number")
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_numeric_stepper<
|
||||
T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From<u8> + 'static,
|
||||
>(
|
||||
fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
|
||||
value: SettingsValue<T>,
|
||||
saturating_sub: fn(T, T) -> T,
|
||||
saturating_add: fn(T, T) -> T,
|
||||
saturating_sub_1: fn(T) -> T,
|
||||
saturating_add_1: fn(T) -> T,
|
||||
to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
@@ -640,9 +745,9 @@ fn render_numeric_stepper<
|
||||
id,
|
||||
num.to_string(),
|
||||
{
|
||||
let path = value.path.clone();
|
||||
let path = value.path;
|
||||
move |_, _, cx| {
|
||||
let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else {
|
||||
let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else {
|
||||
return;
|
||||
};
|
||||
let new_value = serde_json::Value::Number(number);
|
||||
@@ -650,7 +755,7 @@ fn render_numeric_stepper<
|
||||
}
|
||||
},
|
||||
move |_, _, cx| {
|
||||
let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else {
|
||||
let Some(number) = to_serde_number(saturating_add_1(num)).ok() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -672,8 +777,8 @@ fn render_switch_field(
|
||||
let path = value.path.clone();
|
||||
SwitchField::new(
|
||||
id,
|
||||
SharedString::new_static(value.title),
|
||||
value.documentation.map(SharedString::new_static),
|
||||
value.title.clone(),
|
||||
value.documentation.clone(),
|
||||
match value.read() {
|
||||
true => ToggleState::Selected,
|
||||
false => ToggleState::Unselected,
|
||||
@@ -703,7 +808,6 @@ fn render_toggle_button_group(
|
||||
let value = downcast_any_item::<String>(value);
|
||||
|
||||
fn make_toggle_group<const LEN: usize>(
|
||||
group_name: &'static str,
|
||||
value: SettingsValue<String>,
|
||||
variants: &'static [&'static str],
|
||||
labels: &'static [&'static str],
|
||||
@@ -727,7 +831,7 @@ fn render_toggle_button_group(
|
||||
|
||||
let mut idx = 0;
|
||||
ToggleButtonGroup::single_row(
|
||||
group_name,
|
||||
value.title.clone(),
|
||||
variants_array.map(|(variant, label)| {
|
||||
let path = value.path.clone();
|
||||
idx += 1;
|
||||
@@ -748,7 +852,7 @@ fn render_toggle_button_group(
|
||||
macro_rules! templ_toggl_with_const_param {
|
||||
($len:expr) => {
|
||||
if variants.len() == $len {
|
||||
return make_toggle_group::<$len>(value.title, value, variants, labels);
|
||||
return make_toggle_group::<$len>(value, variants, labels);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -762,13 +866,19 @@ fn render_toggle_button_group(
|
||||
}
|
||||
|
||||
fn settings_value_from_settings_and_path(
|
||||
path: SmallVec<[&'static str; 1]>,
|
||||
title: &'static str,
|
||||
documentation: Option<&'static str>,
|
||||
path: SmallVec<[SharedString; 1]>,
|
||||
fallback_path: Option<&[SharedString]>,
|
||||
title: SharedString,
|
||||
documentation: Option<SharedString>,
|
||||
user_settings: &serde_json::Value,
|
||||
default_settings: &serde_json::Value,
|
||||
) -> SettingsValue<serde_json::Value> {
|
||||
let default_value = read_settings_value_from_path(default_settings, &path)
|
||||
.or_else(|| {
|
||||
fallback_path.and_then(|fallback_path| {
|
||||
read_settings_value_from_path(default_settings, fallback_path)
|
||||
})
|
||||
})
|
||||
.with_context(|| format!("No default value for item at path {:?}", path.join(".")))
|
||||
.expect("Default value set for item")
|
||||
.clone();
|
||||
@@ -778,7 +888,7 @@ fn settings_value_from_settings_and_path(
|
||||
default_value,
|
||||
value,
|
||||
documentation,
|
||||
path: path.clone(),
|
||||
path,
|
||||
// todo(settings_ui) is title required inside SettingsValue?
|
||||
title,
|
||||
};
|
||||
|
||||
@@ -425,7 +425,7 @@ pub struct ToggleButtonGroup<T, const COLS: usize = 3, const ROWS: usize = 1>
|
||||
where
|
||||
T: ButtonBuilder,
|
||||
{
|
||||
group_name: &'static str,
|
||||
group_name: SharedString,
|
||||
rows: [[T; COLS]; ROWS],
|
||||
style: ToggleButtonGroupStyle,
|
||||
size: ToggleButtonGroupSize,
|
||||
@@ -435,9 +435,9 @@ where
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
|
||||
pub fn single_row(group_name: &'static str, buttons: [T; COLS]) -> Self {
|
||||
pub fn single_row(group_name: impl Into<SharedString>, buttons: [T; COLS]) -> Self {
|
||||
Self {
|
||||
group_name,
|
||||
group_name: group_name.into(),
|
||||
rows: [buttons],
|
||||
style: ToggleButtonGroupStyle::Transparent,
|
||||
size: ToggleButtonGroupSize::Default,
|
||||
@@ -449,9 +449,13 @@ impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
|
||||
pub fn two_rows(group_name: &'static str, first_row: [T; COLS], second_row: [T; COLS]) -> Self {
|
||||
pub fn two_rows(
|
||||
group_name: impl Into<SharedString>,
|
||||
first_row: [T; COLS],
|
||||
second_row: [T; COLS],
|
||||
) -> Self {
|
||||
Self {
|
||||
group_name,
|
||||
group_name: group_name.into(),
|
||||
rows: [first_row, second_row],
|
||||
style: ToggleButtonGroupStyle::Transparent,
|
||||
size: ToggleButtonGroupSize::Default,
|
||||
@@ -512,6 +516,7 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let entries =
|
||||
self.rows.into_iter().enumerate().map(|(row_index, row)| {
|
||||
let group_name = self.group_name.clone();
|
||||
row.into_iter().enumerate().map(move |(col_index, button)| {
|
||||
let ButtonConfiguration {
|
||||
label,
|
||||
@@ -523,7 +528,7 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||
|
||||
let entry_index = row_index * COLS + col_index;
|
||||
|
||||
ButtonLike::new((self.group_name, entry_index))
|
||||
ButtonLike::new((group_name.clone(), entry_index))
|
||||
.full_width()
|
||||
.rounding(None)
|
||||
.when_some(self.tab_index, |this, tab_index| {
|
||||
|
||||
Reference in New Issue
Block a user