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