message_editor.rs

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