3434 lines · 123.7 KB · Rust
RawDownload
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.