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