message_editor.rs

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