message_editor.rs

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