4465 lines · 157.1 KB · Rust
RawDownload
use crate::DEFAULT_THREAD_TITLE;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},};use acp_thread::MentionUri;use agent::ThreadStore;use agent_client_protocol as acp;use anyhow::{Result, anyhow};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_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::{fmt::Write, ops::Range, rc::Rc, sync::Arc};use theme_settings::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 {        text: Arc<str>,        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>>,        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_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.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()                {                    let editor = editor.read(cx);                    let cursor_anchor = editor.selections.newest_anchor().head();                    let cursor_offset = cursor_anchor                        .to_offset(&editor.buffer().read(cx).snapshot(cx))                        .0;                    cx.emit(MessageEditorEvent::InputAttempted {                        text: text.clone(),                        cursor_offset,                    });                }

Lines 1–500 of 4465.