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