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_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                description: "Insert any directory".into(),
 46                icon: IconName::Folder,
 47            },
 48            ContextPickerEntry {
 49                name: "file".into(),
 50                description: "Insert any file".into(),
 51                icon: IconName::File,
 52            },
 53            ContextPickerEntry {
 54                name: "fetch".into(),
 55                description: "Fetch content from URL".into(),
 56                icon: IconName::Globe,
 57            },
 58        ];
 59
 60        if thread_store.is_some() {
 61            entries.push(ContextPickerEntry {
 62                name: "thread".into(),
 63                description: "Insert any thread".into(),
 64                icon: IconName::MessageBubbles,
 65            });
 66        }
 67
 68        let delegate = ContextPickerDelegate {
 69            context_picker: cx.view().downgrade(),
 70            workspace,
 71            thread_store,
 72            context_store,
 73            entries,
 74            selected_ix: 0,
 75        };
 76
 77        let picker = cx.new_view(|cx| {
 78            Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
 79        });
 80
 81        ContextPicker {
 82            mode: ContextPickerMode::Default,
 83            picker,
 84        }
 85    }
 86
 87    pub fn reset_mode(&mut self) {
 88        self.mode = ContextPickerMode::Default;
 89    }
 90}
 91
 92impl EventEmitter<DismissEvent> for ContextPicker {}
 93
 94impl FocusableView for ContextPicker {
 95    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
 96        match &self.mode {
 97            ContextPickerMode::Default => self.picker.focus_handle(cx),
 98            ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
 99            ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
100            ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
101        }
102    }
103}
104
105impl Render for ContextPicker {
106    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
107        v_flex()
108            .w(px(400.))
109            .min_w(px(400.))
110            .map(|parent| match &self.mode {
111                ContextPickerMode::Default => parent.child(self.picker.clone()),
112                ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
113                ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
114                ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
115            })
116    }
117}
118
119#[derive(Clone)]
120struct ContextPickerEntry {
121    name: SharedString,
122    description: SharedString,
123    icon: IconName,
124}
125
126pub(crate) struct ContextPickerDelegate {
127    context_picker: WeakView<ContextPicker>,
128    workspace: WeakView<Workspace>,
129    thread_store: Option<WeakModel<ThreadStore>>,
130    context_store: WeakModel<ContextStore>,
131    entries: Vec<ContextPickerEntry>,
132    selected_ix: usize,
133}
134
135impl PickerDelegate for ContextPickerDelegate {
136    type ListItem = ListItem;
137
138    fn match_count(&self) -> usize {
139        self.entries.len()
140    }
141
142    fn selected_index(&self) -> usize {
143        self.selected_ix
144    }
145
146    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
147        self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
148        cx.notify();
149    }
150
151    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
152        "Select a context source…".into()
153    }
154
155    fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
156        Task::ready(())
157    }
158
159    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
160        if let Some(entry) = self.entries.get(self.selected_ix) {
161            self.context_picker
162                .update(cx, |this, cx| {
163                    match entry.name.to_string().as_str() {
164                        "file" => {
165                            this.mode = ContextPickerMode::File(cx.new_view(|cx| {
166                                FileContextPicker::new(
167                                    self.context_picker.clone(),
168                                    self.workspace.clone(),
169                                    self.context_store.clone(),
170                                    cx,
171                                )
172                            }));
173                        }
174                        "fetch" => {
175                            this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
176                                FetchContextPicker::new(
177                                    self.context_picker.clone(),
178                                    self.workspace.clone(),
179                                    self.context_store.clone(),
180                                    cx,
181                                )
182                            }));
183                        }
184                        "thread" => {
185                            if let Some(thread_store) = self.thread_store.as_ref() {
186                                this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
187                                    ThreadContextPicker::new(
188                                        thread_store.clone(),
189                                        self.context_picker.clone(),
190                                        self.context_store.clone(),
191                                        cx,
192                                    )
193                                }));
194                            }
195                        }
196                        _ => {}
197                    }
198
199                    cx.focus_self();
200                })
201                .log_err();
202        }
203    }
204
205    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
206        self.context_picker
207            .update(cx, |this, cx| match this.mode {
208                ContextPickerMode::Default => cx.emit(DismissEvent),
209                ContextPickerMode::File(_)
210                | ContextPickerMode::Fetch(_)
211                | ContextPickerMode::Thread(_) => {}
212            })
213            .log_err();
214    }
215
216    fn render_match(
217        &self,
218        ix: usize,
219        selected: bool,
220        _cx: &mut ViewContext<Picker<Self>>,
221    ) -> Option<Self::ListItem> {
222        let entry = &self.entries[ix];
223
224        Some(
225            ListItem::new(ix)
226                .inset(true)
227                .spacing(ListItemSpacing::Dense)
228                .toggle_state(selected)
229                .tooltip({
230                    let description = entry.description.clone();
231                    move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
232                })
233                .child(
234                    v_flex()
235                        .group(format!("context-entry-label-{ix}"))
236                        .w_full()
237                        .py_0p5()
238                        .min_w(px(250.))
239                        .max_w(px(400.))
240                        .child(
241                            h_flex()
242                                .gap_1p5()
243                                .child(Icon::new(entry.icon).size(IconSize::XSmall))
244                                .child(
245                                    Label::new(entry.name.clone())
246                                        .single_line()
247                                        .size(LabelSize::Small),
248                                ),
249                        )
250                        .child(
251                            div().overflow_hidden().text_ellipsis().child(
252                                Label::new(entry.description.clone())
253                                    .size(LabelSize::Small)
254                                    .color(Color::Muted),
255                            ),
256                        ),
257                ),
258        )
259    }
260}