message_editor.rs

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