From 16c399876ca48808df5452e6502ae67fa144d11d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:15:08 -0300 Subject: [PATCH] settings_ui: Add ability to copy a link for a given setting (#41172) 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 Co-authored-by: Ben Kunkle --- 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 | 333 +++++++++++++++++--------- crates/zed/src/main.rs | 11 +- crates/zed/src/zed/open_listener.rs | 21 +- 6 files changed, 264 insertions(+), 140 deletions(-) create mode 100644 assets/icons/link.svg diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 0000000000..739d41b231 --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 4c026bc2b8..1442c482d8 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -145,6 +145,7 @@ pub enum IconName { Keyboard, Library, LineHeight, + Link, ListCollapse, ListFilter, ListTodo, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 394e6821c8..db791c0ab3 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -177,18 +177,17 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }), 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 { 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 { }, 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 { }, 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 { }, 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 { }, 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 { }, 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 { 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 { }, 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 { 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 { }, 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 { 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() diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index e60304a9f7..b2cc1dcf13 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/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 SettingField { 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, ) -> Stateful
, @@ -268,6 +269,7 @@ impl SettingFieldRenderer { field: SettingField, settings_file: SettingsUiFile, metadata: Option<&SettingsFieldMetadata>, + sub_field: bool, window: &mut Window, cx: &mut Context| { 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, SettingsUiFile, Option<&SettingsFieldMetadata>, + bool, &mut Window, &mut Context, ) -> Stateful
@@ -301,6 +304,7 @@ impl SettingFieldRenderer { item: &SettingItem, settings_file: SettingsUiFile, metadata: Option<&SettingsFieldMetadata>, + sub_field: bool, window: &mut Window, cx: &mut Context| { 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, fuzzy_match_candidates: Vec, - key_lut: Vec, + key_lut: Vec, } -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
| -> Stateful
{ 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| { + |setting_item: &SettingItem, + padding: bool, + sub_field: bool, + cx: &mut Context| { let renderer = cx.default_global::().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(); + .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; - } + 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") - }; + 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
{ 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) { 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 = vec![]; + let mut key_lut: Vec = 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) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 93feb4a71d..b873a58d3b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -853,10 +853,13 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, 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, + ), + } }); } } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 855c8d39e7..3abb76715d 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -43,11 +43,20 @@ pub struct OpenRequest { #[derive(Debug)] pub enum OpenRequestKind { CliConnection((mpsc::Receiver, IpcSender)), - 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, + }, } 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)?