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}