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