message_editor.rs

   1use crate::{
   2    acp::completion_provider::ContextPickerCompletionProvider,
   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::{Context as _, Result, anyhow};
   9use collections::{HashMap, 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::{
  17    FutureExt as _, TryFutureExt as _,
  18    future::{Shared, try_join_all},
  19};
  20use gpui::{
  21    AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
  22    ImageFormat, Img, Task, TextStyle, WeakEntity,
  23};
  24use language::{Buffer, Language};
  25use language_model::LanguageModelImage;
  26use project::{CompletionIntent, Project};
  27use rope::Point;
  28use settings::Settings;
  29use std::{
  30    ffi::OsStr,
  31    fmt::Write,
  32    ops::Range,
  33    path::{Path, PathBuf},
  34    rc::Rc,
  35    sync::Arc,
  36};
  37use text::OffsetRangeExt;
  38use theme::ThemeSettings;
  39use ui::{
  40    ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
  41    IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
  42    Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
  43    h_flex,
  44};
  45use url::Url;
  46use util::ResultExt;
  47use workspace::{Workspace, notifications::NotifyResultExt as _};
  48use zed_actions::agent::Chat;
  49
  50pub struct MessageEditor {
  51    mention_set: MentionSet,
  52    editor: Entity<Editor>,
  53    project: Entity<Project>,
  54    workspace: WeakEntity<Workspace>,
  55    thread_store: Entity<ThreadStore>,
  56    text_thread_store: Entity<TextThreadStore>,
  57}
  58
  59#[derive(Clone, Copy)]
  60pub enum MessageEditorEvent {
  61    Send,
  62    Cancel,
  63    Focus,
  64}
  65
  66impl EventEmitter<MessageEditorEvent> for MessageEditor {}
  67
  68impl MessageEditor {
  69    pub fn new(
  70        workspace: WeakEntity<Workspace>,
  71        project: Entity<Project>,
  72        thread_store: Entity<ThreadStore>,
  73        text_thread_store: Entity<TextThreadStore>,
  74        mode: EditorMode,
  75        window: &mut Window,
  76        cx: &mut Context<Self>,
  77    ) -> Self {
  78        let language = Language::new(
  79            language::LanguageConfig {
  80                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
  81                ..Default::default()
  82            },
  83            None,
  84        );
  85        let completion_provider = ContextPickerCompletionProvider::new(
  86            workspace.clone(),
  87            thread_store.downgrade(),
  88            text_thread_store.downgrade(),
  89            cx.weak_entity(),
  90        );
  91        let mention_set = MentionSet::default();
  92        let editor = cx.new(|cx| {
  93            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
  94            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
  95
  96            let mut editor = Editor::new(mode, buffer, None, window, cx);
  97            editor.set_placeholder_text("Message the agent - @ to include files", cx);
  98            editor.set_show_indent_guides(false, cx);
  99            editor.set_soft_wrap();
 100            editor.set_use_modal_editing(true);
 101            editor.set_completion_provider(Some(Rc::new(completion_provider)));
 102            editor.set_context_menu_options(ContextMenuOptions {
 103                min_entries_visible: 12,
 104                max_entries_visible: 12,
 105                placement: Some(ContextMenuPlacement::Above),
 106            });
 107            editor
 108        });
 109
 110        cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
 111            cx.emit(MessageEditorEvent::Focus)
 112        })
 113        .detach();
 114
 115        Self {
 116            editor,
 117            project,
 118            mention_set,
 119            thread_store,
 120            text_thread_store,
 121            workspace,
 122        }
 123    }
 124
 125    #[cfg(test)]
 126    pub(crate) fn editor(&self) -> &Entity<Editor> {
 127        &self.editor
 128    }
 129
 130    #[cfg(test)]
 131    pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
 132        &mut self.mention_set
 133    }
 134
 135    pub fn is_empty(&self, cx: &App) -> bool {
 136        self.editor.read(cx).is_empty(cx)
 137    }
 138
 139    pub fn mentioned_path_and_threads(&self) -> (HashSet<PathBuf>, HashSet<ThreadId>) {
 140        let mut excluded_paths = HashSet::default();
 141        let mut excluded_threads = HashSet::default();
 142
 143        for uri in self.mention_set.uri_by_crease_id.values() {
 144            match uri {
 145                MentionUri::File { abs_path, .. } => {
 146                    excluded_paths.insert(abs_path.clone());
 147                }
 148                MentionUri::Thread { id, .. } => {
 149                    excluded_threads.insert(id.clone());
 150                }
 151                _ => {}
 152            }
 153        }
 154
 155        (excluded_paths, excluded_threads)
 156    }
 157
 158    pub fn confirm_completion(
 159        &mut self,
 160        crease_text: SharedString,
 161        start: text::Anchor,
 162        content_len: usize,
 163        mention_uri: MentionUri,
 164        window: &mut Window,
 165        cx: &mut Context<Self>,
 166    ) {
 167        let snapshot = self
 168            .editor
 169            .update(cx, |editor, cx| editor.snapshot(window, cx));
 170        let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
 171            return;
 172        };
 173        let Some(anchor) = snapshot
 174            .buffer_snapshot
 175            .anchor_in_excerpt(*excerpt_id, start)
 176        else {
 177            return;
 178        };
 179
 180        let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
 181            *excerpt_id,
 182            start,
 183            content_len,
 184            crease_text.clone(),
 185            mention_uri.icon_path(cx),
 186            self.editor.clone(),
 187            window,
 188            cx,
 189        ) else {
 190            return;
 191        };
 192
 193        match mention_uri {
 194            MentionUri::Fetch { url } => {
 195                self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx);
 196            }
 197            MentionUri::File {
 198                abs_path,
 199                is_directory,
 200            } => {
 201                self.confirm_mention_for_file(
 202                    crease_id,
 203                    anchor,
 204                    abs_path,
 205                    is_directory,
 206                    window,
 207                    cx,
 208                );
 209            }
 210            MentionUri::Thread { id, name } => {
 211                self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx);
 212            }
 213            MentionUri::TextThread { path, name } => {
 214                self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx);
 215            }
 216            MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => {
 217                self.mention_set.insert_uri(crease_id, mention_uri.clone());
 218            }
 219        }
 220    }
 221
 222    fn confirm_mention_for_file(
 223        &mut self,
 224        crease_id: CreaseId,
 225        anchor: Anchor,
 226        abs_path: PathBuf,
 227        is_directory: bool,
 228        window: &mut Window,
 229        cx: &mut Context<Self>,
 230    ) {
 231        let extension = abs_path
 232            .extension()
 233            .and_then(OsStr::to_str)
 234            .unwrap_or_default();
 235
 236        if Img::extensions().contains(&extension) && !extension.contains("svg") {
 237            let project = self.project.clone();
 238            let Some(project_path) = project
 239                .read(cx)
 240                .project_path_for_absolute_path(&abs_path, cx)
 241            else {
 242                return;
 243            };
 244            let image = cx.spawn(async move |_, cx| {
 245                let image = project
 246                    .update(cx, |project, cx| project.open_image(project_path, cx))?
 247                    .await?;
 248                image.read_with(cx, |image, _cx| image.image.clone())
 249            });
 250            self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx);
 251        } else {
 252            self.mention_set.insert_uri(
 253                crease_id,
 254                MentionUri::File {
 255                    abs_path,
 256                    is_directory,
 257                },
 258            );
 259        }
 260    }
 261
 262    fn confirm_mention_for_fetch(
 263        &mut self,
 264        crease_id: CreaseId,
 265        anchor: Anchor,
 266        url: url::Url,
 267        window: &mut Window,
 268        cx: &mut Context<Self>,
 269    ) {
 270        let Some(http_client) = self
 271            .workspace
 272            .update(cx, |workspace, _cx| workspace.client().http_client())
 273            .ok()
 274        else {
 275            return;
 276        };
 277
 278        let url_string = url.to_string();
 279        let fetch = cx
 280            .background_executor()
 281            .spawn(async move {
 282                fetch_url_content(http_client, url_string)
 283                    .map_err(|e| e.to_string())
 284                    .await
 285            })
 286            .shared();
 287        self.mention_set
 288            .add_fetch_result(url.clone(), fetch.clone());
 289
 290        cx.spawn_in(window, async move |this, cx| {
 291            let fetch = fetch.await.notify_async_err(cx);
 292            this.update(cx, |this, cx| {
 293                let mention_uri = MentionUri::Fetch { url };
 294                if fetch.is_some() {
 295                    this.mention_set.insert_uri(crease_id, mention_uri.clone());
 296                } else {
 297                    // Remove crease if we failed to fetch
 298                    this.editor.update(cx, |editor, cx| {
 299                        editor.display_map.update(cx, |display_map, cx| {
 300                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
 301                        });
 302                        editor.remove_creases([crease_id], cx);
 303                    });
 304                }
 305            })
 306            .ok();
 307        })
 308        .detach();
 309    }
 310
 311    pub fn confirm_mention_for_selection(
 312        &mut self,
 313        source_range: Range<text::Anchor>,
 314        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
 315        window: &mut Window,
 316        cx: &mut Context<Self>,
 317    ) {
 318        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
 319        let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
 320            return;
 321        };
 322        let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
 323            return;
 324        };
 325
 326        let offset = start.to_offset(&snapshot);
 327
 328        for (buffer, selection_range, range_to_fold) in selections {
 329            let range = snapshot.anchor_after(offset + range_to_fold.start)
 330                ..snapshot.anchor_after(offset + range_to_fold.end);
 331
 332            let path = buffer
 333                .read(cx)
 334                .file()
 335                .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf());
 336            let snapshot = buffer.read(cx).snapshot();
 337
 338            let point_range = selection_range.to_point(&snapshot);
 339            let line_range = point_range.start.row..point_range.end.row;
 340
 341            let uri = MentionUri::Selection {
 342                path: path.clone(),
 343                line_range: line_range.clone(),
 344            };
 345            let crease = crate::context_picker::crease_for_mention(
 346                selection_name(&path, &line_range).into(),
 347                uri.icon_path(cx),
 348                range,
 349                self.editor.downgrade(),
 350            );
 351
 352            let crease_id = self.editor.update(cx, |editor, cx| {
 353                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
 354                editor.fold_creases(vec![crease], false, window, cx);
 355                crease_ids.first().copied().unwrap()
 356            });
 357
 358            self.mention_set
 359                .insert_uri(crease_id, MentionUri::Selection { path, line_range });
 360        }
 361    }
 362
 363    pub fn contents(
 364        &self,
 365        window: &mut Window,
 366        cx: &mut Context<Self>,
 367    ) -> Task<Result<Vec<acp::ContentBlock>>> {
 368        let contents =
 369            self.mention_set
 370                .contents(self.project.clone(), self.thread_store.clone(), window, cx);
 371        let editor = self.editor.clone();
 372
 373        cx.spawn(async move |_, cx| {
 374            let contents = contents.await?;
 375
 376            editor.update(cx, |editor, cx| {
 377                let mut ix = 0;
 378                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
 379                let text = editor.text(cx);
 380                editor.display_map.update(cx, |map, cx| {
 381                    let snapshot = map.snapshot(cx);
 382                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
 383                        // Skip creases that have been edited out of the message buffer.
 384                        if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
 385                            continue;
 386                        }
 387
 388                        let Some(mention) = contents.get(&crease_id) else {
 389                            continue;
 390                        };
 391
 392                        let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
 393                        if crease_range.start > ix {
 394                            chunks.push(text[ix..crease_range.start].into());
 395                        }
 396                        let chunk = match mention {
 397                            Mention::Text { uri, content } => {
 398                                acp::ContentBlock::Resource(acp::EmbeddedResource {
 399                                    annotations: None,
 400                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
 401                                        acp::TextResourceContents {
 402                                            mime_type: None,
 403                                            text: content.clone(),
 404                                            uri: uri.to_uri().to_string(),
 405                                        },
 406                                    ),
 407                                })
 408                            }
 409                            Mention::Image(mention_image) => {
 410                                acp::ContentBlock::Image(acp::ImageContent {
 411                                    annotations: None,
 412                                    data: mention_image.data.to_string(),
 413                                    mime_type: mention_image.format.mime_type().into(),
 414                                    uri: mention_image
 415                                        .abs_path
 416                                        .as_ref()
 417                                        .map(|path| format!("file://{}", path.display())),
 418                                })
 419                            }
 420                        };
 421                        chunks.push(chunk);
 422                        ix = crease_range.end;
 423                    }
 424
 425                    if ix < text.len() {
 426                        let last_chunk = text[ix..].trim_end();
 427                        if !last_chunk.is_empty() {
 428                            chunks.push(last_chunk.into());
 429                        }
 430                    }
 431                });
 432
 433                chunks
 434            })
 435        })
 436    }
 437
 438    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 439        self.editor.update(cx, |editor, cx| {
 440            editor.clear(window, cx);
 441            editor.remove_creases(self.mention_set.drain(), cx)
 442        });
 443    }
 444
 445    fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 446        cx.emit(MessageEditorEvent::Send)
 447    }
 448
 449    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 450        cx.emit(MessageEditorEvent::Cancel)
 451    }
 452
 453    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 454        let images = cx
 455            .read_from_clipboard()
 456            .map(|item| {
 457                item.into_entries()
 458                    .filter_map(|entry| {
 459                        if let ClipboardEntry::Image(image) = entry {
 460                            Some(image)
 461                        } else {
 462                            None
 463                        }
 464                    })
 465                    .collect::<Vec<_>>()
 466            })
 467            .unwrap_or_default();
 468
 469        if images.is_empty() {
 470            return;
 471        }
 472        cx.stop_propagation();
 473
 474        let replacement_text = "image";
 475        for image in images {
 476            let (excerpt_id, text_anchor, multibuffer_anchor) =
 477                self.editor.update(cx, |message_editor, cx| {
 478                    let snapshot = message_editor.snapshot(window, cx);
 479                    let (excerpt_id, _, buffer_snapshot) =
 480                        snapshot.buffer_snapshot.as_singleton().unwrap();
 481
 482                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 483                    let multibuffer_anchor = snapshot
 484                        .buffer_snapshot
 485                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 486                    message_editor.edit(
 487                        [(
 488                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 489                            format!("{replacement_text} "),
 490                        )],
 491                        cx,
 492                    );
 493                    (*excerpt_id, text_anchor, multibuffer_anchor)
 494                });
 495
 496            let content_len = replacement_text.len();
 497            let Some(anchor) = multibuffer_anchor else {
 498                return;
 499            };
 500            let Some(crease_id) = insert_crease_for_image(
 501                excerpt_id,
 502                text_anchor,
 503                content_len,
 504                None.clone(),
 505                self.editor.clone(),
 506                window,
 507                cx,
 508            ) else {
 509                return;
 510            };
 511            self.confirm_mention_for_image(
 512                crease_id,
 513                anchor,
 514                None,
 515                Task::ready(Ok(Arc::new(image))),
 516                window,
 517                cx,
 518            );
 519        }
 520    }
 521
 522    pub fn insert_dragged_files(
 523        &self,
 524        paths: Vec<project::ProjectPath>,
 525        window: &mut Window,
 526        cx: &mut Context<Self>,
 527    ) {
 528        let buffer = self.editor.read(cx).buffer().clone();
 529        let Some(buffer) = buffer.read(cx).as_singleton() else {
 530            return;
 531        };
 532        for path in paths {
 533            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
 534                continue;
 535            };
 536            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
 537                continue;
 538            };
 539
 540            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 541            let path_prefix = abs_path
 542                .file_name()
 543                .unwrap_or(path.path.as_os_str())
 544                .display()
 545                .to_string();
 546            let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
 547                path,
 548                &path_prefix,
 549                false,
 550                entry.is_dir(),
 551                anchor..anchor,
 552                cx.weak_entity(),
 553                self.project.clone(),
 554                cx,
 555            ) else {
 556                continue;
 557            };
 558
 559            self.editor.update(cx, |message_editor, cx| {
 560                message_editor.edit(
 561                    [(
 562                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 563                        completion.new_text,
 564                    )],
 565                    cx,
 566                );
 567            });
 568            if let Some(confirm) = completion.confirm.clone() {
 569                confirm(CompletionIntent::Complete, window, cx);
 570            }
 571        }
 572    }
 573
 574    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
 575        self.editor.update(cx, |message_editor, cx| {
 576            message_editor.set_read_only(read_only);
 577            cx.notify()
 578        })
 579    }
 580
 581    fn confirm_mention_for_image(
 582        &mut self,
 583        crease_id: CreaseId,
 584        anchor: Anchor,
 585        abs_path: Option<PathBuf>,
 586        image: Task<Result<Arc<Image>>>,
 587        window: &mut Window,
 588        cx: &mut Context<Self>,
 589    ) {
 590        let editor = self.editor.clone();
 591        let task = cx
 592            .spawn_in(window, {
 593                let abs_path = abs_path.clone();
 594                async move |_, cx| {
 595                    let image = image.await.map_err(|e| e.to_string())?;
 596                    let format = image.format;
 597                    let image = cx
 598                        .update(|_, cx| LanguageModelImage::from_image(image, cx))
 599                        .map_err(|e| e.to_string())?
 600                        .await;
 601                    if let Some(image) = image {
 602                        Ok(MentionImage {
 603                            abs_path,
 604                            data: image.source,
 605                            format,
 606                        })
 607                    } else {
 608                        Err("Failed to convert image".into())
 609                    }
 610                }
 611            })
 612            .shared();
 613
 614        self.mention_set.insert_image(crease_id, task.clone());
 615
 616        cx.spawn_in(window, async move |this, cx| {
 617            if task.await.notify_async_err(cx).is_some() {
 618                if let Some(abs_path) = abs_path.clone() {
 619                    this.update(cx, |this, _cx| {
 620                        this.mention_set.insert_uri(
 621                            crease_id,
 622                            MentionUri::File {
 623                                abs_path,
 624                                is_directory: false,
 625                            },
 626                        );
 627                    })
 628                    .ok();
 629                }
 630            } else {
 631                editor
 632                    .update(cx, |editor, cx| {
 633                        editor.display_map.update(cx, |display_map, cx| {
 634                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
 635                        });
 636                        editor.remove_creases([crease_id], cx);
 637                    })
 638                    .ok();
 639            }
 640        })
 641        .detach();
 642    }
 643
 644    fn confirm_mention_for_thread(
 645        &mut self,
 646        crease_id: CreaseId,
 647        anchor: Anchor,
 648        id: ThreadId,
 649        name: String,
 650        window: &mut Window,
 651        cx: &mut Context<Self>,
 652    ) {
 653        let uri = MentionUri::Thread {
 654            id: id.clone(),
 655            name,
 656        };
 657        let open_task = self.thread_store.update(cx, |thread_store, cx| {
 658            thread_store.open_thread(&id, window, cx)
 659        });
 660        let task = cx
 661            .spawn(async move |_, cx| {
 662                let thread = open_task.await.map_err(|e| e.to_string())?;
 663                let content = thread
 664                    .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
 665                    .map_err(|e| e.to_string())?;
 666                Ok(content)
 667            })
 668            .shared();
 669
 670        self.mention_set.insert_thread(id, task.clone());
 671
 672        let editor = self.editor.clone();
 673        cx.spawn_in(window, async move |this, cx| {
 674            if task.await.notify_async_err(cx).is_some() {
 675                this.update(cx, |this, _| {
 676                    this.mention_set.insert_uri(crease_id, uri);
 677                })
 678                .ok();
 679            } else {
 680                editor
 681                    .update(cx, |editor, cx| {
 682                        editor.display_map.update(cx, |display_map, cx| {
 683                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
 684                        });
 685                        editor.remove_creases([crease_id], cx);
 686                    })
 687                    .ok();
 688            }
 689        })
 690        .detach();
 691    }
 692
 693    fn confirm_mention_for_text_thread(
 694        &mut self,
 695        crease_id: CreaseId,
 696        anchor: Anchor,
 697        path: PathBuf,
 698        name: String,
 699        window: &mut Window,
 700        cx: &mut Context<Self>,
 701    ) {
 702        let uri = MentionUri::TextThread {
 703            path: path.clone(),
 704            name,
 705        };
 706        let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
 707            text_thread_store.open_local_context(path.as_path().into(), cx)
 708        });
 709        let task = cx
 710            .spawn(async move |_, cx| {
 711                let context = context.await.map_err(|e| e.to_string())?;
 712                let xml = context
 713                    .update(cx, |context, cx| context.to_xml(cx))
 714                    .map_err(|e| e.to_string())?;
 715                Ok(xml)
 716            })
 717            .shared();
 718
 719        self.mention_set.insert_text_thread(path, task.clone());
 720
 721        let editor = self.editor.clone();
 722        cx.spawn_in(window, async move |this, cx| {
 723            if task.await.notify_async_err(cx).is_some() {
 724                this.update(cx, |this, _| {
 725                    this.mention_set.insert_uri(crease_id, uri);
 726                })
 727                .ok();
 728            } else {
 729                editor
 730                    .update(cx, |editor, cx| {
 731                        editor.display_map.update(cx, |display_map, cx| {
 732                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
 733                        });
 734                        editor.remove_creases([crease_id], cx);
 735                    })
 736                    .ok();
 737            }
 738        })
 739        .detach();
 740    }
 741
 742    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
 743        self.editor.update(cx, |editor, cx| {
 744            editor.set_mode(mode);
 745            cx.notify()
 746        });
 747    }
 748
 749    pub fn set_message(
 750        &mut self,
 751        message: Vec<acp::ContentBlock>,
 752        window: &mut Window,
 753        cx: &mut Context<Self>,
 754    ) {
 755        self.clear(window, cx);
 756
 757        let mut text = String::new();
 758        let mut mentions = Vec::new();
 759        let mut images = Vec::new();
 760
 761        for chunk in message {
 762            match chunk {
 763                acp::ContentBlock::Text(text_content) => {
 764                    text.push_str(&text_content.text);
 765                }
 766                acp::ContentBlock::Resource(acp::EmbeddedResource {
 767                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
 768                    ..
 769                }) => {
 770                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
 771                        let start = text.len();
 772                        write!(&mut text, "{}", mention_uri.as_link()).ok();
 773                        let end = text.len();
 774                        mentions.push((start..end, mention_uri, resource.text));
 775                    }
 776                }
 777                acp::ContentBlock::Image(content) => {
 778                    let start = text.len();
 779                    text.push_str("image");
 780                    let end = text.len();
 781                    images.push((start..end, content));
 782                }
 783                acp::ContentBlock::Audio(_)
 784                | acp::ContentBlock::Resource(_)
 785                | acp::ContentBlock::ResourceLink(_) => {}
 786            }
 787        }
 788
 789        let snapshot = self.editor.update(cx, |editor, cx| {
 790            editor.set_text(text, window, cx);
 791            editor.buffer().read(cx).snapshot(cx)
 792        });
 793
 794        for (range, mention_uri, text) in mentions {
 795            let anchor = snapshot.anchor_before(range.start);
 796            let crease_id = crate::context_picker::insert_crease_for_mention(
 797                anchor.excerpt_id,
 798                anchor.text_anchor,
 799                range.end - range.start,
 800                mention_uri.name().into(),
 801                mention_uri.icon_path(cx),
 802                self.editor.clone(),
 803                window,
 804                cx,
 805            );
 806
 807            if let Some(crease_id) = crease_id {
 808                self.mention_set.insert_uri(crease_id, mention_uri.clone());
 809            }
 810
 811            match mention_uri {
 812                MentionUri::Thread { id, .. } => {
 813                    self.mention_set
 814                        .insert_thread(id, Task::ready(Ok(text.into())).shared());
 815                }
 816                MentionUri::TextThread { path, .. } => {
 817                    self.mention_set
 818                        .insert_text_thread(path, Task::ready(Ok(text)).shared());
 819                }
 820                MentionUri::Fetch { url } => {
 821                    self.mention_set
 822                        .add_fetch_result(url, Task::ready(Ok(text)).shared());
 823                }
 824                MentionUri::File { .. }
 825                | MentionUri::Symbol { .. }
 826                | MentionUri::Rule { .. }
 827                | MentionUri::Selection { .. } => {}
 828            }
 829        }
 830        for (range, content) in images {
 831            let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else {
 832                continue;
 833            };
 834            let anchor = snapshot.anchor_before(range.start);
 835            let abs_path = content
 836                .uri
 837                .as_ref()
 838                .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into()));
 839
 840            let name = content
 841                .uri
 842                .as_ref()
 843                .and_then(|uri| {
 844                    uri.strip_prefix("file://")
 845                        .and_then(|path| Path::new(path).file_name())
 846                })
 847                .map(|name| name.to_string_lossy().to_string())
 848                .unwrap_or("Image".to_owned());
 849            let crease_id = crate::context_picker::insert_crease_for_mention(
 850                anchor.excerpt_id,
 851                anchor.text_anchor,
 852                range.end - range.start,
 853                name.into(),
 854                IconName::Image.path().into(),
 855                self.editor.clone(),
 856                window,
 857                cx,
 858            );
 859            let data: SharedString = content.data.to_string().into();
 860
 861            if let Some(crease_id) = crease_id {
 862                self.mention_set.insert_image(
 863                    crease_id,
 864                    Task::ready(Ok(MentionImage {
 865                        abs_path,
 866                        data,
 867                        format,
 868                    }))
 869                    .shared(),
 870                );
 871            }
 872        }
 873        cx.notify();
 874    }
 875
 876    #[cfg(test)]
 877    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
 878        self.editor.update(cx, |editor, cx| {
 879            editor.set_text(text, window, cx);
 880        });
 881    }
 882
 883    #[cfg(test)]
 884    pub fn text(&self, cx: &App) -> String {
 885        self.editor.read(cx).text(cx)
 886    }
 887}
 888
 889impl Focusable for MessageEditor {
 890    fn focus_handle(&self, cx: &App) -> FocusHandle {
 891        self.editor.focus_handle(cx)
 892    }
 893}
 894
 895impl Render for MessageEditor {
 896    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 897        div()
 898            .key_context("MessageEditor")
 899            .on_action(cx.listener(Self::send))
 900            .on_action(cx.listener(Self::cancel))
 901            .capture_action(cx.listener(Self::paste))
 902            .flex_1()
 903            .child({
 904                let settings = ThemeSettings::get_global(cx);
 905                let font_size = TextSize::Small
 906                    .rems(cx)
 907                    .to_pixels(settings.agent_font_size(cx));
 908                let line_height = settings.buffer_line_height.value() * font_size;
 909
 910                let text_style = TextStyle {
 911                    color: cx.theme().colors().text,
 912                    font_family: settings.buffer_font.family.clone(),
 913                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
 914                    font_features: settings.buffer_font.features.clone(),
 915                    font_size: font_size.into(),
 916                    line_height: line_height.into(),
 917                    ..Default::default()
 918                };
 919
 920                EditorElement::new(
 921                    &self.editor,
 922                    EditorStyle {
 923                        background: cx.theme().colors().editor_background,
 924                        local_player: cx.theme().players().local(),
 925                        text: text_style,
 926                        syntax: cx.theme().syntax().clone(),
 927                        ..Default::default()
 928                    },
 929                )
 930            })
 931    }
 932}
 933
 934pub(crate) fn insert_crease_for_image(
 935    excerpt_id: ExcerptId,
 936    anchor: text::Anchor,
 937    content_len: usize,
 938    abs_path: Option<Arc<Path>>,
 939    editor: Entity<Editor>,
 940    window: &mut Window,
 941    cx: &mut App,
 942) -> Option<CreaseId> {
 943    let crease_label = abs_path
 944        .as_ref()
 945        .and_then(|path| path.file_name())
 946        .map(|name| name.to_string_lossy().to_string().into())
 947        .unwrap_or(SharedString::from("Image"));
 948
 949    editor.update(cx, |editor, cx| {
 950        let snapshot = editor.buffer().read(cx).snapshot(cx);
 951
 952        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
 953
 954        let start = start.bias_right(&snapshot);
 955        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
 956
 957        let placeholder = FoldPlaceholder {
 958            render: render_image_fold_icon_button(crease_label, cx.weak_entity()),
 959            merge_adjacent: false,
 960            ..Default::default()
 961        };
 962
 963        let crease = Crease::Inline {
 964            range: start..end,
 965            placeholder,
 966            render_toggle: None,
 967            render_trailer: None,
 968            metadata: None,
 969        };
 970
 971        let ids = editor.insert_creases(vec![crease.clone()], cx);
 972        editor.fold_creases(vec![crease], false, window, cx);
 973
 974        Some(ids[0])
 975    })
 976}
 977
 978fn render_image_fold_icon_button(
 979    label: SharedString,
 980    editor: WeakEntity<Editor>,
 981) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
 982    Arc::new({
 983        move |fold_id, fold_range, cx| {
 984            let is_in_text_selection = editor
 985                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
 986                .unwrap_or_default();
 987
 988            ButtonLike::new(fold_id)
 989                .style(ButtonStyle::Filled)
 990                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 991                .toggle_state(is_in_text_selection)
 992                .child(
 993                    h_flex()
 994                        .gap_1()
 995                        .child(
 996                            Icon::new(IconName::Image)
 997                                .size(IconSize::XSmall)
 998                                .color(Color::Muted),
 999                        )
1000                        .child(
1001                            Label::new(label.clone())
1002                                .size(LabelSize::Small)
1003                                .buffer_font(cx)
1004                                .single_line(),
1005                        ),
1006                )
1007                .into_any_element()
1008        }
1009    })
1010}
1011
1012#[derive(Debug, Eq, PartialEq)]
1013pub enum Mention {
1014    Text { uri: MentionUri, content: String },
1015    Image(MentionImage),
1016}
1017
1018#[derive(Clone, Debug, Eq, PartialEq)]
1019pub struct MentionImage {
1020    pub abs_path: Option<PathBuf>,
1021    pub data: SharedString,
1022    pub format: ImageFormat,
1023}
1024
1025#[derive(Default)]
1026pub struct MentionSet {
1027    uri_by_crease_id: HashMap<CreaseId, MentionUri>,
1028    fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
1029    images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
1030    thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
1031    text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
1032}
1033
1034impl MentionSet {
1035    pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
1036        self.uri_by_crease_id.insert(crease_id, uri);
1037    }
1038
1039    pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
1040        self.fetch_results.insert(url, content);
1041    }
1042
1043    pub fn insert_image(
1044        &mut self,
1045        crease_id: CreaseId,
1046        task: Shared<Task<Result<MentionImage, String>>>,
1047    ) {
1048        self.images.insert(crease_id, task);
1049    }
1050
1051    fn insert_thread(&mut self, id: ThreadId, task: Shared<Task<Result<SharedString, String>>>) {
1052        self.thread_summaries.insert(id, task);
1053    }
1054
1055    fn insert_text_thread(&mut self, path: PathBuf, task: Shared<Task<Result<String, String>>>) {
1056        self.text_thread_summaries.insert(path, task);
1057    }
1058
1059    pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
1060        self.fetch_results.clear();
1061        self.thread_summaries.clear();
1062        self.text_thread_summaries.clear();
1063        self.uri_by_crease_id
1064            .drain()
1065            .map(|(id, _)| id)
1066            .chain(self.images.drain().map(|(id, _)| id))
1067    }
1068
1069    pub fn contents(
1070        &self,
1071        project: Entity<Project>,
1072        thread_store: Entity<ThreadStore>,
1073        _window: &mut Window,
1074        cx: &mut App,
1075    ) -> Task<Result<HashMap<CreaseId, Mention>>> {
1076        let mut processed_image_creases = HashSet::default();
1077
1078        let mut contents = self
1079            .uri_by_crease_id
1080            .iter()
1081            .map(|(&crease_id, uri)| {
1082                match uri {
1083                    MentionUri::File { abs_path, .. } => {
1084                        // TODO directories
1085                        let uri = uri.clone();
1086                        let abs_path = abs_path.to_path_buf();
1087
1088                        if let Some(task) = self.images.get(&crease_id).cloned() {
1089                            processed_image_creases.insert(crease_id);
1090                            return cx.spawn(async move |_| {
1091                                let image = task.await.map_err(|e| anyhow!("{e}"))?;
1092                                anyhow::Ok((crease_id, Mention::Image(image)))
1093                            });
1094                        }
1095
1096                        let buffer_task = project.update(cx, |project, cx| {
1097                            let path = project
1098                                .find_project_path(abs_path, cx)
1099                                .context("Failed to find project path")?;
1100                            anyhow::Ok(project.open_buffer(path, cx))
1101                        });
1102                        cx.spawn(async move |cx| {
1103                            let buffer = buffer_task?.await?;
1104                            let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
1105
1106                            anyhow::Ok((crease_id, Mention::Text { uri, content }))
1107                        })
1108                    }
1109                    MentionUri::Symbol {
1110                        path, line_range, ..
1111                    }
1112                    | MentionUri::Selection {
1113                        path, line_range, ..
1114                    } => {
1115                        let uri = uri.clone();
1116                        let path_buf = path.clone();
1117                        let line_range = line_range.clone();
1118
1119                        let buffer_task = project.update(cx, |project, cx| {
1120                            let path = project
1121                                .find_project_path(&path_buf, cx)
1122                                .context("Failed to find project path")?;
1123                            anyhow::Ok(project.open_buffer(path, cx))
1124                        });
1125
1126                        cx.spawn(async move |cx| {
1127                            let buffer = buffer_task?.await?;
1128                            let content = buffer.read_with(cx, |buffer, _cx| {
1129                                buffer
1130                                    .text_for_range(
1131                                        Point::new(line_range.start, 0)
1132                                            ..Point::new(
1133                                                line_range.end,
1134                                                buffer.line_len(line_range.end),
1135                                            ),
1136                                    )
1137                                    .collect()
1138                            })?;
1139
1140                            anyhow::Ok((crease_id, Mention::Text { uri, content }))
1141                        })
1142                    }
1143                    MentionUri::Thread { id, .. } => {
1144                        let Some(content) = self.thread_summaries.get(id).cloned() else {
1145                            return Task::ready(Err(anyhow!("missing thread summary")));
1146                        };
1147                        let uri = uri.clone();
1148                        cx.spawn(async move |_| {
1149                            Ok((
1150                                crease_id,
1151                                Mention::Text {
1152                                    uri,
1153                                    content: content
1154                                        .await
1155                                        .map_err(|e| anyhow::anyhow!("{e}"))?
1156                                        .to_string(),
1157                                },
1158                            ))
1159                        })
1160                    }
1161                    MentionUri::TextThread { path, .. } => {
1162                        let Some(content) = self.text_thread_summaries.get(path).cloned() else {
1163                            return Task::ready(Err(anyhow!("missing text thread summary")));
1164                        };
1165                        let uri = uri.clone();
1166                        cx.spawn(async move |_| {
1167                            Ok((
1168                                crease_id,
1169                                Mention::Text {
1170                                    uri,
1171                                    content: content
1172                                        .await
1173                                        .map_err(|e| anyhow::anyhow!("{e}"))?
1174                                        .to_string(),
1175                                },
1176                            ))
1177                        })
1178                    }
1179                    MentionUri::Rule { id: prompt_id, .. } => {
1180                        let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
1181                        else {
1182                            return Task::ready(Err(anyhow!("missing prompt store")));
1183                        };
1184                        let text_task = prompt_store.read(cx).load(*prompt_id, cx);
1185                        let uri = uri.clone();
1186                        cx.spawn(async move |_| {
1187                            // TODO: report load errors instead of just logging
1188                            let text = text_task.await?;
1189                            anyhow::Ok((crease_id, Mention::Text { uri, content: text }))
1190                        })
1191                    }
1192                    MentionUri::Fetch { url } => {
1193                        let Some(content) = self.fetch_results.get(&url).cloned() else {
1194                            return Task::ready(Err(anyhow!("missing fetch result")));
1195                        };
1196                        let uri = uri.clone();
1197                        cx.spawn(async move |_| {
1198                            Ok((
1199                                crease_id,
1200                                Mention::Text {
1201                                    uri,
1202                                    content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
1203                                },
1204                            ))
1205                        })
1206                    }
1207                }
1208            })
1209            .collect::<Vec<_>>();
1210
1211        // Handle images that didn't have a mention URI (because they were added by the paste handler).
1212        contents.extend(self.images.iter().filter_map(|(crease_id, image)| {
1213            if processed_image_creases.contains(crease_id) {
1214                return None;
1215            }
1216            let crease_id = *crease_id;
1217            let image = image.clone();
1218            Some(cx.spawn(async move |_| {
1219                Ok((
1220                    crease_id,
1221                    Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
1222                ))
1223            }))
1224        }));
1225
1226        cx.spawn(async move |_cx| {
1227            let contents = try_join_all(contents).await?.into_iter().collect();
1228            anyhow::Ok(contents)
1229        })
1230    }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235    use std::{ops::Range, path::Path, sync::Arc};
1236
1237    use agent::{TextThreadStore, ThreadStore};
1238    use agent_client_protocol as acp;
1239    use editor::{AnchorRangeExt as _, Editor, EditorMode};
1240    use fs::FakeFs;
1241    use futures::StreamExt as _;
1242    use gpui::{
1243        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1244    };
1245    use lsp::{CompletionContext, CompletionTriggerKind};
1246    use project::{CompletionIntent, Project, ProjectPath};
1247    use serde_json::json;
1248    use text::Point;
1249    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1250    use util::path;
1251    use workspace::{AppState, Item, Workspace};
1252
1253    use crate::acp::{
1254        message_editor::{Mention, MessageEditor},
1255        thread_view::tests::init_test,
1256    };
1257
1258    #[gpui::test]
1259    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1260        init_test(cx);
1261
1262        let fs = FakeFs::new(cx.executor());
1263        fs.insert_tree("/project", json!({"file": ""})).await;
1264        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1265
1266        let (workspace, cx) =
1267            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1268
1269        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1270        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1271
1272        let message_editor = cx.update(|window, cx| {
1273            cx.new(|cx| {
1274                MessageEditor::new(
1275                    workspace.downgrade(),
1276                    project.clone(),
1277                    thread_store.clone(),
1278                    text_thread_store.clone(),
1279                    EditorMode::AutoHeight {
1280                        min_lines: 1,
1281                        max_lines: None,
1282                    },
1283                    window,
1284                    cx,
1285                )
1286            })
1287        });
1288        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1289
1290        cx.run_until_parked();
1291
1292        let excerpt_id = editor.update(cx, |editor, cx| {
1293            editor
1294                .buffer()
1295                .read(cx)
1296                .excerpt_ids()
1297                .into_iter()
1298                .next()
1299                .unwrap()
1300        });
1301        let completions = editor.update_in(cx, |editor, window, cx| {
1302            editor.set_text("Hello @file ", window, cx);
1303            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1304            let completion_provider = editor.completion_provider().unwrap();
1305            completion_provider.completions(
1306                excerpt_id,
1307                &buffer,
1308                text::Anchor::MAX,
1309                CompletionContext {
1310                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1311                    trigger_character: Some("@".into()),
1312                },
1313                window,
1314                cx,
1315            )
1316        });
1317        let [_, completion]: [_; 2] = completions
1318            .await
1319            .unwrap()
1320            .into_iter()
1321            .flat_map(|response| response.completions)
1322            .collect::<Vec<_>>()
1323            .try_into()
1324            .unwrap();
1325
1326        editor.update_in(cx, |editor, window, cx| {
1327            let snapshot = editor.buffer().read(cx).snapshot(cx);
1328            let start = snapshot
1329                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1330                .unwrap();
1331            let end = snapshot
1332                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1333                .unwrap();
1334            editor.edit([(start..end, completion.new_text)], cx);
1335            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1336        });
1337
1338        cx.run_until_parked();
1339
1340        // Backspace over the inserted crease (and the following space).
1341        editor.update_in(cx, |editor, window, cx| {
1342            editor.backspace(&Default::default(), window, cx);
1343            editor.backspace(&Default::default(), window, cx);
1344        });
1345
1346        let content = message_editor
1347            .update_in(cx, |message_editor, window, cx| {
1348                message_editor.contents(window, cx)
1349            })
1350            .await
1351            .unwrap();
1352
1353        // We don't send a resource link for the deleted crease.
1354        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1355    }
1356
1357    struct MessageEditorItem(Entity<MessageEditor>);
1358
1359    impl Item for MessageEditorItem {
1360        type Event = ();
1361
1362        fn include_in_nav_history() -> bool {
1363            false
1364        }
1365
1366        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1367            "Test".into()
1368        }
1369    }
1370
1371    impl EventEmitter<()> for MessageEditorItem {}
1372
1373    impl Focusable for MessageEditorItem {
1374        fn focus_handle(&self, cx: &App) -> FocusHandle {
1375            self.0.read(cx).focus_handle(cx).clone()
1376        }
1377    }
1378
1379    impl Render for MessageEditorItem {
1380        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1381            self.0.clone().into_any_element()
1382        }
1383    }
1384
1385    #[gpui::test]
1386    async fn test_context_completion_provider(cx: &mut TestAppContext) {
1387        init_test(cx);
1388
1389        let app_state = cx.update(AppState::test);
1390
1391        cx.update(|cx| {
1392            language::init(cx);
1393            editor::init(cx);
1394            workspace::init(app_state.clone(), cx);
1395            Project::init_settings(cx);
1396        });
1397
1398        app_state
1399            .fs
1400            .as_fake()
1401            .insert_tree(
1402                path!("/dir"),
1403                json!({
1404                    "editor": "",
1405                    "a": {
1406                        "one.txt": "1",
1407                        "two.txt": "2",
1408                        "three.txt": "3",
1409                        "four.txt": "4"
1410                    },
1411                    "b": {
1412                        "five.txt": "5",
1413                        "six.txt": "6",
1414                        "seven.txt": "7",
1415                        "eight.txt": "8",
1416                    }
1417                }),
1418            )
1419            .await;
1420
1421        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1422        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1423        let workspace = window.root(cx).unwrap();
1424
1425        let worktree = project.update(cx, |project, cx| {
1426            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1427            assert_eq!(worktrees.len(), 1);
1428            worktrees.pop().unwrap()
1429        });
1430        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1431
1432        let mut cx = VisualTestContext::from_window(*window, cx);
1433
1434        let paths = vec![
1435            path!("a/one.txt"),
1436            path!("a/two.txt"),
1437            path!("a/three.txt"),
1438            path!("a/four.txt"),
1439            path!("b/five.txt"),
1440            path!("b/six.txt"),
1441            path!("b/seven.txt"),
1442            path!("b/eight.txt"),
1443        ];
1444
1445        let mut opened_editors = Vec::new();
1446        for path in paths {
1447            let buffer = workspace
1448                .update_in(&mut cx, |workspace, window, cx| {
1449                    workspace.open_path(
1450                        ProjectPath {
1451                            worktree_id,
1452                            path: Path::new(path).into(),
1453                        },
1454                        None,
1455                        false,
1456                        window,
1457                        cx,
1458                    )
1459                })
1460                .await
1461                .unwrap();
1462            opened_editors.push(buffer);
1463        }
1464
1465        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1466        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1467
1468        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1469            let workspace_handle = cx.weak_entity();
1470            let message_editor = cx.new(|cx| {
1471                MessageEditor::new(
1472                    workspace_handle,
1473                    project.clone(),
1474                    thread_store.clone(),
1475                    text_thread_store.clone(),
1476                    EditorMode::AutoHeight {
1477                        max_lines: None,
1478                        min_lines: 1,
1479                    },
1480                    window,
1481                    cx,
1482                )
1483            });
1484            workspace.active_pane().update(cx, |pane, cx| {
1485                pane.add_item(
1486                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1487                    true,
1488                    true,
1489                    None,
1490                    window,
1491                    cx,
1492                );
1493            });
1494            message_editor.read(cx).focus_handle(cx).focus(window);
1495            let editor = message_editor.read(cx).editor().clone();
1496            (message_editor, editor)
1497        });
1498
1499        cx.simulate_input("Lorem ");
1500
1501        editor.update(&mut cx, |editor, cx| {
1502            assert_eq!(editor.text(cx), "Lorem ");
1503            assert!(!editor.has_visible_completions_menu());
1504        });
1505
1506        cx.simulate_input("@");
1507
1508        editor.update(&mut cx, |editor, cx| {
1509            assert_eq!(editor.text(cx), "Lorem @");
1510            assert!(editor.has_visible_completions_menu());
1511            assert_eq!(
1512                current_completion_labels(editor),
1513                &[
1514                    "eight.txt dir/b/",
1515                    "seven.txt dir/b/",
1516                    "six.txt dir/b/",
1517                    "five.txt dir/b/",
1518                    "Files & Directories",
1519                    "Symbols",
1520                    "Threads",
1521                    "Fetch"
1522                ]
1523            );
1524        });
1525
1526        // Select and confirm "File"
1527        editor.update_in(&mut cx, |editor, window, cx| {
1528            assert!(editor.has_visible_completions_menu());
1529            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1530            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1531            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1532            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1533            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1534        });
1535
1536        cx.run_until_parked();
1537
1538        editor.update(&mut cx, |editor, cx| {
1539            assert_eq!(editor.text(cx), "Lorem @file ");
1540            assert!(editor.has_visible_completions_menu());
1541        });
1542
1543        cx.simulate_input("one");
1544
1545        editor.update(&mut cx, |editor, cx| {
1546            assert_eq!(editor.text(cx), "Lorem @file one");
1547            assert!(editor.has_visible_completions_menu());
1548            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
1549        });
1550
1551        editor.update_in(&mut cx, |editor, window, cx| {
1552            assert!(editor.has_visible_completions_menu());
1553            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1554        });
1555
1556        editor.update(&mut cx, |editor, cx| {
1557            assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) ");
1558            assert!(!editor.has_visible_completions_menu());
1559            assert_eq!(
1560                fold_ranges(editor, cx),
1561                vec![Point::new(0, 6)..Point::new(0, 39)]
1562            );
1563        });
1564
1565        let contents = message_editor
1566            .update_in(&mut cx, |message_editor, window, cx| {
1567                message_editor.mention_set().contents(
1568                    project.clone(),
1569                    thread_store.clone(),
1570                    window,
1571                    cx,
1572                )
1573            })
1574            .await
1575            .unwrap()
1576            .into_values()
1577            .collect::<Vec<_>>();
1578
1579        pretty_assertions::assert_eq!(
1580            contents,
1581            [Mention::Text {
1582                content: "1".into(),
1583                uri: "file:///dir/a/one.txt".parse().unwrap()
1584            }]
1585        );
1586
1587        cx.simulate_input(" ");
1588
1589        editor.update(&mut cx, |editor, cx| {
1590            assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt)  ");
1591            assert!(!editor.has_visible_completions_menu());
1592            assert_eq!(
1593                fold_ranges(editor, cx),
1594                vec![Point::new(0, 6)..Point::new(0, 39)]
1595            );
1596        });
1597
1598        cx.simulate_input("Ipsum ");
1599
1600        editor.update(&mut cx, |editor, cx| {
1601            assert_eq!(
1602                editor.text(cx),
1603                "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum ",
1604            );
1605            assert!(!editor.has_visible_completions_menu());
1606            assert_eq!(
1607                fold_ranges(editor, cx),
1608                vec![Point::new(0, 6)..Point::new(0, 39)]
1609            );
1610        });
1611
1612        cx.simulate_input("@file ");
1613
1614        editor.update(&mut cx, |editor, cx| {
1615            assert_eq!(
1616                editor.text(cx),
1617                "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum @file ",
1618            );
1619            assert!(editor.has_visible_completions_menu());
1620            assert_eq!(
1621                fold_ranges(editor, cx),
1622                vec![Point::new(0, 6)..Point::new(0, 39)]
1623            );
1624        });
1625
1626        editor.update_in(&mut cx, |editor, window, cx| {
1627            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1628        });
1629
1630        cx.run_until_parked();
1631
1632        let contents = message_editor
1633            .update_in(&mut cx, |message_editor, window, cx| {
1634                message_editor.mention_set().contents(
1635                    project.clone(),
1636                    thread_store.clone(),
1637                    window,
1638                    cx,
1639                )
1640            })
1641            .await
1642            .unwrap()
1643            .into_values()
1644            .collect::<Vec<_>>();
1645
1646        assert_eq!(contents.len(), 2);
1647        pretty_assertions::assert_eq!(
1648            contents[1],
1649            Mention::Text {
1650                content: "8".to_string(),
1651                uri: "file:///dir/b/eight.txt".parse().unwrap(),
1652            }
1653        );
1654
1655        editor.update(&mut cx, |editor, cx| {
1656                assert_eq!(
1657                    editor.text(cx),
1658                    "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum [@eight.txt](file:///dir/b/eight.txt) "
1659                );
1660                assert!(!editor.has_visible_completions_menu());
1661                assert_eq!(
1662                    fold_ranges(editor, cx),
1663                    vec![
1664                        Point::new(0, 6)..Point::new(0, 39),
1665                        Point::new(0, 47)..Point::new(0, 84)
1666                    ]
1667                );
1668            });
1669
1670        let plain_text_language = Arc::new(language::Language::new(
1671            language::LanguageConfig {
1672                name: "Plain Text".into(),
1673                matcher: language::LanguageMatcher {
1674                    path_suffixes: vec!["txt".to_string()],
1675                    ..Default::default()
1676                },
1677                ..Default::default()
1678            },
1679            None,
1680        ));
1681
1682        // Register the language and fake LSP
1683        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1684        language_registry.add(plain_text_language);
1685
1686        let mut fake_language_servers = language_registry.register_fake_lsp(
1687            "Plain Text",
1688            language::FakeLspAdapter {
1689                capabilities: lsp::ServerCapabilities {
1690                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1691                    ..Default::default()
1692                },
1693                ..Default::default()
1694            },
1695        );
1696
1697        // Open the buffer to trigger LSP initialization
1698        let buffer = project
1699            .update(&mut cx, |project, cx| {
1700                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1701            })
1702            .await
1703            .unwrap();
1704
1705        // Register the buffer with language servers
1706        let _handle = project.update(&mut cx, |project, cx| {
1707            project.register_buffer_with_language_servers(&buffer, cx)
1708        });
1709
1710        cx.run_until_parked();
1711
1712        let fake_language_server = fake_language_servers.next().await.unwrap();
1713        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1714            |_, _| async move {
1715                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1716                    #[allow(deprecated)]
1717                    lsp::SymbolInformation {
1718                        name: "MySymbol".into(),
1719                        location: lsp::Location {
1720                            uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1721                            range: lsp::Range::new(
1722                                lsp::Position::new(0, 0),
1723                                lsp::Position::new(0, 1),
1724                            ),
1725                        },
1726                        kind: lsp::SymbolKind::CONSTANT,
1727                        tags: None,
1728                        container_name: None,
1729                        deprecated: None,
1730                    },
1731                ])))
1732            },
1733        );
1734
1735        cx.simulate_input("@symbol ");
1736
1737        editor.update(&mut cx, |editor, cx| {
1738                assert_eq!(
1739                    editor.text(cx),
1740                    "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol "
1741                );
1742                assert!(editor.has_visible_completions_menu());
1743                assert_eq!(
1744                    current_completion_labels(editor),
1745                    &[
1746                        "MySymbol",
1747                    ]
1748                );
1749            });
1750
1751        editor.update_in(&mut cx, |editor, window, cx| {
1752            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1753        });
1754
1755        let contents = message_editor
1756            .update_in(&mut cx, |message_editor, window, cx| {
1757                message_editor
1758                    .mention_set()
1759                    .contents(project.clone(), thread_store, window, cx)
1760            })
1761            .await
1762            .unwrap()
1763            .into_values()
1764            .collect::<Vec<_>>();
1765
1766        assert_eq!(contents.len(), 3);
1767        pretty_assertions::assert_eq!(
1768            contents[2],
1769            Mention::Text {
1770                content: "1".into(),
1771                uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1"
1772                    .parse()
1773                    .unwrap(),
1774            }
1775        );
1776
1777        cx.run_until_parked();
1778
1779        editor.read_with(&mut cx, |editor, cx| {
1780                assert_eq!(
1781                    editor.text(cx),
1782                    "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) "
1783                );
1784            });
1785    }
1786
1787    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1788        let snapshot = editor.buffer().read(cx).snapshot(cx);
1789        editor.display_map.update(cx, |display_map, cx| {
1790            display_map
1791                .snapshot(cx)
1792                .folds_in_range(0..snapshot.len())
1793                .map(|fold| fold.range.to_point(&snapshot))
1794                .collect()
1795        })
1796    }
1797
1798    fn current_completion_labels(editor: &Editor) -> Vec<String> {
1799        let completions = editor.current_completions().expect("Missing completions");
1800        completions
1801            .into_iter()
1802            .map(|completion| completion.label.text.to_string())
1803            .collect::<Vec<_>>()
1804    }
1805}