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