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