message_editor.rs

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