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