Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions crates/agent_ui/src/agent_configuration/tool_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,7 @@ impl PickerDelegate for ToolPickerDelegate {
self.selected_index = ix;
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
let item = &self.filtered_items[ix];
match item {
PickerItem::Tool { .. } => true,
Expand Down
7 changes: 1 addition & 6 deletions crates/agent_ui/src/config_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,12 +493,7 @@ impl PickerDelegate for ConfigOptionPickerDelegate {
cx.notify();
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
match self.filtered_entries.get(ix) {
Some(ConfigOptionPickerEntry::Option(_)) => true,
Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
Expand Down
7 changes: 1 addition & 6 deletions crates/agent_ui/src/language_model_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx.notify();
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
match self.filtered_entries.get(ix) {
Some(LanguageModelPickerEntry::Model(_)) => true,
Some(LanguageModelPickerEntry::Separator(_)) | None => false,
Expand Down
7 changes: 1 addition & 6 deletions crates/agent_ui/src/model_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,7 @@ impl PickerDelegate for ModelPickerDelegate {
cx.notify();
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
match self.filtered_entries.get(ix) {
Some(ModelPickerEntry::Model(_, _)) => true,
Some(ModelPickerEntry::Separator(_)) | None => false,
Expand Down
7 changes: 1 addition & 6 deletions crates/agent_ui/src/profile_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,12 +443,7 @@ impl PickerDelegate for ProfilePickerDelegate {
cx.notify();
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
match self.filtered_entries.get(ix) {
Some(ProfilePickerEntry::Profile(_)) => true,
Some(ProfilePickerEntry::Header(_)) | None => false,
Expand Down
4 changes: 1 addition & 3 deletions crates/picker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ workspace.workspace = true
zed_actions.workspace = true

[dev-dependencies]
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
settings.workspace = true
177 changes: 175 additions & 2 deletions crates/picker/src/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ pub trait PickerDelegate: Sized + 'static {
None
}
fn can_select(
&mut self,
&self,
_ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
Expand Down Expand Up @@ -619,6 +619,9 @@ impl<D: PickerDelegate> Picker<D> {
) {
cx.stop_propagation();
window.prevent_default();
if !self.delegate.can_select(ix, window, cx) {
return;
}
self.set_selected_index(ix, None, false, window, cx);
self.do_confirm(secondary, window, cx)
}
Expand Down Expand Up @@ -753,10 +756,11 @@ impl<D: PickerDelegate> Picker<D> {
ix: usize,
) -> impl IntoElement + use<D> {
let item_bounds = self.item_bounds.clone();
let selectable = self.delegate.can_select(ix, window, cx);

div()
.id(("item", ix))
.cursor_pointer()
.when(selectable, |this| this.cursor_pointer())
.child(
canvas(
move |bounds, _window, _cx| {
Expand Down Expand Up @@ -850,6 +854,175 @@ impl<D: PickerDelegate> Picker<D> {
}
}

#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use std::cell::Cell;

struct TestDelegate {
items: Vec<bool>,
selected_index: usize,
confirmed_index: Rc<Cell<Option<usize>>>,
}

impl TestDelegate {
fn new(items: Vec<bool>) -> Self {
Self {
items,
selected_index: 0,
confirmed_index: Rc::new(Cell::new(None)),
}
}
}

impl PickerDelegate for TestDelegate {
type ListItem = ui::ListItem;

fn match_count(&self) -> usize {
self.items.len()
}

fn selected_index(&self) -> usize {
self.selected_index
}

fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}

fn can_select(
&self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
self.items.get(ix).copied().unwrap_or(false)
}

fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Test".into()
}

fn update_matches(
&mut self,
_query: String,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Task<()> {
Task::ready(())
}

fn confirm(
&mut self,
_secondary: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.confirmed_index.set(Some(self.selected_index));
}

fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}

fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
Some(
ui::ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(ui::Label::new(format!("Item {ix}"))),
)
}
}

fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = settings::SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
editor::init(cx);
});
}
Comment thread
Dnreikronos marked this conversation as resolved.

#[gpui::test]
async fn test_clicking_non_selectable_item_does_not_confirm(cx: &mut TestAppContext) {
init_test(cx);

let confirmed_index = Rc::new(Cell::new(None));
let (picker, cx) = cx.add_window_view(|window, cx| {
let mut delegate = TestDelegate::new(vec![true, false, true]);
delegate.confirmed_index = confirmed_index.clone();
Picker::uniform_list(delegate, window, cx)
});

picker.update(cx, |picker, _cx| {
assert_eq!(picker.delegate.selected_index(), 0);
});

picker.update_in(cx, |picker, window, cx| {
picker.handle_click(1, false, window, cx);
});
assert!(
confirmed_index.get().is_none(),
"clicking a non-selectable item should not confirm"
);

picker.update_in(cx, |picker, window, cx| {
picker.handle_click(0, false, window, cx);
});
assert_eq!(
confirmed_index.get(),
Some(0),
"clicking a selectable item should confirm"
);
}

#[gpui::test]
async fn test_keyboard_navigation_skips_non_selectable_items(cx: &mut TestAppContext) {
init_test(cx);

let (picker, cx) = cx.add_window_view(|window, cx| {
Picker::uniform_list(TestDelegate::new(vec![true, false, true]), window, cx)
});

picker.update(cx, |picker, _cx| {
assert_eq!(picker.delegate.selected_index(), 0);
});

picker.update_in(cx, |picker, window, cx| {
picker.select_next(&menu::SelectNext, window, cx);
});
picker.update(cx, |picker, _cx| {
assert_eq!(
picker.delegate.selected_index(),
2,
"select_next should skip non-selectable item at index 1"
);
});

picker.update_in(cx, |picker, window, cx| {
picker.select_previous(&menu::SelectPrevious, window, cx);
});
picker.update(cx, |picker, _cx| {
assert_eq!(
picker.delegate.selected_index(),
0,
"select_previous should skip non-selectable item at index 1"
);
});
}
}

impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
impl<D: PickerDelegate> ModalView for Picker<D> {}

Expand Down
7 changes: 1 addition & 6 deletions crates/recent_projects/src/recent_projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,12 +750,7 @@ impl PickerDelegate for RecentProjectsDelegate {
self.selected_index = ix;
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
matches!(
self.filtered_entries.get(ix),
Some(ProjectPickerEntry::OpenFolder { .. } | ProjectPickerEntry::RecentProject(_))
Expand Down
2 changes: 1 addition & 1 deletion crates/rules_library/src/rules_library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ impl PickerDelegate for RulePickerDelegate {
cx.notify();
}

fn can_select(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) -> bool {
fn can_select(&self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) -> bool {
match self.filtered_entries.get(ix) {
Some(RulePickerEntry::Rule(_)) => true,
Some(RulePickerEntry::Header(_)) | Some(RulePickerEntry::Separator) | None => false,
Expand Down
7 changes: 1 addition & 6 deletions crates/sidebar/src/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,12 +386,7 @@ impl PickerDelegate for WorkspacePickerDelegate {
self.selected_index = ix;
}

fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
match self.matches.get(ix) {
Some(SidebarMatch {
entry: SidebarEntry::Separator(_),
Expand Down
Loading