context_picker.rs

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