message_editor.rs

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