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