5465 lines · 194.3 KB · Rust
RawDownload
use crate::DEFAULT_THREAD_TITLE;use crate::SendImmediately;use crate::{    ChatWithFollow,    completion_provider::{        AgentContextSelection, AvailableCommand, AvailableSkill, PromptCompletionProvider,        PromptCompletionProviderDelegate, PromptContextAction, PromptContextType,        SlashCommandCompletion,    },    mention_set::{Mention, MentionImage, MentionSet, insert_crease_for_mention},};use acp_thread::MentionUri;use agent::ThreadStore;use agent_client_protocol::schema as acp;use anyhow::{Result, anyhow};use editor::{    Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,    EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,    actions::{Copy, Cut, Paste},    code_context_menus::CodeContextMenu,    display_map::{CreaseId, CreaseSnapshot},    scroll::Autoscroll,};use futures::{FutureExt as _, future::join_all};use gpui::{    AppContext, ClipboardEntry, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,    Focusable, ImageFormat, KeyContext, SharedString, Subscription, Task, TaskExt, TextStyle,    WeakEntity,};use language::{Buffer, language_settings::InlayHintKind};use parking_lot::RwLock;use project::AgentId;use project::{    CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,};use prompt_store::PromptStore;use rope::Point;use settings::Settings;use std::{cmp::min, fmt::Write, ops::Range, rc::Rc, sync::Arc};use theme_settings::ThemeSettings;use ui::{ContextMenu, 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>,    available_skills: Vec<AvailableSkill>,} impl SessionCapabilities {    pub fn new(        prompt_capabilities: acp::PromptCapabilities,        available_commands: Vec<acp::AvailableCommand>,        available_skills: Vec<AvailableSkill>,    ) -> Self {        Self {            prompt_capabilities,            available_commands,            available_skills,        }    }     pub fn from_acp_commands(        prompt_capabilities: acp::PromptCapabilities,        available_commands: Vec<acp::AvailableCommand>,    ) -> Self {        Self::new(prompt_capabilities, available_commands, Vec::new())    }     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    }     pub fn available_skills(&self) -> &[AvailableSkill] {        &self.available_skills    }     pub fn has_slash_completions(&self) -> bool {        !self.available_commands.is_empty() || !self.available_skills.is_empty()    }     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::Skill,                PromptContextType::BranchDiff,            ]);        }        supported    }     pub fn completion_commands(&self) -> Vec<AvailableCommand> {        self.available_commands            .iter()            .map(|command| AvailableCommand {                name: command.name.clone().into(),                description: command.description.clone().into(),                requires_argument: command.input.is_some(),                source: None,            })            .collect()    }     pub fn completion_skills(&self) -> Vec<AvailableSkill> {        self.available_skills.clone()    }     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 fn set_available_skills(&mut self, available_skills: Vec<AvailableSkill>) {        self.available_skills = available_skills;    }} 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<AvailableCommand> {        self.session_capabilities.read().completion_commands()    }     fn available_skills(&self, _cx: &App) -> Vec<AvailableSkill> {        self.session_capabilities.read().completion_skills()    }     fn slash_autocomplete_invoked(&self, cx: &mut App) {        // This may be called synchronously from inside a `MessageEditor`        // update (e.g. when pasting a slash command triggers completions),        // so we defer the emit to avoid a reentrant update panic.        let Some(editor) = self.message_editor.upgrade() else {            return;        };        cx.defer(move |cx| {            editor.update(cx, |_editor, cx| {                cx.emit(MessageEditorEvent::SlashAutocompleteOpened);            });        });    }     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 InputAttempt {    Text(Arc<str>),    Paste(ClipboardItem),} #[derive(Clone, Debug)]pub enum MessageEditorEvent {    Send,    SendImmediately,    Cancel,    Focus,    LostFocus,    /// Emitted when the user opens slash-command autocomplete in this    /// editor. Used by `ThreadView` to fire the global-skills scan    /// trigger; see `NativeAgent::ensure_skills_scan_started`.    SlashAutocompleteOpened,    InputAttempted {        attempt: InputAttempt,        cursor_offset: usize,    },} impl EventEmitter<MessageEditorEvent> for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); enum MentionInsertPosition {    AtCursor,    EndOfBuffer,} fn insert_mention_for_project_path(    project_path: &ProjectPath,    position: MentionInsertPosition,    editor: &Entity<Editor>,    mention_set: &Entity<MentionSet>,    project: &Entity<Project>,    workspace: &Entity<Workspace>,    supports_images: bool,    window: &mut Window,    cx: &mut App,) -> Option<Task<()>> {    let (file_name, mention_uri) = {        let project = project.read(cx);        let path_style = project.path_style(cx);        let entry = project.entry_for_path(project_path, cx)?;        let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;        let abs_path = worktree.read(cx).absolutize(&project_path.path);        let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(            &project_path.path,            worktree.read(cx).root_name(),            path_style,        );        let mention_uri = if entry.is_dir() {            MentionUri::Directory { abs_path }        } else {            MentionUri::File { abs_path }        };        (file_name, mention_uri)    };     let mention_text = mention_uri.as_link().to_string();    let content_len = mention_text.len();     let text_anchor = match position {        MentionInsertPosition::AtCursor => editor.update(cx, |editor, cx| {            let buffer = editor.buffer().read(cx);            let snapshot = buffer.snapshot(cx);            let buffer_snapshot = snapshot.as_singleton()?;            let text_anchor = snapshot                .anchor_to_buffer_anchor(editor.selections.newest_anchor().start)?                .0                .bias_left(&buffer_snapshot);             editor.insert(&mention_text, window, cx);            editor.insert(" ", window, cx);             Some(text_anchor)        }),        MentionInsertPosition::EndOfBuffer => {            let multi_buffer = editor.read(cx).buffer().clone();            let buffer = multi_buffer.read(cx).as_singleton()?;            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));            let new_text = format!("{mention_text} ");            editor.update(cx, |editor, cx| {                editor.edit(                    [(                        multi_buffer::Anchor::Max..multi_buffer::Anchor::Max,                        new_text,                    )],                    cx,                );            });            Some(anchor)        }    }?;     Some(mention_set.update(cx, |mention_set, cx| {        mention_set.confirm_mention_completion(            file_name,            text_anchor,            content_len,            mention_uri,            supports_images,            editor.clone(),            workspace,            window,            cx,        )    }))} enum ResolvedPastedContextItem {    Image(gpui::Image, gpui::SharedString),    ProjectPath(ProjectPath),} async fn resolve_pasted_context_items(    project: Entity<Project>,    project_is_local: bool,    supports_images: bool,    entries: Vec<ClipboardEntry>,    cx: &mut gpui::AsyncWindowContext,) -> (Vec<ResolvedPastedContextItem>, Vec<Entity<Worktree>>) {    let mut items = Vec::new();    let mut added_worktrees = Vec::new();    let default_image_name: SharedString = "Image".into();     for entry in entries {        match entry {            ClipboardEntry::String(_) => {}            ClipboardEntry::Image(image) => {                if supports_images {                    items.push(ResolvedPastedContextItem::Image(                        image,                        default_image_name.clone(),                    ));                }            }            ClipboardEntry::ExternalPaths(paths) => {                for path in paths.paths().iter() {                    if let Some((image, name)) = cx                        .background_spawn({                            let path = path.clone();                            let default_image_name = default_image_name.clone();                            async move {                                crate::mention_set::load_external_image_from_path(                                    &path,                                    &default_image_name,                                )                            }                        })                        .await                    {                        if supports_images {                            items.push(ResolvedPastedContextItem::Image(image, name));                        }                        continue;                    }                     if !project_is_local {                        continue;                    }                     let path = path.clone();                    let Ok(resolve_task) = cx.update({                        let project = project.clone();                        move |_, cx| Workspace::project_path_for_path(project, &path, false, cx)                    }) else {                        continue;                    };                     if let Some((worktree, project_path)) = resolve_task.await.log_err() {                        added_worktrees.push(worktree);                        items.push(ResolvedPastedContextItem::ProjectPath(project_path));                    }                }            }        }    }     (items, added_worktrees)} fn insert_project_path_as_context(    project_path: ProjectPath,    editor: Entity<Editor>,    mention_set: Entity<MentionSet>,    workspace: WeakEntity<Workspace>,    supports_images: bool,    cx: &mut gpui::AsyncWindowContext,) -> Option<Task<()>> {    let workspace = workspace.upgrade()?;     cx.update(move |window, cx| {        let project = workspace.read(cx).project().clone();        insert_mention_for_project_path(            &project_path,            MentionInsertPosition::AtCursor,            &editor,            &mention_set,            &project,            &workspace,            supports_images,            window,            cx,        )    })    .ok()    .flatten()} async fn insert_resolved_pasted_context_items(    items: Vec<ResolvedPastedContextItem>,    added_worktrees: Vec<Entity<Worktree>>,    editor: Entity<Editor>,    mention_set: Entity<MentionSet>,    workspace: WeakEntity<Workspace>,    supports_images: bool,    cx: &mut gpui::AsyncWindowContext,) {    let mut path_mention_tasks = Vec::new();     for item in items {        match item {            ResolvedPastedContextItem::Image(image, name) => {                crate::mention_set::insert_images_as_context(                    vec![(image, name)],                    editor.clone(),                    mention_set.clone(),                    workspace.clone(),                    cx,                )                .await;            }            ResolvedPastedContextItem::ProjectPath(project_path) => {                if let Some(task) = insert_project_path_as_context(                    project_path,                    editor.clone(),                    mention_set.clone(),                    workspace.clone(),                    supports_images,                    cx,                ) {                    path_mention_tasks.push(task);                }            }        }    }     join_all(path_mention_tasks).await;    drop(added_worktrees);} impl MessageEditor {    pub fn new(        workspace: WeakEntity<Workspace>,        project: WeakEntity<Project>,        thread_store: Option<Entity<ThreadStore>>,        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_registry = project            .upgrade()            .map(|project| project.read(cx).languages().clone());         let editor = cx.new(|cx| {            let buffer = cx.new(|cx| {                let buffer = Buffer::local("", cx);                if let Some(language_registry) = language_registry.as_ref() {                    buffer.set_language_registry(language_registry.clone());                }                buffer            });            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.disable_mouse_wheel_zoom();            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),

Lines 1–500 of 5465.