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. Load more
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, }); }