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