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 <cameron.studdstreet@gmail.com> Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
co-authored by
cameron
Ben Kunkle
parent
3583e129d1
commit
16c399876c
@@ -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>
|
||||
|
After Width: | Height: | Size: 735 B |
@@ -145,6 +145,7 @@ pub enum IconName {
|
||||
Keyboard,
|
||||
Library,
|
||||
LineHeight,
|
||||
Link,
|
||||
ListCollapse,
|
||||
ListFilter,
|
||||
ListTodo,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
.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<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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?
|
||||
|
||||
Reference in New Issue
Block a user