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