message_editor.rs

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