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