message_editor.rs

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