context_picker.rs

  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}