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)]
 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}