1mod fetch_context_picker;
2mod file_context_picker;
3mod thread_context_picker;
4
5use std::sync::Arc;
6
7use gpui::{
8 AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
9 WeakModel, WeakView,
10};
11use picker::{Picker, PickerDelegate};
12use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
13use util::ResultExt;
14use workspace::Workspace;
15
16use crate::context_picker::fetch_context_picker::FetchContextPicker;
17use crate::context_picker::file_context_picker::FileContextPicker;
18use crate::context_picker::thread_context_picker::ThreadContextPicker;
19use crate::context_store::ContextStore;
20use crate::thread_store::ThreadStore;
21
22#[derive(Debug, Clone)]
23enum ContextPickerMode {
24 Default,
25 File(View<FileContextPicker>),
26 Fetch(View<FetchContextPicker>),
27 Thread(View<ThreadContextPicker>),
28}
29
30pub(super) struct ContextPicker {
31 mode: ContextPickerMode,
32 picker: View<Picker<ContextPickerDelegate>>,
33}
34
35impl ContextPicker {
36 pub fn new(
37 workspace: WeakView<Workspace>,
38 thread_store: Option<WeakModel<ThreadStore>>,
39 context_store: WeakModel<ContextStore>,
40 cx: &mut ViewContext<Self>,
41 ) -> Self {
42 let mut entries = vec![
43 ContextPickerEntry {
44 name: "directory".into(),
45 description: "Insert any directory".into(),
46 icon: IconName::Folder,
47 },
48 ContextPickerEntry {
49 name: "file".into(),
50 description: "Insert any file".into(),
51 icon: IconName::File,
52 },
53 ContextPickerEntry {
54 name: "fetch".into(),
55 description: "Fetch content from URL".into(),
56 icon: IconName::Globe,
57 },
58 ];
59
60 if thread_store.is_some() {
61 entries.push(ContextPickerEntry {
62 name: "thread".into(),
63 description: "Insert any thread".into(),
64 icon: IconName::MessageBubbles,
65 });
66 }
67
68 let delegate = ContextPickerDelegate {
69 context_picker: cx.view().downgrade(),
70 workspace,
71 thread_store,
72 context_store,
73 entries,
74 selected_ix: 0,
75 };
76
77 let picker = cx.new_view(|cx| {
78 Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
79 });
80
81 ContextPicker {
82 mode: ContextPickerMode::Default,
83 picker,
84 }
85 }
86
87 pub fn reset_mode(&mut self) {
88 self.mode = ContextPickerMode::Default;
89 }
90}
91
92impl EventEmitter<DismissEvent> for ContextPicker {}
93
94impl FocusableView for ContextPicker {
95 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
96 match &self.mode {
97 ContextPickerMode::Default => self.picker.focus_handle(cx),
98 ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
99 ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
100 ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
101 }
102 }
103}
104
105impl Render for ContextPicker {
106 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
107 v_flex()
108 .w(px(400.))
109 .min_w(px(400.))
110 .map(|parent| match &self.mode {
111 ContextPickerMode::Default => parent.child(self.picker.clone()),
112 ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
113 ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
114 ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
115 })
116 }
117}
118
119#[derive(Clone)]
120struct ContextPickerEntry {
121 name: SharedString,
122 description: SharedString,
123 icon: IconName,
124}
125
126pub(crate) struct ContextPickerDelegate {
127 context_picker: WeakView<ContextPicker>,
128 workspace: WeakView<Workspace>,
129 thread_store: Option<WeakModel<ThreadStore>>,
130 context_store: WeakModel<ContextStore>,
131 entries: Vec<ContextPickerEntry>,
132 selected_ix: usize,
133}
134
135impl PickerDelegate for ContextPickerDelegate {
136 type ListItem = ListItem;
137
138 fn match_count(&self) -> usize {
139 self.entries.len()
140 }
141
142 fn selected_index(&self) -> usize {
143 self.selected_ix
144 }
145
146 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
147 self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
148 cx.notify();
149 }
150
151 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
152 "Select a context source…".into()
153 }
154
155 fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
156 Task::ready(())
157 }
158
159 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
160 if let Some(entry) = self.entries.get(self.selected_ix) {
161 self.context_picker
162 .update(cx, |this, cx| {
163 match entry.name.to_string().as_str() {
164 "file" => {
165 this.mode = ContextPickerMode::File(cx.new_view(|cx| {
166 FileContextPicker::new(
167 self.context_picker.clone(),
168 self.workspace.clone(),
169 self.context_store.clone(),
170 cx,
171 )
172 }));
173 }
174 "fetch" => {
175 this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
176 FetchContextPicker::new(
177 self.context_picker.clone(),
178 self.workspace.clone(),
179 self.context_store.clone(),
180 cx,
181 )
182 }));
183 }
184 "thread" => {
185 if let Some(thread_store) = self.thread_store.as_ref() {
186 this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
187 ThreadContextPicker::new(
188 thread_store.clone(),
189 self.context_picker.clone(),
190 self.context_store.clone(),
191 cx,
192 )
193 }));
194 }
195 }
196 _ => {}
197 }
198
199 cx.focus_self();
200 })
201 .log_err();
202 }
203 }
204
205 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
206 self.context_picker
207 .update(cx, |this, cx| match this.mode {
208 ContextPickerMode::Default => cx.emit(DismissEvent),
209 ContextPickerMode::File(_)
210 | ContextPickerMode::Fetch(_)
211 | ContextPickerMode::Thread(_) => {}
212 })
213 .log_err();
214 }
215
216 fn render_match(
217 &self,
218 ix: usize,
219 selected: bool,
220 _cx: &mut ViewContext<Picker<Self>>,
221 ) -> Option<Self::ListItem> {
222 let entry = &self.entries[ix];
223
224 Some(
225 ListItem::new(ix)
226 .inset(true)
227 .spacing(ListItemSpacing::Dense)
228 .toggle_state(selected)
229 .tooltip({
230 let description = entry.description.clone();
231 move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
232 })
233 .child(
234 v_flex()
235 .group(format!("context-entry-label-{ix}"))
236 .w_full()
237 .py_0p5()
238 .min_w(px(250.))
239 .max_w(px(400.))
240 .child(
241 h_flex()
242 .gap_1p5()
243 .child(Icon::new(entry.icon).size(IconSize::XSmall))
244 .child(
245 Label::new(entry.name.clone())
246 .single_line()
247 .size(LabelSize::Small),
248 ),
249 )
250 .child(
251 div().overflow_hidden().text_ellipsis().child(
252 Label::new(entry.description.clone())
253 .size(LabelSize::Small)
254 .color(Color::Muted),
255 ),
256 ),
257 ),
258 )
259 }
260}