4003 lines · 141.9 KB · Rust
RawDownload
use crate::SendImmediately;use crate::ThreadHistory;use crate::{    ChatWithFollow,    completion_provider::{        PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,        PromptContextType, SlashCommandCompletion,    },    mention_set::{        Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,    },};use acp_thread::MentionUri;use agent::ThreadStore;use agent_client_protocol as acp;use anyhow::{Result, anyhow};use collections::HashSet;use editor::{    Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,    EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,    actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,};use futures::{FutureExt as _, future::join_all};use gpui::{    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,    KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,};use language::{Buffer, Language, language_settings::InlayHintKind};use parking_lot::RwLock;use project::AgentId;use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};use prompt_store::PromptStore;use rope::Point;use settings::Settings;use std::{fmt::Write, ops::Range, rc::Rc, sync::Arc};use theme::ThemeSettings;use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};use util::paths::PathStyle;use util::{ResultExt, debug_panic};use workspace::{CollaboratorId, Workspace};use zed_actions::agent::{Chat, PasteRaw}; #[derive(Default)]pub struct SessionCapabilities {    prompt_capabilities: acp::PromptCapabilities,    available_commands: Vec<acp::AvailableCommand>,} impl SessionCapabilities {    pub fn new(        prompt_capabilities: acp::PromptCapabilities,        available_commands: Vec<acp::AvailableCommand>,    ) -> Self {        Self {            prompt_capabilities,            available_commands,        }    }     pub fn supports_images(&self) -> bool {        self.prompt_capabilities.image    }     pub fn supports_embedded_context(&self) -> bool {        self.prompt_capabilities.embedded_context    }     pub fn available_commands(&self) -> &[acp::AvailableCommand] {        &self.available_commands    }     fn supported_modes(&self, has_thread_store: bool) -> Vec<PromptContextType> {        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];        if self.prompt_capabilities.embedded_context {            if has_thread_store {                supported.push(PromptContextType::Thread);            }            supported.extend(&[                PromptContextType::Diagnostics,                PromptContextType::Fetch,                PromptContextType::Rules,                PromptContextType::BranchDiff,            ]);        }        supported    }     pub fn completion_commands(&self) -> Vec<crate::completion_provider::AvailableCommand> {        self.available_commands            .iter()            .map(|cmd| crate::completion_provider::AvailableCommand {                name: cmd.name.clone().into(),                description: cmd.description.clone().into(),                requires_argument: cmd.input.is_some(),            })            .collect()    }     pub fn set_prompt_capabilities(&mut self, prompt_capabilities: acp::PromptCapabilities) {        self.prompt_capabilities = prompt_capabilities;    }     pub fn set_available_commands(&mut self, available_commands: Vec<acp::AvailableCommand>) {        self.available_commands = available_commands;    }} pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>; struct MessageEditorCompletionDelegate {    session_capabilities: SharedSessionCapabilities,    has_thread_store: bool,    message_editor: WeakEntity<MessageEditor>,} impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate {    fn supports_images(&self, _cx: &App) -> bool {        self.session_capabilities.read().supports_images()    }     fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {        self.session_capabilities            .read()            .supported_modes(self.has_thread_store)    }     fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {        self.session_capabilities.read().completion_commands()    }     fn confirm_command(&self, cx: &mut App) {        let _ = self.message_editor.update(cx, |this, cx| this.send(cx));    }} pub struct MessageEditor {    mention_set: Entity<MentionSet>,    editor: Entity<Editor>,    workspace: WeakEntity<Workspace>,    session_capabilities: SharedSessionCapabilities,    agent_id: AgentId,    thread_store: Option<Entity<ThreadStore>>,    _subscriptions: Vec<Subscription>,    _parse_slash_command_task: Task<()>,} #[derive(Clone, Debug)]pub enum MessageEditorEvent {    Send,    SendImmediately,    Cancel,    Focus,    LostFocus,    InputAttempted(Arc<str>),} impl EventEmitter<MessageEditorEvent> for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl MessageEditor {    pub fn new(        workspace: WeakEntity<Workspace>,        project: WeakEntity<Project>,        thread_store: Option<Entity<ThreadStore>>,        history: Option<WeakEntity<ThreadHistory>>,        prompt_store: Option<Entity<PromptStore>>,        session_capabilities: SharedSessionCapabilities,        agent_id: AgentId,        placeholder: &str,        mode: EditorMode,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let language = Language::new(            language::LanguageConfig {                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),                ..Default::default()            },            None,        );         let editor = cx.new(|cx| {            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));             let mut editor = Editor::new(mode, buffer, None, window, cx);            editor.set_placeholder_text(placeholder, window, cx);            editor.set_show_indent_guides(false, cx);            editor.set_show_completions_on_input(Some(true));            editor.set_soft_wrap();            editor.set_use_modal_editing(true);            editor.set_context_menu_options(ContextMenuOptions {                min_entries_visible: 12,                max_entries_visible: 12,                placement: None,            });            editor.register_addon(MessageEditorAddon::new());             editor.set_custom_context_menu(|editor, _point, window, cx| {                let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));                 Some(ContextMenu::build(window, cx, |menu, _, _| {                    menu.action("Cut", Box::new(editor::actions::Cut))                        .action_disabled_when(                            !has_selection,                            "Copy",                            Box::new(editor::actions::Copy),                        )                        .action("Paste", Box::new(editor::actions::Paste))                        .action("Paste as Plain Text", Box::new(PasteRaw))                }))            });             editor        });        let mention_set =            cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));        let completion_provider = Rc::new(PromptCompletionProvider::new(            MessageEditorCompletionDelegate {                session_capabilities: session_capabilities.clone(),                has_thread_store: thread_store.is_some(),                message_editor: cx.weak_entity(),            },            editor.downgrade(),            mention_set.clone(),            history,            prompt_store.clone(),            workspace.clone(),        ));        editor.update(cx, |editor, _cx| {            editor.set_completion_provider(Some(completion_provider.clone()))        });         cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {            cx.emit(MessageEditorEvent::Focus)        })        .detach();        cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {            cx.emit(MessageEditorEvent::LostFocus)        })        .detach();         let mut has_hint = false;        let mut subscriptions = Vec::new();         subscriptions.push(cx.subscribe_in(&editor, window, {            move |this, editor, event, window, cx| {                let input_attempted_text = match event {                    EditorEvent::InputHandled { text, .. } => Some(text),                    EditorEvent::InputIgnored { text } => Some(text),                    _ => None,                };                if let Some(text) = input_attempted_text                    && editor.read(cx).read_only(cx)                    && !text.is_empty()                {                    cx.emit(MessageEditorEvent::InputAttempted(text.clone()));                }                 if let EditorEvent::Edited { .. } = event                    && !editor.read(cx).read_only(cx)                {                    editor.update(cx, |editor, cx| {                        let snapshot = editor.snapshot(window, cx);                        this.mention_set                            .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));                         let new_hints = this                            .command_hint(snapshot.buffer())                            .into_iter()                            .collect::<Vec<_>>();                        let has_new_hint = !new_hints.is_empty();                        editor.splice_inlays(                            if has_hint {                                &[COMMAND_HINT_INLAY_ID]                            } else {                                &[]                            },                            new_hints,                            cx,                        );                        has_hint = has_new_hint;                    });                    cx.notify();                }            }        }));         Self {            editor,            mention_set,            workspace,            session_capabilities,            agent_id,            thread_store,            _subscriptions: subscriptions,            _parse_slash_command_task: Task::ready(()),        }    }     pub fn set_session_capabilities(        &mut self,        session_capabilities: SharedSessionCapabilities,        _cx: &mut Context<Self>,    ) {        self.session_capabilities = session_capabilities;    }     fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {        let session_capabilities = self.session_capabilities.read();        let available_commands = session_capabilities.available_commands();        if available_commands.is_empty() {            return None;        }         let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;        if parsed_command.argument.is_some() {            return None;        }         let command_name = parsed_command.command?;        let available_command = available_commands            .iter()            .find(|command| command.name == command_name)?;         let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {            mut hint,            ..        }) = available_command.input.clone()?        else {            return None;        };         let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;        if hint_pos > snapshot.len() {            hint_pos = snapshot.len();            hint.insert(0, ' ');        }         let hint_pos = snapshot.anchor_after(hint_pos);         Some(Inlay::hint(            COMMAND_HINT_INLAY_ID,            hint_pos,            &InlayHint {                position: hint_pos.text_anchor,                label: InlayHintLabel::String(hint),                kind: Some(InlayHintKind::Parameter),                padding_left: false,                padding_right: false,                tooltip: None,                resolve_state: project::ResolveState::Resolved,            },        ))    }     pub fn insert_thread_summary(        &mut self,        session_id: acp::SessionId,        title: Option<SharedString>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if self.thread_store.is_none() {            return;        }        let Some(workspace) = self.workspace.upgrade() else {            return;        };        let thread_title = title            .filter(|title| !title.is_empty())            .unwrap_or_else(|| SharedString::new_static("New Thread"));        let uri = MentionUri::Thread {            id: session_id,            name: thread_title.to_string(),        };        let content = format!("{}\n", uri.as_link());         let content_len = content.len() - 1;         let start = self.editor.update(cx, |editor, cx| {            editor.set_text(content, window, cx);            editor                .buffer()                .read(cx)                .snapshot(cx)                .anchor_before(Point::zero())                .text_anchor        });         let supports_images = self.session_capabilities.read().supports_images();         self.mention_set            .update(cx, |mention_set, cx| {                mention_set.confirm_mention_completion(                    thread_title,                    start,                    content_len,                    uri,                    supports_images,                    self.editor.clone(),                    &workspace,                    window,                    cx,                )            })            .detach();    }     #[cfg(test)]    pub(crate) fn editor(&self) -> &Entity<Editor> {        &self.editor    }     pub fn is_empty(&self, cx: &App) -> bool {        self.editor.read(cx).is_empty(cx)    }     pub fn is_completions_menu_visible(&self, cx: &App) -> bool {        self.editor            .read(cx)            .context_menu()            .borrow()            .as_ref()            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())    }     #[cfg(test)]    pub fn mention_set(&self) -> &Entity<MentionSet> {        &self.mention_set    }     fn validate_slash_commands(        text: &str,        available_commands: &[acp::AvailableCommand],        agent_id: &AgentId,    ) -> Result<()> {        if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {            if let Some(command_name) = parsed_command.command {                // Check if this command is in the list of available commands from the server                let is_supported = available_commands                    .iter()                    .any(|cmd| cmd.name == command_name);                 if !is_supported {                    return Err(anyhow!(                        "The /{} command is not supported by {}.\n\nAvailable commands: {}",                        command_name,                        agent_id,                        if available_commands.is_empty() {                            "none".to_string()                        } else {                            available_commands                                .iter()                                .map(|cmd| format!("/{}", cmd.name))                                .collect::<Vec<_>>()                                .join(", ")                        }                    ));                }            }        }        Ok(())    }     pub fn contents(        &self,        full_mention_content: bool,        cx: &mut Context<Self>,    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {        let text = self.editor.read(cx).text(cx);        let available_commands = self            .session_capabilities            .read()            .available_commands()            .to_vec();        let agent_id = self.agent_id.clone();        let build_task = self.build_content_blocks(full_mention_content, cx);         cx.spawn(async move |_, _cx| {            Self::validate_slash_commands(&text, &available_commands, &agent_id)?;            build_task.await        })    }     pub fn draft_contents(&self, cx: &mut Context<Self>) -> Task<Result<Vec<acp::ContentBlock>>> {        let build_task = self.build_content_blocks(false, cx);        cx.spawn(async move |_, _cx| {            let (blocks, _tracked_buffers) = build_task.await?;            Ok(blocks)        })    }     fn build_content_blocks(        &self,        full_mention_content: bool,        cx: &mut Context<Self>,    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {        let contents = self

Lines 1–500 of 4003.