{
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)?