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