diff --git a/Cargo.lock b/Cargo.lock index 14f2952b78fafbd947b7c6732339ac378ab9275e..3fcdbf1c2dc3702ce18ef76f79993682a581f366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14462,6 +14462,7 @@ dependencies = [ "serde_with", "settings_macros", "smallvec", + "strum 0.27.1", "tree-sitter", "tree-sitter-json", "unindent", @@ -14522,6 +14523,7 @@ dependencies = [ "serde", "session", "settings", + "strum 0.27.1", "theme", "ui", "util", diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index cfa3238ad65b22c1dcba2daaa9e70322819a493a..eb884c1e3f856a58938c679c3d781c1716e516fd 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -28,14 +28,15 @@ paths.workspace = true release_channel.workspace = true rust-embed.workspace = true schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings_macros = { path = "../settings_macros" } serde_json_lenient.workspace = true -serde_repr.workspace = true +serde_json.workspace = true serde_path_to_error.workspace = true +serde_repr.workspace = true serde_with.workspace = true +serde.workspace = true +settings_macros = { path = "../settings_macros" } smallvec.workspace = true +strum.workspace = true tree-sitter-json.workspace = true tree-sitter.workspace = true util.workspace = true diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 265d37be33349917d29048949be699fd03acd950..3651409130dd5255a968cd022fdc24b18a754d32 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -870,6 +870,12 @@ impl From for SaturatingBool { } } +impl From for bool { + fn from(value: SaturatingBool) -> bool { + value.0 + } +} + impl merge_from::MergeFrom for SaturatingBool { fn merge_from(&mut self, other: &Self) { self.0 |= other.0 diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index a443cf3faccb8755c6743acbe8ee11b0471a3e7f..d4879403a906399d81c0b0079167f8aa9673fd76 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -485,7 +485,18 @@ pub enum ScrollBeyondLastLine { /// The shape of a selection cursor. #[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, + Copy, + Clone, + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum CursorShape { diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index 017e89102dc2580e4924488809531806d6d662e8..29294c1e55a8994f2a5c1fbbe060789fb38545dd 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -217,6 +217,7 @@ pub enum ShowScrollbar { Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, )] #[serde(rename_all = "snake_case")] +// todo() -> combine with CursorShape pub enum CursorShapeContent { /// Cursor is a block like `█`. #[default] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 27ca294adde582a8580b83af207dc17d8e00d2c2..0024ff6069a6e768a4f553ec82e6965f9cadfd11 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -7,7 +7,7 @@ use futures::{ channel::{mpsc, oneshot}, future::LocalBoxFuture, }; -use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; +use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::{JsonSchema, json_schema}; @@ -34,7 +34,7 @@ use crate::{ ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent, LanguageToSettingsMap, SettingsJsonSchemaParams, ThemeName, VsCodeSettings, WorktreeId, merge_from::MergeFrom, - parse_json_with_comments, replace_value_in_json_text, + parse_json_with_comments, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent, }, @@ -439,34 +439,6 @@ impl SettingsStore { return rx; } - pub fn update_settings_file_at_path( - &self, - fs: Arc, - path: &[impl AsRef], - new_value: serde_json::Value, - ) -> oneshot::Receiver> { - let key_path = path - .into_iter() - .map(AsRef::as_ref) - .map(SharedString::new) - .collect::>(); - let update = move |mut old_text: String, cx: AsyncApp| { - cx.read_global(|store: &SettingsStore, _cx| { - // todo(settings_ui) use `update_value_in_json_text` for merging new and old objects with comment preservation, needs old value though... - let (range, replacement) = replace_value_in_json_text( - &old_text, - key_path.as_slice(), - store.json_tab_size(), - Some(&new_value), - None, - ); - old_text.replace_range(range, &replacement); - old_text - }) - }; - self.update_settings_file_inner(fs, update) - } - pub fn update_settings_file( &self, fs: Arc, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 53300f71e49f535638caf2c2152f9b6552ea882d..7b0c3200ac37fa27b5c376d42348239fe7c161fa 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -26,6 +26,7 @@ gpui.workspace = true menu.workspace = true serde.workspace = true settings.workspace = true +strum.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 680d46bdcfea85d906c4849c4dc0a7fdda664a3b..b8f974741002741c791459a08f76ccfc3173442c 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,5 +1,5 @@ //! # settings_ui -use std::{rc::Rc, sync::Arc}; +use std::sync::Arc; use editor::Editor; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; @@ -11,9 +11,9 @@ use project::WorktreeId; use settings::{SettingsContent, SettingsStore}; use ui::{ ActiveTheme as _, AnyElement, BorrowAppContext as _, Button, Clickable as _, Color, - FluentBuilder as _, Icon, IconName, InteractiveElement as _, Label, LabelCommon as _, - LabelSize, ParentElement, SharedString, StatefulInteractiveElement as _, Styled, Switch, - v_flex, + DropdownMenu, FluentBuilder as _, Icon, IconName, InteractiveElement as _, Label, + LabelCommon as _, LabelSize, ParentElement, SharedString, StatefulInteractiveElement as _, + Styled, Switch, v_flex, }; use util::{paths::PathStyle, rel_path::RelPath}; @@ -22,30 +22,24 @@ fn user_settings_data() -> Vec { SettingsPage { title: "General Page", items: vec![ - SettingsPageItem::SectionHeader("General Section"), + SettingsPageItem::SectionHeader("General"), SettingsPageItem::SettingItem(SettingItem { title: "Confirm Quit", description: "Whether to confirm before quitting Zed", - render: Rc::new(|_, cx| { - render_toggle_button( - "confirm_quit", - SettingsFile::User, - cx, - |settings_content| &mut settings_content.workspace.confirm_quit, - ) - }), + render: |file, _, cx| { + render_toggle_button("confirm_quit", file, cx, |settings_content| { + &mut settings_content.workspace.confirm_quit + }) + }, }), SettingsPageItem::SettingItem(SettingItem { title: "Auto Update", description: "Automatically update Zed (may be ignored on Linux if installed through a package manager)", - render: Rc::new(|_, cx| { - render_toggle_button( - "Auto Update", - SettingsFile::User, - cx, - |settings_content| &mut settings_content.auto_update, - ) - }), + render: |file, _, cx| { + render_toggle_button("Auto Update", file, cx, |settings_content| { + &mut settings_content.auto_update + }) + }, }), ], }, @@ -56,15 +50,45 @@ fn user_settings_data() -> Vec { SettingsPageItem::SettingItem(SettingItem { title: "Project Name", description: "The displayed name of this project. If not set, the root directory name", - render: Rc::new(|window, cx| { - render_text_field( - "project_name", - SettingsFile::User, + render: |file, window, cx| { + render_text_field("project_name", file, window, cx, |settings_content| { + &mut settings_content.project.worktree.project_name + }) + }, + }), + ], + }, + SettingsPage { + title: "AI", + items: vec![ + SettingsPageItem::SectionHeader("General"), + SettingsPageItem::SettingItem(SettingItem { + title: "Disable AI", + description: "Whether to disable all AI features in Zed", + render: |file, _, cx| { + render_toggle_button("disable_AI", file, cx, |settings_content| { + &mut settings_content.disable_ai + }) + }, + }), + ], + }, + SettingsPage { + title: "Appearance & Behavior", + items: vec![ + SettingsPageItem::SectionHeader("Cursor"), + SettingsPageItem::SettingItem(SettingItem { + title: "Cursor Shape", + description: "Cursor shape for the editor", + render: |file, window, cx| { + render_dropdown::( + "cursor_shape", + file, window, cx, - |settings_content| &mut settings_content.project.worktree.project_name, + |settings_content| &mut settings_content.editor.cursor_shape, ) - }), + }, }), ], }, @@ -79,18 +103,11 @@ fn project_settings_data() -> Vec { SettingsPageItem::SettingItem(SettingItem { title: "Project Name", description: " The displayed name of this project. If not set, the root directory name", - render: Rc::new(|window, cx| { - render_text_field( - "project_name", - SettingsFile::Local(( - WorktreeId::from_usize(0), - Arc::from(RelPath::new("TODO: actually pass through file").unwrap()), - )), - window, - cx, - |settings_content| &mut settings_content.project.worktree.project_name, - ) - }), + render: |file, window, cx| { + render_text_field("project_name", file, window, cx, |settings_content| { + &mut settings_content.project.worktree.project_name + }) + }, }), ], }] @@ -169,7 +186,7 @@ enum SettingsPageItem { } impl SettingsPageItem { - fn render(&self, window: &mut Window, cx: &mut App) -> AnyElement { + fn render(&self, file: SettingsFile, window: &mut Window, cx: &mut App) -> AnyElement { match self { SettingsPageItem::SectionHeader(header) => Label::new(SharedString::new_static(header)) .size(LabelSize::Large) @@ -177,7 +194,7 @@ impl SettingsPageItem { SettingsPageItem::SettingItem(setting_item) => div() .child(setting_item.title) .child(setting_item.description) - .child((setting_item.render)(window, cx)) + .child((setting_item.render)(file, window, cx)) .into_any_element(), } } @@ -196,7 +213,7 @@ impl SettingsPageItem { struct SettingItem { title: &'static str, description: &'static str, - render: std::rc::Rc AnyElement>, + render: fn(file: SettingsFile, &mut Window, &mut App) -> AnyElement, } #[allow(unused)] @@ -345,7 +362,11 @@ impl SettingsWindow { div() .child(self.render_files(window, cx)) .child(Label::new(page.title)) - .children(page.items.iter().map(|item| item.render(window, cx))) + .children( + page.items + .iter() + .map(|item| item.render(self.current_file.clone(), window, cx)), + ) } fn current_page(&self) -> &SettingsPage { @@ -440,11 +461,11 @@ fn render_text_field( .into_any_element() } -fn render_toggle_button( +fn render_toggle_button + From + Copy + Send + 'static>( id: &'static str, _: SettingsFile, cx: &mut App, - get_value: fn(&mut SettingsContent) -> &mut Option, + get_value: fn(&mut SettingsContent) -> &mut Option, ) -> AnyElement { // TODO: in settings window state let store = SettingsStore::global(cx); @@ -457,18 +478,78 @@ fn render_toggle_button( .unwrap_or_default() .content; - let toggle_state = - if get_value(&mut user_settings).unwrap_or_else(|| get_value(&mut defaults).unwrap()) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }; + let toggle_state = if get_value(&mut user_settings) + .unwrap_or_else(|| get_value(&mut defaults).unwrap()) + .into() + { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }; Switch::new(id, toggle_state) .on_click({ move |state, _window, cx| { - write_setting_value(get_value, Some(*state == ui::ToggleState::Selected), cx); + write_setting_value( + get_value, + Some((*state == ui::ToggleState::Selected).into()), + cx, + ); } }) .into_any_element() } + +fn render_dropdown( + id: &'static str, + _: SettingsFile, + window: &mut Window, + cx: &mut App, + get_value: fn(&mut SettingsContent) -> &mut Option, +) -> AnyElement +where + T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + 'static, +{ + let variants = || -> &'static [T] { ::VARIANTS }; + let labels = || -> &'static [&'static str] { ::VARIANTS }; + + let store = SettingsStore::global(cx); + let mut defaults = store.raw_default_settings().clone(); + let mut user_settings = store + .raw_user_settings() + .cloned() + .unwrap_or_default() + .content; + + let current_value = + get_value(&mut user_settings).unwrap_or_else(|| get_value(&mut defaults).unwrap()); + let current_value_label = + labels()[variants().iter().position(|v| *v == current_value).unwrap()]; + + DropdownMenu::new( + id, + current_value_label, + ui::ContextMenu::build(window, cx, move |mut menu, _, _| { + for (value, label) in variants() + .into_iter() + .copied() + .zip(labels().into_iter().copied()) + { + menu = menu.toggleable_entry( + label, + value == current_value, + ui::IconPosition::Start, + None, + move |_, cx| { + if value == current_value { + return; + } + write_setting_value(get_value, Some(value), cx); + }, + ); + } + menu + }), + ) + .into_any_element() +}