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::{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<Cell<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<Cell<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.get().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.get(), 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                                        },
 838                                    ),
 839                                })
 840                            }
 841                            Mention::Image(mention_image) => {
 842                                let uri = match uri {
 843                                    MentionUri::File { .. } => Some(uri.to_uri().to_string()),
 844                                    MentionUri::PastedImage => None,
 845                                    other => {
 846                                        debug_panic!(
 847                                            "unexpected mention uri for image: {:?}",
 848                                            other
 849                                        );
 850                                        None
 851                                    }
 852                                };
 853                                acp::ContentBlock::Image(acp::ImageContent {
 854                                    annotations: None,
 855                                    data: mention_image.data.to_string(),
 856                                    mime_type: mention_image.format.mime_type().into(),
 857                                    uri,
 858                                })
 859                            }
 860                            Mention::UriOnly => {
 861                                acp::ContentBlock::ResourceLink(acp::ResourceLink {
 862                                    name: uri.name(),
 863                                    uri: uri.to_uri().to_string(),
 864                                    annotations: None,
 865                                    description: None,
 866                                    mime_type: None,
 867                                    size: None,
 868                                    title: None,
 869                                })
 870                            }
 871                        };
 872                        chunks.push(chunk);
 873                        ix = crease_range.end;
 874                    }
 875
 876                    if ix < text.len() {
 877                        //todo(): Custom slash command ContentBlock?
 878                        // let last_chunk = if prevent_slash_commands
 879                        //     && ix == 0
 880                        //     && parse_slash_command(&text[ix..]).is_some()
 881                        // {
 882                        //     format!(" {}", text[ix..].trim_end())
 883                        // } else {
 884                        //     text[ix..].trim_end().to_owned()
 885                        // };
 886                        let last_chunk = text[ix..].trim_end().to_owned();
 887                        if !last_chunk.is_empty() {
 888                            chunks.push(last_chunk.into());
 889                        }
 890                    }
 891                });
 892                Ok((chunks, all_tracked_buffers))
 893            })?;
 894            result
 895        })
 896    }
 897
 898    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 899        self.editor.update(cx, |editor, cx| {
 900            editor.clear(window, cx);
 901            editor.remove_creases(
 902                self.mention_set
 903                    .mentions
 904                    .drain()
 905                    .map(|(crease_id, _)| crease_id),
 906                cx,
 907            )
 908        });
 909    }
 910
 911    fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
 912        if self.is_empty(cx) {
 913            return;
 914        }
 915        cx.emit(MessageEditorEvent::Send)
 916    }
 917
 918    fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 919        cx.emit(MessageEditorEvent::Cancel)
 920    }
 921
 922    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 923        if !self.prompt_capabilities.get().image {
 924            return;
 925        }
 926
 927        let images = cx
 928            .read_from_clipboard()
 929            .map(|item| {
 930                item.into_entries()
 931                    .filter_map(|entry| {
 932                        if let ClipboardEntry::Image(image) = entry {
 933                            Some(image)
 934                        } else {
 935                            None
 936                        }
 937                    })
 938                    .collect::<Vec<_>>()
 939            })
 940            .unwrap_or_default();
 941
 942        if images.is_empty() {
 943            return;
 944        }
 945        cx.stop_propagation();
 946
 947        let replacement_text = MentionUri::PastedImage.as_link().to_string();
 948        for image in images {
 949            let (excerpt_id, text_anchor, multibuffer_anchor) =
 950                self.editor.update(cx, |message_editor, cx| {
 951                    let snapshot = message_editor.snapshot(window, cx);
 952                    let (excerpt_id, _, buffer_snapshot) =
 953                        snapshot.buffer_snapshot.as_singleton().unwrap();
 954
 955                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
 956                    let multibuffer_anchor = snapshot
 957                        .buffer_snapshot
 958                        .anchor_in_excerpt(*excerpt_id, text_anchor);
 959                    message_editor.edit(
 960                        [(
 961                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
 962                            format!("{replacement_text} "),
 963                        )],
 964                        cx,
 965                    );
 966                    (*excerpt_id, text_anchor, multibuffer_anchor)
 967                });
 968
 969            let content_len = replacement_text.len();
 970            let Some(start_anchor) = multibuffer_anchor else {
 971                continue;
 972            };
 973            let end_anchor = self.editor.update(cx, |editor, cx| {
 974                let snapshot = editor.buffer().read(cx).snapshot(cx);
 975                snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
 976            });
 977            let image = Arc::new(image);
 978            let Some((crease_id, tx)) = insert_crease_for_mention(
 979                excerpt_id,
 980                text_anchor,
 981                content_len,
 982                MentionUri::PastedImage.name().into(),
 983                IconName::Image.path().into(),
 984                Some(Task::ready(Ok(image.clone())).shared()),
 985                self.editor.clone(),
 986                window,
 987                cx,
 988            ) else {
 989                continue;
 990            };
 991            let task = cx
 992                .spawn_in(window, {
 993                    async move |_, cx| {
 994                        let format = image.format;
 995                        let image = cx
 996                            .update(|_, cx| LanguageModelImage::from_image(image, cx))
 997                            .map_err(|e| e.to_string())?
 998                            .await;
 999                        drop(tx);
1000                        if let Some(image) = image {
1001                            Ok(Mention::Image(MentionImage {
1002                                data: image.source,
1003                                format,
1004                            }))
1005                        } else {
1006                            Err("Failed to convert image".into())
1007                        }
1008                    }
1009                })
1010                .shared();
1011
1012            self.mention_set
1013                .mentions
1014                .insert(crease_id, (MentionUri::PastedImage, task.clone()));
1015
1016            cx.spawn_in(window, async move |this, cx| {
1017                if task.await.notify_async_err(cx).is_none() {
1018                    this.update(cx, |this, cx| {
1019                        this.editor.update(cx, |editor, cx| {
1020                            editor.edit([(start_anchor..end_anchor, "")], cx);
1021                        });
1022                        this.mention_set.mentions.remove(&crease_id);
1023                    })
1024                    .ok();
1025                }
1026            })
1027            .detach();
1028        }
1029    }
1030
1031    pub fn insert_dragged_files(
1032        &mut self,
1033        paths: Vec<project::ProjectPath>,
1034        added_worktrees: Vec<Entity<Worktree>>,
1035        window: &mut Window,
1036        cx: &mut Context<Self>,
1037    ) {
1038        let buffer = self.editor.read(cx).buffer().clone();
1039        let Some(buffer) = buffer.read(cx).as_singleton() else {
1040            return;
1041        };
1042        let mut tasks = Vec::new();
1043        for path in paths {
1044            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
1045                continue;
1046            };
1047            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
1048                continue;
1049            };
1050            let path_prefix = abs_path
1051                .file_name()
1052                .unwrap_or(path.path.as_os_str())
1053                .display()
1054                .to_string();
1055            let (file_name, _) =
1056                crate::context_picker::file_context_picker::extract_file_name_and_directory(
1057                    &path.path,
1058                    &path_prefix,
1059                );
1060
1061            let uri = if entry.is_dir() {
1062                MentionUri::Directory { abs_path }
1063            } else {
1064                MentionUri::File { abs_path }
1065            };
1066
1067            let new_text = format!("{} ", uri.as_link());
1068            let content_len = new_text.len() - 1;
1069
1070            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
1071
1072            self.editor.update(cx, |message_editor, cx| {
1073                message_editor.edit(
1074                    [(
1075                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
1076                        new_text,
1077                    )],
1078                    cx,
1079                );
1080            });
1081            tasks.push(self.confirm_mention_completion(
1082                file_name,
1083                anchor,
1084                content_len,
1085                uri,
1086                window,
1087                cx,
1088            ));
1089        }
1090        cx.spawn(async move |_, _| {
1091            join_all(tasks).await;
1092            drop(added_worktrees);
1093        })
1094        .detach();
1095    }
1096
1097    pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1098        let buffer = self.editor.read(cx).buffer().clone();
1099        let Some(buffer) = buffer.read(cx).as_singleton() else {
1100            return;
1101        };
1102        let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
1103        let Some(workspace) = self.workspace.upgrade() else {
1104            return;
1105        };
1106        let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
1107            ContextPickerAction::AddSelections,
1108            anchor..anchor,
1109            cx.weak_entity(),
1110            &workspace,
1111            cx,
1112        ) else {
1113            return;
1114        };
1115        self.editor.update(cx, |message_editor, cx| {
1116            message_editor.edit(
1117                [(
1118                    multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
1119                    completion.new_text,
1120                )],
1121                cx,
1122            );
1123        });
1124        if let Some(confirm) = completion.confirm {
1125            confirm(CompletionIntent::Complete, window, cx);
1126        }
1127    }
1128
1129    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1130        self.editor.update(cx, |message_editor, cx| {
1131            message_editor.set_read_only(read_only);
1132            cx.notify()
1133        })
1134    }
1135
1136    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
1137        self.editor.update(cx, |editor, cx| {
1138            editor.set_mode(mode);
1139            cx.notify()
1140        });
1141    }
1142
1143    pub fn set_message(
1144        &mut self,
1145        message: Vec<acp::ContentBlock>,
1146        window: &mut Window,
1147        cx: &mut Context<Self>,
1148    ) {
1149        self.clear(window, cx);
1150
1151        let mut text = String::new();
1152        let mut mentions = Vec::new();
1153
1154        for chunk in message {
1155            match chunk {
1156                acp::ContentBlock::Text(text_content) => {
1157                    text.push_str(&text_content.text);
1158                }
1159                acp::ContentBlock::Resource(acp::EmbeddedResource {
1160                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
1161                    ..
1162                }) => {
1163                    let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else {
1164                        continue;
1165                    };
1166                    let start = text.len();
1167                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1168                    let end = text.len();
1169                    mentions.push((
1170                        start..end,
1171                        mention_uri,
1172                        Mention::Text {
1173                            content: resource.text,
1174                            tracked_buffers: Vec::new(),
1175                        },
1176                    ));
1177                }
1178                acp::ContentBlock::ResourceLink(resource) => {
1179                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
1180                        let start = text.len();
1181                        write!(&mut text, "{}", mention_uri.as_link()).ok();
1182                        let end = text.len();
1183                        mentions.push((start..end, mention_uri, Mention::UriOnly));
1184                    }
1185                }
1186                acp::ContentBlock::Image(acp::ImageContent {
1187                    uri,
1188                    data,
1189                    mime_type,
1190                    annotations: _,
1191                }) => {
1192                    let mention_uri = if let Some(uri) = uri {
1193                        MentionUri::parse(&uri)
1194                    } else {
1195                        Ok(MentionUri::PastedImage)
1196                    };
1197                    let Some(mention_uri) = mention_uri.log_err() else {
1198                        continue;
1199                    };
1200                    let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
1201                        log::error!("failed to parse MIME type for image: {mime_type:?}");
1202                        continue;
1203                    };
1204                    let start = text.len();
1205                    write!(&mut text, "{}", mention_uri.as_link()).ok();
1206                    let end = text.len();
1207                    mentions.push((
1208                        start..end,
1209                        mention_uri,
1210                        Mention::Image(MentionImage {
1211                            data: data.into(),
1212                            format,
1213                        }),
1214                    ));
1215                }
1216                acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
1217            }
1218        }
1219
1220        let snapshot = self.editor.update(cx, |editor, cx| {
1221            editor.set_text(text, window, cx);
1222            editor.buffer().read(cx).snapshot(cx)
1223        });
1224
1225        for (range, mention_uri, mention) in mentions {
1226            let anchor = snapshot.anchor_before(range.start);
1227            let Some((crease_id, tx)) = insert_crease_for_mention(
1228                anchor.excerpt_id,
1229                anchor.text_anchor,
1230                range.end - range.start,
1231                mention_uri.name().into(),
1232                mention_uri.icon_path(cx),
1233                None,
1234                self.editor.clone(),
1235                window,
1236                cx,
1237            ) else {
1238                continue;
1239            };
1240            drop(tx);
1241
1242            self.mention_set.mentions.insert(
1243                crease_id,
1244                (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
1245            );
1246        }
1247        cx.notify();
1248    }
1249
1250    pub fn text(&self, cx: &App) -> String {
1251        self.editor.read(cx).text(cx)
1252    }
1253
1254    #[cfg(test)]
1255    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
1256        self.editor.update(cx, |editor, cx| {
1257            editor.set_text(text, window, cx);
1258        });
1259    }
1260}
1261
1262fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
1263    let mut output = String::new();
1264    for (_relative_path, full_path, content) in entries {
1265        let fence = codeblock_fence_for_path(Some(&full_path), None);
1266        write!(output, "\n{fence}\n{content}\n```").unwrap();
1267    }
1268    output
1269}
1270
1271impl Focusable for MessageEditor {
1272    fn focus_handle(&self, cx: &App) -> FocusHandle {
1273        self.editor.focus_handle(cx)
1274    }
1275}
1276
1277impl Render for MessageEditor {
1278    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1279        div()
1280            .key_context("MessageEditor")
1281            .on_action(cx.listener(Self::send))
1282            .on_action(cx.listener(Self::cancel))
1283            .capture_action(cx.listener(Self::paste))
1284            .flex_1()
1285            .child({
1286                let settings = ThemeSettings::get_global(cx);
1287                let font_size = TextSize::Small
1288                    .rems(cx)
1289                    .to_pixels(settings.agent_font_size(cx));
1290                let line_height = settings.buffer_line_height.value() * font_size;
1291
1292                let text_style = TextStyle {
1293                    color: cx.theme().colors().text,
1294                    font_family: settings.buffer_font.family.clone(),
1295                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
1296                    font_features: settings.buffer_font.features.clone(),
1297                    font_size: font_size.into(),
1298                    line_height: line_height.into(),
1299                    ..Default::default()
1300                };
1301
1302                EditorElement::new(
1303                    &self.editor,
1304                    EditorStyle {
1305                        background: cx.theme().colors().editor_background,
1306                        local_player: cx.theme().players().local(),
1307                        text: text_style,
1308                        syntax: cx.theme().syntax().clone(),
1309                        inlay_hints_style: editor::make_inlay_hints_style(cx),
1310                        ..Default::default()
1311                    },
1312                )
1313            })
1314    }
1315}
1316
1317pub(crate) fn insert_crease_for_mention(
1318    excerpt_id: ExcerptId,
1319    anchor: text::Anchor,
1320    content_len: usize,
1321    crease_label: SharedString,
1322    crease_icon: SharedString,
1323    // abs_path: Option<Arc<Path>>,
1324    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1325    editor: Entity<Editor>,
1326    window: &mut Window,
1327    cx: &mut App,
1328) -> Option<(CreaseId, postage::barrier::Sender)> {
1329    let (tx, rx) = postage::barrier::channel();
1330
1331    let crease_id = editor.update(cx, |editor, cx| {
1332        let snapshot = editor.buffer().read(cx).snapshot(cx);
1333
1334        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
1335
1336        let start = start.bias_right(&snapshot);
1337        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
1338
1339        let placeholder = FoldPlaceholder {
1340            render: render_mention_fold_button(
1341                crease_label,
1342                crease_icon,
1343                start..end,
1344                rx,
1345                image,
1346                cx.weak_entity(),
1347                cx,
1348            ),
1349            merge_adjacent: false,
1350            ..Default::default()
1351        };
1352
1353        let crease = Crease::Inline {
1354            range: start..end,
1355            placeholder,
1356            render_toggle: None,
1357            render_trailer: None,
1358            metadata: None,
1359        };
1360
1361        let ids = editor.insert_creases(vec![crease.clone()], cx);
1362        editor.fold_creases(vec![crease], false, window, cx);
1363
1364        Some(ids[0])
1365    })?;
1366
1367    Some((crease_id, tx))
1368}
1369
1370fn render_mention_fold_button(
1371    label: SharedString,
1372    icon: SharedString,
1373    range: Range<Anchor>,
1374    mut loading_finished: postage::barrier::Receiver,
1375    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1376    editor: WeakEntity<Editor>,
1377    cx: &mut App,
1378) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
1379    let loading = cx.new(|cx| {
1380        let loading = cx.spawn(async move |this, cx| {
1381            loading_finished.recv().await;
1382            this.update(cx, |this: &mut LoadingContext, cx| {
1383                this.loading = None;
1384                cx.notify();
1385            })
1386            .ok();
1387        });
1388        LoadingContext {
1389            id: cx.entity_id(),
1390            label,
1391            icon,
1392            range,
1393            editor,
1394            loading: Some(loading),
1395            image: image_task.clone(),
1396        }
1397    });
1398    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
1399}
1400
1401struct LoadingContext {
1402    id: EntityId,
1403    label: SharedString,
1404    icon: SharedString,
1405    range: Range<Anchor>,
1406    editor: WeakEntity<Editor>,
1407    loading: Option<Task<()>>,
1408    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
1409}
1410
1411impl Render for LoadingContext {
1412    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1413        let is_in_text_selection = self
1414            .editor
1415            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
1416            .unwrap_or_default();
1417        ButtonLike::new(("loading-context", self.id))
1418            .style(ButtonStyle::Filled)
1419            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1420            .toggle_state(is_in_text_selection)
1421            .when_some(self.image.clone(), |el, image_task| {
1422                el.hoverable_tooltip(move |_, cx| {
1423                    let image = image_task.peek().cloned().transpose().ok().flatten();
1424                    let image_task = image_task.clone();
1425                    cx.new::<ImageHover>(|cx| ImageHover {
1426                        image,
1427                        _task: cx.spawn(async move |this, cx| {
1428                            if let Ok(image) = image_task.clone().await {
1429                                this.update(cx, |this, cx| {
1430                                    if this.image.replace(image).is_none() {
1431                                        cx.notify();
1432                                    }
1433                                })
1434                                .ok();
1435                            }
1436                        }),
1437                    })
1438                    .into()
1439                })
1440            })
1441            .child(
1442                h_flex()
1443                    .gap_1()
1444                    .child(
1445                        Icon::from_path(self.icon.clone())
1446                            .size(IconSize::XSmall)
1447                            .color(Color::Muted),
1448                    )
1449                    .child(
1450                        Label::new(self.label.clone())
1451                            .size(LabelSize::Small)
1452                            .buffer_font(cx)
1453                            .single_line(),
1454                    )
1455                    .map(|el| {
1456                        if self.loading.is_some() {
1457                            el.with_animation(
1458                                "loading-context-crease",
1459                                Animation::new(Duration::from_secs(2))
1460                                    .repeat()
1461                                    .with_easing(pulsating_between(0.4, 0.8)),
1462                                |label, delta| label.opacity(delta),
1463                            )
1464                            .into_any()
1465                        } else {
1466                            el.into_any()
1467                        }
1468                    }),
1469            )
1470    }
1471}
1472
1473struct ImageHover {
1474    image: Option<Arc<Image>>,
1475    _task: Task<()>,
1476}
1477
1478impl Render for ImageHover {
1479    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1480        if let Some(image) = self.image.clone() {
1481            gpui::img(image).max_w_96().max_h_96().into_any_element()
1482        } else {
1483            gpui::Empty.into_any_element()
1484        }
1485    }
1486}
1487
1488#[derive(Debug, Clone, Eq, PartialEq)]
1489pub enum Mention {
1490    Text {
1491        content: String,
1492        tracked_buffers: Vec<Entity<Buffer>>,
1493    },
1494    Image(MentionImage),
1495    UriOnly,
1496}
1497
1498#[derive(Clone, Debug, Eq, PartialEq)]
1499pub struct MentionImage {
1500    pub data: SharedString,
1501    pub format: ImageFormat,
1502}
1503
1504#[derive(Default)]
1505pub struct MentionSet {
1506    mentions: HashMap<CreaseId, (MentionUri, Shared<Task<Result<Mention, String>>>)>,
1507}
1508
1509impl MentionSet {
1510    fn contents(
1511        &self,
1512        prompt_capabilities: &acp::PromptCapabilities,
1513        cx: &mut App,
1514    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
1515        if !prompt_capabilities.embedded_context {
1516            let mentions = self
1517                .mentions
1518                .iter()
1519                .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
1520                .collect();
1521
1522            return Task::ready(Ok(mentions));
1523        }
1524
1525        let mentions = self.mentions.clone();
1526        cx.spawn(async move |_cx| {
1527            let mut contents = HashMap::default();
1528            for (crease_id, (mention_uri, task)) in mentions {
1529                contents.insert(
1530                    crease_id,
1531                    (mention_uri, task.await.map_err(|e| anyhow!("{e}"))?),
1532                );
1533            }
1534            Ok(contents)
1535        })
1536    }
1537
1538    fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
1539        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
1540            if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
1541                self.mentions.remove(&crease_id);
1542            }
1543        }
1544    }
1545}
1546
1547pub struct MessageEditorAddon {}
1548
1549impl MessageEditorAddon {
1550    pub fn new() -> Self {
1551        Self {}
1552    }
1553}
1554
1555impl Addon for MessageEditorAddon {
1556    fn to_any(&self) -> &dyn std::any::Any {
1557        self
1558    }
1559
1560    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1561        Some(self)
1562    }
1563
1564    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1565        let settings = agent_settings::AgentSettings::get_global(cx);
1566        if settings.use_modifier_to_send {
1567            key_context.add("use_modifier_to_send");
1568        }
1569    }
1570}
1571
1572#[cfg(test)]
1573mod tests {
1574    use std::{
1575        cell::{Cell, RefCell},
1576        ops::Range,
1577        path::Path,
1578        rc::Rc,
1579        sync::Arc,
1580    };
1581
1582    use acp_thread::MentionUri;
1583    use agent_client_protocol as acp;
1584    use agent2::HistoryStore;
1585    use assistant_context::ContextStore;
1586    use assistant_tool::outline;
1587    use editor::{AnchorRangeExt as _, Editor, EditorMode};
1588    use fs::FakeFs;
1589    use futures::StreamExt as _;
1590    use gpui::{
1591        AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1592    };
1593    use lsp::{CompletionContext, CompletionTriggerKind};
1594    use project::{CompletionIntent, Project, ProjectPath};
1595    use serde_json::json;
1596    use text::Point;
1597    use ui::{App, Context, IntoElement, Render, SharedString, Window};
1598    use util::{path, uri};
1599    use workspace::{AppState, Item, Workspace};
1600
1601    use crate::acp::{
1602        message_editor::{Mention, MessageEditor},
1603        thread_view::tests::init_test,
1604    };
1605
1606    #[gpui::test]
1607    async fn test_at_mention_removal(cx: &mut TestAppContext) {
1608        init_test(cx);
1609
1610        let fs = FakeFs::new(cx.executor());
1611        fs.insert_tree("/project", json!({"file": ""})).await;
1612        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1613
1614        let (workspace, cx) =
1615            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1616
1617        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1618        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1619
1620        let message_editor = cx.update(|window, cx| {
1621            cx.new(|cx| {
1622                MessageEditor::new(
1623                    workspace.downgrade(),
1624                    project.clone(),
1625                    history_store.clone(),
1626                    None,
1627                    Default::default(),
1628                    Default::default(),
1629                    "Test Agent".into(),
1630                    "Test",
1631                    EditorMode::AutoHeight {
1632                        min_lines: 1,
1633                        max_lines: None,
1634                    },
1635                    window,
1636                    cx,
1637                )
1638            })
1639        });
1640        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1641
1642        cx.run_until_parked();
1643
1644        let excerpt_id = editor.update(cx, |editor, cx| {
1645            editor
1646                .buffer()
1647                .read(cx)
1648                .excerpt_ids()
1649                .into_iter()
1650                .next()
1651                .unwrap()
1652        });
1653        let completions = editor.update_in(cx, |editor, window, cx| {
1654            editor.set_text("Hello @file ", window, cx);
1655            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1656            let completion_provider = editor.completion_provider().unwrap();
1657            completion_provider.completions(
1658                excerpt_id,
1659                &buffer,
1660                text::Anchor::MAX,
1661                CompletionContext {
1662                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1663                    trigger_character: Some("@".into()),
1664                },
1665                window,
1666                cx,
1667            )
1668        });
1669        let [_, completion]: [_; 2] = completions
1670            .await
1671            .unwrap()
1672            .into_iter()
1673            .flat_map(|response| response.completions)
1674            .collect::<Vec<_>>()
1675            .try_into()
1676            .unwrap();
1677
1678        editor.update_in(cx, |editor, window, cx| {
1679            let snapshot = editor.buffer().read(cx).snapshot(cx);
1680            let start = snapshot
1681                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1682                .unwrap();
1683            let end = snapshot
1684                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1685                .unwrap();
1686            editor.edit([(start..end, completion.new_text)], cx);
1687            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1688        });
1689
1690        cx.run_until_parked();
1691
1692        // Backspace over the inserted crease (and the following space).
1693        editor.update_in(cx, |editor, window, cx| {
1694            editor.backspace(&Default::default(), window, cx);
1695            editor.backspace(&Default::default(), window, cx);
1696        });
1697
1698        let (content, _) = message_editor
1699            .update(cx, |message_editor, cx| message_editor.contents(cx))
1700            .await
1701            .unwrap();
1702
1703        // We don't send a resource link for the deleted crease.
1704        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1705    }
1706
1707    #[gpui::test]
1708    async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
1709        init_test(cx);
1710        let fs = FakeFs::new(cx.executor());
1711        fs.insert_tree(
1712            "/test",
1713            json!({
1714                ".zed": {
1715                    "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
1716                },
1717                "src": {
1718                    "main.rs": "fn main() {}",
1719                },
1720            }),
1721        )
1722        .await;
1723
1724        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1725        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1726        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1727        let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
1728        // Start with no available commands - simulating Claude which doesn't support slash commands
1729        let available_commands = Rc::new(RefCell::new(vec![]));
1730
1731        let (workspace, cx) =
1732            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1733        let workspace_handle = workspace.downgrade();
1734        let message_editor = workspace.update_in(cx, |_, window, cx| {
1735            cx.new(|cx| {
1736                MessageEditor::new(
1737                    workspace_handle.clone(),
1738                    project.clone(),
1739                    history_store.clone(),
1740                    None,
1741                    prompt_capabilities.clone(),
1742                    available_commands.clone(),
1743                    "Claude Code".into(),
1744                    "Test",
1745                    EditorMode::AutoHeight {
1746                        min_lines: 1,
1747                        max_lines: None,
1748                    },
1749                    window,
1750                    cx,
1751                )
1752            })
1753        });
1754        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1755
1756        // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
1757        editor.update_in(cx, |editor, window, cx| {
1758            editor.set_text("/file test.txt", window, cx);
1759        });
1760
1761        let contents_result = message_editor
1762            .update(cx, |message_editor, cx| message_editor.contents(cx))
1763            .await;
1764
1765        // Should fail because available_commands is empty (no commands supported)
1766        assert!(contents_result.is_err());
1767        let error_message = contents_result.unwrap_err().to_string();
1768        assert!(error_message.contains("not supported by Claude Code"));
1769        assert!(error_message.contains("Available commands: none"));
1770
1771        // Now simulate Claude providing its list of available commands (which doesn't include file)
1772        available_commands.replace(vec![acp::AvailableCommand {
1773            name: "help".to_string(),
1774            description: "Get help".to_string(),
1775            input: 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(Cell::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            },
1897            acp::AvailableCommand {
1898                name: "say-hello".to_string(),
1899                description: "Say hello to whoever you want".to_string(),
1900                input: Some(acp::AvailableCommandInput::Unstructured {
1901                    hint: "<name>".to_string(),
1902                }),
1903            },
1904        ]));
1905
1906        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1907            let workspace_handle = cx.weak_entity();
1908            let message_editor = cx.new(|cx| {
1909                MessageEditor::new(
1910                    workspace_handle,
1911                    project.clone(),
1912                    history_store.clone(),
1913                    None,
1914                    prompt_capabilities.clone(),
1915                    available_commands.clone(),
1916                    "Test Agent".into(),
1917                    "Test",
1918                    EditorMode::AutoHeight {
1919                        max_lines: None,
1920                        min_lines: 1,
1921                    },
1922                    window,
1923                    cx,
1924                )
1925            });
1926            workspace.active_pane().update(cx, |pane, cx| {
1927                pane.add_item(
1928                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1929                    true,
1930                    true,
1931                    None,
1932                    window,
1933                    cx,
1934                );
1935            });
1936            message_editor.read(cx).focus_handle(cx).focus(window);
1937            message_editor.read(cx).editor().clone()
1938        });
1939
1940        cx.simulate_input("/");
1941
1942        editor.update_in(&mut cx, |editor, window, cx| {
1943            assert_eq!(editor.text(cx), "/");
1944            assert!(editor.has_visible_completions_menu());
1945
1946            assert_eq!(
1947                current_completion_labels_with_documentation(editor),
1948                &[
1949                    ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
1950                    ("say-hello".into(), "Say hello to whoever you want".into())
1951                ]
1952            );
1953            editor.set_text("", window, cx);
1954        });
1955
1956        cx.simulate_input("/qui");
1957
1958        editor.update_in(&mut cx, |editor, window, cx| {
1959            assert_eq!(editor.text(cx), "/qui");
1960            assert!(editor.has_visible_completions_menu());
1961
1962            assert_eq!(
1963                current_completion_labels_with_documentation(editor),
1964                &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
1965            );
1966            editor.set_text("", window, cx);
1967        });
1968
1969        editor.update_in(&mut cx, |editor, window, cx| {
1970            assert!(editor.has_visible_completions_menu());
1971            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1972        });
1973
1974        cx.run_until_parked();
1975
1976        editor.update_in(&mut cx, |editor, window, cx| {
1977            assert_eq!(editor.display_text(cx), "/quick-math ");
1978            assert!(!editor.has_visible_completions_menu());
1979            editor.set_text("", window, cx);
1980        });
1981
1982        cx.simulate_input("/say");
1983
1984        editor.update_in(&mut cx, |editor, _window, cx| {
1985            assert_eq!(editor.display_text(cx), "/say");
1986            assert!(editor.has_visible_completions_menu());
1987
1988            assert_eq!(
1989                current_completion_labels_with_documentation(editor),
1990                &[("say-hello".into(), "Say hello to whoever you want".into())]
1991            );
1992        });
1993
1994        editor.update_in(&mut cx, |editor, window, cx| {
1995            assert!(editor.has_visible_completions_menu());
1996            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1997        });
1998
1999        cx.run_until_parked();
2000
2001        editor.update_in(&mut cx, |editor, _window, cx| {
2002            assert_eq!(editor.text(cx), "/say-hello ");
2003            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2004            assert!(editor.has_visible_completions_menu());
2005
2006            assert_eq!(
2007                current_completion_labels_with_documentation(editor),
2008                &[("say-hello".into(), "Say hello to whoever you want".into())]
2009            );
2010        });
2011
2012        cx.simulate_input("GPT5");
2013
2014        editor.update_in(&mut cx, |editor, window, cx| {
2015            assert!(editor.has_visible_completions_menu());
2016            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2017        });
2018
2019        cx.run_until_parked();
2020
2021        editor.update_in(&mut cx, |editor, window, cx| {
2022            assert_eq!(editor.text(cx), "/say-hello GPT5");
2023            assert_eq!(editor.display_text(cx), "/say-hello GPT5");
2024            assert!(!editor.has_visible_completions_menu());
2025
2026            // Delete argument
2027            for _ in 0..4 {
2028                editor.backspace(&editor::actions::Backspace, window, cx);
2029            }
2030        });
2031
2032        cx.run_until_parked();
2033
2034        editor.update_in(&mut cx, |editor, window, cx| {
2035            assert_eq!(editor.text(cx), "/say-hello ");
2036            // Hint is visible because argument was deleted
2037            assert_eq!(editor.display_text(cx), "/say-hello <name>");
2038
2039            // Delete last command letter
2040            editor.backspace(&editor::actions::Backspace, window, cx);
2041            editor.backspace(&editor::actions::Backspace, window, cx);
2042        });
2043
2044        cx.run_until_parked();
2045
2046        editor.update_in(&mut cx, |editor, _window, cx| {
2047            // Hint goes away once command no longer matches an available one
2048            assert_eq!(editor.text(cx), "/say-hell");
2049            assert_eq!(editor.display_text(cx), "/say-hell");
2050            assert!(!editor.has_visible_completions_menu());
2051        });
2052    }
2053
2054    #[gpui::test]
2055    async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
2056        init_test(cx);
2057
2058        let app_state = cx.update(AppState::test);
2059
2060        cx.update(|cx| {
2061            language::init(cx);
2062            editor::init(cx);
2063            workspace::init(app_state.clone(), cx);
2064            Project::init_settings(cx);
2065        });
2066
2067        app_state
2068            .fs
2069            .as_fake()
2070            .insert_tree(
2071                path!("/dir"),
2072                json!({
2073                    "editor": "",
2074                    "a": {
2075                        "one.txt": "1",
2076                        "two.txt": "2",
2077                        "three.txt": "3",
2078                        "four.txt": "4"
2079                    },
2080                    "b": {
2081                        "five.txt": "5",
2082                        "six.txt": "6",
2083                        "seven.txt": "7",
2084                        "eight.txt": "8",
2085                    },
2086                    "x.png": "",
2087                }),
2088            )
2089            .await;
2090
2091        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
2092        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
2093        let workspace = window.root(cx).unwrap();
2094
2095        let worktree = project.update(cx, |project, cx| {
2096            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
2097            assert_eq!(worktrees.len(), 1);
2098            worktrees.pop().unwrap()
2099        });
2100        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2101
2102        let mut cx = VisualTestContext::from_window(*window, cx);
2103
2104        let paths = vec![
2105            path!("a/one.txt"),
2106            path!("a/two.txt"),
2107            path!("a/three.txt"),
2108            path!("a/four.txt"),
2109            path!("b/five.txt"),
2110            path!("b/six.txt"),
2111            path!("b/seven.txt"),
2112            path!("b/eight.txt"),
2113        ];
2114
2115        let mut opened_editors = Vec::new();
2116        for path in paths {
2117            let buffer = workspace
2118                .update_in(&mut cx, |workspace, window, cx| {
2119                    workspace.open_path(
2120                        ProjectPath {
2121                            worktree_id,
2122                            path: Path::new(path).into(),
2123                        },
2124                        None,
2125                        false,
2126                        window,
2127                        cx,
2128                    )
2129                })
2130                .await
2131                .unwrap();
2132            opened_editors.push(buffer);
2133        }
2134
2135        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2136        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2137        let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
2138
2139        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
2140            let workspace_handle = cx.weak_entity();
2141            let message_editor = cx.new(|cx| {
2142                MessageEditor::new(
2143                    workspace_handle,
2144                    project.clone(),
2145                    history_store.clone(),
2146                    None,
2147                    prompt_capabilities.clone(),
2148                    Default::default(),
2149                    "Test Agent".into(),
2150                    "Test",
2151                    EditorMode::AutoHeight {
2152                        max_lines: None,
2153                        min_lines: 1,
2154                    },
2155                    window,
2156                    cx,
2157                )
2158            });
2159            workspace.active_pane().update(cx, |pane, cx| {
2160                pane.add_item(
2161                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
2162                    true,
2163                    true,
2164                    None,
2165                    window,
2166                    cx,
2167                );
2168            });
2169            message_editor.read(cx).focus_handle(cx).focus(window);
2170            let editor = message_editor.read(cx).editor().clone();
2171            (message_editor, editor)
2172        });
2173
2174        cx.simulate_input("Lorem @");
2175
2176        editor.update_in(&mut cx, |editor, window, cx| {
2177            assert_eq!(editor.text(cx), "Lorem @");
2178            assert!(editor.has_visible_completions_menu());
2179
2180            assert_eq!(
2181                current_completion_labels(editor),
2182                &[
2183                    "eight.txt dir/b/",
2184                    "seven.txt dir/b/",
2185                    "six.txt dir/b/",
2186                    "five.txt dir/b/",
2187                ]
2188            );
2189            editor.set_text("", window, cx);
2190        });
2191
2192        prompt_capabilities.set(acp::PromptCapabilities {
2193            image: true,
2194            audio: true,
2195            embedded_context: true,
2196        });
2197
2198        cx.simulate_input("Lorem ");
2199
2200        editor.update(&mut cx, |editor, cx| {
2201            assert_eq!(editor.text(cx), "Lorem ");
2202            assert!(!editor.has_visible_completions_menu());
2203        });
2204
2205        cx.simulate_input("@");
2206
2207        editor.update(&mut cx, |editor, cx| {
2208            assert_eq!(editor.text(cx), "Lorem @");
2209            assert!(editor.has_visible_completions_menu());
2210            assert_eq!(
2211                current_completion_labels(editor),
2212                &[
2213                    "eight.txt dir/b/",
2214                    "seven.txt dir/b/",
2215                    "six.txt dir/b/",
2216                    "five.txt dir/b/",
2217                    "Files & Directories",
2218                    "Symbols",
2219                    "Threads",
2220                    "Fetch"
2221                ]
2222            );
2223        });
2224
2225        // Select and confirm "File"
2226        editor.update_in(&mut cx, |editor, window, cx| {
2227            assert!(editor.has_visible_completions_menu());
2228            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2229            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2230            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2231            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
2232            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2233        });
2234
2235        cx.run_until_parked();
2236
2237        editor.update(&mut cx, |editor, cx| {
2238            assert_eq!(editor.text(cx), "Lorem @file ");
2239            assert!(editor.has_visible_completions_menu());
2240        });
2241
2242        cx.simulate_input("one");
2243
2244        editor.update(&mut cx, |editor, cx| {
2245            assert_eq!(editor.text(cx), "Lorem @file one");
2246            assert!(editor.has_visible_completions_menu());
2247            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
2248        });
2249
2250        editor.update_in(&mut cx, |editor, window, cx| {
2251            assert!(editor.has_visible_completions_menu());
2252            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2253        });
2254
2255        let url_one = uri!("file:///dir/a/one.txt");
2256        editor.update(&mut cx, |editor, cx| {
2257            let text = editor.text(cx);
2258            assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2259            assert!(!editor.has_visible_completions_menu());
2260            assert_eq!(fold_ranges(editor, cx).len(), 1);
2261        });
2262
2263        let all_prompt_capabilities = acp::PromptCapabilities {
2264            image: true,
2265            audio: true,
2266            embedded_context: true,
2267        };
2268
2269        let contents = message_editor
2270            .update(&mut cx, |message_editor, cx| {
2271                message_editor
2272                    .mention_set()
2273                    .contents(&all_prompt_capabilities, cx)
2274            })
2275            .await
2276            .unwrap()
2277            .into_values()
2278            .collect::<Vec<_>>();
2279
2280        {
2281            let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
2282                panic!("Unexpected mentions");
2283            };
2284            pretty_assertions::assert_eq!(content, "1");
2285            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
2286        }
2287
2288        let contents = message_editor
2289            .update(&mut cx, |message_editor, cx| {
2290                message_editor
2291                    .mention_set()
2292                    .contents(&acp::PromptCapabilities::default(), cx)
2293            })
2294            .await
2295            .unwrap()
2296            .into_values()
2297            .collect::<Vec<_>>();
2298
2299        {
2300            let [(uri, Mention::UriOnly)] = contents.as_slice() else {
2301                panic!("Unexpected mentions");
2302            };
2303            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
2304        }
2305
2306        cx.simulate_input(" ");
2307
2308        editor.update(&mut cx, |editor, cx| {
2309            let text = editor.text(cx);
2310            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  "));
2311            assert!(!editor.has_visible_completions_menu());
2312            assert_eq!(fold_ranges(editor, cx).len(), 1);
2313        });
2314
2315        cx.simulate_input("Ipsum ");
2316
2317        editor.update(&mut cx, |editor, cx| {
2318            let text = editor.text(cx);
2319            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum "),);
2320            assert!(!editor.has_visible_completions_menu());
2321            assert_eq!(fold_ranges(editor, cx).len(), 1);
2322        });
2323
2324        cx.simulate_input("@file ");
2325
2326        editor.update(&mut cx, |editor, cx| {
2327            let text = editor.text(cx);
2328            assert_eq!(text, format!("Lorem [@one.txt]({url_one})  Ipsum @file "),);
2329            assert!(editor.has_visible_completions_menu());
2330            assert_eq!(fold_ranges(editor, cx).len(), 1);
2331        });
2332
2333        editor.update_in(&mut cx, |editor, window, cx| {
2334            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2335        });
2336
2337        cx.run_until_parked();
2338
2339        let contents = message_editor
2340            .update(&mut cx, |message_editor, cx| {
2341                message_editor
2342                    .mention_set()
2343                    .contents(&all_prompt_capabilities, cx)
2344            })
2345            .await
2346            .unwrap()
2347            .into_values()
2348            .collect::<Vec<_>>();
2349
2350        let url_eight = uri!("file:///dir/b/eight.txt");
2351
2352        {
2353            let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2354                panic!("Unexpected mentions");
2355            };
2356            pretty_assertions::assert_eq!(content, "8");
2357            pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
2358        }
2359
2360        editor.update(&mut cx, |editor, cx| {
2361            assert_eq!(
2362                editor.text(cx),
2363                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) ")
2364            );
2365            assert!(!editor.has_visible_completions_menu());
2366            assert_eq!(fold_ranges(editor, cx).len(), 2);
2367        });
2368
2369        let plain_text_language = Arc::new(language::Language::new(
2370            language::LanguageConfig {
2371                name: "Plain Text".into(),
2372                matcher: language::LanguageMatcher {
2373                    path_suffixes: vec!["txt".to_string()],
2374                    ..Default::default()
2375                },
2376                ..Default::default()
2377            },
2378            None,
2379        ));
2380
2381        // Register the language and fake LSP
2382        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2383        language_registry.add(plain_text_language);
2384
2385        let mut fake_language_servers = language_registry.register_fake_lsp(
2386            "Plain Text",
2387            language::FakeLspAdapter {
2388                capabilities: lsp::ServerCapabilities {
2389                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2390                    ..Default::default()
2391                },
2392                ..Default::default()
2393            },
2394        );
2395
2396        // Open the buffer to trigger LSP initialization
2397        let buffer = project
2398            .update(&mut cx, |project, cx| {
2399                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2400            })
2401            .await
2402            .unwrap();
2403
2404        // Register the buffer with language servers
2405        let _handle = project.update(&mut cx, |project, cx| {
2406            project.register_buffer_with_language_servers(&buffer, cx)
2407        });
2408
2409        cx.run_until_parked();
2410
2411        let fake_language_server = fake_language_servers.next().await.unwrap();
2412        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2413            move |_, _| async move {
2414                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2415                    #[allow(deprecated)]
2416                    lsp::SymbolInformation {
2417                        name: "MySymbol".into(),
2418                        location: lsp::Location {
2419                            uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2420                            range: lsp::Range::new(
2421                                lsp::Position::new(0, 0),
2422                                lsp::Position::new(0, 1),
2423                            ),
2424                        },
2425                        kind: lsp::SymbolKind::CONSTANT,
2426                        tags: None,
2427                        container_name: None,
2428                        deprecated: None,
2429                    },
2430                ])))
2431            },
2432        );
2433
2434        cx.simulate_input("@symbol ");
2435
2436        editor.update(&mut cx, |editor, cx| {
2437            assert_eq!(
2438                editor.text(cx),
2439                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) @symbol ")
2440            );
2441            assert!(editor.has_visible_completions_menu());
2442            assert_eq!(current_completion_labels(editor), &["MySymbol"]);
2443        });
2444
2445        editor.update_in(&mut cx, |editor, window, cx| {
2446            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2447        });
2448
2449        let contents = message_editor
2450            .update(&mut cx, |message_editor, cx| {
2451                message_editor
2452                    .mention_set()
2453                    .contents(&all_prompt_capabilities, cx)
2454            })
2455            .await
2456            .unwrap()
2457            .into_values()
2458            .collect::<Vec<_>>();
2459
2460        {
2461            let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
2462                panic!("Unexpected mentions");
2463            };
2464            pretty_assertions::assert_eq!(content, "1");
2465            pretty_assertions::assert_eq!(
2466                uri,
2467                &format!("{url_one}?symbol=MySymbol#L1:1")
2468                    .parse::<MentionUri>()
2469                    .unwrap()
2470            );
2471        }
2472
2473        cx.run_until_parked();
2474
2475        editor.read_with(&cx, |editor, cx| {
2476            assert_eq!(
2477                editor.text(cx),
2478                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2479            );
2480        });
2481
2482        // Try to mention an "image" file that will fail to load
2483        cx.simulate_input("@file x.png");
2484
2485        editor.update(&mut cx, |editor, cx| {
2486            assert_eq!(
2487                editor.text(cx),
2488                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
2489            );
2490            assert!(editor.has_visible_completions_menu());
2491            assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
2492        });
2493
2494        editor.update_in(&mut cx, |editor, window, cx| {
2495            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2496        });
2497
2498        // Getting the message contents fails
2499        message_editor
2500            .update(&mut cx, |message_editor, cx| {
2501                message_editor
2502                    .mention_set()
2503                    .contents(&all_prompt_capabilities, cx)
2504            })
2505            .await
2506            .expect_err("Should fail to load x.png");
2507
2508        cx.run_until_parked();
2509
2510        // Mention was removed
2511        editor.read_with(&cx, |editor, cx| {
2512            assert_eq!(
2513                editor.text(cx),
2514                format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2515            );
2516        });
2517
2518        // Once more
2519        cx.simulate_input("@file x.png");
2520
2521        editor.update(&mut cx, |editor, cx| {
2522                    assert_eq!(
2523                        editor.text(cx),
2524                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
2525                    );
2526                    assert!(editor.has_visible_completions_menu());
2527                    assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
2528                });
2529
2530        editor.update_in(&mut cx, |editor, window, cx| {
2531            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2532        });
2533
2534        // This time don't immediately get the contents, just let the confirmed completion settle
2535        cx.run_until_parked();
2536
2537        // Mention was removed
2538        editor.read_with(&cx, |editor, cx| {
2539                    assert_eq!(
2540                        editor.text(cx),
2541                        format!("Lorem [@one.txt]({url_one})  Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2542                    );
2543                });
2544
2545        // Now getting the contents succeeds, because the invalid mention was removed
2546        let contents = message_editor
2547            .update(&mut cx, |message_editor, cx| {
2548                message_editor
2549                    .mention_set()
2550                    .contents(&all_prompt_capabilities, cx)
2551            })
2552            .await
2553            .unwrap();
2554        assert_eq!(contents.len(), 3);
2555    }
2556
2557    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2558        let snapshot = editor.buffer().read(cx).snapshot(cx);
2559        editor.display_map.update(cx, |display_map, cx| {
2560            display_map
2561                .snapshot(cx)
2562                .folds_in_range(0..snapshot.len())
2563                .map(|fold| fold.range.to_point(&snapshot))
2564                .collect()
2565        })
2566    }
2567
2568    fn current_completion_labels(editor: &Editor) -> Vec<String> {
2569        let completions = editor.current_completions().expect("Missing completions");
2570        completions
2571            .into_iter()
2572            .map(|completion| completion.label.text)
2573            .collect::<Vec<_>>()
2574    }
2575
2576    fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
2577        let completions = editor.current_completions().expect("Missing completions");
2578        completions
2579            .into_iter()
2580            .map(|completion| {
2581                (
2582                    completion.label.text,
2583                    completion
2584                        .documentation
2585                        .map(|d| d.text().to_string())
2586                        .unwrap_or_default(),
2587                )
2588            })
2589            .collect::<Vec<_>>()
2590    }
2591
2592    #[gpui::test]
2593    async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
2594        init_test(cx);
2595
2596        let fs = FakeFs::new(cx.executor());
2597
2598        // Create a large file that exceeds AUTO_OUTLINE_SIZE
2599        const LINE: &str = "fn example_function() { /* some code */ }\n";
2600        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
2601        assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
2602
2603        // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
2604        let small_content = "fn small_function() { /* small */ }\n";
2605        assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
2606
2607        fs.insert_tree(
2608            "/project",
2609            json!({
2610                "large_file.rs": large_content.clone(),
2611                "small_file.rs": small_content,
2612            }),
2613        )
2614        .await;
2615
2616        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
2617
2618        let (workspace, cx) =
2619            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2620
2621        let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
2622        let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
2623
2624        let message_editor = cx.update(|window, cx| {
2625            cx.new(|cx| {
2626                let editor = MessageEditor::new(
2627                    workspace.downgrade(),
2628                    project.clone(),
2629                    history_store.clone(),
2630                    None,
2631                    Default::default(),
2632                    Default::default(),
2633                    "Test Agent".into(),
2634                    "Test",
2635                    EditorMode::AutoHeight {
2636                        min_lines: 1,
2637                        max_lines: None,
2638                    },
2639                    window,
2640                    cx,
2641                );
2642                // Enable embedded context so files are actually included
2643                editor.prompt_capabilities.set(acp::PromptCapabilities {
2644                    embedded_context: true,
2645                    ..Default::default()
2646                });
2647                editor
2648            })
2649        });
2650
2651        // Test large file mention
2652        // Get the absolute path using the project's worktree
2653        let large_file_abs_path = project.read_with(cx, |project, cx| {
2654            let worktree = project.worktrees(cx).next().unwrap();
2655            let worktree_root = worktree.read(cx).abs_path();
2656            worktree_root.join("large_file.rs")
2657        });
2658        let large_file_task = message_editor.update(cx, |editor, cx| {
2659            editor.confirm_mention_for_file(large_file_abs_path, cx)
2660        });
2661
2662        let large_file_mention = large_file_task.await.unwrap();
2663        match large_file_mention {
2664            Mention::Text { content, .. } => {
2665                // Should contain outline header for large files
2666                assert!(content.contains("File outline for"));
2667                assert!(content.contains("file too large to show full content"));
2668                // Should not contain the full repeated content
2669                assert!(!content.contains(&LINE.repeat(100)));
2670            }
2671            _ => panic!("Expected Text mention for large file"),
2672        }
2673
2674        // Test small file mention
2675        // Get the absolute path using the project's worktree
2676        let small_file_abs_path = project.read_with(cx, |project, cx| {
2677            let worktree = project.worktrees(cx).next().unwrap();
2678            let worktree_root = worktree.read(cx).abs_path();
2679            worktree_root.join("small_file.rs")
2680        });
2681        let small_file_task = message_editor.update(cx, |editor, cx| {
2682            editor.confirm_mention_for_file(small_file_abs_path, cx)
2683        });
2684
2685        let small_file_mention = small_file_task.await.unwrap();
2686        match small_file_mention {
2687            Mention::Text { content, .. } => {
2688                // Should contain the actual content
2689                assert_eq!(content, small_content);
2690                // Should not contain outline header
2691                assert!(!content.contains("File outline for"));
2692            }
2693            _ => panic!("Expected Text mention for small file"),
2694        }
2695    }
2696}