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