diff --git a/assets/settings/default.json b/assets/settings/default.json index 81c25ef54066f257d31dcb5273ea1dd2496a4cad..a77ce82f00b746adf9efa3886474f4d0fe53847c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1876,21 +1876,19 @@ // Allows to enable/disable formatting with Prettier // and configure default Prettier, used when no project-level Prettier installation is found. "prettier": { - // // Whether to consider prettier formatter or not when attempting to format a file. - "allowed": false - // - // // Use regular Prettier json configuration. - // // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if - // // the project has no other Prettier installed. - // "plugins": [], - // - // // Use regular Prettier json configuration. - // // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if - // // the project has no other Prettier installed. + // Enables or disables formatting with Prettier for any given language. + "allowed": false, + // Forces Prettier integration to use a specific parser name when formatting files with the language. + "plugins": [], + // 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. // "trailingComma": "es5", // "tabWidth": 4, // "semi": false, // "singleQuote": true + // Forces Prettier integration to use a specific parser name when formatting files with the language + // when set to a non-empty string. + "parser": "" }, // Settings for auto-closing of JSX tags. "jsx_tag_auto_close": { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 53b8fcb85cde41e94c54289bb11b94314da76868..f815ecc8517d1e3e83f8614c21786e7b1a6cbfd0 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -533,9 +533,9 @@ impl settings::Settings for AllLanguageSettings { formatter: settings.formatter.unwrap(), prettier: PrettierSettings { allowed: prettier.allowed.unwrap(), - parser: prettier.parser, - plugins: prettier.plugins, - options: prettier.options, + parser: prettier.parser.filter(|parser| !parser.is_empty()), + plugins: prettier.plugins.unwrap_or_default(), + options: prettier.options.unwrap_or_default(), }, jsx_tag_auto_close: settings.jsx_tag_auto_close.unwrap().enabled.unwrap(), enable_language_server: settings.enable_language_server.unwrap(), @@ -571,7 +571,7 @@ impl settings::Settings for AllLanguageSettings { code_actions_on_format: settings.code_actions_on_format.unwrap(), linked_edits: settings.linked_edits.unwrap(), tasks: LanguageTaskSettings { - variables: tasks.variables, + variables: tasks.variables.unwrap_or_default(), enabled: tasks.enabled.unwrap(), prefer_lsp: tasks.prefer_lsp.unwrap(), }, @@ -579,7 +579,7 @@ impl settings::Settings for AllLanguageSettings { show_completion_documentation: settings.show_completion_documentation.unwrap(), completions: CompletionSettings { words: completions.words.unwrap(), - words_min_length: completions.words_min_length.unwrap(), + words_min_length: completions.words_min_length.unwrap() as usize, lsp: completions.lsp.unwrap(), lsp_fetch_timeout_ms: completions.lsp_fetch_timeout_ms.unwrap(), lsp_insert_mode: completions.lsp_insert_mode.unwrap(), diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index a91127eb46aa855c4ce62411842d18c8abba61a4..b56e64465336dbc7726c20ed187fe9e71068cb65 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -385,7 +385,19 @@ pub struct WhitespaceMapContent { } /// The behavior of `editor::Rewrap`. -#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] +#[derive( + Debug, + PartialEq, + Clone, + Copy, + Default, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum RewrapBehavior { /// Only rewrap within comments. @@ -500,7 +512,7 @@ pub struct CompletionSettingsContent { /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. /// /// Default: 3 - pub words_min_length: Option, + pub words_min_length: Option, /// Whether to fetch LSP completions or not. /// /// Default: true @@ -516,7 +528,19 @@ pub struct CompletionSettingsContent { pub lsp_insert_mode: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum LspInsertMode { /// Replaces text before the cursor, using the `insert` range described in the LSP specification. @@ -532,7 +556,19 @@ pub enum LspInsertMode { } /// Controls how document's words are completed. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "snake_case")] pub enum WordsCompletionMode { /// Always fetch document's words for completions along with LSP completions. @@ -559,17 +595,29 @@ pub struct PrettierSettingsContent { /// Forces Prettier integration to use specific plugins when formatting files with the language. /// The default Prettier will be installed with these plugins. - #[serde(default)] - pub plugins: HashSet, + pub plugins: Option>, /// 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)] - pub options: HashMap, + pub options: Option>, } +/// TODO: this should just be a bool /// Controls the behavior of formatting files when they are saved. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] #[serde(rename_all = "lowercase")] pub enum FormatOnSave { /// Files should be formatted on save. @@ -745,11 +793,10 @@ pub struct IndentGuideSettingsContent { /// The task settings for a particular language. #[skip_serializing_none] -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)] pub struct LanguageTaskSettingsContent { /// Extra task variables to set for a particular language. - #[serde(default)] - pub variables: HashMap, + pub variables: Option>, pub enabled: Option, /// Use LSP tasks over Zed language extension ones. /// If no LSP tasks are returned due to error/timeout or regular execution, @@ -768,7 +815,18 @@ pub struct LanguageToSettingsMap(pub HashMap = serde_json::from_str(raw_auto); assert!(result.is_err()); } + + #[test] + fn test_prettier_options() { + let raw_prettier = r#"{"allowed": false, "tabWidth": 4, "semi": false}"#; + let result = serde_json::from_str::(raw_prettier) + .expect("Failed to parse prettier options"); + assert!( + result + .options + .as_ref() + .expect("options were flattened") + .contains_key("semi") + ); + assert!( + result + .options + .as_ref() + .expect("options were flattened") + .contains_key("tabWidth") + ); + } } diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index b31e6f07d65a208c0dd22cec0e8bc777bb87809b..372cb7decdfda789196ca8ebfda6537f015c455d 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1,8 +1,10 @@ -use gpui::IntoElement; -use std::rc::Rc; +use settings::{LanguageSettingsContent, SettingsContent}; +use std::sync::Arc; +use ui::{IntoElement, SharedString}; use crate::{ SettingField, SettingItem, SettingsFieldMetadata, SettingsPage, SettingsPageItem, SubPageLink, + sub_page_stack, }; pub(crate) fn user_settings_data() -> Vec { @@ -488,7 +490,7 @@ pub(crate) fn user_settings_data() -> Vec { }), metadata: None, }), - // todo(settings_ui): Needs numeric stepper + option within an option + // todo(settings_ui): Needs numeric stepper option within an option // SettingsPageItem::SettingItem(SettingItem { // title: "Centered Layout Left Padding", // description: "Left padding for centered layout", @@ -1258,35 +1260,51 @@ pub(crate) fn user_settings_data() -> Vec { }, SettingsPage { title: "Languages & Frameworks", - items: vec![ - SettingsPageItem::SectionHeader("General"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Language Server", - description: "Whether to use language servers to provide code intelligence", - field: Box::new(SettingField { - pick: |settings_content| { - &settings_content - .project - .all_languages - .defaults - .enable_language_server - }, - pick_mut: |settings_content| { - &mut settings_content - .project - .all_languages - .defaults - .enable_language_server - }, - }), - metadata: None, - }), - SettingsPageItem::SectionHeader("Languages"), - SettingsPageItem::SubPageLink(SubPageLink { - title: "JSON", - render: Rc::new(|_, _, _| "A settings page!".into_any_element()), - }), - ], + items: { + let mut items = vec![ + SettingsPageItem::SectionHeader(LANGUAGES_SECTION_HEADER), + SettingsPageItem::SubPageLink(SubPageLink { + title: "JSON", + render: Arc::new(|this, window, cx| { + this.render_page_items(language_settings_data().iter(), window, cx) + .into_any_element() + }), + }), + SettingsPageItem::SubPageLink(SubPageLink { + title: "JSONC", + render: Arc::new(|this, window, cx| { + this.render_page_items(language_settings_data().iter(), window, cx) + .into_any_element() + }), + }), + SettingsPageItem::SubPageLink(SubPageLink { + title: "Rust", + render: Arc::new(|this, window, cx| { + this.render_page_items(language_settings_data().iter(), window, cx) + .into_any_element() + }), + }), + SettingsPageItem::SubPageLink(SubPageLink { + title: "Python", + render: Arc::new(|this, window, cx| { + this.render_page_items(language_settings_data().iter(), window, cx) + .into_any_element() + }), + }), + SettingsPageItem::SubPageLink(SubPageLink { + title: "TSX", + render: Arc::new(|this, window, cx| { + this.render_page_items(language_settings_data().iter(), window, cx) + .into_any_element() + }), + }), + ]; + + items.push(SettingsPageItem::SectionHeader("Default Language Settings")); + items.extend(language_settings_data()); + + items + }, }, SettingsPage { title: "Workbench & Window", @@ -2559,3 +2577,1190 @@ pub(crate) fn project_settings_data() -> Vec { }, ] } + +const LANGUAGES_SECTION_HEADER: &'static str = "Languages"; + +fn language_settings_data() -> Vec { + fn current_language() -> Option { + sub_page_stack().iter().find_map(|page| { + (page.section_header == LANGUAGES_SECTION_HEADER) + .then(|| SharedString::new_static(page.link.title)) + }) + } + + fn language_settings_field( + settings_content: &SettingsContent, + get: fn(&LanguageSettingsContent) -> &Option, + ) -> &Option { + let all_languages = &settings_content.project.all_languages; + let mut language_content = None; + if let Some(current_language) = current_language() { + language_content = all_languages.languages.0.get(¤t_language); + } + let value = language_content + .map(get) + .unwrap_or_else(|| get(&all_languages.defaults)); + return value; + } + + fn language_settings_field_mut( + settings_content: &mut SettingsContent, + get: fn(&mut LanguageSettingsContent) -> &mut Option, + ) -> &mut Option { + let all_languages = &mut settings_content.project.all_languages; + let language_content = if let Some(current_language) = current_language() { + all_languages + .languages + .0 + .entry(current_language) + .or_default() + } else { + &mut all_languages.defaults + }; + return get(language_content); + } + + vec![ + SettingsPageItem::SectionHeader("Indentation"), + SettingsPageItem::SettingItem(SettingItem { + title: "Tab Size", + description: "How many columns a tab should occupy", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.tab_size) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| &mut language.tab_size) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Hard Tabs", + description: "Whether to indent lines using tab characters, as opposed to multiple spaces", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.hard_tabs) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.hard_tabs + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Auto Indent", + description: "Whether indentation should be adjusted based on the context whilst typing", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.auto_indent) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.auto_indent + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Auto Indent On Paste", + description: "Whether indentation of pasted content should be adjusted based on the context", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.auto_indent_on_paste + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.auto_indent_on_paste + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Wrapping"), + SettingsPageItem::SettingItem(SettingItem { + title: "Soft Wrap", + description: "How to soft-wrap long lines of text", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.soft_wrap) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.soft_wrap + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Wrap Guides", + description: "Whether to show wrap guides in the editor. Setting this to true will show a guide at the 'preferred_line_length' value if softwrap is set to 'preferred_line_length', and will show any additional guides as specified by the 'wrap_guides' setting", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.show_wrap_guides) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.show_wrap_guides + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Preferred Line Length", + description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.preferred_line_length + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.preferred_line_length + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Wrap Guides", + description: "Character counts at which to show wrap guides in the editor", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.wrap_guides) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.wrap_guides + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Allow Rewrap", + description: "Controls where the `editor::Rewrap` action is allowed for this language", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.allow_rewrap) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.allow_rewrap + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Indent Guides"), + SettingsPageItem::SettingItem(SettingItem { + title: "Enabled", + description: "Whether to display indent guides in the editor", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(indent_guides) = &language.indent_guides { + &indent_guides.enabled + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.indent_guides.get_or_insert_default().enabled + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Line Width", + description: "The width of the indent guides in pixels, between 1 and 10", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(indent_guides) = &language.indent_guides { + &indent_guides.line_width + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.indent_guides.get_or_insert_default().line_width + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Active Line Width", + description: "The width of the active indent guide in pixels, between 1 and 10", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(indent_guides) = &language.indent_guides { + &indent_guides.active_line_width + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .indent_guides + .get_or_insert_default() + .active_line_width + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Coloring", + description: "Determines how indent guides are colored", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(indent_guides) = &language.indent_guides { + &indent_guides.coloring + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.indent_guides.get_or_insert_default().coloring + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Background Coloring", + description: "Determines how indent guide backgrounds are colored", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(indent_guides) = &language.indent_guides { + &indent_guides.background_coloring + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .indent_guides + .get_or_insert_default() + .background_coloring + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Formatting"), + SettingsPageItem::SettingItem(SettingItem { + title: "Format On Save", + description: "Whether or not to perform a buffer format before saving", + field: Box::new( + // TODO(settings_ui): this setting should just be a bool + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.format_on_save + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.format_on_save + }) + }, + }, + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Remove Trailing Whitespace On Save", + description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.remove_trailing_whitespace_on_save + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.remove_trailing_whitespace_on_save + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Ensure Final Newline On Save", + description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.ensure_final_newline_on_save + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.ensure_final_newline_on_save + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Formatter", + description: "How to perform a buffer format", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.formatter) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.formatter + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Use On Type Format", + description: "Whether to use additional LSP queries to format (and amend) the code after every \"trigger\" symbol input, defined by LSP server capabilities", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.use_on_type_format + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.use_on_type_format + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Code Actions On Format", + description: "Which code actions to run on save after the formatter. These are not run if formatting is off", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.code_actions_on_format + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.code_actions_on_format + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SectionHeader("Prettier"), + SettingsPageItem::SettingItem(SettingItem { + title: "Allowed", + description: "Enables or disables formatting with Prettier for a given language", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(prettier) = &language.prettier { + &prettier.allowed + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.prettier.get_or_insert_default().allowed + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Parser", + description: "Forces Prettier integration to use a specific parser name when formatting files with the language", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(prettier) = &language.prettier { + &prettier.parser + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.prettier.get_or_insert_default().parser + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Plugins", + description: "Forces Prettier integration to use specific plugins when formatting files with the language", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(prettier) = &language.prettier { + &prettier.plugins + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.prettier.get_or_insert_default().plugins + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Options", + description: "Default Prettier options, in the format as in package.json section for Prettier", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(prettier) = &language.prettier { + &prettier.options + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.prettier.get_or_insert_default().options + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SectionHeader("Autoclose"), + SettingsPageItem::SettingItem(SettingItem { + title: "Use Autoclose", + description: "Whether to automatically type closing characters for you. For example, when you type (, Zed will automatically add a closing ) at the correct position", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.use_autoclose) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.use_autoclose + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Use Auto Surround", + description: "Whether to automatically surround text with characters for you. For example, when you select text and type (, Zed will automatically surround text with ()", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.use_auto_surround + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.use_auto_surround + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Always Treat Brackets As Autoclosed", + description: "Controls how the editor handles the autoclosed characters. When set to `false`(default), skipping over and auto-removing of the closing characters happen only for auto-inserted characters. Otherwise(when `true`), the closing characters are always skipped over and auto-removed no matter how they were inserted", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.always_treat_brackets_as_autoclosed + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.always_treat_brackets_as_autoclosed + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Jsx Tag Auto Close", + description: "Whether to automatically close JSX tags", + field: Box::new(SettingField { + // TODO(settings_ui): this setting should just be a bool + pick: |settings_content| { + language_settings_field(settings_content, |language| { + match language.jsx_tag_auto_close.as_ref() { + Some(s) => &s.enabled, + None => &None, + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.jsx_tag_auto_close.get_or_insert_default().enabled + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("LSP"), + SettingsPageItem::SettingItem(SettingItem { + title: "Enable Language Server", + description: "Whether to use language servers to provide code intelligence", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.enable_language_server + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.enable_language_server + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Language Servers", + description: "The list of language servers to use (or disable) for this language", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.language_servers + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.language_servers + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Linked Edits", + description: "Whether to perform linked edits of associated ranges, if the language server supports it. For example, when editing opening tag, the contents of the closing tag will be edited as well", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.linked_edits) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.linked_edits + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Edit Predictions"), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Edit Predictions", + description: "Controls whether edit predictions are shown immediately (true) or manually by triggering `editor::ShowEditPrediction` (false)", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.show_edit_predictions + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.show_edit_predictions + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Edit Predictions Disabled In", + description: "Controls whether edit predictions are shown in the given language scopes", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.edit_predictions_disabled_in + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.edit_predictions_disabled_in + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SectionHeader("Whitespace"), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Whitespaces", + description: "Whether to show tabs and spaces in the editor", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.show_whitespaces) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.show_whitespaces + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Space Whitespace Indicator", + description: "Visible character used to render space characters when show_whitespaces is enabled (default: \"•\")", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(whitespace_map) = &language.whitespace_map { + &whitespace_map.space + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.whitespace_map.get_or_insert_default().space + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Tab Whitespace Indicator", + description: "Visible character used to render tab characters when show_whitespaces is enabled (default: \"→\")", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(whitespace_map) = &language.whitespace_map { + &whitespace_map.tab + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.whitespace_map.get_or_insert_default().tab + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SectionHeader("Completions"), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Completions On Input", + description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.show_completions_on_input + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.show_completions_on_input + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Completion Documentation", + description: "Whether to display inline and alongside documentation for items in the completions menu", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.show_completion_documentation + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.show_completion_documentation + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Words", + description: "Controls how words are completed", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(completions) = &language.completions { + &completions.words + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.completions.get_or_insert_default().words + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Words Min Length", + description: "How many characters has to be in the completions query to automatically show the words-based completions", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(completions) = &language.completions { + &completions.words_min_length + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .completions + .get_or_insert_default() + .words_min_length + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Lsp", + description: "Whether to fetch LSP completions or not", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(completions) = &language.completions { + &completions.lsp + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.completions.get_or_insert_default().lsp + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Lsp Fetch Timeout Ms", + description: "When fetching LSP completions, determines how long to wait for a response of a particular server (set to 0 to wait indefinitely)", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(completions) = &language.completions { + &completions.lsp_fetch_timeout_ms + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .completions + .get_or_insert_default() + .lsp_fetch_timeout_ms + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Lsp Insert Mode", + description: "Controls how LSP completions are inserted", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(completions) = &language.completions { + &completions.lsp_insert_mode + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.completions.get_or_insert_default().lsp_insert_mode + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Inlay Hints"), + SettingsPageItem::SettingItem(SettingItem { + title: "Enabled", + description: "Global switch to toggle hints on and off", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.enabled + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.inlay_hints.get_or_insert_default().enabled + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Value Hints", + description: "Global switch to toggle inline values on and off when debugging", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.show_value_hints + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .inlay_hints + .get_or_insert_default() + .show_value_hints + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Type Hints", + description: "Whether type hints should be shown", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.show_type_hints + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.inlay_hints.get_or_insert_default().show_type_hints + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Parameter Hints", + description: "Whether parameter hints should be shown", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.show_parameter_hints + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .inlay_hints + .get_or_insert_default() + .show_parameter_hints + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Other Hints", + description: "Whether other hints should be shown", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.show_other_hints + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .inlay_hints + .get_or_insert_default() + .show_other_hints + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Show Background", + description: "Whether to show a background for inlay hints", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.show_background + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.inlay_hints.get_or_insert_default().show_background + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Edit Debounce Ms", + description: "Whether or not to debounce inlay hints updates after buffer edits (set to 0 to disable debouncing)", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.edit_debounce_ms + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .inlay_hints + .get_or_insert_default() + .edit_debounce_ms + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Scroll Debounce Ms", + description: "Whether or not to debounce inlay hints updates after buffer scrolls (set to 0 to disable debouncing)", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.scroll_debounce_ms + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .inlay_hints + .get_or_insert_default() + .scroll_debounce_ms + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Toggle On Modifiers Press", + description: "Toggles inlay hints (hides or shows) when the user presses the modifiers specified", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(inlay_hints) = &language.inlay_hints { + &inlay_hints.toggle_on_modifiers_press + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language + .inlay_hints + .get_or_insert_default() + .toggle_on_modifiers_press + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SectionHeader("Tasks"), + SettingsPageItem::SettingItem(SettingItem { + title: "Enabled", + description: "Whether tasks are enabled for this language", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(tasks) = &language.tasks { + &tasks.enabled + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.tasks.get_or_insert_default().enabled + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Variables", + description: "Extra task variables to set for a particular language", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(tasks) = &language.tasks { + &tasks.variables + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.tasks.get_or_insert_default().variables + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Prefer Lsp", + description: "Use LSP tasks over Zed language extension ones", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + if let Some(tasks) = &language.tasks { + &tasks.prefer_lsp + } else { + &None + } + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.tasks.get_or_insert_default().prefer_lsp + }) + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Miscellaneous"), + SettingsPageItem::SettingItem(SettingItem { + title: "Debuggers", + description: "Preferred debuggers for this language", + field: Box::new( + SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| &language.debuggers) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.debuggers + }) + }, + } + .unimplemented(), + ), + metadata: None, + }), + SettingsPageItem::SettingItem(SettingItem { + title: "Extend Comment On Newline", + description: "Whether to start a new line with a comment when a previous line is a comment as well", + field: Box::new(SettingField { + pick: |settings_content| { + language_settings_field(settings_content, |language| { + &language.extend_comment_on_newline + }) + }, + pick_mut: |settings_content| { + language_settings_field_mut(settings_content, |language| { + &mut language.extend_comment_on_newline + }) + }, + }), + metadata: None, + }), + ] +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 955a94eb0d215126d47b4623544cee2bfba12d7c..25617d3244306d6e5df112453f1521243b73aaf5 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -23,7 +23,7 @@ use std::{ num::NonZeroU32, ops::Range, rc::Rc, - sync::{Arc, atomic::AtomicBool}, + sync::{Arc, LazyLock, RwLock, atomic::AtomicBool}, }; use ui::{ ButtonLike, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, PopoverMenu, @@ -309,6 +309,26 @@ fn init_renderers(cx: &mut App) { .add_renderer::(|settings_field, file, _, window, cx| { render_dropdown(*settings_field, file, window, cx) }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) + .add_renderer::( + |settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }, + ) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) + .add_renderer::(|settings_field, file, _, window, cx| { + render_dropdown(*settings_field, file, window, cx) + }) .add_renderer::(|settings_field, file, _, window, cx| { render_numeric_stepper(*settings_field, file, window, cx) }) @@ -353,6 +373,26 @@ pub fn open_settings_editor(cx: &mut App) -> anyhow::Result>> = LazyLock::new(|| RwLock::new(Vec::new())); + +fn sub_page_stack() -> std::sync::RwLockReadGuard<'static, Vec> { + SUB_PAGE_STACK + .read() + .expect("SUB_PAGE_STACK is never poisoned") +} + +fn sub_page_stack_mut() -> std::sync::RwLockWriteGuard<'static, Vec> { + SUB_PAGE_STACK + .write() + .expect("SUB_PAGE_STACK is never poisoned") +} + pub struct SettingsWindow { files: Vec, current_file: SettingsUiFile, @@ -363,10 +403,6 @@ pub struct SettingsWindow { navbar_entries: Vec, list_handle: UniformListScrollHandle, search_matches: Vec>, - /// The current sub page path that is selected. - /// If this is empty the selected page is rendered, - /// otherwise the last sub page gets rendered. - sub_page_stack: Vec, scroll_handle: ScrollHandle, } @@ -548,7 +584,12 @@ impl PartialEq for SettingItem { #[derive(Clone)] struct SubPageLink { title: &'static str, - render: Rc AnyElement>, + render: Arc< + dyn Fn(&mut SettingsWindow, &mut Window, &mut Context) -> AnyElement + + 'static + + Send + + Sync, + >, } impl PartialEq for SubPageLink { @@ -647,7 +688,6 @@ impl SettingsWindow { search_bar, search_task: None, search_matches: vec![], - sub_page_stack: vec![], scroll_handle: ScrollHandle::new(), }; @@ -978,7 +1018,7 @@ impl SettingsWindow { let mut items = vec![]; items.push(self.current_page().title); items.extend( - self.sub_page_stack + sub_page_stack() .iter() .flat_map(|page| [page.section_header, page.link.title]), ); @@ -995,6 +1035,73 @@ impl SettingsWindow { .child(Label::new(last)) } + fn render_page_items<'a, Items: Iterator>( + &self, + items: Items, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let mut page_content = v_flex() + .id("settings-ui-page") + .size_full() + .gap_4() + .overflow_y_scroll() + .track_scroll(&self.scroll_handle); + + let items: Vec<_> = items.collect(); + let items_len = items.len(); + let mut section_header = None; + + let has_active_search = !self.search_bar.read(cx).is_empty(cx); + let has_no_results = items_len == 0 && has_active_search; + + if has_no_results { + let search_query = self.search_bar.read(cx).text(cx); + page_content = page_content.child( + v_flex() + .size_full() + .items_center() + .justify_center() + .gap_1() + .child(div().child("No Results")) + .child( + div() + .text_sm() + .text_color(cx.theme().colors().text_muted) + .child(format!("No settings match \"{}\"", search_query)), + ), + ) + } else { + let last_non_header_index = items + .iter() + .enumerate() + .rev() + .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_))) + .map(|(index, _)| index); + + page_content = + page_content.children(items.clone().into_iter().enumerate().map(|(index, item)| { + let no_bottom_border = items + .get(index + 1) + .map(|next_item| matches!(next_item, SettingsPageItem::SectionHeader(_))) + .unwrap_or(false); + let is_last = Some(index) == last_non_header_index; + + if let SettingsPageItem::SectionHeader(header) = item { + section_header = Some(*header); + } + item.render( + self.current_file.clone(), + section_header.expect("All items rendered after a section header"), + no_bottom_border || is_last, + window, + cx, + ) + })) + } + page_content + } + fn render_page( &mut self, window: &mut Window, @@ -1009,70 +1116,13 @@ impl SettingsWindow { .bg(cx.theme().colors().editor_background) .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx); - let mut page_content = v_flex() - .id("settings-ui-page") - .size_full() - .gap_4() - .overflow_y_scroll() - .track_scroll(&self.scroll_handle); + let page_content; - if self.sub_page_stack.len() == 0 { + if sub_page_stack().len() == 0 { page = page.child(self.render_files(window, cx)); - - let items: Vec<_> = self.page_items().collect(); - let items_len = items.len(); - let mut section_header = None; - - let search_query = self.search_bar.read(cx).text(cx); - let has_active_search = !search_query.is_empty(); - let has_no_results = items_len == 0 && has_active_search; - - if has_no_results { - page_content = page_content.child( - v_flex() - .size_full() - .items_center() - .justify_center() - .gap_1() - .child(div().child("No Results")) - .child( - div() - .text_sm() - .text_color(cx.theme().colors().text_muted) - .child(format!("No settings match \"{}\"", search_query)), - ), - ) - } else { - let last_non_header_index = items - .iter() - .enumerate() - .rev() - .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_))) - .map(|(index, _)| index); - - page_content = page_content.children(items.clone().into_iter().enumerate().map( - |(index, item)| { - let no_bottom_border = items - .get(index + 1) - .map(|next_item| { - matches!(next_item, SettingsPageItem::SectionHeader(_)) - }) - .unwrap_or(false); - let is_last = Some(index) == last_non_header_index; - - if let SettingsPageItem::SectionHeader(header) = item { - section_header = Some(*header); - } - item.render( - self.current_file.clone(), - section_header.expect("All items rendered after a section header"), - no_bottom_border || is_last, - window, - cx, - ) - }, - )) - } + page_content = self + .render_page_items(self.page_items(), window, cx) + .into_any_element(); } else { page = page.child( h_flex() @@ -1089,8 +1139,8 @@ impl SettingsWindow { .child(self.render_sub_page_breadcrumbs()), ); - let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone(); - page_content = page_content.child((active_page_render_fn)(self, window, cx)); + let active_page_render_fn = sub_page_stack().last().unwrap().link.render.clone(); + page_content = (active_page_render_fn)(self, window, cx); } return page.child(page_content); @@ -1122,7 +1172,7 @@ impl SettingsWindow { section_header: &'static str, cx: &mut Context, ) { - self.sub_page_stack.push(SubPage { + sub_page_stack_mut().push(SubPage { link: sub_page_link, section_header, }); @@ -1130,7 +1180,7 @@ impl SettingsWindow { } fn pop_sub_page(&mut self, cx: &mut Context) { - self.sub_page_stack.pop(); + sub_page_stack_mut().pop(); cx.notify(); } } @@ -1558,7 +1608,6 @@ mod test { list_handle: UniformListScrollHandle::default(), search_matches: vec![], search_task: None, - sub_page_stack: vec![], scroll_handle: ScrollHandle::new(), };