mod directory_context_picker; mod fetch_context_picker; mod file_context_picker; mod thread_context_picker; use std::path::PathBuf; use std::sync::Arc; use editor::Editor; use file_context_picker::render_file_context_entry; use gpui::{ AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakModel, WeakView, }; use project::ProjectPath; use thread_context_picker::{render_thread_context_entry, ThreadContextEntry}; use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem}; use workspace::{notifications::NotifyResultExt, Workspace}; use crate::context::ContextKind; use crate::context_picker::directory_context_picker::DirectoryContextPicker; use crate::context_picker::fetch_context_picker::FetchContextPicker; use crate::context_picker::file_context_picker::FileContextPicker; use crate::context_picker::thread_context_picker::ThreadContextPicker; use crate::context_store::ContextStore; use crate::thread_store::ThreadStore; use crate::AssistantPanel; #[derive(Debug, Clone, Copy)] pub enum ConfirmBehavior { KeepOpen, Close, } #[derive(Debug, Clone)] enum ContextPickerMode { Default(View), File(View), Directory(View), Fetch(View), Thread(View), } pub(super) struct ContextPicker { mode: ContextPickerMode, workspace: WeakView, editor: WeakView, context_store: WeakModel, thread_store: Option>, confirm_behavior: ConfirmBehavior, } impl ContextPicker { pub fn new( workspace: WeakView, thread_store: Option>, context_store: WeakModel, editor: WeakView, confirm_behavior: ConfirmBehavior, cx: &mut ViewContext, ) -> Self { ContextPicker { mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)), workspace, context_store, thread_store, editor, confirm_behavior, } } pub fn init(&mut self, cx: &mut ViewContext) { self.mode = ContextPickerMode::Default(self.build_menu(cx)); cx.notify(); } fn build_menu(&mut self, cx: &mut ViewContext) -> View { let context_picker = cx.view().clone(); let menu = ContextMenu::build(cx, move |menu, cx| { let recent = self.recent_entries(cx); let has_recent = !recent.is_empty(); let recent_entries = recent .into_iter() .enumerate() .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry)); let mut context_kinds = vec![ ContextKind::File, ContextKind::Directory, ContextKind::FetchedUrl, ]; if self.allow_threads() { context_kinds.push(ContextKind::Thread); } let menu = menu .when(has_recent, |menu| { menu.custom_row(|_| { div() .mb_1() .child( Label::new("Recent") .color(Color::Muted) .size(LabelSize::Small), ) .into_any_element() }) }) .extend(recent_entries) .when(has_recent, |menu| menu.separator()) .extend(context_kinds.into_iter().map(|kind| { let context_picker = context_picker.clone(); ContextMenuEntry::new(kind.label()) .icon(kind.icon()) .handler(move |cx| { context_picker.update(cx, |this, cx| this.select_kind(kind, cx)) }) })); match self.confirm_behavior { ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(), ConfirmBehavior::Close => menu, } }); cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| { cx.emit(DismissEvent); }) .detach(); menu } /// Whether threads are allowed as context. pub fn allow_threads(&self) -> bool { self.thread_store.is_some() } fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext) { let context_picker = cx.view().downgrade(); match kind { ContextKind::File => { self.mode = ContextPickerMode::File(cx.new_view(|cx| { FileContextPicker::new( context_picker.clone(), self.workspace.clone(), self.editor.clone(), self.context_store.clone(), self.confirm_behavior, cx, ) })); } ContextKind::Directory => { self.mode = ContextPickerMode::Directory(cx.new_view(|cx| { DirectoryContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), self.confirm_behavior, cx, ) })); } ContextKind::FetchedUrl => { self.mode = ContextPickerMode::Fetch(cx.new_view(|cx| { FetchContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), self.confirm_behavior, cx, ) })); } ContextKind::Thread => { if let Some(thread_store) = self.thread_store.as_ref() { self.mode = ContextPickerMode::Thread(cx.new_view(|cx| { ThreadContextPicker::new( thread_store.clone(), context_picker.clone(), self.context_store.clone(), self.confirm_behavior, cx, ) })); } } } cx.notify(); cx.focus_self(); } fn recent_menu_item( &self, context_picker: View, ix: usize, entry: RecentEntry, ) -> ContextMenuItem { match entry { RecentEntry::File { project_path, path_prefix, } => { let context_store = self.context_store.clone(); let path = project_path.path.clone(); ContextMenuItem::custom_entry( move |cx| { render_file_context_entry( ElementId::NamedInteger("ctx-recent".into(), ix), &path, &path_prefix, context_store.clone(), cx, ) .into_any() }, move |cx| { context_picker.update(cx, |this, cx| { this.add_recent_file(project_path.clone(), cx); }) }, ) } RecentEntry::Thread(thread) => { let context_store = self.context_store.clone(); let view_thread = thread.clone(); ContextMenuItem::custom_entry( move |cx| { render_thread_context_entry(&view_thread, context_store.clone(), cx) .into_any() }, move |cx| { context_picker.update(cx, |this, cx| { this.add_recent_thread(thread.clone(), cx); }) }, ) } } } fn add_recent_file(&self, project_path: ProjectPath, cx: &mut ViewContext) { let Some(context_store) = self.context_store.upgrade() else { return; }; let task = context_store.update(cx, |context_store, cx| { context_store.add_file_from_path(project_path.clone(), cx) }); cx.spawn(|_, mut cx| async move { task.await.notify_async_err(&mut cx) }) .detach(); cx.notify(); } fn add_recent_thread(&self, thread: ThreadContextEntry, cx: &mut ViewContext) { let Some(context_store) = self.context_store.upgrade() else { return; }; let Some(thread) = self .thread_store .clone() .and_then(|this| this.upgrade()) .and_then(|this| this.update(cx, |this, cx| this.open_thread(&thread.id, cx))) else { return; }; context_store.update(cx, |context_store, cx| { context_store.add_thread(thread, cx); }); cx.notify(); } fn recent_entries(&self, cx: &mut WindowContext) -> Vec { let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else { return vec![]; }; let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else { return vec![]; }; let mut recent = Vec::with_capacity(6); let mut current_files = context_store.file_paths(cx); if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) { current_files.insert(active_path); } let project = workspace.project().read(cx); recent.extend( workspace .recent_navigation_history_iter(cx) .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf())) .take(4) .filter_map(|(project_path, _)| { project .worktree_for_id(project_path.worktree_id, cx) .map(|worktree| RecentEntry::File { project_path, path_prefix: worktree.read(cx).root_name().into(), }) }), ); let mut current_threads = context_store.thread_ids(); if let Some(active_thread) = workspace .panel::(cx) .map(|panel| panel.read(cx).active_thread(cx)) { current_threads.insert(active_thread.read(cx).id().clone()); } let Some(thread_store) = self .thread_store .as_ref() .and_then(|thread_store| thread_store.upgrade()) else { return recent; }; thread_store.update(cx, |thread_store, cx| { recent.extend( thread_store .threads(cx) .into_iter() .filter(|thread| !current_threads.contains(thread.read(cx).id())) .take(2) .map(|thread| { let thread = thread.read(cx); RecentEntry::Thread(ThreadContextEntry { id: thread.id().clone(), summary: thread.summary_or_default(), }) }), ) }); recent } fn active_singleton_buffer_path(workspace: &Workspace, cx: &AppContext) -> Option { let active_item = workspace.active_item(cx)?; let editor = active_item.to_any().downcast::().ok()?.read(cx); let buffer = editor.buffer().read(cx).as_singleton()?; let path = buffer.read(cx).file()?.path().to_path_buf(); Some(path) } } impl EventEmitter for ContextPicker {} impl FocusableView for ContextPicker { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { match &self.mode { ContextPickerMode::Default(menu) => menu.focus_handle(cx), ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx), ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx), ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx), ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx), } } } impl Render for ContextPicker { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() .w(px(400.)) .min_w(px(400.)) .map(|parent| match &self.mode { ContextPickerMode::Default(menu) => parent.child(menu.clone()), ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()), ContextPickerMode::Directory(directory_picker) => { parent.child(directory_picker.clone()) } ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()), ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()), }) } } enum RecentEntry { File { project_path: ProjectPath, path_prefix: Arc, }, Thread(ThreadContextEntry), }