context_picker.rs

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