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