context_picker.rs

  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}