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}