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::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}