message_editor.rs

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