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