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