settings_ui: Language settings UI (#39640)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

assets/settings/default.json                     |   20 
crates/language/src/language_settings.rs         |   10 
crates/settings/src/settings_content/language.rs |  116 +
crates/settings_ui/src/page_data.rs              | 1269 +++++++++++++++++
crates/settings_ui/src/settings_ui.rs            |  199 +-
5 files changed, 1,478 insertions(+), 136 deletions(-)

Detailed changes

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": {

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(),

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<usize>,
+    pub words_min_length: Option<u32>,
     /// Whether to fetch LSP completions or not.
     ///
     /// Default: true
@@ -516,7 +528,19 @@ pub struct CompletionSettingsContent {
     pub lsp_insert_mode: Option<LspInsertMode>,
 }
 
-#[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<String>,
+    pub plugins: Option<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)]
-    pub options: HashMap<String, serde_json::Value>,
+    pub options: Option<HashMap<String, serde_json::Value>>,
 }
 
+/// 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<String, String>,
+    pub variables: Option<HashMap<String, String>>,
     pub enabled: Option<bool>,
     /// 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<SharedString, LanguageSettingsConte
 
 /// Determines how indent guides are colored.
 #[derive(
-    Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+    Default,
+    Debug,
+    Copy,
+    Clone,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum IndentGuideColoring {
@@ -783,7 +841,18 @@ pub enum IndentGuideColoring {
 
 /// Determines how indent guide backgrounds are colored.
 #[derive(
-    Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+    Default,
+    Debug,
+    Copy,
+    Clone,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum IndentGuideBackgroundColoring {
@@ -836,4 +905,25 @@ mod test {
         let result: Result<LanguageSettingsContent, _> = 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::<PrettierSettingsContent>(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")
+        );
+    }
 }

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<SettingsPage> {
@@ -488,7 +490,7 @@ pub(crate) fn user_settings_data() -> Vec<SettingsPage> {
                     }),
                     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> {
         },
         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<SettingsPage> {
         },
     ]
 }
+
+const LANGUAGES_SECTION_HEADER: &'static str = "Languages";
+
+fn language_settings_data() -> Vec<SettingsPageItem> {
+    fn current_language() -> Option<SharedString> {
+        sub_page_stack().iter().find_map(|page| {
+            (page.section_header == LANGUAGES_SECTION_HEADER)
+                .then(|| SharedString::new_static(page.link.title))
+        })
+    }
+
+    fn language_settings_field<T>(
+        settings_content: &SettingsContent,
+        get: fn(&LanguageSettingsContent) -> &Option<T>,
+    ) -> &Option<T> {
+        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(&current_language);
+        }
+        let value = language_content
+            .map(get)
+            .unwrap_or_else(|| get(&all_languages.defaults));
+        return value;
+    }
+
+    fn language_settings_field_mut<T>(
+        settings_content: &mut SettingsContent,
+        get: fn(&mut LanguageSettingsContent) -> &mut Option<T>,
+    ) -> &mut Option<T> {
+        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 <html> tag, the contents of the closing </html> 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,
+        }),
+    ]
+}

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::ShowCloseButton>(|settings_field, file, _, window, cx| {
             render_dropdown(*settings_field, file, window, cx)
         })
+        .add_renderer::<settings::RewrapBehavior>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::FormatOnSave>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::IndentGuideColoring>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::IndentGuideBackgroundColoring>(
+            |settings_field, file, _, window, cx| {
+                render_dropdown(*settings_field, file, window, cx)
+            },
+        )
+        .add_renderer::<settings::WordsCompletionMode>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::LspInsertMode>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
         .add_renderer::<f32>(|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<WindowHandle<Setting
     )
 }
 
+/// The current sub page path that is selected.
+/// If this is empty the selected page is rendered,
+/// otherwise the last sub page gets rendered.
+///
+/// Global so that `pick` and `pick_mut` callbacks can access it
+/// and use it to dynamically render sub pages (e.g. for language settings)
+static SUB_PAGE_STACK: LazyLock<RwLock<Vec<SubPage>>> = LazyLock::new(|| RwLock::new(Vec::new()));
+
+fn sub_page_stack() -> std::sync::RwLockReadGuard<'static, Vec<SubPage>> {
+    SUB_PAGE_STACK
+        .read()
+        .expect("SUB_PAGE_STACK is never poisoned")
+}
+
+fn sub_page_stack_mut() -> std::sync::RwLockWriteGuard<'static, Vec<SubPage>> {
+    SUB_PAGE_STACK
+        .write()
+        .expect("SUB_PAGE_STACK is never poisoned")
+}
+
 pub struct SettingsWindow {
     files: Vec<SettingsUiFile>,
     current_file: SettingsUiFile,
@@ -363,10 +403,6 @@ pub struct SettingsWindow {
     navbar_entries: Vec<NavBarEntry>,
     list_handle: UniformListScrollHandle,
     search_matches: Vec<Vec<bool>>,
-    /// 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<SubPage>,
     scroll_handle: ScrollHandle,
 }
 
@@ -548,7 +584,12 @@ impl PartialEq for SettingItem {
 #[derive(Clone)]
 struct SubPageLink {
     title: &'static str,
-    render: Rc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) -> AnyElement>,
+    render: Arc<
+        dyn Fn(&mut SettingsWindow, &mut Window, &mut Context<SettingsWindow>) -> 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<Item = &'a SettingsPageItem>>(
+        &self,
+        items: Items,
+        window: &mut Window,
+        cx: &mut Context<SettingsWindow>,
+    ) -> 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<SettingsWindow>,
     ) {
-        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<SettingsWindow>) {
-        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(),
         };