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, Tooltip};
 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_strip::ContextStrip;
 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: WeakModel<ThreadStore>,
 39        context_strip: WeakView<ContextStrip>,
 40        cx: &mut ViewContext<Self>,
 41    ) -> Self {
 42        let delegate = ContextPickerDelegate {
 43            context_picker: cx.view().downgrade(),
 44            workspace,
 45            thread_store,
 46            context_strip,
 47            entries: vec![
 48                ContextPickerEntry {
 49                    name: "directory".into(),
 50                    description: "Insert any directory".into(),
 51                    icon: IconName::Folder,
 52                },
 53                ContextPickerEntry {
 54                    name: "file".into(),
 55                    description: "Insert any file".into(),
 56                    icon: IconName::File,
 57                },
 58                ContextPickerEntry {
 59                    name: "fetch".into(),
 60                    description: "Fetch content from URL".into(),
 61                    icon: IconName::Globe,
 62                },
 63                ContextPickerEntry {
 64                    name: "thread".into(),
 65                    description: "Insert any thread".into(),
 66                    icon: IconName::MessageBubbles,
 67                },
 68            ],
 69            selected_ix: 0,
 70        };
 71
 72        let picker = cx.new_view(|cx| {
 73            Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
 74        });
 75
 76        ContextPicker {
 77            mode: ContextPickerMode::Default,
 78            picker,
 79        }
 80    }
 81
 82    pub fn reset_mode(&mut self) {
 83        self.mode = ContextPickerMode::Default;
 84    }
 85}
 86
 87impl EventEmitter<DismissEvent> for ContextPicker {}
 88
 89impl FocusableView for ContextPicker {
 90    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 91        match &self.mode {
 92            ContextPickerMode::Default => self.picker.focus_handle(cx),
 93            ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
 94            ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
 95            ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
 96        }
 97    }
 98}
 99
100impl Render for ContextPicker {
101    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
102        v_flex()
103            .w(px(400.))
104            .min_w(px(400.))
105            .map(|parent| match &self.mode {
106                ContextPickerMode::Default => parent.child(self.picker.clone()),
107                ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
108                ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
109                ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
110            })
111    }
112}
113
114#[derive(Clone)]
115struct ContextPickerEntry {
116    name: SharedString,
117    description: SharedString,
118    icon: IconName,
119}
120
121pub(crate) struct ContextPickerDelegate {
122    context_picker: WeakView<ContextPicker>,
123    workspace: WeakView<Workspace>,
124    thread_store: WeakModel<ThreadStore>,
125    context_strip: WeakView<ContextStrip>,
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_strip.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_strip.clone(),
175                                    cx,
176                                )
177                            }));
178                        }
179                        "thread" => {
180                            this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
181                                ThreadContextPicker::new(
182                                    self.thread_store.clone(),
183                                    self.context_picker.clone(),
184                                    self.context_strip.clone(),
185                                    cx,
186                                )
187                            }));
188                        }
189                        _ => {}
190                    }
191
192                    cx.focus_self();
193                })
194                .log_err();
195        }
196    }
197
198    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
199        self.context_picker
200            .update(cx, |this, cx| match this.mode {
201                ContextPickerMode::Default => cx.emit(DismissEvent),
202                ContextPickerMode::File(_)
203                | ContextPickerMode::Fetch(_)
204                | ContextPickerMode::Thread(_) => {}
205            })
206            .log_err();
207    }
208
209    fn render_match(
210        &self,
211        ix: usize,
212        selected: bool,
213        _cx: &mut ViewContext<Picker<Self>>,
214    ) -> Option<Self::ListItem> {
215        let entry = &self.entries[ix];
216
217        Some(
218            ListItem::new(ix)
219                .inset(true)
220                .spacing(ListItemSpacing::Dense)
221                .toggle_state(selected)
222                .tooltip({
223                    let description = entry.description.clone();
224                    move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
225                })
226                .child(
227                    v_flex()
228                        .group(format!("context-entry-label-{ix}"))
229                        .w_full()
230                        .py_0p5()
231                        .min_w(px(250.))
232                        .max_w(px(400.))
233                        .child(
234                            h_flex()
235                                .gap_1p5()
236                                .child(Icon::new(entry.icon).size(IconSize::XSmall))
237                                .child(
238                                    Label::new(entry.name.clone())
239                                        .single_line()
240                                        .size(LabelSize::Small),
241                                ),
242                        )
243                        .child(
244                            div().overflow_hidden().text_ellipsis().child(
245                                Label::new(entry.description.clone())
246                                    .size(LabelSize::Small)
247                                    .color(Color::Muted),
248                            ),
249                        ),
250                ),
251        )
252    }
253}