1use std::sync::Arc;
2
3use gpui::{DismissEvent, SharedString, Task, WeakView};
4use picker::{Picker, PickerDelegate, PickerEditorPosition};
5use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
6
7use crate::message_editor::MessageEditor;
8
9#[derive(IntoElement)]
10pub(super) struct ContextPicker<T: PopoverTrigger> {
11 message_editor: WeakView<MessageEditor>,
12 trigger: T,
13}
14
15#[derive(Clone)]
16struct ContextPickerEntry {
17 name: SharedString,
18 description: SharedString,
19 icon: IconName,
20}
21
22pub(crate) struct ContextPickerDelegate {
23 all_entries: Vec<ContextPickerEntry>,
24 filtered_entries: Vec<ContextPickerEntry>,
25 message_editor: WeakView<MessageEditor>,
26 selected_ix: usize,
27}
28
29impl<T: PopoverTrigger> ContextPicker<T> {
30 pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
31 ContextPicker {
32 message_editor,
33 trigger,
34 }
35 }
36}
37
38impl PickerDelegate for ContextPickerDelegate {
39 type ListItem = ListItem;
40
41 fn match_count(&self) -> usize {
42 self.filtered_entries.len()
43 }
44
45 fn selected_index(&self) -> usize {
46 self.selected_ix
47 }
48
49 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
50 self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
51 cx.notify();
52 }
53
54 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
55 "Select a context source…".into()
56 }
57
58 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
59 let all_commands = self.all_entries.clone();
60 cx.spawn(|this, mut cx| async move {
61 let filtered_commands = cx
62 .background_executor()
63 .spawn(async move {
64 if query.is_empty() {
65 all_commands
66 } else {
67 all_commands
68 .into_iter()
69 .filter(|model_info| {
70 model_info
71 .name
72 .to_lowercase()
73 .contains(&query.to_lowercase())
74 })
75 .collect()
76 }
77 })
78 .await;
79
80 this.update(&mut cx, |this, cx| {
81 this.delegate.filtered_entries = filtered_commands;
82 this.delegate.set_selected_index(0, cx);
83 cx.notify();
84 })
85 .ok();
86 })
87 }
88
89 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
90 if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
91 self.message_editor
92 .update(cx, |_message_editor, _cx| {
93 println!("Insert context from {}", entry.name);
94 })
95 .ok();
96 cx.emit(DismissEvent);
97 }
98 }
99
100 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
101
102 fn editor_position(&self) -> PickerEditorPosition {
103 PickerEditorPosition::End
104 }
105
106 fn render_match(
107 &self,
108 ix: usize,
109 selected: bool,
110 _cx: &mut ViewContext<Picker<Self>>,
111 ) -> Option<Self::ListItem> {
112 let entry = self.filtered_entries.get(ix)?;
113
114 Some(
115 ListItem::new(ix)
116 .inset(true)
117 .spacing(ListItemSpacing::Dense)
118 .selected(selected)
119 .tooltip({
120 let description = entry.description.clone();
121 move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
122 })
123 .child(
124 v_flex()
125 .group(format!("context-entry-label-{ix}"))
126 .w_full()
127 .py_0p5()
128 .min_w(px(250.))
129 .max_w(px(400.))
130 .child(
131 h_flex()
132 .gap_1p5()
133 .child(Icon::new(entry.icon).size(IconSize::XSmall))
134 .child(
135 Label::new(entry.name.clone())
136 .single_line()
137 .size(LabelSize::Small),
138 ),
139 )
140 .child(
141 div().overflow_hidden().text_ellipsis().child(
142 Label::new(entry.description.clone())
143 .size(LabelSize::Small)
144 .color(Color::Muted),
145 ),
146 ),
147 ),
148 )
149 }
150}
151
152impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
153 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
154 let entries = vec![
155 ContextPickerEntry {
156 name: "directory".into(),
157 description: "Insert any directory".into(),
158 icon: IconName::Folder,
159 },
160 ContextPickerEntry {
161 name: "file".into(),
162 description: "Insert any file".into(),
163 icon: IconName::File,
164 },
165 ContextPickerEntry {
166 name: "web".into(),
167 description: "Fetch content from URL".into(),
168 icon: IconName::Globe,
169 },
170 ];
171
172 let delegate = ContextPickerDelegate {
173 all_entries: entries.clone(),
174 message_editor: self.message_editor.clone(),
175 filtered_entries: entries,
176 selected_ix: 0,
177 };
178
179 let picker =
180 cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
181
182 let handle = self
183 .message_editor
184 .update(cx, |this, _| this.context_picker_handle.clone())
185 .ok();
186 PopoverMenu::new("context-picker")
187 .menu(move |_cx| Some(picker.clone()))
188 .trigger(self.trigger)
189 .attach(gpui::AnchorCorner::TopLeft)
190 .anchor(gpui::AnchorCorner::BottomLeft)
191 .offset(gpui::Point {
192 x: px(0.0),
193 y: px(-16.0),
194 })
195 .when_some(handle, |this, handle| this.with_handle(handle))
196 }
197}