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