message_editor.rs

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