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