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