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