message_editor.rs

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