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