1mod directory_context_picker;
2mod fetch_context_picker;
3mod file_context_picker;
4mod thread_context_picker;
5
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use editor::Editor;
10use file_context_picker::render_file_context_entry;
11use gpui::{
12 AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakModel, WeakView,
13};
14use project::ProjectPath;
15use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
16use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
17use workspace::Workspace;
18
19use crate::context::ContextKind;
20use crate::context_picker::directory_context_picker::DirectoryContextPicker;
21use crate::context_picker::fetch_context_picker::FetchContextPicker;
22use crate::context_picker::file_context_picker::FileContextPicker;
23use crate::context_picker::thread_context_picker::ThreadContextPicker;
24use crate::context_store::ContextStore;
25use crate::thread_store::ThreadStore;
26use crate::AssistantPanel;
27
28#[derive(Debug, Clone, Copy)]
29pub enum ConfirmBehavior {
30 KeepOpen,
31 Close,
32}
33
34#[derive(Debug, Clone)]
35enum ContextPickerMode {
36 Default(View<ContextMenu>),
37 File(View<FileContextPicker>),
38 Directory(View<DirectoryContextPicker>),
39 Fetch(View<FetchContextPicker>),
40 Thread(View<ThreadContextPicker>),
41}
42
43pub(super) struct ContextPicker {
44 mode: ContextPickerMode,
45 workspace: WeakView<Workspace>,
46 context_store: WeakModel<ContextStore>,
47 thread_store: Option<WeakModel<ThreadStore>>,
48 confirm_behavior: ConfirmBehavior,
49}
50
51impl ContextPicker {
52 pub fn new(
53 workspace: WeakView<Workspace>,
54 thread_store: Option<WeakModel<ThreadStore>>,
55 context_store: WeakModel<ContextStore>,
56 confirm_behavior: ConfirmBehavior,
57 cx: &mut ViewContext<Self>,
58 ) -> Self {
59 ContextPicker {
60 mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)),
61 workspace,
62 context_store,
63 thread_store,
64 confirm_behavior,
65 }
66 }
67
68 pub fn reset_mode(&mut self, cx: &mut ViewContext<Self>) {
69 self.mode = ContextPickerMode::Default(self.build(cx));
70 }
71
72 fn build(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
73 let context_picker = cx.view().clone();
74
75 ContextMenu::build(cx, move |menu, cx| {
76 let kind_entry = |kind: &'static ContextKind| {
77 let context_picker = context_picker.clone();
78
79 ContextMenuEntry::new(kind.label())
80 .icon(kind.icon())
81 .handler(move |cx| {
82 context_picker.update(cx, |this, cx| this.select_kind(*kind, cx))
83 })
84 };
85
86 let recent = self.recent_entries(cx);
87 let has_recent = !recent.is_empty();
88 let recent_entries = recent
89 .into_iter()
90 .enumerate()
91 .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
92
93 menu.when(has_recent, |menu| menu.label("Recent"))
94 .extend(recent_entries)
95 .when(has_recent, |menu| menu.separator())
96 .extend(ContextKind::all().into_iter().map(kind_entry))
97 })
98 }
99
100 fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext<Self>) {
101 let context_picker = cx.view().downgrade();
102
103 match kind {
104 ContextKind::File => {
105 self.mode = ContextPickerMode::File(cx.new_view(|cx| {
106 FileContextPicker::new(
107 context_picker.clone(),
108 self.workspace.clone(),
109 self.context_store.clone(),
110 self.confirm_behavior,
111 cx,
112 )
113 }));
114 }
115 ContextKind::Directory => {
116 self.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
117 DirectoryContextPicker::new(
118 context_picker.clone(),
119 self.workspace.clone(),
120 self.context_store.clone(),
121 self.confirm_behavior,
122 cx,
123 )
124 }));
125 }
126 ContextKind::FetchedUrl => {
127 self.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
128 FetchContextPicker::new(
129 context_picker.clone(),
130 self.workspace.clone(),
131 self.context_store.clone(),
132 self.confirm_behavior,
133 cx,
134 )
135 }));
136 }
137 ContextKind::Thread => {
138 if let Some(thread_store) = self.thread_store.as_ref() {
139 self.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
140 ThreadContextPicker::new(
141 thread_store.clone(),
142 context_picker.clone(),
143 self.context_store.clone(),
144 self.confirm_behavior,
145 cx,
146 )
147 }));
148 }
149 }
150 }
151
152 cx.notify();
153 cx.focus_self();
154 }
155
156 fn recent_menu_item(
157 &self,
158 context_picker: View<ContextPicker>,
159 ix: usize,
160 entry: RecentEntry,
161 ) -> ContextMenuItem {
162 match entry {
163 RecentEntry::File {
164 project_path,
165 path_prefix,
166 } => {
167 let context_store = self.context_store.clone();
168 let path = project_path.path.clone();
169
170 ContextMenuItem::custom_entry(
171 move |cx| {
172 render_file_context_entry(
173 ElementId::NamedInteger("ctx-recent".into(), ix),
174 &path,
175 &path_prefix,
176 context_store.clone(),
177 cx,
178 )
179 .into_any()
180 },
181 move |cx| {
182 context_picker.update(cx, |this, cx| {
183 this.add_recent_file(project_path.clone(), cx);
184 })
185 },
186 )
187 }
188 RecentEntry::Thread(thread) => {
189 let context_store = self.context_store.clone();
190 let view_thread = thread.clone();
191
192 ContextMenuItem::custom_entry(
193 move |cx| {
194 render_thread_context_entry(&view_thread, context_store.clone(), cx)
195 .into_any()
196 },
197 move |cx| {
198 context_picker.update(cx, |this, cx| {
199 this.add_recent_thread(thread.clone(), cx);
200 })
201 },
202 )
203 }
204 }
205 }
206
207 fn add_recent_file(&self, project_path: ProjectPath, cx: &mut ViewContext<Self>) {
208 let Some(context_store) = self.context_store.upgrade() else {
209 return;
210 };
211
212 let task = context_store.update(cx, |context_store, cx| {
213 context_store.add_file_from_path(project_path.clone(), cx)
214 });
215
216 let workspace = self.workspace.clone();
217
218 cx.spawn(|_, mut cx| async move {
219 match task.await {
220 Ok(_) => {
221 return anyhow::Ok(());
222 }
223 Err(err) => {
224 let Some(workspace) = workspace.upgrade() else {
225 return anyhow::Ok(());
226 };
227
228 workspace.update(&mut cx, |workspace, cx| {
229 workspace.show_error(&err, cx);
230 })
231 }
232 }
233 })
234 .detach_and_log_err(cx);
235
236 cx.notify();
237 }
238
239 fn add_recent_thread(&self, thread: ThreadContextEntry, cx: &mut ViewContext<Self>) {
240 let Some(context_store) = self.context_store.upgrade() else {
241 return;
242 };
243
244 let Some(thread) = self
245 .thread_store
246 .clone()
247 .and_then(|this| this.upgrade())
248 .and_then(|this| this.update(cx, |this, cx| this.open_thread(&thread.id, cx)))
249 else {
250 return;
251 };
252
253 context_store.update(cx, |context_store, cx| {
254 context_store.add_thread(thread, cx);
255 });
256
257 cx.notify();
258 }
259
260 fn recent_entries(&self, cx: &mut WindowContext) -> Vec<RecentEntry> {
261 let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
262 return vec![];
263 };
264
265 let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
266 return vec![];
267 };
268
269 let mut recent = Vec::with_capacity(6);
270
271 let mut current_files = context_store.file_paths(cx);
272
273 if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
274 current_files.insert(active_path);
275 }
276
277 let project = workspace.project().read(cx);
278
279 recent.extend(
280 workspace
281 .recent_navigation_history_iter(cx)
282 .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
283 .take(4)
284 .filter_map(|(project_path, _)| {
285 project
286 .worktree_for_id(project_path.worktree_id, cx)
287 .map(|worktree| RecentEntry::File {
288 project_path,
289 path_prefix: worktree.read(cx).root_name().into(),
290 })
291 }),
292 );
293
294 let mut current_threads = context_store.thread_ids();
295
296 if let Some(active_thread) = workspace
297 .panel::<AssistantPanel>(cx)
298 .map(|panel| panel.read(cx).active_thread(cx))
299 {
300 current_threads.insert(active_thread.read(cx).id().clone());
301 }
302
303 let Some(thread_store) = self
304 .thread_store
305 .as_ref()
306 .and_then(|thread_store| thread_store.upgrade())
307 else {
308 return recent;
309 };
310
311 thread_store.update(cx, |thread_store, cx| {
312 recent.extend(
313 thread_store
314 .threads(cx)
315 .into_iter()
316 .filter(|thread| !current_threads.contains(thread.read(cx).id()))
317 .take(2)
318 .map(|thread| {
319 let thread = thread.read(cx);
320
321 RecentEntry::Thread(ThreadContextEntry {
322 id: thread.id().clone(),
323 summary: thread.summary_or_default(),
324 })
325 }),
326 )
327 });
328
329 recent
330 }
331
332 fn active_singleton_buffer_path(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
333 let active_item = workspace.active_item(cx)?;
334
335 let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
336 let buffer = editor.buffer().read(cx).as_singleton()?;
337
338 let path = buffer.read(cx).file()?.path().to_path_buf();
339 Some(path)
340 }
341}
342
343impl EventEmitter<DismissEvent> for ContextPicker {}
344
345impl FocusableView for ContextPicker {
346 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
347 match &self.mode {
348 ContextPickerMode::Default(menu) => menu.focus_handle(cx),
349 ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
350 ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
351 ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
352 ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
353 }
354 }
355}
356
357impl Render for ContextPicker {
358 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
359 v_flex()
360 .w(px(400.))
361 .min_w(px(400.))
362 .map(|parent| match &self.mode {
363 ContextPickerMode::Default(menu) => parent.child(menu.clone()),
364 ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
365 ContextPickerMode::Directory(directory_picker) => {
366 parent.child(directory_picker.clone())
367 }
368 ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
369 ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
370 })
371 }
372}
373enum RecentEntry {
374 File {
375 project_path: ProjectPath,
376 path_prefix: Arc<str>,
377 },
378 Thread(ThreadContextEntry),
379}