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 .icon_size(IconSize::XSmall)
119 .icon_color(Color::Muted)
120 .handler(move |cx| {
121 context_picker.update(cx, |this, cx| this.select_kind(kind, cx))
122 })
123 }));
124
125 match self.confirm_behavior {
126 ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(),
127 ConfirmBehavior::Close => menu,
128 }
129 });
130
131 cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
132 cx.emit(DismissEvent);
133 })
134 .detach();
135
136 menu
137 }
138
139 /// Whether threads are allowed as context.
140 pub fn allow_threads(&self) -> bool {
141 self.thread_store.is_some()
142 }
143
144 fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext<Self>) {
145 let context_picker = cx.view().downgrade();
146
147 match kind {
148 ContextKind::File => {
149 self.mode = ContextPickerMode::File(cx.new_view(|cx| {
150 FileContextPicker::new(
151 context_picker.clone(),
152 self.workspace.clone(),
153 self.editor.clone(),
154 self.context_store.clone(),
155 self.confirm_behavior,
156 cx,
157 )
158 }));
159 }
160 ContextKind::Directory => {
161 self.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
162 DirectoryContextPicker::new(
163 context_picker.clone(),
164 self.workspace.clone(),
165 self.context_store.clone(),
166 self.confirm_behavior,
167 cx,
168 )
169 }));
170 }
171 ContextKind::FetchedUrl => {
172 self.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
173 FetchContextPicker::new(
174 context_picker.clone(),
175 self.workspace.clone(),
176 self.context_store.clone(),
177 self.confirm_behavior,
178 cx,
179 )
180 }));
181 }
182 ContextKind::Thread => {
183 if let Some(thread_store) = self.thread_store.as_ref() {
184 self.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
185 ThreadContextPicker::new(
186 thread_store.clone(),
187 context_picker.clone(),
188 self.context_store.clone(),
189 self.confirm_behavior,
190 cx,
191 )
192 }));
193 }
194 }
195 }
196
197 cx.notify();
198 cx.focus_self();
199 }
200
201 fn recent_menu_item(
202 &self,
203 context_picker: View<ContextPicker>,
204 ix: usize,
205 entry: RecentEntry,
206 ) -> ContextMenuItem {
207 match entry {
208 RecentEntry::File {
209 project_path,
210 path_prefix,
211 } => {
212 let context_store = self.context_store.clone();
213 let path = project_path.path.clone();
214
215 ContextMenuItem::custom_entry(
216 move |cx| {
217 render_file_context_entry(
218 ElementId::NamedInteger("ctx-recent".into(), ix),
219 &path,
220 &path_prefix,
221 context_store.clone(),
222 cx,
223 )
224 .into_any()
225 },
226 move |cx| {
227 context_picker.update(cx, |this, cx| {
228 this.add_recent_file(project_path.clone(), cx);
229 })
230 },
231 )
232 }
233 RecentEntry::Thread(thread) => {
234 let context_store = self.context_store.clone();
235 let view_thread = thread.clone();
236
237 ContextMenuItem::custom_entry(
238 move |cx| {
239 render_thread_context_entry(&view_thread, context_store.clone(), cx)
240 .into_any()
241 },
242 move |cx| {
243 context_picker.update(cx, |this, cx| {
244 this.add_recent_thread(thread.clone(), cx)
245 .detach_and_log_err(cx);
246 })
247 },
248 )
249 }
250 }
251 }
252
253 fn add_recent_file(&self, project_path: ProjectPath, cx: &mut ViewContext<Self>) {
254 let Some(context_store) = self.context_store.upgrade() else {
255 return;
256 };
257
258 let task = context_store.update(cx, |context_store, cx| {
259 context_store.add_file_from_path(project_path.clone(), cx)
260 });
261
262 cx.spawn(|_, mut cx| async move { task.await.notify_async_err(&mut cx) })
263 .detach();
264
265 cx.notify();
266 }
267
268 fn add_recent_thread(
269 &self,
270 thread: ThreadContextEntry,
271 cx: &mut ViewContext<Self>,
272 ) -> Task<Result<()>> {
273 let Some(context_store) = self.context_store.upgrade() else {
274 return Task::ready(Err(anyhow!("context store not available")));
275 };
276
277 let Some(thread_store) = self
278 .thread_store
279 .as_ref()
280 .and_then(|thread_store| thread_store.upgrade())
281 else {
282 return Task::ready(Err(anyhow!("thread store not available")));
283 };
284
285 let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx));
286 cx.spawn(|this, mut cx| async move {
287 let thread = open_thread_task.await?;
288 context_store.update(&mut cx, |context_store, cx| {
289 context_store.add_thread(thread, cx);
290 })?;
291
292 this.update(&mut cx, |_this, cx| cx.notify())
293 })
294 }
295
296 fn recent_entries(&self, cx: &mut WindowContext) -> Vec<RecentEntry> {
297 let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
298 return vec![];
299 };
300
301 let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
302 return vec![];
303 };
304
305 let mut recent = Vec::with_capacity(6);
306
307 let mut current_files = context_store.file_paths(cx);
308
309 if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
310 current_files.insert(active_path);
311 }
312
313 let project = workspace.project().read(cx);
314
315 recent.extend(
316 workspace
317 .recent_navigation_history_iter(cx)
318 .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
319 .take(4)
320 .filter_map(|(project_path, _)| {
321 project
322 .worktree_for_id(project_path.worktree_id, cx)
323 .map(|worktree| RecentEntry::File {
324 project_path,
325 path_prefix: worktree.read(cx).root_name().into(),
326 })
327 }),
328 );
329
330 let mut current_threads = context_store.thread_ids();
331
332 if let Some(active_thread) = workspace
333 .panel::<AssistantPanel>(cx)
334 .map(|panel| panel.read(cx).active_thread(cx))
335 {
336 current_threads.insert(active_thread.read(cx).id().clone());
337 }
338
339 let Some(thread_store) = self
340 .thread_store
341 .as_ref()
342 .and_then(|thread_store| thread_store.upgrade())
343 else {
344 return recent;
345 };
346
347 thread_store.update(cx, |thread_store, _cx| {
348 recent.extend(
349 thread_store
350 .threads()
351 .into_iter()
352 .filter(|thread| !current_threads.contains(&thread.id))
353 .take(2)
354 .map(|thread| {
355 RecentEntry::Thread(ThreadContextEntry {
356 id: thread.id,
357 summary: thread.summary,
358 })
359 }),
360 )
361 });
362
363 recent
364 }
365
366 fn active_singleton_buffer_path(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
367 let active_item = workspace.active_item(cx)?;
368
369 let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
370 let buffer = editor.buffer().read(cx).as_singleton()?;
371
372 let path = buffer.read(cx).file()?.path().to_path_buf();
373 Some(path)
374 }
375}
376
377impl EventEmitter<DismissEvent> for ContextPicker {}
378
379impl FocusableView for ContextPicker {
380 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
381 match &self.mode {
382 ContextPickerMode::Default(menu) => menu.focus_handle(cx),
383 ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
384 ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
385 ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
386 ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
387 }
388 }
389}
390
391impl Render for ContextPicker {
392 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
393 v_flex()
394 .w(px(400.))
395 .min_w(px(400.))
396 .map(|parent| match &self.mode {
397 ContextPickerMode::Default(menu) => parent.child(menu.clone()),
398 ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
399 ContextPickerMode::Directory(directory_picker) => {
400 parent.child(directory_picker.clone())
401 }
402 ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
403 ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
404 })
405 }
406}
407enum RecentEntry {
408 File {
409 project_path: ProjectPath,
410 path_prefix: Arc<str>,
411 },
412 Thread(ThreadContextEntry),
413}