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