message_editor.rs

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