use crate::QueueMessage;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::HistoryStore;use agent_client_protocol as acp;use anyhow::{Result, anyhow};use collections::HashSet;use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, 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 project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};use prompt_store::PromptStore;use rope::Point;use settings::Settings;use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};use theme::ThemeSettings;use ui::{ContextMenu, prelude::*};use util::{ResultExt, debug_panic};use workspace::{CollaboratorId, Workspace};use zed_actions::agent::{Chat, PasteRaw}; pub struct MessageEditor { mention_set: Entity<MentionSet>, editor: Entity<Editor>, workspace: WeakEntity<Workspace>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>,} #[derive(Clone, Copy, Debug)]pub enum MessageEditorEvent { Send, Queue, Cancel, Focus, LostFocus,} impl EventEmitter<MessageEditorEvent> for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl PromptCompletionProviderDelegate for Entity<MessageEditor> { fn supports_images(&self, cx: &App) -> bool { self.read(cx).prompt_capabilities.borrow().image } fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> { let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; if self.read(cx).prompt_capabilities.borrow().embedded_context { supported.extend(&[ PromptContextType::Thread, PromptContextType::Fetch, PromptContextType::Rules, ]); } supported } fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> { self.read(cx) .available_commands .borrow() .iter() .map(|cmd| crate::completion_provider::AvailableCommand { name: cmd.name.clone().into(), description: cmd.description.clone().into(), requires_argument: cmd.input.is_some(), }) .collect() } fn confirm_command(&self, cx: &mut App) { self.update(cx, |this, cx| this.send(cx)); }} impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, 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: Some(ContextMenuPlacement::Above), }); 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)) })) }); editor }); let mention_set = cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone())); let completion_provider = Rc::new(PromptCompletionProvider::new( cx.entity(), editor.downgrade(), mention_set.clone(), history_store.clone(), 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| { 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, prompt_capabilities, available_commands, agent_name, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> { let available_commands = self.available_commands.borrow(); 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, thread: agent::DbThreadMetadata, window: &mut Window, cx: &mut Context<Self>, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; let uri = MentionUri::Thread { id: thread.id.clone(), 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.prompt_capabilities.borrow().image; 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_name: &str, ) -> 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_name, 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>>)>> { // Check for unsupported slash commands before spawning async task let text = self.editor.read(cx).text(cx); let available_commands = self.available_commands.borrow().clone(); if let Err(err) = Self::validate_slash_commands(&text, &available_commands, &self.agent_name) { return Task::ready(Err(err)); } let contents = self .mention_set .update(cx, |store, cx| store.contents(full_mention_content, cx)); let editor = self.editor.clone(); let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context; cx.spawn(async move |_, cx| { let contents = contents.await?; let mut all_tracked_buffers = Vec::new(); let result = editor.update(cx, |editor, cx| { let (mut ix, _) = text .char_indices() .find(|(_, c)| !c.is_whitespace()) .unwrap_or((0, '\0')); let mut chunks: Vec<acp::ContentBlock> = Vec::new(); let text = editor.text(cx); editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot()); if crease_range.start.0 > ix { let chunk = text[ix..crease_range.start.0].into(); chunks.push(chunk); } let chunk = match mention { Mention::Text { content, tracked_buffers, } => { all_tracked_buffers.extend(tracked_buffers.iter().cloned()); if supports_embedded_context { acp::ContentBlock::Resource(acp::EmbeddedResource::new( acp::EmbeddedResourceResource::TextResourceContents( acp::TextResourceContents::new( content.clone(), uri.to_uri().to_string(), ), ), )) } else { acp::ContentBlock::ResourceLink(acp::ResourceLink::new( uri.name(), uri.to_uri().to_string(), )) } } Mention::Image(mention_image) => acp::ContentBlock::Image( acp::ImageContent::new( mention_image.data.clone(), mention_image.format.mime_type(), ) .uri(match uri { MentionUri::File { .. } => Some(uri.to_uri().to_string()), MentionUri::PastedImage => None, other => { debug_panic!( "unexpected mention uri for image: {:?}", other ); None } }), ), Mention::Link => acp::ContentBlock::ResourceLink( acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()), ), }; chunks.push(chunk); ix = crease_range.end.0; } if ix < text.len() { let last_chunk = text[ix..].trim_end().to_owned(); if !last_chunk.is_empty() { chunks.push(last_chunk.into()); } } }); Ok((chunks, all_tracked_buffers)) })?; result }) } pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); editor.remove_creases( self.mention_set.update(cx, |mention_set, _cx| { mention_set .clear() .map(|(crease_id, _)| crease_id) .collect::<Vec<_>>() }), cx, ) }); } pub fn send(&mut self, cx: &mut Context<Self>) { if self.is_empty(cx) { return; } self.editor.update(cx, |editor, cx| { editor.clear_inlay_hints(cx); }); cx.emit(MessageEditorEvent::Send) } pub fn queue(&mut self, cx: &mut Context<Self>) {Lines 1–500 of 2571. Load more
use crate::QueueMessage; 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::HistoryStore; use agent_client_protocol as acp; use anyhow::{Result, anyhow}; use collections::HashSet; use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, 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 project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree}; use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc}; use theme::ThemeSettings; use ui::{ContextMenu, prelude::*}; use util::{ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, PasteRaw}; pub struct MessageEditor { mention_set: Entity<MentionSet>, editor: Entity<Editor>, workspace: WeakEntity<Workspace>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>, } #[derive(Clone, Copy, Debug)] pub enum MessageEditorEvent { Send, Queue, Cancel, Focus, LostFocus, } impl EventEmitter<MessageEditorEvent> for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl PromptCompletionProviderDelegate for Entity<MessageEditor> { fn supports_images(&self, cx: &App) -> bool { self.read(cx).prompt_capabilities.borrow().image } fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> { let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; if self.read(cx).prompt_capabilities.borrow().embedded_context { supported.extend(&[ PromptContextType::Thread, PromptContextType::Fetch, PromptContextType::Rules, ]); } supported } fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> { self.read(cx) .available_commands .borrow() .iter() .map(|cmd| crate::completion_provider::AvailableCommand { name: cmd.name.clone().into(), description: cmd.description.clone().into(), requires_argument: cmd.input.is_some(), }) .collect() } fn confirm_command(&self, cx: &mut App) { self.update(cx, |this, cx| this.send(cx)); } } impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: WeakEntity<Project>, history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>, available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>, agent_name: SharedString, 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: Some(ContextMenuPlacement::Above), }); 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)) })) }); editor }); let mention_set = cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone())); let completion_provider = Rc::new(PromptCompletionProvider::new( cx.entity(), editor.downgrade(), mention_set.clone(), history_store.clone(), 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| { 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, prompt_capabilities, available_commands, agent_name, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } } fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> { let available_commands = self.available_commands.borrow(); 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, thread: agent::DbThreadMetadata, window: &mut Window, cx: &mut Context<Self>, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; let uri = MentionUri::Thread { id: thread.id.clone(), 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.prompt_capabilities.borrow().image; 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_name: &str, ) -> 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_name, 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>>)>> { // Check for unsupported slash commands before spawning async task let text = self.editor.read(cx).text(cx); let available_commands = self.available_commands.borrow().clone(); if let Err(err) = Self::validate_slash_commands(&text, &available_commands, &self.agent_name) { return Task::ready(Err(err)); } let contents = self .mention_set .update(cx, |store, cx| store.contents(full_mention_content, cx)); let editor = self.editor.clone(); let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context; cx.spawn(async move |_, cx| { let contents = contents.await?; let mut all_tracked_buffers = Vec::new(); let result = editor.update(cx, |editor, cx| { let (mut ix, _) = text .char_indices() .find(|(_, c)| !c.is_whitespace()) .unwrap_or((0, '\0')); let mut chunks: Vec<acp::ContentBlock> = Vec::new(); let text = editor.text(cx); editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot()); if crease_range.start.0 > ix { let chunk = text[ix..crease_range.start.0].into(); chunks.push(chunk); } let chunk = match mention { Mention::Text { content, tracked_buffers, } => { all_tracked_buffers.extend(tracked_buffers.iter().cloned()); if supports_embedded_context { acp::ContentBlock::Resource(acp::EmbeddedResource::new( acp::EmbeddedResourceResource::TextResourceContents( acp::TextResourceContents::new( content.clone(), uri.to_uri().to_string(), ), ), )) } else { acp::ContentBlock::ResourceLink(acp::ResourceLink::new( uri.name(), uri.to_uri().to_string(), )) } } Mention::Image(mention_image) => acp::ContentBlock::Image( acp::ImageContent::new( mention_image.data.clone(), mention_image.format.mime_type(), ) .uri(match uri { MentionUri::File { .. } => Some(uri.to_uri().to_string()), MentionUri::PastedImage => None, other => { debug_panic!( "unexpected mention uri for image: {:?}", other ); None } }), ), Mention::Link => acp::ContentBlock::ResourceLink( acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()), ), }; chunks.push(chunk); ix = crease_range.end.0; } if ix < text.len() { let last_chunk = text[ix..].trim_end().to_owned(); if !last_chunk.is_empty() { chunks.push(last_chunk.into()); } } }); Ok((chunks, all_tracked_buffers)) })?; result }) } pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); editor.remove_creases( self.mention_set.update(cx, |mention_set, _cx| { mention_set .clear() .map(|(crease_id, _)| crease_id) .collect::<Vec<_>>() }), cx, ) }); } pub fn send(&mut self, cx: &mut Context<Self>) { if self.is_empty(cx) { return; } self.editor.update(cx, |editor, cx| { editor.clear_inlay_hints(cx); }); cx.emit(MessageEditorEvent::Send) } pub fn queue(&mut self, cx: &mut Context<Self>) {