Compare commits

...

4 Commits

Author SHA1 Message Date
cameron
4a8c602f45 fmt 2025-10-24 23:58:35 +01:00
cameron
30551ba66e [WIP] - add "copy link" button to settings UI
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-10-24 22:47:20 +01:00
cameron
d689465e60 allow raw zed://settings and zed://settings/ links to open settings without a specific path 2025-10-24 22:46:41 +01:00
cameron
4a64796d7b change settings link implementation to use search filters, prefixed by
`#`

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-10-24 20:45:32 +01:00
4 changed files with 139 additions and 58 deletions

View File

@@ -4500,7 +4500,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),

View File

@@ -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,
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, 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};
@@ -18,13 +18,13 @@ use schemars::JsonSchema;
use serde::Deserialize;
use settings::{Settings, SettingsContent, SettingsStore};
use std::{
any::{Any, TypeId, type_name},
any::{type_name, Any, TypeId},
cell::RefCell,
collections::HashMap,
num::{NonZero, NonZeroU32},
ops::Range,
rc::Rc,
sync::{Arc, LazyLock, RwLock},
sync::{atomic::AtomicI32, Arc, LazyLock, RwLock},
};
use title_bar::platform_title_bar::PlatformTitleBar;
use ui::{
@@ -512,43 +512,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
@@ -673,13 +640,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 {
@@ -933,17 +901,47 @@ fn render_settings_item(
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| {
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::Hash, Color::Muted)
};
h_flex()
.id(setting_item.title)
.relative()
.min_w_0()
.justify_between()
.child(
v_flex()
.w_1_2()
.group("setting-item")
.child(
h_flex()
.w_full()
.gap_1()
.ml_neg_8()
// .group_hover("setting-item", |s| s.gap_10())
.child(
IconButton::new("copy-link-btn", 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();
}))
})
)
.child(Label::new(SharedString::new_static(setting_item.title)))
.when_some(
setting_item
@@ -986,6 +984,38 @@ fn render_settings_item(
),
)
.child(control)
// .when(sub_page_stack().is_empty(), |this| {
// this.child(
// div()
// .visible_on_hover("setting-item")
// .absolute()
// .top_0()
// .left_neg_5(
// )
// .child({
// IconButton::new("copy-link-btn", 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 {
@@ -1462,7 +1492,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);
@@ -1474,6 +1504,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(
@@ -1487,10 +1525,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;
@@ -1504,6 +1543,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();
@@ -1591,7 +1653,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();
@@ -1613,11 +1675,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]
@@ -1651,10 +1718,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,
});
}
}
@@ -2744,12 +2812,14 @@ impl SettingsWindow {
.track_focus(&self.content_focus_handle.focus_handle(cx))
.flex_1()
.pt_6()
.px_8()
// .px_8()
.bg(cx.theme().colors().editor_background)
.child(warning_banner)
.child(page_header)
.child(
div()
.px_8()
// .debug_bg_red()
.size_full()
.tab_group()
.tab_index(CONTENT_GROUP_TAB_INDEX)

View File

@@ -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,
),
}
});
}
}

View File

@@ -47,7 +47,10 @@ pub enum OpenRequestKind {
AgentPanel,
DockMenuAction { index: usize },
BuiltinJsonSchema { schema_path: String },
Setting { setting_path: String },
Setting {
// None just opens settings without navigating to a specific path
setting_path: Option<String> ,
},
}
impl OpenRequest {
@@ -94,9 +97,14 @@ impl OpenRequest {
this.kind = Some(OpenRequestKind::BuiltinJsonSchema {
schema_path: schema_path.to_string(),
});
} else if let Some(setting_path) = url.strip_prefix("zed://settings/") {
} else if url == "zed://settings" || url == "zed://settings/" {
this.kind = Some(OpenRequestKind::Setting {
setting_path: setting_path.to_string(),
setting_path: None
});
}
else if let Some(setting_path) = url.strip_prefix("zed://settings/") {
this.kind = Some(OpenRequestKind::Setting {
setting_path: Some(setting_path.to_string()),
});
} else if url.starts_with("ssh://") {
this.parse_ssh_file_path(&url, cx)?