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