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    Addon, 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, KeyContext, Subscription, Task, TextStyle,
  25    UnderlineStyle, 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, Debug)]
  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.register_addon(MessageEditorAddon::new());
 126            editor
 127        });
 128
 129        cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
 130            cx.emit(MessageEditorEvent::Focus)
 131        })
 132        .detach();
 133
 134        let mut subscriptions = Vec::new();
 135        if prevent_slash_commands {
 136            subscriptions.push(cx.subscribe_in(&editor, window, {
 137                let semantics_provider = semantics_provider.clone();
 138                move |this, editor, event, window, cx| {
 139                    if let EditorEvent::Edited { .. } = event {
 140                        this.highlight_slash_command(
 141                            semantics_provider.clone(),
 142                            editor.clone(),
 143                            window,
 144                            cx,
 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        if self.is_empty(cx) {
 733            return;
 734        }
 735        cx.emit(MessageEditorEvent::Send)
 736    }
 737
 738    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 739        cx.emit(MessageEditorEvent::Cancel)
 740    }
 741
 742    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 743        let images = cx
 744            .read_from_clipboard()
 745            .map(|item| {
 746                item.into_entries()
 747                    .filter_map(|entry| {
 748                        if let ClipboardEntry::Image(image) = entry {
 749                            Some(image)
 750                        } else {
 751                            None
 752                        }
 753                    })
 754                    .collect::<Vec<_>>()
 755            })
 756            .unwrap_or_default();
 757
 758        if images.is_empty() {
 759            return;
 760        }
 761        cx.stop_propagation();
 762
 763        let replacement_text = "image";
 764        for image in images {
 765            let (excerpt_id, text_anchor, multibuffer_anchor) =
 766                self.editor.update(cx, |message_editor, cx| {
 767                    let snapshot = message_editor.snapshot(window, cx);
 768                    let (excerpt_id, _, buffer_snapshot) =
 769                        snapshot.buffer_snapshot.as_singleton().unwrap();
 770
 771                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 772                    let multibuffer_anchor = snapshot
 773                        .buffer_snapshot
 774                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 775                    message_editor.edit(
 776                        [(
 777                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 778                            format!("{replacement_text} "),
 779                        )],
 780                        cx,
 781                    );
 782                    (*excerpt_id, text_anchor, multibuffer_anchor)
 783                });
 784
 785            let content_len = replacement_text.len();
 786            let Some(anchor) = multibuffer_anchor else {
 787                return;
 788            };
 789            let task = Task::ready(Ok(Arc::new(image))).shared();
 790            let Some(crease_id) = insert_crease_for_image(
 791                excerpt_id,
 792                text_anchor,
 793                content_len,
 794                None.clone(),
 795                task.clone(),
 796                self.editor.clone(),
 797                window,
 798                cx,
 799            ) else {
 800                return;
 801            };
 802            self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx)
 803                .detach();
 804        }
 805    }
 806
 807    pub fn insert_dragged_files(
 808        &mut self,
 809        paths: Vec<project::ProjectPath>,
 810        added_worktrees: Vec<Entity<Worktree>>,
 811        window: &mut Window,
 812        cx: &mut Context<Self>,
 813    ) {
 814        let buffer = self.editor.read(cx).buffer().clone();
 815        let Some(buffer) = buffer.read(cx).as_singleton() else {
 816            return;
 817        };
 818        let mut tasks = Vec::new();
 819        for path in paths {
 820            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
 821                continue;
 822            };
 823            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
 824                continue;
 825            };
 826            let path_prefix = abs_path
 827                .file_name()
 828                .unwrap_or(path.path.as_os_str())
 829                .display()
 830                .to_string();
 831            let (file_name, _) =
 832                crate::context_picker::file_context_picker::extract_file_name_and_directory(
 833                    &path.path,
 834                    &path_prefix,
 835                );
 836
 837            let uri = if entry.is_dir() {
 838                MentionUri::Directory { abs_path }
 839            } else {
 840                MentionUri::File { abs_path }
 841            };
 842
 843            let new_text = format!("{} ", uri.as_link());
 844            let content_len = new_text.len() - 1;
 845
 846            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
 847
 848            self.editor.update(cx, |message_editor, cx| {
 849                message_editor.edit(
 850                    [(
 851                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 852                        new_text,
 853                    )],
 854                    cx,
 855                );
 856            });
 857            tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx));
 858        }
 859        cx.spawn(async move |_, _| {
 860            join_all(tasks).await;
 861            drop(added_worktrees);
 862        })
 863        .detach();
 864    }
 865
 866    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
 867        self.editor.update(cx, |message_editor, cx| {
 868            message_editor.set_read_only(read_only);
 869            cx.notify()
 870        })
 871    }
 872
 873    fn confirm_mention_for_image(
 874        &mut self,
 875        crease_id: CreaseId,
 876        anchor: Anchor,
 877        abs_path: Option<PathBuf>,
 878        image: Shared<Task<Result<Arc<Image>, String>>>,
 879        window: &mut Window,
 880        cx: &mut Context<Self>,
 881    ) -> Task<()> {
 882        let editor = self.editor.clone();
 883        let task = cx
 884            .spawn_in(window, {
 885                let abs_path = abs_path.clone();
 886                async move |_, cx| {
 887                    let image = image.await.map_err(|e| e.to_string())?;
 888                    let format = image.format;
 889                    let image = cx
 890                        .update(|_, cx| LanguageModelImage::from_image(image, cx))
 891                        .map_err(|e| e.to_string())?
 892                        .await;
 893                    if let Some(image) = image {
 894                        Ok(MentionImage {
 895                            abs_path,
 896                            data: image.source,
 897                            format,
 898                        })
 899                    } else {
 900                        Err("Failed to convert image".into())
 901                    }
 902                }
 903            })
 904            .shared();
 905
 906        self.mention_set.insert_image(crease_id, task.clone());
 907
 908        cx.spawn_in(window, async move |this, cx| {
 909            if task.await.notify_async_err(cx).is_some() {
 910                if let Some(abs_path) = abs_path.clone() {
 911                    this.update(cx, |this, _cx| {
 912                        this.mention_set
 913                            .insert_uri(crease_id, MentionUri::File { abs_path });
 914                    })
 915                    .ok();
 916                }
 917            } else {
 918                editor
 919                    .update(cx, |editor, cx| {
 920                        editor.display_map.update(cx, |display_map, cx| {
 921                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
 922                        });
 923                        editor.remove_creases([crease_id], cx);
 924                    })
 925                    .ok();
 926                this.update(cx, |this, _cx| {
 927                    this.mention_set.images.remove(&crease_id);
 928                })
 929                .ok();
 930            }
 931        })
 932    }
 933
 934    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
 935        self.editor.update(cx, |editor, cx| {
 936            editor.set_mode(mode);
 937            cx.notify()
 938        });
 939    }
 940
 941    pub fn set_message(
 942        &mut self,
 943        message: Vec<acp::ContentBlock>,
 944        window: &mut Window,
 945        cx: &mut Context<Self>,
 946    ) {
 947        self.clear(window, cx);
 948
 949        let mut text = String::new();
 950        let mut mentions = Vec::new();
 951        let mut images = Vec::new();
 952
 953        for chunk in message {
 954            match chunk {
 955                acp::ContentBlock::Text(text_content) => {
 956                    text.push_str(&text_content.text);
 957                }
 958                acp::ContentBlock::Resource(acp::EmbeddedResource {
 959                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
 960                    ..
 961                }) => {
 962                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
 963                        let start = text.len();
 964                        write!(&mut text, "{}", mention_uri.as_link()).ok();
 965                        let end = text.len();
 966                        mentions.push((start..end, mention_uri, resource.text));
 967                    }
 968                }
 969                acp::ContentBlock::Image(content) => {
 970                    let start = text.len();
 971                    text.push_str("image");
 972                    let end = text.len();
 973                    images.push((start..end, content));
 974                }
 975                acp::ContentBlock::Audio(_)
 976                | acp::ContentBlock::Resource(_)
 977                | acp::ContentBlock::ResourceLink(_) => {}
 978            }
 979        }
 980
 981        let snapshot = self.editor.update(cx, |editor, cx| {
 982            editor.set_text(text, window, cx);
 983            editor.buffer().read(cx).snapshot(cx)
 984        });
 985
 986        for (range, mention_uri, text) in mentions {
 987            let anchor = snapshot.anchor_before(range.start);
 988            let crease_id = crate::context_picker::insert_crease_for_mention(
 989                anchor.excerpt_id,
 990                anchor.text_anchor,
 991                range.end - range.start,
 992                mention_uri.name().into(),
 993                mention_uri.icon_path(cx),
 994                self.editor.clone(),
 995                window,
 996                cx,
 997            );
 998
 999            if let Some(crease_id) = crease_id {
1000                self.mention_set.insert_uri(crease_id, mention_uri.clone());
1001            }
1002
1003            match mention_uri {
1004                MentionUri::Thread { id, .. } => {
1005                    self.mention_set
1006                        .insert_thread(id, Task::ready(Ok(text.into())).shared());
1007                }
1008                MentionUri::TextThread { path, .. } => {
1009                    self.mention_set
1010                        .insert_text_thread(path, Task::ready(Ok(text)).shared());
1011                }
1012                MentionUri::Fetch { url } => {
1013                    self.mention_set
1014                        .add_fetch_result(url, Task::ready(Ok(text)).shared());
1015                }
1016                MentionUri::Directory { abs_path } => {
1017                    let task = Task::ready(Ok(text)).shared();
1018                    self.mention_set.directories.insert(abs_path, task);
1019                }
1020                MentionUri::File { .. }
1021                | MentionUri::Symbol { .. }
1022                | MentionUri::Rule { .. }
1023                | MentionUri::Selection { .. } => {}
1024            }
1025        }
1026        for (range, content) in images {
1027            let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else {
1028                continue;
1029            };
1030            let anchor = snapshot.anchor_before(range.start);
1031            let abs_path = content
1032                .uri
1033                .as_ref()
1034                .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into()));
1035
1036            let name = content
1037                .uri
1038                .as_ref()
1039                .and_then(|uri| {
1040                    uri.strip_prefix("file://")
1041                        .and_then(|path| Path::new(path).file_name())
1042                })
1043                .map(|name| name.to_string_lossy().to_string())
1044                .unwrap_or("Image".to_owned());
1045            let crease_id = crate::context_picker::insert_crease_for_mention(
1046                anchor.excerpt_id,
1047                anchor.text_anchor,
1048                range.end - range.start,
1049                name.into(),
1050                IconName::Image.path().into(),
1051                self.editor.clone(),
1052                window,
1053                cx,
1054            );
1055            let data: SharedString = content.data.to_string().into();
1056
1057            if let Some(crease_id) = crease_id {
1058                self.mention_set.insert_image(
1059                    crease_id,
1060                    Task::ready(Ok(MentionImage {
1061                        abs_path,
1062                        data,
1063                        format,
1064                    }))
1065                    .shared(),
1066                );
1067            }
1068        }
1069        cx.notify();
1070    }
1071
1072    fn highlight_slash_command(
1073        &mut self,
1074        semantics_provider: Rc<SlashCommandSemanticsProvider>,
1075        editor: Entity<Editor>,
1076        window: &mut Window,
1077        cx: &mut Context<Self>,
1078    ) {
1079        struct InvalidSlashCommand;
1080
1081        self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| {
1082            cx.background_executor()
1083                .timer(PARSE_SLASH_COMMAND_DEBOUNCE)
1084                .await;
1085            editor
1086                .update_in(cx, |editor, window, cx| {
1087                    let snapshot = editor.snapshot(window, cx);
1088                    let range = parse_slash_command(&editor.text(cx));
1089                    semantics_provider.range.set(range);
1090                    if let Some((start, end)) = range {
1091                        editor.highlight_text::<InvalidSlashCommand>(
1092                            vec![
1093                                snapshot.buffer_snapshot.anchor_after(start)
1094                                    ..snapshot.buffer_snapshot.anchor_before(end),
1095                            ],
1096                            HighlightStyle {
1097                                underline: Some(UnderlineStyle {
1098                                    thickness: px(1.),
1099                                    color: Some(gpui::red()),
1100                                    wavy: true,
1101                                }),
1102                                ..Default::default()
1103                            },
1104                            cx,
1105                        );
1106                    } else {
1107                        editor.clear_highlights::<InvalidSlashCommand>(cx);
1108                    }
1109                })
1110                .ok();
1111        })
1112    }
1113
1114    #[cfg(test)]
1115    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1116        self.editor.update(cx, |editor, cx| {
1117            editor.set_text(text, window, cx);
1118        });
1119    }
1120
1121    #[cfg(test)]
1122    pub fn text(&self, cx: &App) -> String {
1123        self.editor.read(cx).text(cx)
1124    }
1125}
1126
1127struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>);
1128
1129impl Display for DirectoryContents {
1130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131        for (_relative_path, full_path, content) in self.0.iter() {
1132            let fence = codeblock_fence_for_path(Some(full_path), None);
1133            write!(f, "\n{fence}\n{content}\n```")?;
1134        }
1135        Ok(())
1136    }
1137}
1138
1139impl Focusable for MessageEditor {
1140    fn focus_handle(&self, cx: &App) -> FocusHandle {
1141        self.editor.focus_handle(cx)
1142    }
1143}
1144
1145impl Render for MessageEditor {
1146    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1147        div()
1148            .key_context("MessageEditor")
1149            .on_action(cx.listener(Self::send))
1150            .on_action(cx.listener(Self::cancel))
1151            .capture_action(cx.listener(Self::paste))
1152            .flex_1()
1153            .child({
1154                let settings = ThemeSettings::get_global(cx);
1155                let font_size = TextSize::Small
1156                    .rems(cx)
1157                    .to_pixels(settings.agent_font_size(cx));
1158                let line_height = settings.buffer_line_height.value() * font_size;
1159
1160                let text_style = TextStyle {
1161                    color: cx.theme().colors().text,
1162                    font_family: settings.buffer_font.family.clone(),
1163                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1164                    font_features: settings.buffer_font.features.clone(),
1165                    font_size: font_size.into(),
1166                    line_height: line_height.into(),
1167                    ..Default::default()
1168                };
1169
1170                EditorElement::new(
1171                    &self.editor,
1172                    EditorStyle {
1173                        background: cx.theme().colors().editor_background,
1174                        local_player: cx.theme().players().local(),
1175                        text: text_style,
1176                        syntax: cx.theme().syntax().clone(),
1177                        ..Default::default()
1178                    },
1179                )
1180            })
1181    }
1182}
1183
1184pub(crate) fn insert_crease_for_image(
1185    excerpt_id: ExcerptId,
1186    anchor: text::Anchor,
1187    content_len: usize,
1188    abs_path: Option<Arc<Path>>,
1189    image: Shared<Task<Result<Arc<Image>, String>>>,
1190    editor: Entity<Editor>,
1191    window: &mut Window,
1192    cx: &mut App,
1193) -> Option<CreaseId> {
1194    let crease_label = abs_path
1195        .as_ref()
1196        .and_then(|path| path.file_name())
1197        .map(|name| name.to_string_lossy().to_string().into())
1198        .unwrap_or(SharedString::from("Image"));
1199
1200    editor.update(cx, |editor, cx| {
1201        let snapshot = editor.buffer().read(cx).snapshot(cx);
1202
1203        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
1204
1205        let start = start.bias_right(&snapshot);
1206        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
1207
1208        let placeholder = FoldPlaceholder {
1209            render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()),
1210            merge_adjacent: false,
1211            ..Default::default()
1212        };
1213
1214        let crease = Crease::Inline {
1215            range: start..end,
1216            placeholder,
1217            render_toggle: None,
1218            render_trailer: None,
1219            metadata: None,
1220        };
1221
1222        let ids = editor.insert_creases(vec![crease.clone()], cx);
1223        editor.fold_creases(vec![crease], false, window, cx);
1224
1225        Some(ids[0])
1226    })
1227}
1228
1229fn render_image_fold_icon_button(
1230    label: SharedString,
1231    image_task: Shared<Task<Result<Arc<Image>, String>>>,
1232    editor: WeakEntity<Editor>,
1233) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
1234    Arc::new({
1235        let image_task = image_task.clone();
1236        move |fold_id, fold_range, cx| {
1237            let is_in_text_selection = editor
1238                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
1239                .unwrap_or_default();
1240
1241            ButtonLike::new(fold_id)
1242                .style(ButtonStyle::Filled)
1243                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1244                .toggle_state(is_in_text_selection)
1245                .child(
1246                    h_flex()
1247                        .gap_1()
1248                        .child(
1249                            Icon::new(IconName::Image)
1250                                .size(IconSize::XSmall)
1251                                .color(Color::Muted),
1252                        )
1253                        .child(
1254                            Label::new(label.clone())
1255                                .size(LabelSize::Small)
1256                                .buffer_font(cx)
1257                                .single_line(),
1258                        ),
1259                )
1260                .hoverable_tooltip({
1261                    let image_task = image_task.clone();
1262                    move |_, cx| {
1263                        let image = image_task.peek().cloned().transpose().ok().flatten();
1264                        let image_task = image_task.clone();
1265                        cx.new::<ImageHover>(|cx| ImageHover {
1266                            image,
1267                            _task: cx.spawn(async move |this, cx| {
1268                                if let Ok(image) = image_task.clone().await {
1269                                    this.update(cx, |this, cx| {
1270                                        if this.image.replace(image).is_none() {
1271                                            cx.notify();
1272                                        }
1273                                    })
1274                                    .ok();
1275                                }
1276                            }),
1277                        })
1278                        .into()
1279                    }
1280                })
1281                .into_any_element()
1282        }
1283    })
1284}
1285
1286struct ImageHover {
1287    image: Option<Arc<Image>>,
1288    _task: Task<()>,
1289}
1290
1291impl Render for ImageHover {
1292    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1293        if let Some(image) = self.image.clone() {
1294            gpui::img(image).max_w_96().max_h_96().into_any_element()
1295        } else {
1296            gpui::Empty.into_any_element()
1297        }
1298    }
1299}
1300
1301#[derive(Debug, Eq, PartialEq)]
1302pub enum Mention {
1303    Text { uri: MentionUri, content: String },
1304    Image(MentionImage),
1305}
1306
1307#[derive(Clone, Debug, Eq, PartialEq)]
1308pub struct MentionImage {
1309    pub abs_path: Option<PathBuf>,
1310    pub data: SharedString,
1311    pub format: ImageFormat,
1312}
1313
1314#[derive(Default)]
1315pub struct MentionSet {
1316    uri_by_crease_id: HashMap<CreaseId, MentionUri>,
1317    fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
1318    images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
1319    thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
1320    text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
1321    directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
1322}
1323
1324impl MentionSet {
1325    pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
1326        self.uri_by_crease_id.insert(crease_id, uri);
1327    }
1328
1329    pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
1330        self.fetch_results.insert(url, content);
1331    }
1332
1333    pub fn insert_image(
1334        &mut self,
1335        crease_id: CreaseId,
1336        task: Shared<Task<Result<MentionImage, String>>>,
1337    ) {
1338        self.images.insert(crease_id, task);
1339    }
1340
1341    fn insert_thread(&mut self, id: ThreadId, task: Shared<Task<Result<SharedString, String>>>) {
1342        self.thread_summaries.insert(id, task);
1343    }
1344
1345    fn insert_text_thread(&mut self, path: PathBuf, task: Shared<Task<Result<String, String>>>) {
1346        self.text_thread_summaries.insert(path, task);
1347    }
1348
1349    pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
1350        self.fetch_results.clear();
1351        self.thread_summaries.clear();
1352        self.text_thread_summaries.clear();
1353        self.uri_by_crease_id
1354            .drain()
1355            .map(|(id, _)| id)
1356            .chain(self.images.drain().map(|(id, _)| id))
1357    }
1358
1359    pub fn contents(
1360        &self,
1361        project: Entity<Project>,
1362        thread_store: Entity<ThreadStore>,
1363        _window: &mut Window,
1364        cx: &mut App,
1365    ) -> Task<Result<HashMap<CreaseId, Mention>>> {
1366        let mut processed_image_creases = HashSet::default();
1367
1368        let mut contents = self
1369            .uri_by_crease_id
1370            .iter()
1371            .map(|(&crease_id, uri)| {
1372                match uri {
1373                    MentionUri::File { abs_path, .. } => {
1374                        let uri = uri.clone();
1375                        let abs_path = abs_path.to_path_buf();
1376
1377                        if let Some(task) = self.images.get(&crease_id).cloned() {
1378                            processed_image_creases.insert(crease_id);
1379                            return cx.spawn(async move |_| {
1380                                let image = task.await.map_err(|e| anyhow!("{e}"))?;
1381                                anyhow::Ok((crease_id, Mention::Image(image)))
1382                            });
1383                        }
1384
1385                        let buffer_task = project.update(cx, |project, cx| {
1386                            let path = project
1387                                .find_project_path(abs_path, cx)
1388                                .context("Failed to find project path")?;
1389                            anyhow::Ok(project.open_buffer(path, cx))
1390                        });
1391                        cx.spawn(async move |cx| {
1392                            let buffer = buffer_task?.await?;
1393                            let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
1394
1395                            anyhow::Ok((crease_id, Mention::Text { uri, content }))
1396                        })
1397                    }
1398                    MentionUri::Directory { abs_path } => {
1399                        let Some(content) = self.directories.get(abs_path).cloned() else {
1400                            return Task::ready(Err(anyhow!("missing directory load task")));
1401                        };
1402                        let uri = uri.clone();
1403                        cx.spawn(async move |_| {
1404                            Ok((
1405                                crease_id,
1406                                Mention::Text {
1407                                    uri,
1408                                    content: content
1409                                        .await
1410                                        .map_err(|e| anyhow::anyhow!("{e}"))?
1411                                        .to_string(),
1412                                },
1413                            ))
1414                        })
1415                    }
1416                    MentionUri::Symbol {
1417                        path, line_range, ..
1418                    }
1419                    | MentionUri::Selection {
1420                        path, line_range, ..
1421                    } => {
1422                        let uri = uri.clone();
1423                        let path_buf = path.clone();
1424                        let line_range = line_range.clone();
1425
1426                        let buffer_task = project.update(cx, |project, cx| {
1427                            let path = project
1428                                .find_project_path(&path_buf, cx)
1429                                .context("Failed to find project path")?;
1430                            anyhow::Ok(project.open_buffer(path, cx))
1431                        });
1432
1433                        cx.spawn(async move |cx| {
1434                            let buffer = buffer_task?.await?;
1435                            let content = buffer.read_with(cx, |buffer, _cx| {
1436                                buffer
1437                                    .text_for_range(
1438                                        Point::new(line_range.start, 0)
1439                                            ..Point::new(
1440                                                line_range.end,
1441                                                buffer.line_len(line_range.end),
1442                                            ),
1443                                    )
1444                                    .collect()
1445                            })?;
1446
1447                            anyhow::Ok((crease_id, Mention::Text { uri, content }))
1448                        })
1449                    }
1450                    MentionUri::Thread { id, .. } => {
1451                        let Some(content) = self.thread_summaries.get(id).cloned() else {
1452                            return Task::ready(Err(anyhow!("missing thread summary")));
1453                        };
1454                        let uri = uri.clone();
1455                        cx.spawn(async move |_| {
1456                            Ok((
1457                                crease_id,
1458                                Mention::Text {
1459                                    uri,
1460                                    content: content
1461                                        .await
1462                                        .map_err(|e| anyhow::anyhow!("{e}"))?
1463                                        .to_string(),
1464                                },
1465                            ))
1466                        })
1467                    }
1468                    MentionUri::TextThread { path, .. } => {
1469                        let Some(content) = self.text_thread_summaries.get(path).cloned() else {
1470                            return Task::ready(Err(anyhow!("missing text thread summary")));
1471                        };
1472                        let uri = uri.clone();
1473                        cx.spawn(async move |_| {
1474                            Ok((
1475                                crease_id,
1476                                Mention::Text {
1477                                    uri,
1478                                    content: content
1479                                        .await
1480                                        .map_err(|e| anyhow::anyhow!("{e}"))?
1481                                        .to_string(),
1482                                },
1483                            ))
1484                        })
1485                    }
1486                    MentionUri::Rule { id: prompt_id, .. } => {
1487                        let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
1488                        else {
1489                            return Task::ready(Err(anyhow!("missing prompt store")));
1490                        };
1491                        let text_task = prompt_store.read(cx).load(*prompt_id, cx);
1492                        let uri = uri.clone();
1493                        cx.spawn(async move |_| {
1494                            // TODO: report load errors instead of just logging
1495                            let text = text_task.await?;
1496                            anyhow::Ok((crease_id, Mention::Text { uri, content: text }))
1497                        })
1498                    }
1499                    MentionUri::Fetch { url } => {
1500                        let Some(content) = self.fetch_results.get(url).cloned() else {
1501                            return Task::ready(Err(anyhow!("missing fetch result")));
1502                        };
1503                        let uri = uri.clone();
1504                        cx.spawn(async move |_| {
1505                            Ok((
1506                                crease_id,
1507                                Mention::Text {
1508                                    uri,
1509                                    content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
1510                                },
1511                            ))
1512                        })
1513                    }
1514                }
1515            })
1516            .collect::<Vec<_>>();
1517
1518        // Handle images that didn't have a mention URI (because they were added by the paste handler).
1519        contents.extend(self.images.iter().filter_map(|(crease_id, image)| {
1520            if processed_image_creases.contains(crease_id) {
1521                return None;
1522            }
1523            let crease_id = *crease_id;
1524            let image = image.clone();
1525            Some(cx.spawn(async move |_| {
1526                Ok((
1527                    crease_id,
1528                    Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
1529                ))
1530            }))
1531        }));
1532
1533        cx.spawn(async move |_cx| {
1534            let contents = try_join_all(contents).await?.into_iter().collect();
1535            anyhow::Ok(contents)
1536        })
1537    }
1538}
1539
1540struct SlashCommandSemanticsProvider {
1541    range: Cell<Option<(usize, usize)>>,
1542}
1543
1544impl SemanticsProvider for SlashCommandSemanticsProvider {
1545    fn hover(
1546        &self,
1547        buffer: &Entity<Buffer>,
1548        position: text::Anchor,
1549        cx: &mut App,
1550    ) -> Option<Task<Vec<project::Hover>>> {
1551        let snapshot = buffer.read(cx).snapshot();
1552        let offset = position.to_offset(&snapshot);
1553        let (start, end) = self.range.get()?;
1554        if !(start..end).contains(&offset) {
1555            return None;
1556        }
1557        let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
1558        Some(Task::ready(vec![project::Hover {
1559            contents: vec![project::HoverBlock {
1560                text: "Slash commands are not supported".into(),
1561                kind: project::HoverBlockKind::PlainText,
1562            }],
1563            range: Some(range),
1564            language: None,
1565        }]))
1566    }
1567
1568    fn inline_values(
1569        &self,
1570        _buffer_handle: Entity<Buffer>,
1571        _range: Range<text::Anchor>,
1572        _cx: &mut App,
1573    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
1574        None
1575    }
1576
1577    fn inlay_hints(
1578        &self,
1579        _buffer_handle: Entity<Buffer>,
1580        _range: Range<text::Anchor>,
1581        _cx: &mut App,
1582    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
1583        None
1584    }
1585
1586    fn resolve_inlay_hint(
1587        &self,
1588        _hint: project::InlayHint,
1589        _buffer_handle: Entity<Buffer>,
1590        _server_id: lsp::LanguageServerId,
1591        _cx: &mut App,
1592    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
1593        None
1594    }
1595
1596    fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool {
1597        false
1598    }
1599
1600    fn document_highlights(
1601        &self,
1602        _buffer: &Entity<Buffer>,
1603        _position: text::Anchor,
1604        _cx: &mut App,
1605    ) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> {
1606        None
1607    }
1608
1609    fn definitions(
1610        &self,
1611        _buffer: &Entity<Buffer>,
1612        _position: text::Anchor,
1613        _kind: editor::GotoDefinitionKind,
1614        _cx: &mut App,
1615    ) -> Option<Task<Result<Vec<project::LocationLink>>>> {
1616        None
1617    }
1618
1619    fn range_for_rename(
1620        &self,
1621        _buffer: &Entity<Buffer>,
1622        _position: text::Anchor,
1623        _cx: &mut App,
1624    ) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
1625        None
1626    }
1627
1628    fn perform_rename(
1629        &self,
1630        _buffer: &Entity<Buffer>,
1631        _position: text::Anchor,
1632        _new_name: String,
1633        _cx: &mut App,
1634    ) -> Option<Task<Result<project::ProjectTransaction>>> {
1635        None
1636    }
1637}
1638
1639fn parse_slash_command(text: &str) -> Option<(usize, usize)> {
1640    if let Some(remainder) = text.strip_prefix('/') {
1641        let pos = remainder
1642            .find(char::is_whitespace)
1643            .unwrap_or(remainder.len());
1644        let command = &remainder[..pos];
1645        if !command.is_empty() && command.chars().all(char::is_alphanumeric) {
1646            return Some((0, 1 + command.len()));
1647        }
1648    }
1649    None
1650}
1651
1652pub struct MessageEditorAddon {}
1653
1654impl MessageEditorAddon {
1655    pub fn new() -> Self {
1656        Self {}
1657    }
1658}
1659
1660impl Addon for MessageEditorAddon {
1661    fn to_any(&self) -> &dyn std::any::Any {
1662        self
1663    }
1664
1665    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1666        Some(self)
1667    }
1668
1669    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1670        let settings = agent_settings::AgentSettings::get_global(cx);
1671        if settings.use_modifier_to_send {
1672            key_context.add("use_modifier_to_send");
1673        }
1674    }
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679    use std::{ops::Range, path::Path, sync::Arc};
1680
1681    use agent::{TextThreadStore, ThreadStore};
1682    use agent_client_protocol as acp;
1683    use editor::{AnchorRangeExt as _, Editor, EditorMode};
1684    use fs::FakeFs;
1685    use futures::StreamExt as _;
1686    use gpui::{
1687        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1688    };
1689    use lsp::{CompletionContext, CompletionTriggerKind};
1690    use project::{CompletionIntent, Project, ProjectPath};
1691    use serde_json::json;
1692    use text::Point;
1693    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1694    use util::{path, uri};
1695    use workspace::{AppState, Item, Workspace};
1696
1697    use crate::acp::{
1698        message_editor::{Mention, MessageEditor},
1699        thread_view::tests::init_test,
1700    };
1701
1702    #[gpui::test]
1703    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1704        init_test(cx);
1705
1706        let fs = FakeFs::new(cx.executor());
1707        fs.insert_tree("/project", json!({"file": ""})).await;
1708        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1709
1710        let (workspace, cx) =
1711            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1712
1713        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1714        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1715
1716        let message_editor = cx.update(|window, cx| {
1717            cx.new(|cx| {
1718                MessageEditor::new(
1719                    workspace.downgrade(),
1720                    project.clone(),
1721                    thread_store.clone(),
1722                    text_thread_store.clone(),
1723                    "Test",
1724                    false,
1725                    EditorMode::AutoHeight {
1726                        min_lines: 1,
1727                        max_lines: None,
1728                    },
1729                    window,
1730                    cx,
1731                )
1732            })
1733        });
1734        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1735
1736        cx.run_until_parked();
1737
1738        let excerpt_id = editor.update(cx, |editor, cx| {
1739            editor
1740                .buffer()
1741                .read(cx)
1742                .excerpt_ids()
1743                .into_iter()
1744                .next()
1745                .unwrap()
1746        });
1747        let completions = editor.update_in(cx, |editor, window, cx| {
1748            editor.set_text("Hello @file ", window, cx);
1749            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1750            let completion_provider = editor.completion_provider().unwrap();
1751            completion_provider.completions(
1752                excerpt_id,
1753                &buffer,
1754                text::Anchor::MAX,
1755                CompletionContext {
1756                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1757                    trigger_character: Some("@".into()),
1758                },
1759                window,
1760                cx,
1761            )
1762        });
1763        let [_, completion]: [_; 2] = completions
1764            .await
1765            .unwrap()
1766            .into_iter()
1767            .flat_map(|response| response.completions)
1768            .collect::<Vec<_>>()
1769            .try_into()
1770            .unwrap();
1771
1772        editor.update_in(cx, |editor, window, cx| {
1773            let snapshot = editor.buffer().read(cx).snapshot(cx);
1774            let start = snapshot
1775                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1776                .unwrap();
1777            let end = snapshot
1778                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1779                .unwrap();
1780            editor.edit([(start..end, completion.new_text)], cx);
1781            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1782        });
1783
1784        cx.run_until_parked();
1785
1786        // Backspace over the inserted crease (and the following space).
1787        editor.update_in(cx, |editor, window, cx| {
1788            editor.backspace(&Default::default(), window, cx);
1789            editor.backspace(&Default::default(), window, cx);
1790        });
1791
1792        let content = message_editor
1793            .update_in(cx, |message_editor, window, cx| {
1794                message_editor.contents(window, cx)
1795            })
1796            .await
1797            .unwrap();
1798
1799        // We don't send a resource link for the deleted crease.
1800        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1801    }
1802
1803    struct MessageEditorItem(Entity<MessageEditor>);
1804
1805    impl Item for MessageEditorItem {
1806        type Event = ();
1807
1808        fn include_in_nav_history() -> bool {
1809            false
1810        }
1811
1812        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1813            "Test".into()
1814        }
1815    }
1816
1817    impl EventEmitter<()> for MessageEditorItem {}
1818
1819    impl Focusable for MessageEditorItem {
1820        fn focus_handle(&self, cx: &App) -> FocusHandle {
1821            self.0.read(cx).focus_handle(cx).clone()
1822        }
1823    }
1824
1825    impl Render for MessageEditorItem {
1826        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1827            self.0.clone().into_any_element()
1828        }
1829    }
1830
1831    #[gpui::test]
1832    async fn test_context_completion_provider(cx: &mut TestAppContext) {
1833        init_test(cx);
1834
1835        let app_state = cx.update(AppState::test);
1836
1837        cx.update(|cx| {
1838            language::init(cx);
1839            editor::init(cx);
1840            workspace::init(app_state.clone(), cx);
1841            Project::init_settings(cx);
1842        });
1843
1844        app_state
1845            .fs
1846            .as_fake()
1847            .insert_tree(
1848                path!("/dir"),
1849                json!({
1850                    "editor": "",
1851                    "a": {
1852                        "one.txt": "1",
1853                        "two.txt": "2",
1854                        "three.txt": "3",
1855                        "four.txt": "4"
1856                    },
1857                    "b": {
1858                        "five.txt": "5",
1859                        "six.txt": "6",
1860                        "seven.txt": "7",
1861                        "eight.txt": "8",
1862                    }
1863                }),
1864            )
1865            .await;
1866
1867        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1868        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1869        let workspace = window.root(cx).unwrap();
1870
1871        let worktree = project.update(cx, |project, cx| {
1872            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1873            assert_eq!(worktrees.len(), 1);
1874            worktrees.pop().unwrap()
1875        });
1876        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1877
1878        let mut cx = VisualTestContext::from_window(*window, cx);
1879
1880        let paths = vec![
1881            path!("a/one.txt"),
1882            path!("a/two.txt"),
1883            path!("a/three.txt"),
1884            path!("a/four.txt"),
1885            path!("b/five.txt"),
1886            path!("b/six.txt"),
1887            path!("b/seven.txt"),
1888            path!("b/eight.txt"),
1889        ];
1890
1891        let mut opened_editors = Vec::new();
1892        for path in paths {
1893            let buffer = workspace
1894                .update_in(&mut cx, |workspace, window, cx| {
1895                    workspace.open_path(
1896                        ProjectPath {
1897                            worktree_id,
1898                            path: Path::new(path).into(),
1899                        },
1900                        None,
1901                        false,
1902                        window,
1903                        cx,
1904                    )
1905                })
1906                .await
1907                .unwrap();
1908            opened_editors.push(buffer);
1909        }
1910
1911        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1912        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1913
1914        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1915            let workspace_handle = cx.weak_entity();
1916            let message_editor = cx.new(|cx| {
1917                MessageEditor::new(
1918                    workspace_handle,
1919                    project.clone(),
1920                    thread_store.clone(),
1921                    text_thread_store.clone(),
1922                    "Test",
1923                    false,
1924                    EditorMode::AutoHeight {
1925                        max_lines: None,
1926                        min_lines: 1,
1927                    },
1928                    window,
1929                    cx,
1930                )
1931            });
1932            workspace.active_pane().update(cx, |pane, cx| {
1933                pane.add_item(
1934                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1935                    true,
1936                    true,
1937                    None,
1938                    window,
1939                    cx,
1940                );
1941            });
1942            message_editor.read(cx).focus_handle(cx).focus(window);
1943            let editor = message_editor.read(cx).editor().clone();
1944            (message_editor, editor)
1945        });
1946
1947        cx.simulate_input("Lorem ");
1948
1949        editor.update(&mut cx, |editor, cx| {
1950            assert_eq!(editor.text(cx), "Lorem ");
1951            assert!(!editor.has_visible_completions_menu());
1952        });
1953
1954        cx.simulate_input("@");
1955
1956        editor.update(&mut cx, |editor, cx| {
1957            assert_eq!(editor.text(cx), "Lorem @");
1958            assert!(editor.has_visible_completions_menu());
1959            assert_eq!(
1960                current_completion_labels(editor),
1961                &[
1962                    "eight.txt dir/b/",
1963                    "seven.txt dir/b/",
1964                    "six.txt dir/b/",
1965                    "five.txt dir/b/",
1966                    "Files & Directories",
1967                    "Symbols",
1968                    "Threads",
1969                    "Fetch"
1970                ]
1971            );
1972        });
1973
1974        // Select and confirm "File"
1975        editor.update_in(&mut cx, |editor, window, cx| {
1976            assert!(editor.has_visible_completions_menu());
1977            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1978            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1979            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1980            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1981            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1982        });
1983
1984        cx.run_until_parked();
1985
1986        editor.update(&mut cx, |editor, cx| {
1987            assert_eq!(editor.text(cx), "Lorem @file ");
1988            assert!(editor.has_visible_completions_menu());
1989        });
1990
1991        cx.simulate_input("one");
1992
1993        editor.update(&mut cx, |editor, cx| {
1994            assert_eq!(editor.text(cx), "Lorem @file one");
1995            assert!(editor.has_visible_completions_menu());
1996            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
1997        });
1998
1999        editor.update_in(&mut cx, |editor, window, cx| {
2000            assert!(editor.has_visible_completions_menu());
2001            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2002        });
2003
2004        let url_one = uri!("file:///dir/a/one.txt");
2005        editor.update(&mut cx, |editor, cx| {
2006            let text = editor.text(cx);
2007            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2008            assert!(!editor.has_visible_completions_menu());
2009            assert_eq!(fold_ranges(editor, cx).len(), 1);
2010        });
2011
2012        let contents = message_editor
2013            .update_in(&mut cx, |message_editor, window, cx| {
2014                message_editor.mention_set().contents(
2015                    project.clone(),
2016                    thread_store.clone(),
2017                    window,
2018                    cx,
2019                )
2020            })
2021            .await
2022            .unwrap()
2023            .into_values()
2024            .collect::<Vec<_>>();
2025
2026        pretty_assertions::assert_eq!(
2027            contents,
2028            [Mention::Text {
2029                content: "1".into(),
2030                uri: url_one.parse().unwrap()
2031            }]
2032        );
2033
2034        cx.simulate_input(" ");
2035
2036        editor.update(&mut cx, |editor, cx| {
2037            let text = editor.text(cx);
2038            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2039            assert!(!editor.has_visible_completions_menu());
2040            assert_eq!(fold_ranges(editor, cx).len(), 1);
2041        });
2042
2043        cx.simulate_input("Ipsum ");
2044
2045        editor.update(&mut cx, |editor, cx| {
2046            let text = editor.text(cx);
2047            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2048            assert!(!editor.has_visible_completions_menu());
2049            assert_eq!(fold_ranges(editor, cx).len(), 1);
2050        });
2051
2052        cx.simulate_input("@file ");
2053
2054        editor.update(&mut cx, |editor, cx| {
2055            let text = editor.text(cx);
2056            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2057            assert!(editor.has_visible_completions_menu());
2058            assert_eq!(fold_ranges(editor, cx).len(), 1);
2059        });
2060
2061        editor.update_in(&mut cx, |editor, window, cx| {
2062            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2063        });
2064
2065        cx.run_until_parked();
2066
2067        let contents = message_editor
2068            .update_in(&mut cx, |message_editor, window, cx| {
2069                message_editor.mention_set().contents(
2070                    project.clone(),
2071                    thread_store.clone(),
2072                    window,
2073                    cx,
2074                )
2075            })
2076            .await
2077            .unwrap()
2078            .into_values()
2079            .collect::<Vec<_>>();
2080
2081        assert_eq!(contents.len(), 2);
2082        let url_eight = uri!("file:///dir/b/eight.txt");
2083        pretty_assertions::assert_eq!(
2084            contents[1],
2085            Mention::Text {
2086                content: "8".to_string(),
2087                uri: url_eight.parse().unwrap(),
2088            }
2089        );
2090
2091        editor.update(&mut cx, |editor, cx| {
2092            assert_eq!(
2093                editor.text(cx),
2094                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2095            );
2096            assert!(!editor.has_visible_completions_menu());
2097            assert_eq!(fold_ranges(editor, cx).len(), 2);
2098        });
2099
2100        let plain_text_language = Arc::new(language::Language::new(
2101            language::LanguageConfig {
2102                name: "Plain Text".into(),
2103                matcher: language::LanguageMatcher {
2104                    path_suffixes: vec!["txt".to_string()],
2105                    ..Default::default()
2106                },
2107                ..Default::default()
2108            },
2109            None,
2110        ));
2111
2112        // Register the language and fake LSP
2113        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2114        language_registry.add(plain_text_language);
2115
2116        let mut fake_language_servers = language_registry.register_fake_lsp(
2117            "Plain Text",
2118            language::FakeLspAdapter {
2119                capabilities: lsp::ServerCapabilities {
2120                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2121                    ..Default::default()
2122                },
2123                ..Default::default()
2124            },
2125        );
2126
2127        // Open the buffer to trigger LSP initialization
2128        let buffer = project
2129            .update(&mut cx, |project, cx| {
2130                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2131            })
2132            .await
2133            .unwrap();
2134
2135        // Register the buffer with language servers
2136        let _handle = project.update(&mut cx, |project, cx| {
2137            project.register_buffer_with_language_servers(&buffer, cx)
2138        });
2139
2140        cx.run_until_parked();
2141
2142        let fake_language_server = fake_language_servers.next().await.unwrap();
2143        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2144            move |_, _| async move {
2145                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2146                    #[allow(deprecated)]
2147                    lsp::SymbolInformation {
2148                        name: "MySymbol".into(),
2149                        location: lsp::Location {
2150                            uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2151                            range: lsp::Range::new(
2152                                lsp::Position::new(0, 0),
2153                                lsp::Position::new(0, 1),
2154                            ),
2155                        },
2156                        kind: lsp::SymbolKind::CONSTANT,
2157                        tags: None,
2158                        container_name: None,
2159                        deprecated: None,
2160                    },
2161                ])))
2162            },
2163        );
2164
2165        cx.simulate_input("@symbol ");
2166
2167        editor.update(&mut cx, |editor, cx| {
2168            assert_eq!(
2169                editor.text(cx),
2170                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2171            );
2172            assert!(editor.has_visible_completions_menu());
2173            assert_eq!(current_completion_labels(editor), &["MySymbol"]);
2174        });
2175
2176        editor.update_in(&mut cx, |editor, window, cx| {
2177            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2178        });
2179
2180        let contents = message_editor
2181            .update_in(&mut cx, |message_editor, window, cx| {
2182                message_editor
2183                    .mention_set()
2184                    .contents(project.clone(), thread_store, window, cx)
2185            })
2186            .await
2187            .unwrap()
2188            .into_values()
2189            .collect::<Vec<_>>();
2190
2191        assert_eq!(contents.len(), 3);
2192        pretty_assertions::assert_eq!(
2193            contents[2],
2194            Mention::Text {
2195                content: "1".into(),
2196                uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(),
2197            }
2198        );
2199
2200        cx.run_until_parked();
2201
2202        editor.read_with(&cx, |editor, cx| {
2203                assert_eq!(
2204                    editor.text(cx),
2205                    format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2206                );
2207            });
2208    }
2209
2210    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2211        let snapshot = editor.buffer().read(cx).snapshot(cx);
2212        editor.display_map.update(cx, |display_map, cx| {
2213            display_map
2214                .snapshot(cx)
2215                .folds_in_range(0..snapshot.len())
2216                .map(|fold| fold.range.to_point(&snapshot))
2217                .collect()
2218        })
2219    }
2220
2221    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2222        let completions = editor.current_completions().expect("Missing completions");
2223        completions
2224            .into_iter()
2225            .map(|completion| completion.label.text.to_string())
2226            .collect::<Vec<_>>()
2227    }
2228}