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