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