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