settings_ui: Add ability to copy a link for a given setting (#41172)

Danilo Leal , cameron , and Ben Kunkle created

Release Notes:

- settings_ui: Added the ability to copy a link to a given setting,
allowing users to quickly open the settings window at the correct
location in a faster way.

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>

Change summary

assets/icons/link.svg                 |   5 
crates/icons/src/icons.rs             |   1 
crates/settings_ui/src/page_data.rs   |  33 +-
crates/settings_ui/src/settings_ui.rs | 337 ++++++++++++++++++----------
crates/zed/src/main.rs                |  11 
crates/zed/src/zed/open_listener.rs   |  21 +
6 files changed, 266 insertions(+), 142 deletions(-)

Detailed changes

assets/icons/link.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.2 11H5C4.20435 11 3.44129 10.6839 2.87868 10.1213C2.31607 9.55871 2 8.79565 2 8C2 7.20435 2.31607 6.44129 2.87868 5.87868C3.44129 5.31607 4.20435 5 5 5H6.2" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.80005 5H11C11.7957 5 12.5588 5.31607 13.1214 5.87868C13.684 6.44129 14 7.20435 14 8C14 8.79565 13.684 9.55871 13.1214 10.1213C12.5588 10.6839 11.7957 11 11 11H9.80005" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.6001 8H10.4001" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/icons/src/icons.rs 🔗

@@ -145,6 +145,7 @@ pub enum IconName {
     Keyboard,
     Library,
     LineHeight,
+    Link,
     ListCollapse,
     ListFilter,
     ListTodo,

crates/settings_ui/src/page_data.rs 🔗

@@ -177,18 +177,17 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SectionHeader("Scoped Settings"),
                 SettingsPageItem::SettingItem(SettingItem {
-                    // todo(settings_ui): Implement another setting item type that just shows an edit in settings.json
                     files: USER,
                     title: "Preview Channel",
                     description: "Which settings should be activated only in Preview build of Zed.",
                     field: Box::new(
                         SettingField {
-                            json_path: Some("use_system_prompts"),
+                            json_path: Some("preview_channel_settings"),
                             pick: |settings_content| {
-                                settings_content.workspace.use_system_prompts.as_ref()
+                                Some(settings_content)
                             },
-                            write: |settings_content, value| {
-                                settings_content.workspace.use_system_prompts = value;
+                            write: |_settings_content, _value| {
+
                             },
                         }
                         .unimplemented(),
@@ -201,12 +200,11 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.",
                     field: Box::new(
                         SettingField {
-                            json_path: Some(""),
+                            json_path: Some("settings_profiles"),
                             pick: |settings_content| {
-                                settings_content.workspace.use_system_prompts.as_ref()
+                                Some(settings_content)
                             },
-                            write: |settings_content, value| {
-                                settings_content.workspace.use_system_prompts = value;
+                            write: |_settings_content, _value| {
                             },
                         }
                         .unimplemented(),
@@ -290,6 +288,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             },
                             write: |settings_content, value| {
                                 let Some(value) = value else {
+                                    settings_content.theme.theme = None;
                                     return;
                                 };
                                 let settings_value = settings_content.theme.theme.get_or_insert_with(|| {
@@ -459,6 +458,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             },
                             write: |settings_content, value| {
                                 let Some(value) = value else {
+                                    settings_content.theme.icon_theme = None;
                                     return;
                                 };
                                 let settings_value = settings_content.theme.icon_theme.get_or_insert_with(|| {
@@ -662,6 +662,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             },
                             write: |settings_content, value| {
                                 let Some(value) = value else {
+                                    settings_content.theme.buffer_line_height = None;
                                     return;
                                 };
                                 let settings_value = settings_content.theme.buffer_line_height.get_or_insert_with(|| {
@@ -1123,6 +1124,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                 },
                                 write: |settings_content, value| {
                                     let Some(value) = value else {
+                                        settings_content.workspace.autosave = None;
                                         return;
                                     };
                                     let settings_value = settings_content.workspace.autosave.get_or_insert_with(|| {
@@ -1171,6 +1173,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                             },
                                             write: |settings_content, value| {
                                                 let Some(value) = value else {
+                                                    settings_content.workspace.autosave = None;
                                                     return;
                                                 };
                                                 match settings_content
@@ -2105,7 +2108,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                         description: "A mapping from languages to files and file extensions that should be treated as that language.",
                         field: Box::new(
                             SettingField {
-                                json_path: Some("file_types"),
+                                json_path: Some("file_type_associations"),
                                 pick: |settings_content| {
                                     settings_content.project.all_languages.file_types.as_ref()
                                 },
@@ -4449,6 +4452,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             },
                             write: |settings_content, value| {
                                 let Some(value) = value else {
+                                    if let Some(terminal) = settings_content.terminal.as_mut() {
+                                        terminal.project.shell = None;
+                                    }
                                     return;
                                 };
                                 let settings_value = settings_content
@@ -4500,7 +4506,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                                     title: "Program",
                                     description: "The shell program to use.",
                                     field: Box::new(SettingField {
-                                        json_path: Some("terminal.shell.program"),
+                                        json_path: Some("terminal.shell"),
                                         pick: |settings_content| {
                                             match settings_content.terminal.as_ref()?.project.shell.as_ref() {
                                                 Some(settings::Shell::Program(program)) => Some(program),
@@ -4632,6 +4638,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             },
                             write: |settings_content, value| {
                                 let Some(value) = value else {
+                                    if let Some(terminal) = settings_content.terminal.as_mut() {
+                                        terminal.project.working_directory = None;
+                                    }
                                     return;
                                 };
                                 let settings_value = settings_content
@@ -7092,7 +7101,7 @@ fn non_editor_language_settings_data() -> Vec<SettingsPageItem> {
             description: "Default Prettier options, in the format as in package.json section for Prettier.",
             field: Box::new(
                 SettingField {
-                    json_path: Some("languages.$(language).prettier"),
+                    json_path: Some("languages.$(language).prettier.options"),
                     pick: |settings_content| {
                         language_settings_field(settings_content, |language| {
                             language.prettier.as_ref()?.options.as_ref()

crates/settings_ui/src/settings_ui.rs 🔗

@@ -6,10 +6,10 @@ use editor::{Editor, EditorEvent};
 use feature_flags::FeatureFlag;
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    Action, App, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, Focusable, Global,
-    KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful, Subscription, Task,
-    TitlebarOptions, UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions,
-    actions, div, list, point, prelude::*, px, uniform_list,
+    Action, App, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle,
+    Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful,
+    Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowBounds,
+    WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list,
 };
 use heck::ToTitleCase as _;
 use project::{Project, WorktreeId};
@@ -137,7 +137,7 @@ impl<T: 'static> SettingField<T> {
         SettingField {
             pick: |_| Some(&UnimplementedSettingField),
             write: |_, _| unreachable!(),
-            json_path: None,
+            json_path: self.json_path,
         }
     }
 }
@@ -239,6 +239,7 @@ struct SettingFieldRenderer {
                         &SettingItem,
                         SettingsUiFile,
                         Option<&SettingsFieldMetadata>,
+                        bool,
                         &mut Window,
                         &mut Context<SettingsWindow>,
                     ) -> Stateful<Div>,
@@ -268,6 +269,7 @@ impl SettingFieldRenderer {
                   field: SettingField<T>,
                   settings_file: SettingsUiFile,
                   metadata: Option<&SettingsFieldMetadata>,
+                  sub_field: bool,
                   window: &mut Window,
                   cx: &mut Context<SettingsWindow>| {
                 render_settings_item(
@@ -275,7 +277,7 @@ impl SettingFieldRenderer {
                     item,
                     settings_file.clone(),
                     render_control(field, settings_file, metadata, window, cx),
-                    window,
+                    sub_field,
                     cx,
                 )
             },
@@ -290,6 +292,7 @@ impl SettingFieldRenderer {
             SettingField<T>,
             SettingsUiFile,
             Option<&SettingsFieldMetadata>,
+            bool,
             &mut Window,
             &mut Context<SettingsWindow>,
         ) -> Stateful<Div>
@@ -301,6 +304,7 @@ impl SettingFieldRenderer {
                   item: &SettingItem,
                   settings_file: SettingsUiFile,
                   metadata: Option<&SettingsFieldMetadata>,
+                  sub_field: bool,
                   window: &mut Window,
                   cx: &mut Context<SettingsWindow>| {
                 let field = *item
@@ -315,6 +319,7 @@ impl SettingFieldRenderer {
                     field,
                     settings_file,
                     metadata,
+                    sub_field,
                     window,
                     cx,
                 )
@@ -515,43 +520,10 @@ pub fn open_settings_editor(
             return;
         }
 
-        settings_window.current_file = SettingsUiFile::User;
-        settings_window.build_ui(window, cx);
-
-        let mut item_info = None;
-        'search: for (nav_entry_index, entry) in settings_window.navbar_entries.iter().enumerate() {
-            if entry.is_root {
-                continue;
-            }
-            let page_index = entry.page_index;
-            let header_index = entry
-                .item_index
-                .expect("non-root entries should have an item index");
-            for item_index in header_index + 1..settings_window.pages[page_index].items.len() {
-                let item = &settings_window.pages[page_index].items[item_index];
-                if let SettingsPageItem::SectionHeader(_) = item {
-                    break;
-                }
-                if let SettingsPageItem::SettingItem(item) = item {
-                    if item.field.json_path() == Some(path) {
-                        if !item.files.contains(USER) {
-                            log::error!("Found item {}, but it is not a user setting", path);
-                            return;
-                        }
-                        item_info = Some((item_index, nav_entry_index));
-                        break 'search;
-                    }
-                }
-            }
-        }
-        let Some((item_index, navbar_entry_index)) = item_info else {
-            log::error!("Failed to find item for {}", path);
-            return;
-        };
-
-        settings_window.open_navbar_entry_page(navbar_entry_index);
-        window.focus(&settings_window.focus_handle_for_content_element(item_index, cx));
-        settings_window.scroll_to_content_item(item_index, window, cx);
+        settings_window.search_bar.update(cx, |editor, cx| {
+            editor.set_text(format!("#{path}"), window, cx);
+        });
+        settings_window.update_matches(cx);
     }
 
     let existing_window = cx
@@ -677,13 +649,14 @@ pub struct SettingsWindow {
 struct SearchIndex {
     bm25_engine: bm25::SearchEngine<usize>,
     fuzzy_match_candidates: Vec<StringMatchCandidate>,
-    key_lut: Vec<SearchItemKey>,
+    key_lut: Vec<SearchKeyLUTEntry>,
 }
 
-struct SearchItemKey {
+struct SearchKeyLUTEntry {
     page_index: usize,
     header_index: usize,
     item_index: usize,
+    json_path: Option<&'static str>,
 }
 
 struct SubPage {
@@ -742,18 +715,20 @@ impl SettingsPageItem {
     ) -> AnyElement {
         let file = settings_window.current_file.clone();
 
-        let border_variant = cx.theme().colors().border_variant;
         let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
             let element = element.pt_4();
             if is_last {
                 element.pb_10()
             } else {
-                element.pb_4().border_b_1().border_color(border_variant)
+                element.pb_4()
             }
         };
 
         let mut render_setting_item_inner =
-            |setting_item: &SettingItem, padding: bool, cx: &mut Context<SettingsWindow>| {
+            |setting_item: &SettingItem,
+             padding: bool,
+             sub_field: bool,
+             cx: &mut Context<SettingsWindow>| {
                 let renderer = cx.default_global::<SettingFieldRenderer>().clone();
                 let (_, found) = setting_item.field.file_set_in(file.clone(), cx);
 
@@ -777,6 +752,7 @@ impl SettingsPageItem {
                             setting_item,
                             file.clone(),
                             setting_item.metadata.as_deref(),
+                            sub_field,
                             window,
                             cx,
                         )
@@ -794,7 +770,7 @@ impl SettingsPageItem {
                             .tab_index(0_isize)
                             .tooltip(Tooltip::text(setting_item.field.type_name()))
                             .into_any_element(),
-                        window,
+                        sub_field,
                         cx,
                     ),
                 };
@@ -811,6 +787,7 @@ impl SettingsPageItem {
         match self {
             SettingsPageItem::SectionHeader(header) => v_flex()
                 .w_full()
+                .px_8()
                 .gap_1p5()
                 .child(
                     Label::new(SharedString::new_static(header))
@@ -821,56 +798,71 @@ impl SettingsPageItem {
                 .child(Divider::horizontal().color(DividerColor::BorderFaded))
                 .into_any_element(),
             SettingsPageItem::SettingItem(setting_item) => {
-                let (field_with_padding, _) = render_setting_item_inner(setting_item, true, cx);
-                field_with_padding.into_any_element()
+                let (field_with_padding, _) =
+                    render_setting_item_inner(setting_item, true, false, cx);
+
+                v_flex()
+                    .group("setting-item")
+                    .px_8()
+                    .child(field_with_padding)
+                    .when(!is_last, |this| this.child(Divider::horizontal()))
+                    .into_any_element()
             }
-            SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
-                .id(sub_page_link.title.clone())
-                .w_full()
-                .min_w_0()
-                .justify_between()
-                .map(apply_padding)
+            SettingsPageItem::SubPageLink(sub_page_link) => v_flex()
+                .group("setting-item")
+                .px_8()
                 .child(
-                    v_flex()
+                    h_flex()
+                        .id(sub_page_link.title.clone())
                         .w_full()
-                        .max_w_1_2()
-                        .child(Label::new(sub_page_link.title.clone())),
-                )
-                .child(
-                    Button::new(
-                        ("sub-page".into(), sub_page_link.title.clone()),
-                        "Configure",
-                    )
-                    .icon(IconName::ChevronRight)
-                    .tab_index(0_isize)
-                    .icon_position(IconPosition::End)
-                    .icon_color(Color::Muted)
-                    .icon_size(IconSize::Small)
-                    .style(ButtonStyle::OutlinedGhost)
-                    .size(ButtonSize::Medium)
-                    .on_click({
-                        let sub_page_link = sub_page_link.clone();
-                        cx.listener(move |this, _, _, cx| {
-                            let mut section_index = item_index;
-                            let current_page = this.current_page();
-
-                            while !matches!(
-                                current_page.items[section_index],
-                                SettingsPageItem::SectionHeader(_)
-                            ) {
-                                section_index -= 1;
-                            }
-
-                            let SettingsPageItem::SectionHeader(header) =
-                                current_page.items[section_index]
-                            else {
-                                unreachable!("All items always have a section header above them")
-                            };
+                        .min_w_0()
+                        .justify_between()
+                        .map(apply_padding)
+                        .child(
+                            v_flex()
+                                .w_full()
+                                .max_w_1_2()
+                                .child(Label::new(sub_page_link.title.clone())),
+                        )
+                        .child(
+                            Button::new(
+                                ("sub-page".into(), sub_page_link.title.clone()),
+                                "Configure",
+                            )
+                            .icon(IconName::ChevronRight)
+                            .tab_index(0_isize)
+                            .icon_position(IconPosition::End)
+                            .icon_color(Color::Muted)
+                            .icon_size(IconSize::Small)
+                            .style(ButtonStyle::OutlinedGhost)
+                            .size(ButtonSize::Medium)
+                            .on_click({
+                                let sub_page_link = sub_page_link.clone();
+                                cx.listener(move |this, _, _, cx| {
+                                    let mut section_index = item_index;
+                                    let current_page = this.current_page();
+
+                                    while !matches!(
+                                        current_page.items[section_index],
+                                        SettingsPageItem::SectionHeader(_)
+                                    ) {
+                                        section_index -= 1;
+                                    }
+
+                                    let SettingsPageItem::SectionHeader(header) =
+                                        current_page.items[section_index]
+                                    else {
+                                        unreachable!(
+                                            "All items always have a section header above them"
+                                        )
+                                    };
 
-                            this.push_sub_page(sub_page_link.clone(), header, cx)
-                        })
-                    }),
+                                    this.push_sub_page(sub_page_link.clone(), header, cx)
+                                })
+                            }),
+                        ),
                 )
+                .when(!is_last, |this| this.child(Divider::horizontal()))
                 .into_any_element(),
             SettingsPageItem::DynamicItem(DynamicItem {
                 discriminant: discriminant_setting_item,
@@ -883,18 +875,22 @@ impl SettingsPageItem {
                     .1;
 
                 let (discriminant_element, rendered_ok) =
-                    render_setting_item_inner(discriminant_setting_item, true, cx);
+                    render_setting_item_inner(discriminant_setting_item, true, false, cx);
 
                 let has_sub_fields =
                     rendered_ok && discriminant.map(|d| !fields[d].is_empty()).unwrap_or(false);
 
-                let discriminant_element = if has_sub_fields {
-                    discriminant_element.pb_4().border_b_0()
-                } else {
-                    discriminant_element
-                };
-
-                let mut content = v_flex().id("dynamic-item").child(discriminant_element);
+                let mut content = v_flex()
+                    .id("dynamic-item")
+                    .child(
+                        div()
+                            .group("setting-item")
+                            .px_8()
+                            .child(discriminant_element.when(has_sub_fields, |this| this.pb_4())),
+                    )
+                    .when(!has_sub_fields, |this| {
+                        this.child(h_flex().px_8().child(Divider::horizontal()))
+                    });
 
                 if rendered_ok {
                     let discriminant =
@@ -904,12 +900,13 @@ impl SettingsPageItem {
 
                     for (index, field) in sub_fields.iter().enumerate() {
                         let is_last_sub_field = index == sub_field_count - 1;
-                        let (raw_field, _) = render_setting_item_inner(field, false, cx);
+                        let (raw_field, _) = render_setting_item_inner(field, false, true, cx);
 
                         content = content.child(
                             raw_field
+                                .group("setting-sub-item")
+                                .mx_8()
                                 .p_4()
-                                .border_x_1()
                                 .border_t_1()
                                 .when(is_last_sub_field, |this| this.border_b_1())
                                 .when(is_last_sub_field && is_last, |this| this.mb_8())
@@ -931,18 +928,33 @@ fn render_settings_item(
     setting_item: &SettingItem,
     file: SettingsUiFile,
     control: AnyElement,
-    _window: &mut Window,
+    sub_field: bool,
     cx: &mut Context<'_, SettingsWindow>,
 ) -> Stateful<Div> {
     let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx);
     let file_set_in = SettingsUiFile::from_settings(found_in_file.clone());
 
+    let clipboard_has_link = cx
+        .read_from_clipboard()
+        .and_then(|entry| entry.text())
+        .map_or(false, |maybe_url| {
+            setting_item.field.json_path().is_some()
+                && maybe_url.strip_prefix("zed://settings/") == setting_item.field.json_path()
+        });
+
+    let (link_icon, link_icon_color) = if clipboard_has_link {
+        (IconName::Check, Color::Success)
+    } else {
+        (IconName::Link, Color::Muted)
+    };
+
     h_flex()
         .id(setting_item.title)
         .min_w_0()
         .justify_between()
         .child(
             v_flex()
+                .relative()
                 .w_1_2()
                 .child(
                     h_flex()
@@ -950,9 +962,13 @@ fn render_settings_item(
                         .gap_1()
                         .child(Label::new(SharedString::new_static(setting_item.title)))
                         .when_some(
-                            setting_item
-                                .field
-                                .reset_to_default_fn(&file, &found_in_file, cx),
+                            if sub_field {
+                                None
+                            } else {
+                                setting_item
+                                    .field
+                                    .reset_to_default_fn(&file, &found_in_file, cx)
+                            },
                             |this, reset_to_default| {
                                 this.child(
                                     IconButton::new("reset-to-default-btn", IconName::Undo)
@@ -990,6 +1006,41 @@ fn render_settings_item(
                 ),
         )
         .child(control)
+        .when(sub_page_stack().is_empty(), |this| {
+            // Intentionally using the description to make the icon button
+            // unique because some items share the same title (e.g., "Font Size")
+            let icon_button_id =
+                SharedString::new(format!("copy-link-btn-{}", setting_item.description));
+
+            this.child(
+                div()
+                    .absolute()
+                    .top(rems_from_px(18.))
+                    .map(|this| {
+                        if sub_field {
+                            this.visible_on_hover("setting-sub-item")
+                                .left(rems_from_px(-8.5))
+                        } else {
+                            this.visible_on_hover("setting-item")
+                                .left(rems_from_px(-22.))
+                        }
+                    })
+                    .child({
+                        IconButton::new(icon_button_id, link_icon)
+                            .icon_color(link_icon_color)
+                            .icon_size(IconSize::Small)
+                            .shape(IconButtonShape::Square)
+                            .tooltip(Tooltip::text("Copy Link"))
+                            .when_some(setting_item.field.json_path(), |this, path| {
+                                this.on_click(cx.listener(move |_, _, _, cx| {
+                                    let link = format!("zed://settings/{}", path);
+                                    cx.write_to_clipboard(ClipboardItem::new_string(link));
+                                    cx.notify();
+                                }))
+                            })
+                    }),
+            )
+        })
 }
 
 struct SettingItem {
@@ -1478,7 +1529,7 @@ impl SettingsWindow {
 
     fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
         self.search_task.take();
-        let query = self.search_bar.read(cx).text(cx);
+        let mut query = self.search_bar.read(cx).text(cx);
         if query.is_empty() || self.search_index.is_none() {
             for page in &mut self.filter_table {
                 page.fill(true);
@@ -1490,6 +1541,14 @@ impl SettingsWindow {
             return;
         }
 
+        let is_json_link_query;
+        if query.starts_with("#") {
+            query.remove(0);
+            is_json_link_query = true;
+        } else {
+            is_json_link_query = false;
+        }
+
         let search_index = self.search_index.as_ref().unwrap().clone();
 
         fn update_matches_inner(
@@ -1503,10 +1562,11 @@ impl SettingsWindow {
             }
 
             for match_index in match_indices {
-                let SearchItemKey {
+                let SearchKeyLUTEntry {
                     page_index,
                     header_index,
                     item_index,
+                    ..
                 } = search_index.key_lut[match_index];
                 let page = &mut this.filter_table[page_index];
                 page[header_index] = true;
@@ -1520,6 +1580,29 @@ impl SettingsWindow {
         }
 
         self.search_task = Some(cx.spawn(async move |this, cx| {
+            if is_json_link_query {
+                let mut indices = vec![];
+                for (index, SearchKeyLUTEntry { json_path, .. }) in
+                    search_index.key_lut.iter().enumerate()
+                {
+                    let Some(json_path) = json_path else {
+                        continue;
+                    };
+
+                    if let Some(post) = query.strip_prefix(json_path)
+                        && (post.is_empty() || post.starts_with('.'))
+                    {
+                        indices.push(index);
+                    }
+                }
+                if !indices.is_empty() {
+                    this.update(cx, |this, cx| {
+                        update_matches_inner(this, search_index.as_ref(), indices.into_iter(), cx);
+                    })
+                    .ok();
+                    return;
+                }
+            }
             let bm25_task = cx.background_spawn({
                 let search_index = search_index.clone();
                 let max_results = search_index.key_lut.len();
@@ -1610,7 +1693,7 @@ impl SettingsWindow {
     }
 
     fn build_search_index(&mut self) {
-        let mut key_lut: Vec<SearchItemKey> = vec![];
+        let mut key_lut: Vec<SearchKeyLUTEntry> = vec![];
         let mut documents = Vec::default();
         let mut fuzzy_match_candidates = Vec::default();
 
@@ -1632,11 +1715,16 @@ impl SettingsWindow {
             let mut header_str = "";
             for (item_index, item) in page.items.iter().enumerate() {
                 let key_index = key_lut.len();
+                let mut json_path = None;
                 match item {
                     SettingsPageItem::DynamicItem(DynamicItem {
                         discriminant: item, ..
                     })
                     | SettingsPageItem::SettingItem(item) => {
+                        json_path = item
+                            .field
+                            .json_path()
+                            .map(|path| path.trim_end_matches('$'));
                         documents.push(bm25::Document {
                             id: key_index,
                             contents: [page.title, header_str, item.title, item.description]
@@ -1670,10 +1758,11 @@ impl SettingsWindow {
                 push_candidates(&mut fuzzy_match_candidates, key_index, page.title);
                 push_candidates(&mut fuzzy_match_candidates, key_index, header_str);
 
-                key_lut.push(SearchItemKey {
+                key_lut.push(SearchKeyLUTEntry {
                     page_index,
                     header_index,
                     item_index,
+                    json_path,
                 });
             }
         }
@@ -1901,7 +1990,6 @@ impl SettingsWindow {
 
         h_flex()
             .w_full()
-            .pb_4()
             .gap_1()
             .justify_between()
             .track_focus(&self.files_focus_handle)
@@ -2524,6 +2612,7 @@ impl SettingsWindow {
                 cx.processor(move |this, index, window, cx| {
                     if index == 0 {
                         return div()
+                            .px_8()
                             .when(sub_page_stack().is_empty(), |this| {
                                 this.when_some(root_nav_label, |this, title| {
                                     this.child(
@@ -2551,9 +2640,9 @@ impl SettingsWindow {
 
                     v_flex()
                         .id(("settings-page-item", actual_item_index))
+                        .track_focus(&item_focus_handle)
                         .w_full()
                         .min_w_0()
-                        .track_focus(&item_focus_handle)
                         .child(item.render(
                             this,
                             actual_item_index,
@@ -2668,7 +2757,6 @@ impl SettingsWindow {
         } else {
             page_header = h_flex()
                 .ml_neg_1p5()
-                .pb_4()
                 .gap_1()
                 .child(
                     IconButton::new("back-btn", IconName::ArrowLeft)
@@ -2708,7 +2796,7 @@ impl SettingsWindow {
                             .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
                     )
                     .action_slot(
-                        div().pr_1().child(
+                        div().pr_1().pb_1().child(
                             Button::new("fix-in-json", "Fix in settings.json")
                                 .tab_index(0_isize)
                                 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
@@ -2718,11 +2806,12 @@ impl SettingsWindow {
                         ),
                     )
             }
+
             let parse_error = error.parse_error();
             let parse_failed = parse_error.is_some();
+
             warning_banner = v_flex()
                 .gap_2()
-                .pb_4()
                 .when_some(parse_error, |this, err| {
                     this.child(banner(
                         "Failed to load your settings. Some values may be incorrect and changes may be lost.",
@@ -2827,14 +2916,20 @@ impl SettingsWindow {
                 this.vertical_scrollbar_for(self.sub_page_scroll_handle.clone(), window, cx)
             })
             .track_focus(&self.content_focus_handle.focus_handle(cx))
-            .flex_1()
             .pt_6()
-            .px_8()
+            .gap_4()
+            .flex_1()
             .bg(cx.theme().colors().editor_background)
-            .child(page_header)
-            .child(warning_banner)
+            .child(
+                v_flex()
+                    .px_8()
+                    .gap_2()
+                    .child(page_header)
+                    .child(warning_banner),
+            )
             .child(
                 div()
+                    .flex_1()
                     .size_full()
                     .tab_group()
                     .tab_index(CONTENT_GROUP_TAB_INDEX)

crates/zed/src/main.rs 🔗

@@ -853,10 +853,13 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                 // languages.$(language).tab_size
                 // [ languages $(language) tab_size]
                 workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
-                    window.dispatch_action(
-                        Box::new(zed_actions::OpenSettingsAt { path: setting_path }),
-                        cx,
-                    );
+                    match setting_path {
+                        None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx),
+                        Some(setting_path) => window.dispatch_action(
+                            Box::new(zed_actions::OpenSettingsAt { path: setting_path }),
+                            cx,
+                        ),
+                    }
                 });
             }
         }

crates/zed/src/zed/open_listener.rs 🔗

@@ -43,11 +43,20 @@ pub struct OpenRequest {
 #[derive(Debug)]
 pub enum OpenRequestKind {
     CliConnection((mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)),
-    Extension { extension_id: String },
+    Extension {
+        extension_id: String,
+    },
     AgentPanel,
-    DockMenuAction { index: usize },
-    BuiltinJsonSchema { schema_path: String },
-    Setting { setting_path: String },
+    DockMenuAction {
+        index: usize,
+    },
+    BuiltinJsonSchema {
+        schema_path: String,
+    },
+    Setting {
+        // None just opens settings without navigating to a specific path
+        setting_path: Option<String>,
+    },
 }
 
 impl OpenRequest {
@@ -94,9 +103,11 @@ impl OpenRequest {
                 this.kind = Some(OpenRequestKind::BuiltinJsonSchema {
                     schema_path: schema_path.to_string(),
                 });
+            } else if url == "zed://settings" || url == "zed://settings/" {
+                this.kind = Some(OpenRequestKind::Setting { setting_path: None });
             } else if let Some(setting_path) = url.strip_prefix("zed://settings/") {
                 this.kind = Some(OpenRequestKind::Setting {
-                    setting_path: setting_path.to_string(),
+                    setting_path: Some(setting_path.to_string()),
                 });
             } else if url.starts_with("ssh://") {
                 this.parse_ssh_file_path(&url, cx)?