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