context_picker.rs

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