message_editor.rs

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