message_editor.rs

  1use crate::{
  2    acp::completion_provider::{ContextPickerCompletionProvider, MentionImage, MentionSet},
  3    context_picker::fetch_context_picker::fetch_url_content,
  4};
  5use acp_thread::{MentionUri, selection_name};
  6use agent::{TextThreadStore, ThreadId, ThreadStore};
  7use agent_client_protocol as acp;
  8use anyhow::Result;
  9use collections::HashSet;
 10use editor::{
 11    Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
 12    EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset,
 13    actions::Paste,
 14    display_map::{Crease, CreaseId, FoldId},
 15};
 16use futures::{FutureExt as _, TryFutureExt as _};
 17use gpui::{
 18    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
 19    ImageFormat, Img, Task, TextStyle, WeakEntity,
 20};
 21use language::{Buffer, Language};
 22use language_model::LanguageModelImage;
 23use project::{CompletionIntent, Project};
 24use settings::Settings;
 25use std::{
 26    ffi::OsStr,
 27    fmt::Write,
 28    ops::Range,
 29    path::{Path, PathBuf},
 30    rc::Rc,
 31    sync::Arc,
 32};
 33use text::OffsetRangeExt;
 34use theme::ThemeSettings;
 35use ui::{
 36    ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
 37    IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
 38    Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
 39    h_flex,
 40};
 41use util::ResultExt;
 42use workspace::{Workspace, notifications::NotifyResultExt as _};
 43use zed_actions::agent::Chat;
 44
 45use super::completion_provider::Mention;
 46
 47pub struct MessageEditor {
 48    mention_set: MentionSet,
 49    editor: Entity<Editor>,
 50    project: Entity<Project>,
 51    workspace: WeakEntity<Workspace>,
 52    thread_store: Entity<ThreadStore>,
 53    text_thread_store: Entity<TextThreadStore>,
 54}
 55
 56#[derive(Clone, Copy)]
 57pub enum MessageEditorEvent {
 58    Send,
 59    Cancel,
 60    Focus,
 61}
 62
 63impl EventEmitter<MessageEditorEvent> for MessageEditor {}
 64
 65impl MessageEditor {
 66    pub fn new(
 67        workspace: WeakEntity<Workspace>,
 68        project: Entity<Project>,
 69        thread_store: Entity<ThreadStore>,
 70        text_thread_store: Entity<TextThreadStore>,
 71        mode: EditorMode,
 72        window: &mut Window,
 73        cx: &mut Context<Self>,
 74    ) -> Self {
 75        let language = Language::new(
 76            language::LanguageConfig {
 77                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 78                ..Default::default()
 79            },
 80            None,
 81        );
 82        let completion_provider = ContextPickerCompletionProvider::new(
 83            workspace.clone(),
 84            thread_store.downgrade(),
 85            text_thread_store.downgrade(),
 86            cx.weak_entity(),
 87        );
 88        let mention_set = MentionSet::default();
 89        let editor = cx.new(|cx| {
 90            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 91            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 92
 93            let mut editor = Editor::new(mode, buffer, None, window, cx);
 94            editor.set_placeholder_text("Message the agent - @ to include files", cx);
 95            editor.set_show_indent_guides(false, cx);
 96            editor.set_soft_wrap();
 97            editor.set_use_modal_editing(true);
 98            editor.set_completion_provider(Some(Rc::new(completion_provider)));
 99            editor.set_context_menu_options(ContextMenuOptions {
100                min_entries_visible: 12,
101                max_entries_visible: 12,
102                placement: Some(ContextMenuPlacement::Above),
103            });
104            editor
105        });
106
107        cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
108            cx.emit(MessageEditorEvent::Focus)
109        })
110        .detach();
111
112        Self {
113            editor,
114            project,
115            mention_set,
116            thread_store,
117            text_thread_store,
118            workspace,
119        }
120    }
121
122    #[cfg(test)]
123    pub(crate) fn editor(&self) -> &Entity<Editor> {
124        &self.editor
125    }
126
127    #[cfg(test)]
128    pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
129        &mut self.mention_set
130    }
131
132    pub fn is_empty(&self, cx: &App) -> bool {
133        self.editor.read(cx).is_empty(cx)
134    }
135
136    pub fn mentioned_path_and_threads(&self) -> (HashSet<PathBuf>, HashSet<ThreadId>) {
137        let mut excluded_paths = HashSet::default();
138        let mut excluded_threads = HashSet::default();
139
140        for uri in self.mention_set.uri_by_crease_id.values() {
141            match uri {
142                MentionUri::File { abs_path, .. } => {
143                    excluded_paths.insert(abs_path.clone());
144                }
145                MentionUri::Thread { id, .. } => {
146                    excluded_threads.insert(id.clone());
147                }
148                _ => {}
149            }
150        }
151
152        (excluded_paths, excluded_threads)
153    }
154
155    pub fn confirm_completion(
156        &mut self,
157        crease_text: SharedString,
158        start: text::Anchor,
159        content_len: usize,
160        mention_uri: MentionUri,
161        window: &mut Window,
162        cx: &mut Context<Self>,
163    ) {
164        let snapshot = self
165            .editor
166            .update(cx, |editor, cx| editor.snapshot(window, cx));
167        let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
168            return;
169        };
170        let Some(anchor) = snapshot
171            .buffer_snapshot
172            .anchor_in_excerpt(*excerpt_id, start)
173        else {
174            return;
175        };
176
177        let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
178            *excerpt_id,
179            start,
180            content_len,
181            crease_text.clone(),
182            mention_uri.icon_path(cx),
183            self.editor.clone(),
184            window,
185            cx,
186        ) else {
187            return;
188        };
189        self.mention_set.insert_uri(crease_id, mention_uri.clone());
190
191        match mention_uri {
192            MentionUri::Fetch { url } => {
193                self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx);
194            }
195            MentionUri::File {
196                abs_path,
197                is_directory,
198            } => {
199                self.confirm_mention_for_file(
200                    crease_id,
201                    anchor,
202                    abs_path,
203                    is_directory,
204                    window,
205                    cx,
206                );
207            }
208            MentionUri::Symbol { .. }
209            | MentionUri::Thread { .. }
210            | MentionUri::TextThread { .. }
211            | MentionUri::Rule { .. }
212            | MentionUri::Selection { .. } => {}
213        }
214    }
215
216    fn confirm_mention_for_file(
217        &mut self,
218        crease_id: CreaseId,
219        anchor: Anchor,
220        abs_path: PathBuf,
221        _is_directory: bool,
222        window: &mut Window,
223        cx: &mut Context<Self>,
224    ) {
225        let extension = abs_path
226            .extension()
227            .and_then(OsStr::to_str)
228            .unwrap_or_default();
229        let project = self.project.clone();
230        let Some(project_path) = project
231            .read(cx)
232            .project_path_for_absolute_path(&abs_path, cx)
233        else {
234            return;
235        };
236
237        if Img::extensions().contains(&extension) && !extension.contains("svg") {
238            let image = cx.spawn(async move |_, cx| {
239                let image = project
240                    .update(cx, |project, cx| project.open_image(project_path, cx))?
241                    .await?;
242                image.read_with(cx, |image, _cx| image.image.clone())
243            });
244            self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx);
245        }
246    }
247
248    fn confirm_mention_for_fetch(
249        &mut self,
250        crease_id: CreaseId,
251        anchor: Anchor,
252        url: url::Url,
253        window: &mut Window,
254        cx: &mut Context<Self>,
255    ) {
256        let Some(http_client) = self
257            .workspace
258            .update(cx, |workspace, _cx| workspace.client().http_client())
259            .ok()
260        else {
261            return;
262        };
263
264        let url_string = url.to_string();
265        let fetch = cx
266            .background_executor()
267            .spawn(async move {
268                fetch_url_content(http_client, url_string)
269                    .map_err(|e| e.to_string())
270                    .await
271            })
272            .shared();
273        self.mention_set
274            .add_fetch_result(url.clone(), fetch.clone());
275
276        cx.spawn_in(window, async move |this, cx| {
277            let fetch = fetch.await.notify_async_err(cx);
278            this.update(cx, |this, cx| {
279                let mention_uri = MentionUri::Fetch { url };
280                if fetch.is_some() {
281                    this.mention_set.insert_uri(crease_id, mention_uri.clone());
282                } else {
283                    // Remove crease if we failed to fetch
284                    this.editor.update(cx, |editor, cx| {
285                        editor.display_map.update(cx, |display_map, cx| {
286                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
287                        });
288                        editor.remove_creases([crease_id], cx);
289                    });
290                }
291            })
292            .ok();
293        })
294        .detach();
295    }
296
297    pub fn confirm_mention_for_selection(
298        &mut self,
299        source_range: Range<text::Anchor>,
300        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
301        window: &mut Window,
302        cx: &mut Context<Self>,
303    ) {
304        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
305        let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
306            return;
307        };
308        let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
309            return;
310        };
311
312        let offset = start.to_offset(&snapshot);
313
314        for (buffer, selection_range, range_to_fold) in selections {
315            let range = snapshot.anchor_after(offset + range_to_fold.start)
316                ..snapshot.anchor_after(offset + range_to_fold.end);
317
318            let path = buffer
319                .read(cx)
320                .file()
321                .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf());
322            let snapshot = buffer.read(cx).snapshot();
323
324            let point_range = selection_range.to_point(&snapshot);
325            let line_range = point_range.start.row..point_range.end.row;
326
327            let uri = MentionUri::Selection {
328                path: path.clone(),
329                line_range: line_range.clone(),
330            };
331            let crease = crate::context_picker::crease_for_mention(
332                selection_name(&path, &line_range).into(),
333                uri.icon_path(cx),
334                range,
335                self.editor.downgrade(),
336            );
337
338            let crease_id = self.editor.update(cx, |editor, cx| {
339                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
340                editor.fold_creases(vec![crease], false, window, cx);
341                crease_ids.first().copied().unwrap()
342            });
343
344            self.mention_set
345                .insert_uri(crease_id, MentionUri::Selection { path, line_range });
346        }
347    }
348
349    pub fn contents(
350        &self,
351        window: &mut Window,
352        cx: &mut Context<Self>,
353    ) -> Task<Result<Vec<acp::ContentBlock>>> {
354        let contents = self.mention_set.contents(
355            self.project.clone(),
356            self.thread_store.clone(),
357            self.text_thread_store.clone(),
358            window,
359            cx,
360        );
361        let editor = self.editor.clone();
362
363        cx.spawn(async move |_, cx| {
364            let contents = contents.await?;
365
366            editor.update(cx, |editor, cx| {
367                let mut ix = 0;
368                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
369                let text = editor.text(cx);
370                editor.display_map.update(cx, |map, cx| {
371                    let snapshot = map.snapshot(cx);
372                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
373                        // Skip creases that have been edited out of the message buffer.
374                        if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
375                            continue;
376                        }
377
378                        let Some(mention) = contents.get(&crease_id) else {
379                            continue;
380                        };
381
382                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
383                        if crease_range.start > ix {
384                            chunks.push(text[ix..crease_range.start].into());
385                        }
386                        let chunk = match mention {
387                            Mention::Text { uri, content } => {
388                                acp::ContentBlock::Resource(acp::EmbeddedResource {
389                                    annotations: None,
390                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
391                                        acp::TextResourceContents {
392                                            mime_type: None,
393                                            text: content.clone(),
394                                            uri: uri.to_uri().to_string(),
395                                        },
396                                    ),
397                                })
398                            }
399                            Mention::Image(mention_image) => {
400                                acp::ContentBlock::Image(acp::ImageContent {
401                                    annotations: None,
402                                    data: mention_image.data.to_string(),
403                                    mime_type: mention_image.format.mime_type().into(),
404                                    uri: mention_image
405                                        .abs_path
406                                        .as_ref()
407                                        .map(|path| format!("file://{}", path.display())),
408                                })
409                            }
410                        };
411                        chunks.push(chunk);
412                        ix = crease_range.end;
413                    }
414
415                    if ix < text.len() {
416                        let last_chunk = text[ix..].trim_end();
417                        if !last_chunk.is_empty() {
418                            chunks.push(last_chunk.into());
419                        }
420                    }
421                });
422
423                chunks
424            })
425        })
426    }
427
428    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
429        self.editor.update(cx, |editor, cx| {
430            editor.clear(window, cx);
431            editor.remove_creases(self.mention_set.drain(), cx)
432        });
433    }
434
435    fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
436        cx.emit(MessageEditorEvent::Send)
437    }
438
439    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
440        cx.emit(MessageEditorEvent::Cancel)
441    }
442
443    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
444        let images = cx
445            .read_from_clipboard()
446            .map(|item| {
447                item.into_entries()
448                    .filter_map(|entry| {
449                        if let ClipboardEntry::Image(image) = entry {
450                            Some(image)
451                        } else {
452                            None
453                        }
454                    })
455                    .collect::<Vec<_>>()
456            })
457            .unwrap_or_default();
458
459        if images.is_empty() {
460            return;
461        }
462        cx.stop_propagation();
463
464        let replacement_text = "image";
465        for image in images {
466            let (excerpt_id, text_anchor, multibuffer_anchor) =
467                self.editor.update(cx, |message_editor, cx| {
468                    let snapshot = message_editor.snapshot(window, cx);
469                    let (excerpt_id, _, buffer_snapshot) =
470                        snapshot.buffer_snapshot.as_singleton().unwrap();
471
472                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
473                    let multibuffer_anchor = snapshot
474                        .buffer_snapshot
475                        .anchor_in_excerpt(*excerpt_id, text_anchor);
476                    message_editor.edit(
477                        [(
478                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
479                            format!("{replacement_text} "),
480                        )],
481                        cx,
482                    );
483                    (*excerpt_id, text_anchor, multibuffer_anchor)
484                });
485
486            let content_len = replacement_text.len();
487            let Some(anchor) = multibuffer_anchor else {
488                return;
489            };
490            let Some(crease_id) = insert_crease_for_image(
491                excerpt_id,
492                text_anchor,
493                content_len,
494                None.clone(),
495                self.editor.clone(),
496                window,
497                cx,
498            ) else {
499                return;
500            };
501            self.confirm_mention_for_image(
502                crease_id,
503                anchor,
504                None,
505                Task::ready(Ok(Arc::new(image))),
506                window,
507                cx,
508            );
509        }
510    }
511
512    pub fn insert_dragged_files(
513        &self,
514        paths: Vec<project::ProjectPath>,
515        window: &mut Window,
516        cx: &mut Context<Self>,
517    ) {
518        let buffer = self.editor.read(cx).buffer().clone();
519        let Some(buffer) = buffer.read(cx).as_singleton() else {
520            return;
521        };
522        for path in paths {
523            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
524                continue;
525            };
526            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
527                continue;
528            };
529
530            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
531            let path_prefix = abs_path
532                .file_name()
533                .unwrap_or(path.path.as_os_str())
534                .display()
535                .to_string();
536            let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
537                path,
538                &path_prefix,
539                false,
540                entry.is_dir(),
541                anchor..anchor,
542                cx.weak_entity(),
543                self.project.clone(),
544                cx,
545            ) else {
546                continue;
547            };
548
549            self.editor.update(cx, |message_editor, cx| {
550                message_editor.edit(
551                    [(
552                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
553                        completion.new_text,
554                    )],
555                    cx,
556                );
557            });
558            if let Some(confirm) = completion.confirm.clone() {
559                confirm(CompletionIntent::Complete, window, cx);
560            }
561        }
562    }
563
564    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
565        self.editor.update(cx, |message_editor, cx| {
566            message_editor.set_read_only(read_only);
567            cx.notify()
568        })
569    }
570
571    fn confirm_mention_for_image(
572        &mut self,
573        crease_id: CreaseId,
574        anchor: Anchor,
575        abs_path: Option<PathBuf>,
576        image: Task<Result<Arc<Image>>>,
577        window: &mut Window,
578        cx: &mut Context<Self>,
579    ) {
580        self.editor.update(cx, |_editor, cx| {
581            let task = cx
582                .spawn_in(window, async move |editor, cx| {
583                    let image = image.await.map_err(|e| e.to_string())?;
584                    let format = image.format;
585                    let image = cx
586                        .update(|_, cx| LanguageModelImage::from_image(image, cx))
587                        .map_err(|e| e.to_string())?
588                        .await;
589                    if let Some(image) = image {
590                        Ok(MentionImage {
591                            abs_path,
592                            data: image.source,
593                            format,
594                        })
595                    } else {
596                        editor
597                            .update(cx, |editor, cx| {
598                                editor.display_map.update(cx, |display_map, cx| {
599                                    display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
600                                });
601                                editor.remove_creases([crease_id], cx);
602                            })
603                            .ok();
604                        Err("Failed to convert image".to_string())
605                    }
606                })
607                .shared();
608
609            cx.spawn_in(window, {
610                let task = task.clone();
611                async move |_, cx| task.clone().await.notify_async_err(cx)
612            })
613            .detach();
614
615            self.mention_set.insert_image(crease_id, task);
616        });
617    }
618
619    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
620        self.editor.update(cx, |editor, cx| {
621            editor.set_mode(mode);
622            cx.notify()
623        });
624    }
625
626    pub fn set_message(
627        &mut self,
628        message: Vec<acp::ContentBlock>,
629        window: &mut Window,
630        cx: &mut Context<Self>,
631    ) {
632        self.clear(window, cx);
633
634        let mut text = String::new();
635        let mut mentions = Vec::new();
636        let mut images = Vec::new();
637
638        for chunk in message {
639            match chunk {
640                acp::ContentBlock::Text(text_content) => {
641                    text.push_str(&text_content.text);
642                }
643                acp::ContentBlock::Resource(acp::EmbeddedResource {
644                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
645                    ..
646                }) => {
647                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
648                        let start = text.len();
649                        write!(&mut text, "{}", mention_uri.as_link()).ok();
650                        let end = text.len();
651                        mentions.push((start..end, mention_uri));
652                    }
653                }
654                acp::ContentBlock::Image(content) => {
655                    let start = text.len();
656                    text.push_str("image");
657                    let end = text.len();
658                    images.push((start..end, content));
659                }
660                acp::ContentBlock::Audio(_)
661                | acp::ContentBlock::Resource(_)
662                | acp::ContentBlock::ResourceLink(_) => {}
663            }
664        }
665
666        let snapshot = self.editor.update(cx, |editor, cx| {
667            editor.set_text(text, window, cx);
668            editor.buffer().read(cx).snapshot(cx)
669        });
670
671        for (range, mention_uri) in mentions {
672            let anchor = snapshot.anchor_before(range.start);
673            let crease_id = crate::context_picker::insert_crease_for_mention(
674                anchor.excerpt_id,
675                anchor.text_anchor,
676                range.end - range.start,
677                mention_uri.name().into(),
678                mention_uri.icon_path(cx),
679                self.editor.clone(),
680                window,
681                cx,
682            );
683
684            if let Some(crease_id) = crease_id {
685                self.mention_set.insert_uri(crease_id, mention_uri);
686            }
687        }
688        for (range, content) in images {
689            let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else {
690                continue;
691            };
692            let anchor = snapshot.anchor_before(range.start);
693            let abs_path = content
694                .uri
695                .as_ref()
696                .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into()));
697
698            let name = content
699                .uri
700                .as_ref()
701                .and_then(|uri| {
702                    uri.strip_prefix("file://")
703                        .and_then(|path| Path::new(path).file_name())
704                })
705                .map(|name| name.to_string_lossy().to_string())
706                .unwrap_or("Image".to_owned());
707            let crease_id = crate::context_picker::insert_crease_for_mention(
708                anchor.excerpt_id,
709                anchor.text_anchor,
710                range.end - range.start,
711                name.into(),
712                IconName::Image.path().into(),
713                self.editor.clone(),
714                window,
715                cx,
716            );
717            let data: SharedString = content.data.to_string().into();
718
719            if let Some(crease_id) = crease_id {
720                self.mention_set.insert_image(
721                    crease_id,
722                    Task::ready(Ok(MentionImage {
723                        abs_path,
724                        data,
725                        format,
726                    }))
727                    .shared(),
728                );
729            }
730        }
731        cx.notify();
732    }
733
734    #[cfg(test)]
735    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
736        self.editor.update(cx, |editor, cx| {
737            editor.set_text(text, window, cx);
738        });
739    }
740
741    #[cfg(test)]
742    pub fn text(&self, cx: &App) -> String {
743        self.editor.read(cx).text(cx)
744    }
745}
746
747impl Focusable for MessageEditor {
748    fn focus_handle(&self, cx: &App) -> FocusHandle {
749        self.editor.focus_handle(cx)
750    }
751}
752
753impl Render for MessageEditor {
754    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
755        div()
756            .key_context("MessageEditor")
757            .on_action(cx.listener(Self::send))
758            .on_action(cx.listener(Self::cancel))
759            .capture_action(cx.listener(Self::paste))
760            .flex_1()
761            .child({
762                let settings = ThemeSettings::get_global(cx);
763                let font_size = TextSize::Small
764                    .rems(cx)
765                    .to_pixels(settings.agent_font_size(cx));
766                let line_height = settings.buffer_line_height.value() * font_size;
767
768                let text_style = TextStyle {
769                    color: cx.theme().colors().text,
770                    font_family: settings.buffer_font.family.clone(),
771                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
772                    font_features: settings.buffer_font.features.clone(),
773                    font_size: font_size.into(),
774                    line_height: line_height.into(),
775                    ..Default::default()
776                };
777
778                EditorElement::new(
779                    &self.editor,
780                    EditorStyle {
781                        background: cx.theme().colors().editor_background,
782                        local_player: cx.theme().players().local(),
783                        text: text_style,
784                        syntax: cx.theme().syntax().clone(),
785                        ..Default::default()
786                    },
787                )
788            })
789    }
790}
791
792pub(crate) fn insert_crease_for_image(
793    excerpt_id: ExcerptId,
794    anchor: text::Anchor,
795    content_len: usize,
796    abs_path: Option<Arc<Path>>,
797    editor: Entity<Editor>,
798    window: &mut Window,
799    cx: &mut App,
800) -> Option<CreaseId> {
801    let crease_label = abs_path
802        .as_ref()
803        .and_then(|path| path.file_name())
804        .map(|name| name.to_string_lossy().to_string().into())
805        .unwrap_or(SharedString::from("Image"));
806
807    editor.update(cx, |editor, cx| {
808        let snapshot = editor.buffer().read(cx).snapshot(cx);
809
810        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
811
812        let start = start.bias_right(&snapshot);
813        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
814
815        let placeholder = FoldPlaceholder {
816            render: render_image_fold_icon_button(crease_label, cx.weak_entity()),
817            merge_adjacent: false,
818            ..Default::default()
819        };
820
821        let crease = Crease::Inline {
822            range: start..end,
823            placeholder,
824            render_toggle: None,
825            render_trailer: None,
826            metadata: None,
827        };
828
829        let ids = editor.insert_creases(vec![crease.clone()], cx);
830        editor.fold_creases(vec![crease], false, window, cx);
831
832        Some(ids[0])
833    })
834}
835
836fn render_image_fold_icon_button(
837    label: SharedString,
838    editor: WeakEntity<Editor>,
839) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
840    Arc::new({
841        move |fold_id, fold_range, cx| {
842            let is_in_text_selection = editor
843                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
844                .unwrap_or_default();
845
846            ButtonLike::new(fold_id)
847                .style(ButtonStyle::Filled)
848                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
849                .toggle_state(is_in_text_selection)
850                .child(
851                    h_flex()
852                        .gap_1()
853                        .child(
854                            Icon::new(IconName::Image)
855                                .size(IconSize::XSmall)
856                                .color(Color::Muted),
857                        )
858                        .child(
859                            Label::new(label.clone())
860                                .size(LabelSize::Small)
861                                .buffer_font(cx)
862                                .single_line(),
863                        ),
864                )
865                .into_any_element()
866        }
867    })
868}
869
870#[cfg(test)]
871mod tests {
872    use std::path::Path;
873
874    use agent::{TextThreadStore, ThreadStore};
875    use agent_client_protocol as acp;
876    use editor::EditorMode;
877    use fs::FakeFs;
878    use gpui::{AppContext, TestAppContext};
879    use lsp::{CompletionContext, CompletionTriggerKind};
880    use project::{CompletionIntent, Project};
881    use serde_json::json;
882    use util::path;
883    use workspace::Workspace;
884
885    use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test};
886
887    #[gpui::test]
888    async fn test_at_mention_removal(cx: &mut TestAppContext) {
889        init_test(cx);
890
891        let fs = FakeFs::new(cx.executor());
892        fs.insert_tree("/project", json!({"file": ""})).await;
893        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
894
895        let (workspace, cx) =
896            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
897
898        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
899        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
900
901        let message_editor = cx.update(|window, cx| {
902            cx.new(|cx| {
903                MessageEditor::new(
904                    workspace.downgrade(),
905                    project.clone(),
906                    thread_store.clone(),
907                    text_thread_store.clone(),
908                    EditorMode::AutoHeight {
909                        min_lines: 1,
910                        max_lines: None,
911                    },
912                    window,
913                    cx,
914                )
915            })
916        });
917        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
918
919        cx.run_until_parked();
920
921        let excerpt_id = editor.update(cx, |editor, cx| {
922            editor
923                .buffer()
924                .read(cx)
925                .excerpt_ids()
926                .into_iter()
927                .next()
928                .unwrap()
929        });
930        let completions = editor.update_in(cx, |editor, window, cx| {
931            editor.set_text("Hello @file ", window, cx);
932            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
933            let completion_provider = editor.completion_provider().unwrap();
934            completion_provider.completions(
935                excerpt_id,
936                &buffer,
937                text::Anchor::MAX,
938                CompletionContext {
939                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
940                    trigger_character: Some("@".into()),
941                },
942                window,
943                cx,
944            )
945        });
946        let [_, completion]: [_; 2] = completions
947            .await
948            .unwrap()
949            .into_iter()
950            .flat_map(|response| response.completions)
951            .collect::<Vec<_>>()
952            .try_into()
953            .unwrap();
954
955        editor.update_in(cx, |editor, window, cx| {
956            let snapshot = editor.buffer().read(cx).snapshot(cx);
957            let start = snapshot
958                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
959                .unwrap();
960            let end = snapshot
961                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
962                .unwrap();
963            editor.edit([(start..end, completion.new_text)], cx);
964            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
965        });
966
967        cx.run_until_parked();
968
969        // Backspace over the inserted crease (and the following space).
970        editor.update_in(cx, |editor, window, cx| {
971            editor.backspace(&Default::default(), window, cx);
972            editor.backspace(&Default::default(), window, cx);
973        });
974
975        let content = message_editor
976            .update_in(cx, |message_editor, window, cx| {
977                message_editor.contents(window, cx)
978            })
979            .await
980            .unwrap();
981
982        // We don't send a resource link for the deleted crease.
983        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
984    }
985}