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