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