mod completion_provider;mod fetch_context_picker;mod file_context_picker;mod rules_context_picker;mod symbol_context_picker;mod thread_context_picker; use std::ops::Range;use std::path::{Path, PathBuf};use std::sync::Arc; use anyhow::{Result, anyhow};pub use completion_provider::ContextPickerCompletionProvider;use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};use fetch_context_picker::FetchContextPicker;use file_context_picker::FileContextPicker;use file_context_picker::render_file_context_entry;use gpui::{ App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,};use language::Buffer;use multi_buffer::MultiBufferRow;use paths::contexts_dir;use project::{Entry, ProjectPath};use prompt_store::{PromptStore, UserPromptId};use rules_context_picker::{RulesContextEntry, RulesContextPicker};use symbol_context_picker::SymbolContextPicker;use thread_context_picker::{ ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries,};use ui::{ ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,};use uuid::Uuid;use workspace::{Workspace, notifications::NotifyResultExt}; use crate::AgentPanel;use crate::context::RULES_ICON;use crate::context_store::ContextStore;use crate::thread::ThreadId;use crate::thread_store::{TextThreadStore, ThreadStore}; #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum ContextPickerEntry { Mode(ContextPickerMode), Action(ContextPickerAction),} impl ContextPickerEntry { pub fn keyword(&self) -> &'static str { match self { Self::Mode(mode) => mode.keyword(), Self::Action(action) => action.keyword(), } } pub fn label(&self) -> &'static str { match self { Self::Mode(mode) => mode.label(), Self::Action(action) => action.label(), } } pub fn icon(&self) -> IconName { match self { Self::Mode(mode) => mode.icon(), Self::Action(action) => action.icon(), } }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum ContextPickerMode { File, Symbol, Fetch, Thread, Rules,} #[derive(Debug, Clone, Copy, PartialEq, Eq)]enum ContextPickerAction { AddSelections,} impl ContextPickerAction { pub fn keyword(&self) -> &'static str { match self { Self::AddSelections => "selection", } } pub fn label(&self) -> &'static str { match self { Self::AddSelections => "Selection", } } pub fn icon(&self) -> IconName { match self { Self::AddSelections => IconName::Context, } }} impl TryFrom<&str> for ContextPickerMode { type Error = String; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "file" => Ok(Self::File), "symbol" => Ok(Self::Symbol), "fetch" => Ok(Self::Fetch), "thread" => Ok(Self::Thread), "rule" => Ok(Self::Rules), _ => Err(format!("Invalid context picker mode: {}", value)), } }} impl ContextPickerMode { pub fn keyword(&self) -> &'static str { match self { Self::File => "file", Self::Symbol => "symbol", Self::Fetch => "fetch", Self::Thread => "thread", Self::Rules => "rule", } } pub fn label(&self) -> &'static str { match self { Self::File => "Files & Directories", Self::Symbol => "Symbols", Self::Fetch => "Fetch", Self::Thread => "Threads", Self::Rules => "Rules", } } pub fn icon(&self) -> IconName { match self { Self::File => IconName::File, Self::Symbol => IconName::Code, Self::Fetch => IconName::Globe, Self::Thread => IconName::MessageBubbles, Self::Rules => RULES_ICON, } }} #[derive(Debug, Clone)]enum ContextPickerState { Default(Entity<ContextMenu>), File(Entity<FileContextPicker>), Symbol(Entity<SymbolContextPicker>), Fetch(Entity<FetchContextPicker>), Thread(Entity<ThreadContextPicker>), Rules(Entity<RulesContextPicker>),} pub(super) struct ContextPicker { mode: ContextPickerState, workspace: WeakEntity<Workspace>, context_store: WeakEntity<ContextStore>, thread_store: Option<WeakEntity<ThreadStore>>, text_thread_store: Option<WeakEntity<TextThreadStore>>, prompt_store: Option<Entity<PromptStore>>, _subscriptions: Vec<Subscription>,} impl ContextPicker { pub fn new( workspace: WeakEntity<Workspace>, thread_store: Option<WeakEntity<ThreadStore>>, text_thread_store: Option<WeakEntity<TextThreadStore>>, context_store: WeakEntity<ContextStore>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let subscriptions = context_store .upgrade() .map(|context_store| { cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx)) }) .into_iter() .chain( thread_store .as_ref() .and_then(|thread_store| thread_store.upgrade()) .map(|thread_store| { cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx)) }), ) .collect::<Vec<Subscription>>(); let prompt_store = thread_store.as_ref().and_then(|thread_store| { thread_store .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) .ok() .flatten() }); ContextPicker { mode: ContextPickerState::Default(ContextMenu::build( window, cx, |menu, _window, _cx| menu, )), workspace, context_store, thread_store, text_thread_store, prompt_store, _subscriptions: subscriptions, } } pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.mode = ContextPickerState::Default(self.build_menu(window, cx)); cx.notify(); } fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> { let context_picker = cx.entity().clone(); let menu = ContextMenu::build(window, cx, move |menu, _window, 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 entries = self .workspace .upgrade() .map(|workspace| { available_context_picker_entries( &self.prompt_store, &self.thread_store, &workspace, cx, ) }) .unwrap_or_default(); 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(entries.into_iter().map(|entry| { let context_picker = context_picker.clone(); ContextMenuEntry::new(entry.label()) .icon(entry.icon()) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .handler(move |window, cx| { context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx)) }) })) .keep_open_on_confirm(true) }); 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_entry( &mut self, entry: ContextPickerEntry, window: &mut Window, cx: &mut Context<Self>, ) { let context_picker = cx.entity().downgrade(); match entry { ContextPickerEntry::Mode(mode) => match mode { ContextPickerMode::File => { self.mode = ContextPickerState::File(cx.new(|cx| { FileContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), window, cx, ) })); } ContextPickerMode::Symbol => { self.mode = ContextPickerState::Symbol(cx.new(|cx| { SymbolContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), window, cx, ) })); } ContextPickerMode::Rules => { if let Some(prompt_store) = self.prompt_store.as_ref() { self.mode = ContextPickerState::Rules(cx.new(|cx| { RulesContextPicker::new( prompt_store.clone(), context_picker.clone(), self.context_store.clone(), window, cx, ) })); } } ContextPickerMode::Fetch => { self.mode = ContextPickerState::Fetch(cx.new(|cx| { FetchContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), window, cx, ) })); } ContextPickerMode::Thread => { if let Some((thread_store, text_thread_store)) = self .thread_store .as_ref() .zip(self.text_thread_store.as_ref()) { self.mode = ContextPickerState::Thread(cx.new(|cx| { ThreadContextPicker::new( thread_store.clone(), text_thread_store.clone(), context_picker.clone(), self.context_store.clone(), window, cx, ) })); } } }, ContextPickerEntry::Action(action) => match action { ContextPickerAction::AddSelections => { if let Some((context_store, workspace)) = self.context_store.upgrade().zip(self.workspace.upgrade()) { add_selections_as_context(&context_store, &workspace, cx); } cx.emit(DismissEvent); } }, } cx.notify(); cx.focus_self(window); } pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) { match &self.mode { ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| { entity.select_first(&Default::default(), window, cx) }), // Other variants already select their first entry on open automatically _ => {} } } fn recent_menu_item( &self, context_picker: Entity<ContextPicker>, ix: usize, entry: RecentEntry, ) -> ContextMenuItem { match entry { RecentEntry::File { project_path, path_prefix, } => { let context_store = self.context_store.clone(); let worktree_id = project_path.worktree_id; let path = project_path.path.clone(); ContextMenuItem::custom_entry( move |_window, cx| { render_file_context_entry( ElementId::named_usize("ctx-recent", ix), worktree_id, &path, &path_prefix, false, context_store.clone(), cx, ) .into_any() }, move |window, cx| { context_picker.update(cx, |this, cx| { this.add_recent_file(project_path.clone(), window, cx); }) }, ) } RecentEntry::Thread(thread) => { let context_store = self.context_store.clone(); let view_thread = thread.clone(); ContextMenuItem::custom_entry( move |_window, cx| { render_thread_context_entry(&view_thread, context_store.clone(), cx) .into_any() }, move |window, cx| { context_picker.update(cx, |this, cx| { this.add_recent_thread(thread.clone(), window, cx) .detach_and_log_err(cx); }) }, ) } } } fn add_recent_file( &self, project_path: ProjectPath, window: &mut Window, cx: &mut Context<Self>, ) { 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(), true, cx) }); cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx)) .detach(); cx.notify(); } fn add_recent_thread( &self, entry: ThreadContextEntry, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { let Some(context_store) = self.context_store.upgrade() else { return Task::ready(Err(anyhow!("context store not available"))); }; match entry { ThreadContextEntry::Thread { id, .. } => { let Some(thread_store) = self .thread_store .as_ref() .and_then(|thread_store| thread_store.upgrade()) else { return Task::ready(Err(anyhow!("thread store not available"))); }; let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx)); cx.spawn(async move |this, cx| { let thread = open_thread_task.await?; context_store.update(cx, |context_store, cx| { context_store.add_thread(thread, true, cx); })?; this.update(cx, |_this, cx| cx.notify()) }) } ThreadContextEntry::Context { path, .. } => { let Some(text_thread_store) = selfLines 1–500 of 1036. Load more
mod completion_provider; mod fetch_context_picker; mod file_context_picker; mod rules_context_picker; mod symbol_context_picker; mod thread_context_picker; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Result, anyhow}; pub use completion_provider::ContextPickerCompletionProvider; use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId}; use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset}; use fetch_context_picker::FetchContextPicker; use file_context_picker::FileContextPicker; use file_context_picker::render_file_context_entry; use gpui::{ App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use language::Buffer; use multi_buffer::MultiBufferRow; use paths::contexts_dir; use project::{Entry, ProjectPath}; use prompt_store::{PromptStore, UserPromptId}; use rules_context_picker::{RulesContextEntry, RulesContextPicker}; use symbol_context_picker::SymbolContextPicker; use thread_context_picker::{ ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries, }; use ui::{ ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*, }; use uuid::Uuid; use workspace::{Workspace, notifications::NotifyResultExt}; use crate::AgentPanel; use crate::context::RULES_ICON; use crate::context_store::ContextStore; use crate::thread::ThreadId; use crate::thread_store::{TextThreadStore, ThreadStore}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ContextPickerEntry { Mode(ContextPickerMode), Action(ContextPickerAction), } impl ContextPickerEntry { pub fn keyword(&self) -> &'static str { match self { Self::Mode(mode) => mode.keyword(), Self::Action(action) => action.keyword(), } } pub fn label(&self) -> &'static str { match self { Self::Mode(mode) => mode.label(), Self::Action(action) => action.label(), } } pub fn icon(&self) -> IconName { match self { Self::Mode(mode) => mode.icon(), Self::Action(action) => action.icon(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ContextPickerMode { File, Symbol, Fetch, Thread, Rules, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ContextPickerAction { AddSelections, } impl ContextPickerAction { pub fn keyword(&self) -> &'static str { match self { Self::AddSelections => "selection", } } pub fn label(&self) -> &'static str { match self { Self::AddSelections => "Selection", } } pub fn icon(&self) -> IconName { match self { Self::AddSelections => IconName::Context, } } } impl TryFrom<&str> for ContextPickerMode { type Error = String; fn try_from(value: &str) -> Result<Self, Self::Error> { match value { "file" => Ok(Self::File), "symbol" => Ok(Self::Symbol), "fetch" => Ok(Self::Fetch), "thread" => Ok(Self::Thread), "rule" => Ok(Self::Rules), _ => Err(format!("Invalid context picker mode: {}", value)), } } } impl ContextPickerMode { pub fn keyword(&self) -> &'static str { match self { Self::File => "file", Self::Symbol => "symbol", Self::Fetch => "fetch", Self::Thread => "thread", Self::Rules => "rule", } } pub fn label(&self) -> &'static str { match self { Self::File => "Files & Directories", Self::Symbol => "Symbols", Self::Fetch => "Fetch", Self::Thread => "Threads", Self::Rules => "Rules", } } pub fn icon(&self) -> IconName { match self { Self::File => IconName::File, Self::Symbol => IconName::Code, Self::Fetch => IconName::Globe, Self::Thread => IconName::MessageBubbles, Self::Rules => RULES_ICON, } } } #[derive(Debug, Clone)] enum ContextPickerState { Default(Entity<ContextMenu>), File(Entity<FileContextPicker>), Symbol(Entity<SymbolContextPicker>), Fetch(Entity<FetchContextPicker>), Thread(Entity<ThreadContextPicker>), Rules(Entity<RulesContextPicker>), } pub(super) struct ContextPicker { mode: ContextPickerState, workspace: WeakEntity<Workspace>, context_store: WeakEntity<ContextStore>, thread_store: Option<WeakEntity<ThreadStore>>, text_thread_store: Option<WeakEntity<TextThreadStore>>, prompt_store: Option<Entity<PromptStore>>, _subscriptions: Vec<Subscription>, } impl ContextPicker { pub fn new( workspace: WeakEntity<Workspace>, thread_store: Option<WeakEntity<ThreadStore>>, text_thread_store: Option<WeakEntity<TextThreadStore>>, context_store: WeakEntity<ContextStore>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let subscriptions = context_store .upgrade() .map(|context_store| { cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx)) }) .into_iter() .chain( thread_store .as_ref() .and_then(|thread_store| thread_store.upgrade()) .map(|thread_store| { cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx)) }), ) .collect::<Vec<Subscription>>(); let prompt_store = thread_store.as_ref().and_then(|thread_store| { thread_store .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) .ok() .flatten() }); ContextPicker { mode: ContextPickerState::Default(ContextMenu::build( window, cx, |menu, _window, _cx| menu, )), workspace, context_store, thread_store, text_thread_store, prompt_store, _subscriptions: subscriptions, } } pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.mode = ContextPickerState::Default(self.build_menu(window, cx)); cx.notify(); } fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> { let context_picker = cx.entity().clone(); let menu = ContextMenu::build(window, cx, move |menu, _window, 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 entries = self .workspace .upgrade() .map(|workspace| { available_context_picker_entries( &self.prompt_store, &self.thread_store, &workspace, cx, ) }) .unwrap_or_default(); 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(entries.into_iter().map(|entry| { let context_picker = context_picker.clone(); ContextMenuEntry::new(entry.label()) .icon(entry.icon()) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .handler(move |window, cx| { context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx)) }) })) .keep_open_on_confirm(true) }); 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_entry( &mut self, entry: ContextPickerEntry, window: &mut Window, cx: &mut Context<Self>, ) { let context_picker = cx.entity().downgrade(); match entry { ContextPickerEntry::Mode(mode) => match mode { ContextPickerMode::File => { self.mode = ContextPickerState::File(cx.new(|cx| { FileContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), window, cx, ) })); } ContextPickerMode::Symbol => { self.mode = ContextPickerState::Symbol(cx.new(|cx| { SymbolContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), window, cx, ) })); } ContextPickerMode::Rules => { if let Some(prompt_store) = self.prompt_store.as_ref() { self.mode = ContextPickerState::Rules(cx.new(|cx| { RulesContextPicker::new( prompt_store.clone(), context_picker.clone(), self.context_store.clone(), window, cx, ) })); } } ContextPickerMode::Fetch => { self.mode = ContextPickerState::Fetch(cx.new(|cx| { FetchContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), window, cx, ) })); } ContextPickerMode::Thread => { if let Some((thread_store, text_thread_store)) = self .thread_store .as_ref() .zip(self.text_thread_store.as_ref()) { self.mode = ContextPickerState::Thread(cx.new(|cx| { ThreadContextPicker::new( thread_store.clone(), text_thread_store.clone(), context_picker.clone(), self.context_store.clone(), window, cx, ) })); } } }, ContextPickerEntry::Action(action) => match action { ContextPickerAction::AddSelections => { if let Some((context_store, workspace)) = self.context_store.upgrade().zip(self.workspace.upgrade()) { add_selections_as_context(&context_store, &workspace, cx); } cx.emit(DismissEvent); } }, } cx.notify(); cx.focus_self(window); } pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) { match &self.mode { ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| { entity.select_first(&Default::default(), window, cx) }), // Other variants already select their first entry on open automatically _ => {} } } fn recent_menu_item( &self, context_picker: Entity<ContextPicker>, ix: usize, entry: RecentEntry, ) -> ContextMenuItem { match entry { RecentEntry::File { project_path, path_prefix, } => { let context_store = self.context_store.clone(); let worktree_id = project_path.worktree_id; let path = project_path.path.clone(); ContextMenuItem::custom_entry( move |_window, cx| { render_file_context_entry( ElementId::named_usize("ctx-recent", ix), worktree_id, &path, &path_prefix, false, context_store.clone(), cx, ) .into_any() }, move |window, cx| { context_picker.update(cx, |this, cx| { this.add_recent_file(project_path.clone(), window, cx); }) }, ) } RecentEntry::Thread(thread) => { let context_store = self.context_store.clone(); let view_thread = thread.clone(); ContextMenuItem::custom_entry( move |_window, cx| { render_thread_context_entry(&view_thread, context_store.clone(), cx) .into_any() }, move |window, cx| { context_picker.update(cx, |this, cx| { this.add_recent_thread(thread.clone(), window, cx) .detach_and_log_err(cx); }) }, ) } } } fn add_recent_file( &self, project_path: ProjectPath, window: &mut Window, cx: &mut Context<Self>, ) { 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(), true, cx) }); cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx)) .detach(); cx.notify(); } fn add_recent_thread( &self, entry: ThreadContextEntry, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { let Some(context_store) = self.context_store.upgrade() else { return Task::ready(Err(anyhow!("context store not available"))); }; match entry { ThreadContextEntry::Thread { id, .. } => { let Some(thread_store) = self .thread_store .as_ref() .and_then(|thread_store| thread_store.upgrade()) else { return Task::ready(Err(anyhow!("thread store not available"))); }; let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx)); cx.spawn(async move |this, cx| { let thread = open_thread_task.await?; context_store.update(cx, |context_store, cx| { context_store.add_thread(thread, true, cx); })?; this.update(cx, |_this, cx| cx.notify()) }) } ThreadContextEntry::Context { path, .. } => { let Some(text_thread_store) = self