message_editor.rs

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