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