use std::cmp::Reverse;use std::ops::Range;use std::path::PathBuf;use std::sync::Arc;use std::sync::atomic::AtomicBool; use crate::DEFAULT_THREAD_TITLE;use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};use acp_thread::MentionUri;use agent_client_protocol::schema::v1 as acp;use anyhow::Result;use editor::{CompletionProvider, Editor, code_context_menus::COMPLETION_MENU_MAX_WIDTH};use futures::FutureExt as _;use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};use gpui::{ App, BackgroundExecutor, Entity, Focusable, Hsla, SharedString, Task, WeakEntity, Window,};use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};use lsp::CompletionContext;use multi_buffer::ToOffset as _;use ordered_float::OrderedFloat;use project::lsp_store::{CompletionDocumentation, SymbolLocation};use project::{ Completion, CompletionDisplayOptions, CompletionGroup, CompletionIntent, CompletionResponse, DiagnosticSummary, PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,}; use rope::Point;use settings::Settings;use terminal::terminal_settings::TerminalSettings;use terminal_view::{TerminalView, terminal_panel::TerminalPanel};use text::{Anchor, ToOffset as _, ToPoint as _};use ui::IconName;use ui::prelude::*;use util::ResultExt as _;use util::paths::PathStyle;use util::rel_path::RelPath;use util::truncate_and_remove_front;use workspace::Workspace; use crate::AgentPanel;use crate::mention_set::MentionSet; #[derive(Clone)]pub(crate) enum AgentContextSelection { Editor(Vec<(Entity<Buffer>, Range<text::Anchor>)>), Terminal(Vec<String>),} #[derive(Clone)]pub(crate) enum AgentContextSource { Editor(WeakEntity<Editor>), TerminalView(WeakEntity<TerminalView>), TerminalPanel,} impl AgentContextSource { pub(crate) fn read_selection( &self, workspace: &Workspace, include_current_line: bool, cx: &mut App, ) -> Option<AgentContextSelection> { match self { Self::Editor(handle) => { let editor = handle.upgrade()?; let ranges = editor_selection_ranges(&editor, include_current_line, cx); (!ranges.is_empty()).then_some(AgentContextSelection::Editor(ranges)) } Self::TerminalView(handle) => { let terminal_view = handle.upgrade()?; terminal_view_selection(&terminal_view, cx) .map(|text| AgentContextSelection::Terminal(vec![text])) } Self::TerminalPanel => { let panel = workspace.panel::<TerminalPanel>(cx)?; let selections = panel.read(cx).terminal_selections(cx); (!selections.is_empty()).then_some(AgentContextSelection::Terminal(selections)) } } } pub(crate) fn from_focused(workspace: &Workspace, window: &Window, cx: &App) -> Option<Self> { if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) && agent_panel.focus_handle(cx).contains_focused(window, cx) { return None; } if let Some(active_item) = workspace.active_item(cx) { if let Some(editor) = active_item.act_as::<Editor>(cx) { if editor.focus_handle(cx).is_focused(window) { return Some(Self::Editor(editor.downgrade())); } } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(cx) && terminal_view.focus_handle(cx).is_focused(window) { return Some(Self::TerminalView(terminal_view.downgrade())); } } if let Some(panel) = workspace.panel::<TerminalPanel>(cx) && panel.focus_handle(cx).contains_focused(window, cx) { return Some(Self::TerminalPanel); } None } pub(crate) fn from_active(workspace: &Workspace, cx: &App) -> Option<Self> { if let Some(active_item) = workspace.active_item(cx) { if let Some(editor) = active_item.act_as::<Editor>(cx) { return Some(Self::Editor(editor.downgrade())); } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(cx) { return Some(Self::TerminalView(terminal_view.downgrade())); } } if terminal_panel_dock_is_open(workspace, cx) { return Some(Self::TerminalPanel); } None } pub(crate) fn exists(&self, workspace: &Workspace, cx: &App) -> bool { match self { Self::Editor(handle) => handle.upgrade().is_some(), Self::TerminalView(handle) => handle.upgrade().is_some(), Self::TerminalPanel => terminal_panel_dock_is_open(workspace, cx), } }} fn terminal_panel_dock_is_open(workspace: &Workspace, cx: &App) -> bool { if workspace.panel::<TerminalPanel>(cx).is_none() { return false; } let position = TerminalSettings::get_global(cx).dock.into(); workspace.dock_at_position(position).read(cx).is_open()} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub(crate) enum PromptContextEntry { Mode(PromptContextType), Action(PromptContextAction),} impl PromptContextEntry { pub fn keyword(&self) -> &'static str { match self { Self::Mode(mode) => mode.keyword(), Self::Action(action) => action.keyword(), } }} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub(crate) enum PromptContextType { File, Symbol, Fetch, Thread, Skill, Diagnostics, BranchDiff,} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub(crate) enum PromptContextAction { AddSelections,} impl PromptContextAction { 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::Reader, } }} /// A slash command that runs a local UI action against the conversation/// (sending feedback) instead of being sent to the agent as part of a prompt./// Each variant maps to a method on `ThreadView`; the completion provider only/// surfaces them and emits an event, while `ThreadView` performs the actual/// work (see `handle_message_editor_event`).#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum PromptLocalCommand { ThumbsUp, ThumbsDown,} impl PromptLocalCommand { pub fn keyword(&self) -> &'static str { match self { Self::ThumbsUp => "helpful", Self::ThumbsDown => "not-helpful", } } pub fn label(&self) -> &'static str { match self { Self::ThumbsUp => "Positive Feedback", Self::ThumbsDown => "Negative Feedback", } } pub fn description(&self) -> &'static str { match self { Self::ThumbsUp => { "Rate this response as helpful. Sends the current conversation to the Zed team." } Self::ThumbsDown => { "Rate this response as not helpful. Sends the current conversation to the Zed team." } } } pub fn icon(&self) -> IconName { match self { Self::ThumbsUp => IconName::ThumbsUp, Self::ThumbsDown => IconName::ThumbsDown, } }} impl TryFrom<&str> for PromptContextType { 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), "skill" => Ok(Self::Skill), "diagnostics" => Ok(Self::Diagnostics), "diff" => Ok(Self::BranchDiff), _ => Err(format!("Invalid context picker mode: {}", value)), } }} impl PromptContextType { pub fn keyword(&self) -> &'static str { match self { Self::File => "file", Self::Symbol => "symbol", Self::Fetch => "fetch", Self::Thread => "thread", Self::Skill => "skill", Self::Diagnostics => "diagnostics", Self::BranchDiff => "branch diff", } } pub fn label(&self) -> &'static str { match self { Self::File => "Files & Directories", Self::Symbol => "Symbols", Self::Fetch => "Fetch", Self::Thread => "Threads", Self::Skill => "Skills", Self::Diagnostics => "Diagnostics", Self::BranchDiff => "Branch Diff", } } pub fn icon(&self) -> IconName { match self { Self::File => IconName::File, Self::Symbol => IconName::Code, Self::Fetch => IconName::ToolWeb, Self::Thread => IconName::Thread, Self::Skill => IconName::Sparkle, Self::Diagnostics => IconName::Warning, Self::BranchDiff => IconName::GitBranch, } }} pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), Thread(SessionMatch), RecentThread(SessionMatch), Fetch(SharedString), Skill(AvailableSkill), Entry(EntryMatch), BranchDiff(BranchDiffMatch),} #[derive(Debug, Clone)]pub struct BranchDiffMatch { pub base_ref: SharedString,} impl Match { pub fn score(&self) -> f64 { match self { Match::File(file) => file.mat.score, Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), Match::Thread(_) => 1., Match::RecentThread(_) => 1., Match::Symbol(_) => 1., Match::Skill(_) => 1., Match::Fetch(_) => 1., Match::BranchDiff(_) => 1., } }} #[derive(Debug, Clone)]pub struct SessionMatch { session_id: acp::SessionId, title: SharedString,} pub struct EntryMatch { mat: Option<StringMatch>, entry: PromptContextEntry,} fn session_title(title: Option<SharedString>) -> SharedString { title .filter(|title| !title.is_empty()) .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE))} #[derive(Debug, Clone)]pub struct AvailableSkill { pub name: Arc<str>, pub description: Arc<str>, /// Scope prefix for this skill: empty for global skills, or the /// worktree root name for project-local skills. pub source: SharedString, pub skill_file_path: PathBuf, pub warning: Option<SharedString>,} fn skill_completion_icon_path( skill: &AvailableSkill, uri: &MentionUri, cx: &mut App,) -> SharedString { if skill.warning.is_some() { IconName::Warning.path().into() } else { uri.icon_path(cx) }} fn skill_completion_icon_color(skill: &AvailableSkill, cx: &App) -> Option<Hsla> { skill.warning.is_some().then(|| cx.theme().status().warning)} fn skill_completion_documentation(skill: &AvailableSkill) -> CompletionDocumentation { let text = match &skill.warning { Some(warning) => warning.clone(), None => skill.description.to_string().into(), }; CompletionDocumentation::MultiLinePlainText(text)} #[derive(Debug, Clone)]pub struct AvailableCommand { pub name: Arc<str>, pub description: Arc<str>, pub requires_argument: bool, pub source: Option<SharedString>, /// Source category used to group the command in the slash popup. `None` /// means the command came from an external ACP agent. pub category: Option<acp_thread::CommandCategory>,} impl AvailableCommand { fn category_order(&self) -> u8 { match self.category { Some(acp_thread::CommandCategory::Native) => 0, Some(acp_thread::CommandCategory::Mcp) => 1, None => 2, } } /// Completion group key and header label for this command's category. fn group(&self) -> CompletionGroup { let (key, label) = match self.category { Some(acp_thread::CommandCategory::Native) => ("commands", "Commands"), Some(acp_thread::CommandCategory::Mcp) => ("mcp-commands", "MCP Server Commands"), None => ("acp-commands", "Commands"), }; CompletionGroup { key: key.into(), label: Some(label.into()), } }} #[derive(Debug, Clone)]enum SlashCompletionCandidate { Skill(AvailableSkill), Command(AvailableCommand), LocalCommand(PromptLocalCommand),} impl SlashCompletionCandidate { fn name(&self) -> &str { match self { Self::Skill(skill) => &skill.name, Self::Command(command) => &command.name, Self::LocalCommand(command) => command.keyword(), } }} /// Stable group identity for a slash completion: skills are one group, commands/// are grouped by category. This identifies which section header an entry sits/// under; the order the groups appear in is decided by relevance (see/// [`group_by_relevance`]).fn slash_completion_group_key(candidate: &SlashCompletionCandidate) -> u32 { match candidate { SlashCompletionCandidate::Skill(_) => 0, SlashCompletionCandidate::Command(command) => 1 + command.category_order() as u32, SlashCompletionCandidate::LocalCommand(_) => 4, }} /// Reorders `items` (which must already be in relevance/score order, best/// first) so that each group's entries stay contiguous while the groups/// themselves are ordered by their best-ranked member. The sort is stable, so/// within a group the original order is preserved.fn group_by_relevance<T>(items: &mut [T], group_key: impl Fn(&T) -> u32) { let mut group_best_rank: collections::HashMap<u32, usize> = collections::HashMap::default(); for (rank, item) in items.iter().enumerate() { group_best_rank.entry(group_key(item)).or_insert(rank); } items.sort_by_key(|item| group_best_rank[&group_key(item)]);} pub trait PromptCompletionProviderDelegate: Send + Sync + 'static { fn supports_context(&self, mode: PromptContextType, cx: &App) -> bool { self.supported_modes(cx).contains(&mode) } fn supported_modes(&self, cx: &App) -> Vec<PromptContextType>; fn supports_images(&self, cx: &App) -> bool; fn available_commands(&self, cx: &App) -> Vec<AvailableCommand>; fn available_skills(&self, _cx: &App) -> Vec<AvailableSkill> { Vec::new() } fn available_local_commands(&self, _cx: &App) -> Vec<PromptLocalCommand> { Vec::new() } fn run_local_command(&self, _command: PromptLocalCommand, _cx: &mut App) {} fn confirm_command(&self, cx: &mut App); /// Called once each time the user opens slash-command autocomplete /// in the editor this delegate serves. Implementations may use it /// to lazily kick off work that produces commands (for example, /// scanning the global skills directory). The default is a no-op. fn slash_autocomplete_invoked(&self, _cx: &mut App) {}} pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> { source: Arc<T>, editor: WeakEntity<Editor>, mention_set: Entity<MentionSet>, workspace: WeakEntity<Workspace>,} impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> { pub fn new( source: T, editor: WeakEntity<Editor>, mention_set: Entity<MentionSet>, workspace: WeakEntity<Workspace>, ) -> Self { Self { source: Arc::new(source), editor, mention_set, workspace, } } fn completion_for_entry(Lines 1–500 of 3434. Load more
use std::cmp::Reverse; use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use crate::DEFAULT_THREAD_TITLE; use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use acp_thread::MentionUri; use agent_client_protocol::schema::v1 as acp; use anyhow::Result; use editor::{CompletionProvider, Editor, code_context_menus::COMPLETION_MENU_MAX_WIDTH}; use futures::FutureExt as _; use fuzzy::{PathMatch, StringMatch, StringMatchCandidate}; use gpui::{ App, BackgroundExecutor, Entity, Focusable, Hsla, SharedString, Task, WeakEntity, Window, }; use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId}; use lsp::CompletionContext; use multi_buffer::ToOffset as _; use ordered_float::OrderedFloat; use project::lsp_store::{CompletionDocumentation, SymbolLocation}; use project::{ Completion, CompletionDisplayOptions, CompletionGroup, CompletionIntent, CompletionResponse, DiagnosticSummary, PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId, }; use rope::Point; use settings::Settings; use terminal::terminal_settings::TerminalSettings; use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use text::{Anchor, ToOffset as _, ToPoint as _}; use ui::IconName; use ui::prelude::*; use util::ResultExt as _; use util::paths::PathStyle; use util::rel_path::RelPath; use util::truncate_and_remove_front; use workspace::Workspace; use crate::AgentPanel; use crate::mention_set::MentionSet; #[derive(Clone)] pub(crate) enum AgentContextSelection { Editor(Vec<(Entity<Buffer>, Range<text::Anchor>)>), Terminal(Vec<String>), } #[derive(Clone)] pub(crate) enum AgentContextSource { Editor(WeakEntity<Editor>), TerminalView(WeakEntity<TerminalView>), TerminalPanel, } impl AgentContextSource { pub(crate) fn read_selection( &self, workspace: &Workspace, include_current_line: bool, cx: &mut App, ) -> Option<AgentContextSelection> { match self { Self::Editor(handle) => { let editor = handle.upgrade()?; let ranges = editor_selection_ranges(&editor, include_current_line, cx); (!ranges.is_empty()).then_some(AgentContextSelection::Editor(ranges)) } Self::TerminalView(handle) => { let terminal_view = handle.upgrade()?; terminal_view_selection(&terminal_view, cx) .map(|text| AgentContextSelection::Terminal(vec![text])) } Self::TerminalPanel => { let panel = workspace.panel::<TerminalPanel>(cx)?; let selections = panel.read(cx).terminal_selections(cx); (!selections.is_empty()).then_some(AgentContextSelection::Terminal(selections)) } } } pub(crate) fn from_focused(workspace: &Workspace, window: &Window, cx: &App) -> Option<Self> { if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) && agent_panel.focus_handle(cx).contains_focused(window, cx) { return None; } if let Some(active_item) = workspace.active_item(cx) { if let Some(editor) = active_item.act_as::<Editor>(cx) { if editor.focus_handle(cx).is_focused(window) { return Some(Self::Editor(editor.downgrade())); } } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(cx) && terminal_view.focus_handle(cx).is_focused(window) { return Some(Self::TerminalView(terminal_view.downgrade())); } } if let Some(panel) = workspace.panel::<TerminalPanel>(cx) && panel.focus_handle(cx).contains_focused(window, cx) { return Some(Self::TerminalPanel); } None } pub(crate) fn from_active(workspace: &Workspace, cx: &App) -> Option<Self> { if let Some(active_item) = workspace.active_item(cx) { if let Some(editor) = active_item.act_as::<Editor>(cx) { return Some(Self::Editor(editor.downgrade())); } else if let Some(terminal_view) = active_item.act_as::<TerminalView>(cx) { return Some(Self::TerminalView(terminal_view.downgrade())); } } if terminal_panel_dock_is_open(workspace, cx) { return Some(Self::TerminalPanel); } None } pub(crate) fn exists(&self, workspace: &Workspace, cx: &App) -> bool { match self { Self::Editor(handle) => handle.upgrade().is_some(), Self::TerminalView(handle) => handle.upgrade().is_some(), Self::TerminalPanel => terminal_panel_dock_is_open(workspace, cx), } } } fn terminal_panel_dock_is_open(workspace: &Workspace, cx: &App) -> bool { if workspace.panel::<TerminalPanel>(cx).is_none() { return false; } let position = TerminalSettings::get_global(cx).dock.into(); workspace.dock_at_position(position).read(cx).is_open() } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PromptContextEntry { Mode(PromptContextType), Action(PromptContextAction), } impl PromptContextEntry { pub fn keyword(&self) -> &'static str { match self { Self::Mode(mode) => mode.keyword(), Self::Action(action) => action.keyword(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PromptContextType { File, Symbol, Fetch, Thread, Skill, Diagnostics, BranchDiff, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PromptContextAction { AddSelections, } impl PromptContextAction { 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::Reader, } } } /// A slash command that runs a local UI action against the conversation /// (sending feedback) instead of being sent to the agent as part of a prompt. /// Each variant maps to a method on `ThreadView`; the completion provider only /// surfaces them and emits an event, while `ThreadView` performs the actual /// work (see `handle_message_editor_event`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PromptLocalCommand { ThumbsUp, ThumbsDown, } impl PromptLocalCommand { pub fn keyword(&self) -> &'static str { match self { Self::ThumbsUp => "helpful", Self::ThumbsDown => "not-helpful", } } pub fn label(&self) -> &'static str { match self { Self::ThumbsUp => "Positive Feedback", Self::ThumbsDown => "Negative Feedback", } } pub fn description(&self) -> &'static str { match self { Self::ThumbsUp => { "Rate this response as helpful. Sends the current conversation to the Zed team." } Self::ThumbsDown => { "Rate this response as not helpful. Sends the current conversation to the Zed team." } } } pub fn icon(&self) -> IconName { match self { Self::ThumbsUp => IconName::ThumbsUp, Self::ThumbsDown => IconName::ThumbsDown, } } } impl TryFrom<&str> for PromptContextType { 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), "skill" => Ok(Self::Skill), "diagnostics" => Ok(Self::Diagnostics), "diff" => Ok(Self::BranchDiff), _ => Err(format!("Invalid context picker mode: {}", value)), } } } impl PromptContextType { pub fn keyword(&self) -> &'static str { match self { Self::File => "file", Self::Symbol => "symbol", Self::Fetch => "fetch", Self::Thread => "thread", Self::Skill => "skill", Self::Diagnostics => "diagnostics", Self::BranchDiff => "branch diff", } } pub fn label(&self) -> &'static str { match self { Self::File => "Files & Directories", Self::Symbol => "Symbols", Self::Fetch => "Fetch", Self::Thread => "Threads", Self::Skill => "Skills", Self::Diagnostics => "Diagnostics", Self::BranchDiff => "Branch Diff", } } pub fn icon(&self) -> IconName { match self { Self::File => IconName::File, Self::Symbol => IconName::Code, Self::Fetch => IconName::ToolWeb, Self::Thread => IconName::Thread, Self::Skill => IconName::Sparkle, Self::Diagnostics => IconName::Warning, Self::BranchDiff => IconName::GitBranch, } } } pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), Thread(SessionMatch), RecentThread(SessionMatch), Fetch(SharedString), Skill(AvailableSkill), Entry(EntryMatch), BranchDiff(BranchDiffMatch), } #[derive(Debug, Clone)] pub struct BranchDiffMatch { pub base_ref: SharedString, } impl Match { pub fn score(&self) -> f64 { match self { Match::File(file) => file.mat.score, Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), Match::Thread(_) => 1., Match::RecentThread(_) => 1., Match::Symbol(_) => 1., Match::Skill(_) => 1., Match::Fetch(_) => 1., Match::BranchDiff(_) => 1., } } } #[derive(Debug, Clone)] pub struct SessionMatch { session_id: acp::SessionId, title: SharedString, } pub struct EntryMatch { mat: Option<StringMatch>, entry: PromptContextEntry, } fn session_title(title: Option<SharedString>) -> SharedString { title .filter(|title| !title.is_empty()) .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE)) } #[derive(Debug, Clone)] pub struct AvailableSkill { pub name: Arc<str>, pub description: Arc<str>, /// Scope prefix for this skill: empty for global skills, or the /// worktree root name for project-local skills. pub source: SharedString, pub skill_file_path: PathBuf, pub warning: Option<SharedString>, } fn skill_completion_icon_path( skill: &AvailableSkill, uri: &MentionUri, cx: &mut App, ) -> SharedString { if skill.warning.is_some() { IconName::Warning.path().into() } else { uri.icon_path(cx) } } fn skill_completion_icon_color(skill: &AvailableSkill, cx: &App) -> Option<Hsla> { skill.warning.is_some().then(|| cx.theme().status().warning) } fn skill_completion_documentation(skill: &AvailableSkill) -> CompletionDocumentation { let text = match &skill.warning { Some(warning) => warning.clone(), None => skill.description.to_string().into(), }; CompletionDocumentation::MultiLinePlainText(text) } #[derive(Debug, Clone)] pub struct AvailableCommand { pub name: Arc<str>, pub description: Arc<str>, pub requires_argument: bool, pub source: Option<SharedString>, /// Source category used to group the command in the slash popup. `None` /// means the command came from an external ACP agent. pub category: Option<acp_thread::CommandCategory>, } impl AvailableCommand { fn category_order(&self) -> u8 { match self.category { Some(acp_thread::CommandCategory::Native) => 0, Some(acp_thread::CommandCategory::Mcp) => 1, None => 2, } } /// Completion group key and header label for this command's category. fn group(&self) -> CompletionGroup { let (key, label) = match self.category { Some(acp_thread::CommandCategory::Native) => ("commands", "Commands"), Some(acp_thread::CommandCategory::Mcp) => ("mcp-commands", "MCP Server Commands"), None => ("acp-commands", "Commands"), }; CompletionGroup { key: key.into(), label: Some(label.into()), } } } #[derive(Debug, Clone)] enum SlashCompletionCandidate { Skill(AvailableSkill), Command(AvailableCommand), LocalCommand(PromptLocalCommand), } impl SlashCompletionCandidate { fn name(&self) -> &str { match self { Self::Skill(skill) => &skill.name, Self::Command(command) => &command.name, Self::LocalCommand(command) => command.keyword(), } } } /// Stable group identity for a slash completion: skills are one group, commands /// are grouped by category. This identifies which section header an entry sits /// under; the order the groups appear in is decided by relevance (see /// [`group_by_relevance`]). fn slash_completion_group_key(candidate: &SlashCompletionCandidate) -> u32 { match candidate { SlashCompletionCandidate::Skill(_) => 0, SlashCompletionCandidate::Command(command) => 1 + command.category_order() as u32, SlashCompletionCandidate::LocalCommand(_) => 4, } } /// Reorders `items` (which must already be in relevance/score order, best /// first) so that each group's entries stay contiguous while the groups /// themselves are ordered by their best-ranked member. The sort is stable, so /// within a group the original order is preserved. fn group_by_relevance<T>(items: &mut [T], group_key: impl Fn(&T) -> u32) { let mut group_best_rank: collections::HashMap<u32, usize> = collections::HashMap::default(); for (rank, item) in items.iter().enumerate() { group_best_rank.entry(group_key(item)).or_insert(rank); } items.sort_by_key(|item| group_best_rank[&group_key(item)]); } pub trait PromptCompletionProviderDelegate: Send + Sync + 'static { fn supports_context(&self, mode: PromptContextType, cx: &App) -> bool { self.supported_modes(cx).contains(&mode) } fn supported_modes(&self, cx: &App) -> Vec<PromptContextType>; fn supports_images(&self, cx: &App) -> bool; fn available_commands(&self, cx: &App) -> Vec<AvailableCommand>; fn available_skills(&self, _cx: &App) -> Vec<AvailableSkill> { Vec::new() } fn available_local_commands(&self, _cx: &App) -> Vec<PromptLocalCommand> { Vec::new() } fn run_local_command(&self, _command: PromptLocalCommand, _cx: &mut App) {} fn confirm_command(&self, cx: &mut App); /// Called once each time the user opens slash-command autocomplete /// in the editor this delegate serves. Implementations may use it /// to lazily kick off work that produces commands (for example, /// scanning the global skills directory). The default is a no-op. fn slash_autocomplete_invoked(&self, _cx: &mut App) {} } pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> { source: Arc<T>, editor: WeakEntity<Editor>, mention_set: Entity<MentionSet>, workspace: WeakEntity<Workspace>, } impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> { pub fn new( source: T, editor: WeakEntity<Editor>, mention_set: Entity<MentionSet>, workspace: WeakEntity<Workspace>, ) -> Self { Self { source: Arc::new(source), editor, mention_set, workspace, } } fn completion_for_entry(