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