message_editor.rs

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