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