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