text_thread_editor.rs

   1use crate::{
   2    agent_panel::AgentType,
   3    language_model_selector::{LanguageModelSelector, language_model_selector},
   4    ui::ModelSelectorTooltip,
   5};
   6use anyhow::Result;
   7use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
   8use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
   9use client::{proto, zed_urls};
  10use collections::{BTreeSet, HashMap, HashSet, hash_map};
  11use editor::{
  12    Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
  13    MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,
  14    actions::{MoveToEndOfLine, Newline, SendReviewToAgent, ShowCompletions},
  15    display_map::{
  16        BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
  17        RenderBlock, ToDisplayPoint,
  18    },
  19    scroll::ScrollOffset,
  20};
  21use editor::{FoldPlaceholder, display_map::CreaseId};
  22use fs::Fs;
  23use futures::FutureExt;
  24use gpui::{
  25    Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
  26    EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
  27    ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
  28    Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
  29    pulsating_between, size,
  30};
  31use language::{
  32    BufferSnapshot, LspAdapterDelegate, ToOffset,
  33    language_settings::{SoftWrap, all_language_settings},
  34};
  35use language_model::{
  36    ConfigurationError, IconOrSvg, LanguageModelImage, LanguageModelRegistry, Role,
  37};
  38use multi_buffer::MultiBufferRow;
  39use picker::{Picker, popover_menu::PickerPopoverMenu};
  40use project::{Project, Worktree};
  41use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
  42use rope::Point;
  43use serde::{Deserialize, Serialize};
  44use settings::{
  45    LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
  46    update_settings_file,
  47};
  48use std::{
  49    any::{Any, TypeId},
  50    cmp,
  51    ops::Range,
  52    path::{Path, PathBuf},
  53    rc::Rc,
  54    sync::Arc,
  55    time::Duration,
  56};
  57use text::SelectionGoal;
  58use ui::{
  59    ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
  60    TintColor, Tooltip, prelude::*,
  61};
  62use util::{ResultExt, maybe};
  63use workspace::{
  64    CollaboratorId,
  65    searchable::{Direction, SearchableItemHandle},
  66};
  67use workspace::{
  68    Save, Toast, Workspace,
  69    item::{self, FollowableItem, Item},
  70    notifications::NotificationId,
  71    pane,
  72    searchable::{SearchEvent, SearchableItem},
  73};
  74use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
  75
  76use crate::CycleFavoriteModels;
  77
  78use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
  79use assistant_text_thread::{
  80    CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
  81    MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,
  82    TextThreadId, ThoughtProcessOutputSection,
  83};
  84
  85actions!(
  86    assistant,
  87    [
  88        /// Sends the current message to the assistant.
  89        Assist,
  90        /// Confirms and executes the entered slash command.
  91        ConfirmCommand,
  92        /// Copies code from the assistant's response to the clipboard.
  93        CopyCode,
  94        /// Cycles between user and assistant message roles.
  95        CycleMessageRole,
  96        /// Inserts the selected text into the active editor.
  97        InsertIntoEditor,
  98        /// Splits the conversation at the current cursor position.
  99        Split,
 100    ]
 101);
 102
 103/// Inserts files that were dragged and dropped into the assistant conversation.
 104#[derive(PartialEq, Clone, Action)]
 105#[action(namespace = assistant, no_json, no_register)]
 106pub enum InsertDraggedFiles {
 107    ProjectPaths(Vec<ProjectPath>),
 108    ExternalFiles(Vec<PathBuf>),
 109}
 110
 111#[derive(Copy, Clone, Debug, PartialEq)]
 112struct ScrollPosition {
 113    offset_before_cursor: gpui::Point<ScrollOffset>,
 114    cursor: Anchor,
 115}
 116
 117type MessageHeader = MessageMetadata;
 118
 119#[derive(Clone)]
 120enum AssistError {
 121    PaymentRequired,
 122    Message(SharedString),
 123}
 124
 125pub enum ThoughtProcessStatus {
 126    Pending,
 127    Completed,
 128}
 129
 130pub trait AgentPanelDelegate {
 131    fn active_text_thread_editor(
 132        &self,
 133        workspace: &mut Workspace,
 134        window: &mut Window,
 135        cx: &mut Context<Workspace>,
 136    ) -> Option<Entity<TextThreadEditor>>;
 137
 138    fn open_local_text_thread(
 139        &self,
 140        workspace: &mut Workspace,
 141        path: Arc<Path>,
 142        window: &mut Window,
 143        cx: &mut Context<Workspace>,
 144    ) -> Task<Result<()>>;
 145
 146    fn open_remote_text_thread(
 147        &self,
 148        workspace: &mut Workspace,
 149        text_thread_id: TextThreadId,
 150        window: &mut Window,
 151        cx: &mut Context<Workspace>,
 152    ) -> Task<Result<Entity<TextThreadEditor>>>;
 153
 154    fn quote_selection(
 155        &self,
 156        workspace: &mut Workspace,
 157        selection_ranges: Vec<Range<Anchor>>,
 158        buffer: Entity<MultiBuffer>,
 159        window: &mut Window,
 160        cx: &mut Context<Workspace>,
 161    );
 162}
 163
 164impl dyn AgentPanelDelegate {
 165    /// Returns the global [`AssistantPanelDelegate`], if it exists.
 166    pub fn try_global(cx: &App) -> Option<Arc<Self>> {
 167        cx.try_global::<GlobalAssistantPanelDelegate>()
 168            .map(|global| global.0.clone())
 169    }
 170
 171    /// Sets the global [`AssistantPanelDelegate`].
 172    pub fn set_global(delegate: Arc<Self>, cx: &mut App) {
 173        cx.set_global(GlobalAssistantPanelDelegate(delegate));
 174    }
 175}
 176
 177struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
 178
 179impl Global for GlobalAssistantPanelDelegate {}
 180
 181pub struct TextThreadEditor {
 182    text_thread: Entity<TextThread>,
 183    fs: Arc<dyn Fs>,
 184    slash_commands: Arc<SlashCommandWorkingSet>,
 185    workspace: WeakEntity<Workspace>,
 186    project: Entity<Project>,
 187    lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
 188    editor: Entity<Editor>,
 189    pending_thought_process: Option<(CreaseId, language::Anchor)>,
 190    blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
 191    image_blocks: HashSet<CustomBlockId>,
 192    scroll_position: Option<ScrollPosition>,
 193    remote_id: Option<workspace::ViewId>,
 194    pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
 195    invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
 196    _subscriptions: Vec<Subscription>,
 197    last_error: Option<AssistError>,
 198    pub(crate) slash_menu_handle:
 199        PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
 200    // dragged_file_worktrees is used to keep references to worktrees that were added
 201    // when the user drag/dropped an external file onto the context editor. Since
 202    // the worktree is not part of the project panel, it would be dropped as soon as
 203    // the file is opened. In order to keep the worktree alive for the duration of the
 204    // context editor, we keep a reference here.
 205    dragged_file_worktrees: Vec<Entity<Worktree>>,
 206    language_model_selector: Entity<LanguageModelSelector>,
 207    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 208}
 209
 210const MAX_TAB_TITLE_LEN: usize = 16;
 211
 212impl TextThreadEditor {
 213    pub fn init(cx: &mut App) {
 214        workspace::FollowableViewRegistry::register::<TextThreadEditor>(cx);
 215
 216        cx.observe_new(
 217            |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 218                workspace
 219                    .register_action(TextThreadEditor::quote_selection)
 220                    .register_action(TextThreadEditor::insert_selection)
 221                    .register_action(TextThreadEditor::copy_code)
 222                    .register_action(TextThreadEditor::handle_insert_dragged_files)
 223                    .register_action(TextThreadEditor::handle_send_review_to_agent);
 224            },
 225        )
 226        .detach();
 227    }
 228
 229    pub fn for_text_thread(
 230        text_thread: Entity<TextThread>,
 231        fs: Arc<dyn Fs>,
 232        workspace: WeakEntity<Workspace>,
 233        project: Entity<Project>,
 234        lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
 235        window: &mut Window,
 236        cx: &mut Context<Self>,
 237    ) -> Self {
 238        let completion_provider = SlashCommandCompletionProvider::new(
 239            text_thread.read(cx).slash_commands().clone(),
 240            Some(cx.entity().downgrade()),
 241            Some(workspace.clone()),
 242        );
 243
 244        let editor = cx.new(|cx| {
 245            let mut editor =
 246                Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);
 247            editor.disable_scrollbars_and_minimap(window, cx);
 248            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 249            editor.set_show_line_numbers(false, cx);
 250            editor.set_show_git_diff_gutter(false, cx);
 251            editor.set_show_code_actions(false, cx);
 252            editor.set_show_runnables(false, cx);
 253            editor.set_show_breakpoints(false, cx);
 254            editor.set_show_wrap_guides(false, cx);
 255            editor.set_show_indent_guides(false, cx);
 256            editor.set_completion_provider(Some(Rc::new(completion_provider)));
 257            editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
 258            editor.set_collaboration_hub(Box::new(project.clone()));
 259
 260            let show_edit_predictions = all_language_settings(None, cx)
 261                .edit_predictions
 262                .enabled_in_text_threads;
 263
 264            editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
 265
 266            editor
 267        });
 268
 269        let _subscriptions = vec![
 270            cx.observe(&text_thread, |_, _, cx| cx.notify()),
 271            cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),
 272            cx.subscribe_in(&editor, window, Self::handle_editor_event),
 273            cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
 274            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
 275        ];
 276
 277        let slash_command_sections = text_thread
 278            .read(cx)
 279            .slash_command_output_sections()
 280            .to_vec();
 281        let thought_process_sections = text_thread
 282            .read(cx)
 283            .thought_process_output_sections()
 284            .to_vec();
 285        let slash_commands = text_thread.read(cx).slash_commands().clone();
 286        let focus_handle = editor.read(cx).focus_handle(cx);
 287
 288        let mut this = Self {
 289            text_thread,
 290            slash_commands,
 291            editor,
 292            lsp_adapter_delegate,
 293            blocks: Default::default(),
 294            image_blocks: Default::default(),
 295            scroll_position: None,
 296            remote_id: None,
 297            pending_thought_process: None,
 298            fs: fs.clone(),
 299            workspace,
 300            project,
 301            pending_slash_command_creases: HashMap::default(),
 302            invoked_slash_command_creases: HashMap::default(),
 303            _subscriptions,
 304            last_error: None,
 305            slash_menu_handle: Default::default(),
 306            dragged_file_worktrees: Vec::new(),
 307            language_model_selector: cx.new(|cx| {
 308                language_model_selector(
 309                    |cx| LanguageModelRegistry::read_global(cx).default_model(),
 310                    {
 311                        let fs = fs.clone();
 312                        move |model, cx| {
 313                            update_settings_file(fs.clone(), cx, move |settings, _| {
 314                                let provider = model.provider_id().0.to_string();
 315                                let model = model.id().0.to_string();
 316                                settings.agent.get_or_insert_default().set_model(
 317                                    LanguageModelSelection {
 318                                        provider: LanguageModelProviderSetting(provider),
 319                                        model,
 320                                    },
 321                                )
 322                            });
 323                        }
 324                    },
 325                    {
 326                        let fs = fs.clone();
 327                        move |model, should_be_favorite, cx| {
 328                            crate::favorite_models::toggle_in_settings(
 329                                model,
 330                                should_be_favorite,
 331                                fs.clone(),
 332                                cx,
 333                            );
 334                        }
 335                    },
 336                    true, // Use popover styles for picker
 337                    focus_handle,
 338                    window,
 339                    cx,
 340                )
 341            }),
 342            language_model_selector_menu_handle: PopoverMenuHandle::default(),
 343        };
 344        this.update_message_headers(cx);
 345        this.update_image_blocks(cx);
 346        this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);
 347        this.insert_thought_process_output_sections(
 348            thought_process_sections
 349                .into_iter()
 350                .map(|section| (section, ThoughtProcessStatus::Completed)),
 351            window,
 352            cx,
 353        );
 354        this
 355    }
 356
 357    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 358        self.editor.update(cx, |editor, cx| {
 359            let show_edit_predictions = all_language_settings(None, cx)
 360                .edit_predictions
 361                .enabled_in_text_threads;
 362
 363            editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
 364        });
 365    }
 366
 367    pub fn text_thread(&self) -> &Entity<TextThread> {
 368        &self.text_thread
 369    }
 370
 371    pub fn editor(&self) -> &Entity<Editor> {
 372        &self.editor
 373    }
 374
 375    pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 376        let command_name = DefaultSlashCommand.name();
 377        self.editor.update(cx, |editor, cx| {
 378            editor.insert(&format!("/{command_name}\n\n"), window, cx)
 379        });
 380        let command = self.text_thread.update(cx, |text_thread, cx| {
 381            text_thread.reparse(cx);
 382            text_thread.parsed_slash_commands()[0].clone()
 383        });
 384        self.run_command(
 385            command.source_range,
 386            &command.name,
 387            &command.arguments,
 388            false,
 389            self.workspace.clone(),
 390            window,
 391            cx,
 392        );
 393    }
 394
 395    fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
 396        if self.sending_disabled(cx) {
 397            return;
 398        }
 399        telemetry::event!("Agent Message Sent", agent = "zed-text");
 400        self.send_to_model(window, cx);
 401    }
 402
 403    fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 404        self.last_error = None;
 405        if let Some(user_message) = self
 406            .text_thread
 407            .update(cx, |text_thread, cx| text_thread.assist(cx))
 408        {
 409            let new_selection = {
 410                let cursor = user_message
 411                    .start
 412                    .to_offset(self.text_thread.read(cx).buffer().read(cx));
 413                MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
 414            };
 415            self.editor.update(cx, |editor, cx| {
 416                editor.change_selections(Default::default(), window, cx, |selections| {
 417                    selections.select_ranges([new_selection])
 418                });
 419            });
 420            // Avoid scrolling to the new cursor position so the assistant's output is stable.
 421            cx.defer_in(window, |this, _, _| this.scroll_position = None);
 422        }
 423
 424        cx.notify();
 425    }
 426
 427    fn cancel(
 428        &mut self,
 429        _: &editor::actions::Cancel,
 430        _window: &mut Window,
 431        cx: &mut Context<Self>,
 432    ) {
 433        self.last_error = None;
 434
 435        if self
 436            .text_thread
 437            .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))
 438        {
 439            return;
 440        }
 441
 442        cx.propagate();
 443    }
 444
 445    fn cycle_message_role(
 446        &mut self,
 447        _: &CycleMessageRole,
 448        _window: &mut Window,
 449        cx: &mut Context<Self>,
 450    ) {
 451        let cursors = self.cursors(cx);
 452        self.text_thread.update(cx, |text_thread, cx| {
 453            let messages = text_thread
 454                .messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
 455                .into_iter()
 456                .map(|message| message.id)
 457                .collect();
 458            text_thread.cycle_message_roles(messages, cx)
 459        });
 460    }
 461
 462    fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
 463        let selections = self.editor.update(cx, |editor, cx| {
 464            editor
 465                .selections
 466                .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
 467        });
 468        selections
 469            .into_iter()
 470            .map(|selection| selection.head())
 471            .collect()
 472    }
 473
 474    pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
 475        if let Some(command) = self.slash_commands.command(name, cx) {
 476            self.editor.update(cx, |editor, cx| {
 477                editor.transact(window, cx, |editor, window, cx| {
 478                    editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
 479                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 480                    let newest_cursor = editor
 481                        .selections
 482                        .newest::<Point>(&editor.display_snapshot(cx))
 483                        .head();
 484                    if newest_cursor.column > 0
 485                        || snapshot
 486                            .chars_at(newest_cursor)
 487                            .next()
 488                            .is_some_and(|ch| ch != '\n')
 489                    {
 490                        editor.move_to_end_of_line(
 491                            &MoveToEndOfLine {
 492                                stop_at_soft_wraps: false,
 493                            },
 494                            window,
 495                            cx,
 496                        );
 497                        editor.newline(&Newline, window, cx);
 498                    }
 499
 500                    editor.insert(&format!("/{name}"), window, cx);
 501                    if command.accepts_arguments() {
 502                        editor.insert(" ", window, cx);
 503                        editor.show_completions(&ShowCompletions, window, cx);
 504                    }
 505                });
 506            });
 507            if !command.requires_argument() {
 508                self.confirm_command(&ConfirmCommand, window, cx);
 509            }
 510        }
 511    }
 512
 513    pub fn confirm_command(
 514        &mut self,
 515        _: &ConfirmCommand,
 516        window: &mut Window,
 517        cx: &mut Context<Self>,
 518    ) {
 519        if self.editor.read(cx).has_visible_completions_menu() {
 520            return;
 521        }
 522
 523        let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
 524        let mut commands_by_range = HashMap::default();
 525        let workspace = self.workspace.clone();
 526        self.text_thread.update(cx, |text_thread, cx| {
 527            text_thread.reparse(cx);
 528            for selection in selections.iter() {
 529                if let Some(command) =
 530                    text_thread.pending_command_for_position(selection.head().text_anchor, cx)
 531                {
 532                    commands_by_range
 533                        .entry(command.source_range.clone())
 534                        .or_insert_with(|| command.clone());
 535                }
 536            }
 537        });
 538
 539        if commands_by_range.is_empty() {
 540            cx.propagate();
 541        } else {
 542            for command in commands_by_range.into_values() {
 543                self.run_command(
 544                    command.source_range,
 545                    &command.name,
 546                    &command.arguments,
 547                    true,
 548                    workspace.clone(),
 549                    window,
 550                    cx,
 551                );
 552            }
 553            cx.stop_propagation();
 554        }
 555    }
 556
 557    pub fn run_command(
 558        &mut self,
 559        command_range: Range<language::Anchor>,
 560        name: &str,
 561        arguments: &[String],
 562        ensure_trailing_newline: bool,
 563        workspace: WeakEntity<Workspace>,
 564        window: &mut Window,
 565        cx: &mut Context<Self>,
 566    ) {
 567        if let Some(command) = self.slash_commands.command(name, cx) {
 568            let text_thread = self.text_thread.read(cx);
 569            let sections = text_thread
 570                .slash_command_output_sections()
 571                .iter()
 572                .filter(|section| section.is_valid(text_thread.buffer().read(cx)))
 573                .cloned()
 574                .collect::<Vec<_>>();
 575            let snapshot = text_thread.buffer().read(cx).snapshot();
 576            let output = command.run(
 577                arguments,
 578                &sections,
 579                snapshot,
 580                workspace,
 581                self.lsp_adapter_delegate.clone(),
 582                window,
 583                cx,
 584            );
 585            self.text_thread.update(cx, |text_thread, cx| {
 586                text_thread.insert_command_output(
 587                    command_range,
 588                    name,
 589                    output,
 590                    ensure_trailing_newline,
 591                    cx,
 592                )
 593            });
 594        }
 595    }
 596
 597    fn handle_text_thread_event(
 598        &mut self,
 599        _: &Entity<TextThread>,
 600        event: &TextThreadEvent,
 601        window: &mut Window,
 602        cx: &mut Context<Self>,
 603    ) {
 604        let text_thread_editor = cx.entity().downgrade();
 605
 606        match event {
 607            TextThreadEvent::MessagesEdited => {
 608                self.update_message_headers(cx);
 609                self.update_image_blocks(cx);
 610                self.text_thread.update(cx, |text_thread, cx| {
 611                    text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
 612                });
 613            }
 614            TextThreadEvent::SummaryChanged => {
 615                cx.emit(EditorEvent::TitleChanged);
 616                self.text_thread.update(cx, |text_thread, cx| {
 617                    text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
 618                });
 619            }
 620            TextThreadEvent::SummaryGenerated => {}
 621            TextThreadEvent::PathChanged { .. } => {}
 622            TextThreadEvent::StartedThoughtProcess(range) => {
 623                let creases = self.insert_thought_process_output_sections(
 624                    [(
 625                        ThoughtProcessOutputSection {
 626                            range: range.clone(),
 627                        },
 628                        ThoughtProcessStatus::Pending,
 629                    )],
 630                    window,
 631                    cx,
 632                );
 633                self.pending_thought_process = Some((creases[0], range.start));
 634            }
 635            TextThreadEvent::EndedThoughtProcess(end) => {
 636                if let Some((crease_id, start)) = self.pending_thought_process.take() {
 637                    self.editor.update(cx, |editor, cx| {
 638                        let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
 639                        let start_anchor =
 640                            multi_buffer_snapshot.as_singleton_anchor(start).unwrap();
 641
 642                        editor.display_map.update(cx, |display_map, cx| {
 643                            display_map.unfold_intersecting(
 644                                vec![start_anchor..start_anchor],
 645                                true,
 646                                cx,
 647                            );
 648                        });
 649                        editor.remove_creases(vec![crease_id], cx);
 650                    });
 651                    self.insert_thought_process_output_sections(
 652                        [(
 653                            ThoughtProcessOutputSection { range: start..*end },
 654                            ThoughtProcessStatus::Completed,
 655                        )],
 656                        window,
 657                        cx,
 658                    );
 659                }
 660            }
 661            TextThreadEvent::StreamedCompletion => {
 662                self.editor.update(cx, |editor, cx| {
 663                    if let Some(scroll_position) = self.scroll_position {
 664                        let snapshot = editor.snapshot(window, cx);
 665                        let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
 666                        let scroll_top =
 667                            cursor_point.row().as_f64() - scroll_position.offset_before_cursor.y;
 668                        editor.set_scroll_position(
 669                            point(scroll_position.offset_before_cursor.x, scroll_top),
 670                            window,
 671                            cx,
 672                        );
 673                    }
 674                });
 675            }
 676            TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
 677                self.editor.update(cx, |editor, cx| {
 678                    let buffer = editor.buffer().read(cx).snapshot(cx);
 679                    let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
 680
 681                    editor.remove_creases(
 682                        removed
 683                            .iter()
 684                            .filter_map(|range| self.pending_slash_command_creases.remove(range)),
 685                        cx,
 686                    );
 687
 688                    let crease_ids = editor.insert_creases(
 689                        updated.iter().map(|command| {
 690                            let workspace = self.workspace.clone();
 691                            let confirm_command = Arc::new({
 692                                let text_thread_editor = text_thread_editor.clone();
 693                                let command = command.clone();
 694                                move |window: &mut Window, cx: &mut App| {
 695                                    text_thread_editor
 696                                        .update(cx, |text_thread_editor, cx| {
 697                                            text_thread_editor.run_command(
 698                                                command.source_range.clone(),
 699                                                &command.name,
 700                                                &command.arguments,
 701                                                false,
 702                                                workspace.clone(),
 703                                                window,
 704                                                cx,
 705                                            );
 706                                        })
 707                                        .ok();
 708                                }
 709                            });
 710                            let placeholder = FoldPlaceholder {
 711                                render: Arc::new(move |_, _, _| Empty.into_any()),
 712                                ..Default::default()
 713                            };
 714                            let render_toggle = {
 715                                let confirm_command = confirm_command.clone();
 716                                let command = command.clone();
 717                                move |row, _, _, _window: &mut Window, _cx: &mut App| {
 718                                    render_pending_slash_command_gutter_decoration(
 719                                        row,
 720                                        &command.status,
 721                                        confirm_command.clone(),
 722                                    )
 723                                }
 724                            };
 725                            let render_trailer = {
 726                                move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
 727                                    Empty.into_any()
 728                                }
 729                            };
 730
 731                            let range = buffer
 732                                .anchor_range_in_excerpt(excerpt_id, command.source_range.clone())
 733                                .unwrap();
 734                            Crease::inline(range, placeholder, render_toggle, render_trailer)
 735                        }),
 736                        cx,
 737                    );
 738
 739                    self.pending_slash_command_creases.extend(
 740                        updated
 741                            .iter()
 742                            .map(|command| command.source_range.clone())
 743                            .zip(crease_ids),
 744                    );
 745                })
 746            }
 747            TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
 748                self.update_invoked_slash_command(*command_id, window, cx);
 749            }
 750            TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
 751                self.insert_slash_command_output_sections([section.clone()], false, window, cx);
 752            }
 753            TextThreadEvent::Operation(_) => {}
 754            TextThreadEvent::ShowAssistError(error_message) => {
 755                self.last_error = Some(AssistError::Message(error_message.clone()));
 756            }
 757            TextThreadEvent::ShowPaymentRequiredError => {
 758                self.last_error = Some(AssistError::PaymentRequired);
 759            }
 760        }
 761    }
 762
 763    fn update_invoked_slash_command(
 764        &mut self,
 765        command_id: InvokedSlashCommandId,
 766        window: &mut Window,
 767        cx: &mut Context<Self>,
 768    ) {
 769        if let Some(invoked_slash_command) =
 770            self.text_thread.read(cx).invoked_slash_command(&command_id)
 771            && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
 772        {
 773            let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
 774            for range in run_commands_in_ranges {
 775                let commands = self.text_thread.update(cx, |text_thread, cx| {
 776                    text_thread.reparse(cx);
 777                    text_thread
 778                        .pending_commands_for_range(range.clone(), cx)
 779                        .to_vec()
 780                });
 781
 782                for command in commands {
 783                    self.run_command(
 784                        command.source_range,
 785                        &command.name,
 786                        &command.arguments,
 787                        false,
 788                        self.workspace.clone(),
 789                        window,
 790                        cx,
 791                    );
 792                }
 793            }
 794        }
 795
 796        self.editor.update(cx, |editor, cx| {
 797            if let Some(invoked_slash_command) =
 798                self.text_thread.read(cx).invoked_slash_command(&command_id)
 799            {
 800                if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
 801                    let buffer = editor.buffer().read(cx).snapshot(cx);
 802                    let (&excerpt_id, _buffer_id, _buffer_snapshot) =
 803                        buffer.as_singleton().unwrap();
 804
 805                    let range = buffer
 806                        .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
 807                        .unwrap();
 808                    editor.remove_folds_with_type(
 809                        &[range],
 810                        TypeId::of::<PendingSlashCommand>(),
 811                        false,
 812                        cx,
 813                    );
 814
 815                    editor.remove_creases(
 816                        HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
 817                        cx,
 818                    );
 819                } else if let hash_map::Entry::Vacant(entry) =
 820                    self.invoked_slash_command_creases.entry(command_id)
 821                {
 822                    let buffer = editor.buffer().read(cx).snapshot(cx);
 823                    let (&excerpt_id, _buffer_id, _buffer_snapshot) =
 824                        buffer.as_singleton().unwrap();
 825                    let context = self.text_thread.downgrade();
 826                    let range = buffer
 827                        .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
 828                        .unwrap();
 829                    let crease = Crease::inline(
 830                        range,
 831                        invoked_slash_command_fold_placeholder(command_id, context),
 832                        fold_toggle("invoked-slash-command"),
 833                        |_row, _folded, _window, _cx| Empty.into_any(),
 834                    );
 835                    let crease_ids = editor.insert_creases([crease.clone()], cx);
 836                    editor.fold_creases(vec![crease], false, window, cx);
 837                    entry.insert(crease_ids[0]);
 838                } else {
 839                    cx.notify()
 840                }
 841            } else {
 842                editor.remove_creases(
 843                    HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
 844                    cx,
 845                );
 846                cx.notify();
 847            };
 848        });
 849    }
 850
 851    fn insert_thought_process_output_sections(
 852        &mut self,
 853        sections: impl IntoIterator<
 854            Item = (
 855                ThoughtProcessOutputSection<language::Anchor>,
 856                ThoughtProcessStatus,
 857            ),
 858        >,
 859        window: &mut Window,
 860        cx: &mut Context<Self>,
 861    ) -> Vec<CreaseId> {
 862        self.editor.update(cx, |editor, cx| {
 863            let buffer = editor.buffer().read(cx).snapshot(cx);
 864            let excerpt_id = *buffer.as_singleton().unwrap().0;
 865            let mut buffer_rows_to_fold = BTreeSet::new();
 866            let mut creases = Vec::new();
 867            for (section, status) in sections {
 868                let range = buffer
 869                    .anchor_range_in_excerpt(excerpt_id, section.range)
 870                    .unwrap();
 871                let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
 872                buffer_rows_to_fold.insert(buffer_row);
 873                creases.push(
 874                    Crease::inline(
 875                        range,
 876                        FoldPlaceholder {
 877                            render: render_thought_process_fold_icon_button(
 878                                cx.entity().downgrade(),
 879                                status,
 880                            ),
 881                            merge_adjacent: false,
 882                            ..Default::default()
 883                        },
 884                        render_slash_command_output_toggle,
 885                        |_, _, _, _| Empty.into_any_element(),
 886                    )
 887                    .with_metadata(CreaseMetadata {
 888                        icon_path: SharedString::from(IconName::Ai.path()),
 889                        label: "Thinking Process".into(),
 890                    }),
 891                );
 892            }
 893
 894            let creases = editor.insert_creases(creases, cx);
 895
 896            for buffer_row in buffer_rows_to_fold.into_iter().rev() {
 897                editor.fold_at(buffer_row, window, cx);
 898            }
 899
 900            creases
 901        })
 902    }
 903
 904    fn insert_slash_command_output_sections(
 905        &mut self,
 906        sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
 907        expand_result: bool,
 908        window: &mut Window,
 909        cx: &mut Context<Self>,
 910    ) {
 911        self.editor.update(cx, |editor, cx| {
 912            let buffer = editor.buffer().read(cx).snapshot(cx);
 913            let excerpt_id = *buffer.as_singleton().unwrap().0;
 914            let mut buffer_rows_to_fold = BTreeSet::new();
 915            let mut creases = Vec::new();
 916            for section in sections {
 917                let range = buffer
 918                    .anchor_range_in_excerpt(excerpt_id, section.range)
 919                    .unwrap();
 920                let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
 921                buffer_rows_to_fold.insert(buffer_row);
 922                creases.push(
 923                    Crease::inline(
 924                        range,
 925                        FoldPlaceholder {
 926                            render: render_fold_icon_button(
 927                                cx.entity().downgrade(),
 928                                section.icon.path().into(),
 929                                section.label.clone(),
 930                            ),
 931                            merge_adjacent: false,
 932                            ..Default::default()
 933                        },
 934                        render_slash_command_output_toggle,
 935                        |_, _, _, _| Empty.into_any_element(),
 936                    )
 937                    .with_metadata(CreaseMetadata {
 938                        icon_path: section.icon.path().into(),
 939                        label: section.label,
 940                    }),
 941                );
 942            }
 943
 944            editor.insert_creases(creases, cx);
 945
 946            if expand_result {
 947                buffer_rows_to_fold.clear();
 948            }
 949            for buffer_row in buffer_rows_to_fold.into_iter().rev() {
 950                editor.fold_at(buffer_row, window, cx);
 951            }
 952        });
 953    }
 954
 955    fn handle_editor_event(
 956        &mut self,
 957        _: &Entity<Editor>,
 958        event: &EditorEvent,
 959        window: &mut Window,
 960        cx: &mut Context<Self>,
 961    ) {
 962        match event {
 963            EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
 964                let cursor_scroll_position = self.cursor_scroll_position(window, cx);
 965                if *autoscroll {
 966                    self.scroll_position = cursor_scroll_position;
 967                } else if self.scroll_position != cursor_scroll_position {
 968                    self.scroll_position = None;
 969                }
 970            }
 971            EditorEvent::SelectionsChanged { .. } => {
 972                self.scroll_position = self.cursor_scroll_position(window, cx);
 973            }
 974            _ => {}
 975        }
 976        cx.emit(event.clone());
 977    }
 978
 979    fn handle_editor_search_event(
 980        &mut self,
 981        _: &Entity<Editor>,
 982        event: &SearchEvent,
 983        _window: &mut Window,
 984        cx: &mut Context<Self>,
 985    ) {
 986        cx.emit(event.clone());
 987    }
 988
 989    fn cursor_scroll_position(
 990        &self,
 991        window: &mut Window,
 992        cx: &mut Context<Self>,
 993    ) -> Option<ScrollPosition> {
 994        self.editor.update(cx, |editor, cx| {
 995            let snapshot = editor.snapshot(window, cx);
 996            let cursor = editor.selections.newest_anchor().head();
 997            let cursor_row = cursor
 998                .to_display_point(&snapshot.display_snapshot)
 999                .row()
1000                .as_f64();
1001            let scroll_position = editor
1002                .scroll_manager
1003                .anchor()
1004                .scroll_position(&snapshot.display_snapshot);
1005
1006            let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
1007            if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
1008                Some(ScrollPosition {
1009                    cursor,
1010                    offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
1011                })
1012            } else {
1013                None
1014            }
1015        })
1016    }
1017
1018    fn esc_kbd(cx: &App) -> Div {
1019        let colors = cx.theme().colors().clone();
1020
1021        h_flex()
1022            .items_center()
1023            .gap_1()
1024            .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
1025            .text_size(TextSize::XSmall.rems(cx))
1026            .text_color(colors.text_muted)
1027            .child("Press")
1028            .child(
1029                h_flex()
1030                    .rounded_sm()
1031                    .px_1()
1032                    .mr_0p5()
1033                    .border_1()
1034                    .border_color(colors.border_variant.alpha(0.6))
1035                    .bg(colors.element_background.alpha(0.6))
1036                    .child("esc"),
1037            )
1038            .child("to cancel")
1039    }
1040
1041    fn update_message_headers(&mut self, cx: &mut Context<Self>) {
1042        self.editor.update(cx, |editor, cx| {
1043            let buffer = editor.buffer().read(cx).snapshot(cx);
1044
1045            let excerpt_id = *buffer.as_singleton().unwrap().0;
1046            let mut old_blocks = std::mem::take(&mut self.blocks);
1047            let mut blocks_to_remove: HashMap<_, _> = old_blocks
1048                .iter()
1049                .map(|(message_id, (_, block_id))| (*message_id, *block_id))
1050                .collect();
1051            let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
1052
1053            let render_block = |message: MessageMetadata| -> RenderBlock {
1054                Arc::new({
1055                    let text_thread = self.text_thread.clone();
1056
1057                    move |cx| {
1058                        let message_id = MessageId(message.timestamp);
1059                        let llm_loading = message.role == Role::Assistant
1060                            && message.status == MessageStatus::Pending;
1061
1062                        let (label, spinner, note) = match message.role {
1063                            Role::User => (
1064                                Label::new("You").color(Color::Default).into_any_element(),
1065                                None,
1066                                None,
1067                            ),
1068                            Role::Assistant => {
1069                                let base_label = Label::new("Agent").color(Color::Info);
1070                                let mut spinner = None;
1071                                let mut note = None;
1072                                let animated_label = if llm_loading {
1073                                    base_label
1074                                        .with_animation(
1075                                            "pulsating-label",
1076                                            Animation::new(Duration::from_secs(2))
1077                                                .repeat()
1078                                                .with_easing(pulsating_between(0.4, 0.8)),
1079                                            |label, delta| label.alpha(delta),
1080                                        )
1081                                        .into_any_element()
1082                                } else {
1083                                    base_label.into_any_element()
1084                                };
1085                                if llm_loading {
1086                                    spinner = Some(
1087                                        Icon::new(IconName::ArrowCircle)
1088                                            .size(IconSize::XSmall)
1089                                            .color(Color::Info)
1090                                            .with_rotate_animation(2)
1091                                            .into_any_element(),
1092                                    );
1093                                    note = Some(Self::esc_kbd(cx).into_any_element());
1094                                }
1095                                (animated_label, spinner, note)
1096                            }
1097                            Role::System => (
1098                                Label::new("System")
1099                                    .color(Color::Warning)
1100                                    .into_any_element(),
1101                                None,
1102                                None,
1103                            ),
1104                        };
1105
1106                        let sender = h_flex()
1107                            .items_center()
1108                            .gap_2p5()
1109                            .child(
1110                                ButtonLike::new("role")
1111                                    .style(ButtonStyle::Filled)
1112                                    .child(
1113                                        h_flex()
1114                                            .items_center()
1115                                            .gap_1p5()
1116                                            .child(label)
1117                                            .children(spinner),
1118                                    )
1119                                    .tooltip(|_window, cx| {
1120                                        Tooltip::with_meta(
1121                                            "Toggle message role",
1122                                            None,
1123                                            "Available roles: You (User), Agent, System",
1124                                            cx,
1125                                        )
1126                                    })
1127                                    .on_click({
1128                                        let text_thread = text_thread.clone();
1129                                        move |_, _window, cx| {
1130                                            text_thread.update(cx, |text_thread, cx| {
1131                                                text_thread.cycle_message_roles(
1132                                                    HashSet::from_iter(Some(message_id)),
1133                                                    cx,
1134                                                )
1135                                            })
1136                                        }
1137                                    }),
1138                            )
1139                            .children(note);
1140
1141                        h_flex()
1142                            .id(("message_header", message_id.as_u64()))
1143                            .pl(cx.margins.gutter.full_width())
1144                            .h_11()
1145                            .w_full()
1146                            .relative()
1147                            .gap_1p5()
1148                            .child(sender)
1149                            .children(match &message.cache {
1150                                Some(cache) if cache.is_final_anchor => match cache.status {
1151                                    CacheStatus::Cached => Some(
1152                                        div()
1153                                            .id("cached")
1154                                            .child(
1155                                                Icon::new(IconName::DatabaseZap)
1156                                                    .size(IconSize::XSmall)
1157                                                    .color(Color::Hint),
1158                                            )
1159                                            .tooltip(|_window, cx| {
1160                                                Tooltip::with_meta(
1161                                                    "Context Cached",
1162                                                    None,
1163                                                    "Large messages cached to optimize performance",
1164                                                    cx,
1165                                                )
1166                                            })
1167                                            .into_any_element(),
1168                                    ),
1169                                    CacheStatus::Pending => Some(
1170                                        div()
1171                                            .child(
1172                                                Icon::new(IconName::Ellipsis)
1173                                                    .size(IconSize::XSmall)
1174                                                    .color(Color::Hint),
1175                                            )
1176                                            .into_any_element(),
1177                                    ),
1178                                },
1179                                _ => None,
1180                            })
1181                            .children(match &message.status {
1182                                MessageStatus::Error(error) => Some(
1183                                    Button::new("show-error", "Error")
1184                                        .color(Color::Error)
1185                                        .selected_label_color(Color::Error)
1186                                        .selected_icon_color(Color::Error)
1187                                        .icon(IconName::XCircle)
1188                                        .icon_color(Color::Error)
1189                                        .icon_size(IconSize::XSmall)
1190                                        .icon_position(IconPosition::Start)
1191                                        .tooltip(Tooltip::text("View Details"))
1192                                        .on_click({
1193                                            let text_thread = text_thread.clone();
1194                                            let error = error.clone();
1195                                            move |_, _window, cx| {
1196                                                text_thread.update(cx, |_, cx| {
1197                                                    cx.emit(TextThreadEvent::ShowAssistError(
1198                                                        error.clone(),
1199                                                    ));
1200                                                });
1201                                            }
1202                                        })
1203                                        .into_any_element(),
1204                                ),
1205                                MessageStatus::Canceled => Some(
1206                                    h_flex()
1207                                        .gap_1()
1208                                        .items_center()
1209                                        .child(
1210                                            Icon::new(IconName::XCircle)
1211                                                .color(Color::Disabled)
1212                                                .size(IconSize::XSmall),
1213                                        )
1214                                        .child(
1215                                            Label::new("Canceled")
1216                                                .size(LabelSize::Small)
1217                                                .color(Color::Disabled),
1218                                        )
1219                                        .into_any_element(),
1220                                ),
1221                                _ => None,
1222                            })
1223                            .into_any_element()
1224                    }
1225                })
1226            };
1227            let create_block_properties = |message: &Message| BlockProperties {
1228                height: Some(2),
1229                style: BlockStyle::Sticky,
1230                placement: BlockPlacement::Above(
1231                    buffer
1232                        .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
1233                        .unwrap(),
1234                ),
1235                priority: usize::MAX,
1236                render: render_block(MessageMetadata::from(message)),
1237            };
1238            let mut new_blocks = vec![];
1239            let mut block_index_to_message = vec![];
1240            for message in self.text_thread.read(cx).messages(cx) {
1241                if blocks_to_remove.remove(&message.id).is_some() {
1242                    // This is an old message that we might modify.
1243                    let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
1244                        debug_assert!(
1245                            false,
1246                            "old_blocks should contain a message_id we've just removed."
1247                        );
1248                        continue;
1249                    };
1250                    // Should we modify it?
1251                    let message_meta = MessageMetadata::from(&message);
1252                    if meta != &message_meta {
1253                        blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
1254                        *meta = message_meta;
1255                    }
1256                } else {
1257                    // This is a new message.
1258                    new_blocks.push(create_block_properties(&message));
1259                    block_index_to_message.push((message.id, MessageMetadata::from(&message)));
1260                }
1261            }
1262            editor.replace_blocks(blocks_to_replace, None, cx);
1263            editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
1264
1265            let ids = editor.insert_blocks(new_blocks, None, cx);
1266            old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
1267                |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
1268            ));
1269            self.blocks = old_blocks;
1270        });
1271    }
1272
1273    /// Returns either the selected text, or the content of the Markdown code
1274    /// block surrounding the cursor.
1275    fn get_selection_or_code_block(
1276        context_editor_view: &Entity<TextThreadEditor>,
1277        cx: &mut Context<Workspace>,
1278    ) -> Option<(String, bool)> {
1279        const CODE_FENCE_DELIMITER: &str = "```";
1280
1281        let text_thread_editor = context_editor_view.read(cx).editor.clone();
1282        text_thread_editor.update(cx, |text_thread_editor, cx| {
1283            let display_map = text_thread_editor.display_snapshot(cx);
1284            if text_thread_editor
1285                .selections
1286                .newest::<Point>(&display_map)
1287                .is_empty()
1288            {
1289                let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx);
1290                let (_, _, snapshot) = snapshot.as_singleton()?;
1291
1292                let head = text_thread_editor
1293                    .selections
1294                    .newest::<Point>(&display_map)
1295                    .head();
1296                let offset = snapshot.point_to_offset(head);
1297
1298                let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
1299                let mut text = snapshot
1300                    .text_for_range(surrounding_code_block_range)
1301                    .collect::<String>();
1302
1303                // If there is no newline trailing the closing three-backticks, then
1304                // tree-sitter-md extends the range of the content node to include
1305                // the backticks.
1306                if text.ends_with(CODE_FENCE_DELIMITER) {
1307                    text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
1308                }
1309
1310                (!text.is_empty()).then_some((text, true))
1311            } else {
1312                let selection = text_thread_editor.selections.newest_adjusted(&display_map);
1313                let buffer = text_thread_editor.buffer().read(cx).snapshot(cx);
1314                let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
1315
1316                (!selected_text.is_empty()).then_some((selected_text, false))
1317            }
1318        })
1319    }
1320
1321    pub fn insert_selection(
1322        workspace: &mut Workspace,
1323        _: &InsertIntoEditor,
1324        window: &mut Window,
1325        cx: &mut Context<Workspace>,
1326    ) {
1327        let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1328            return;
1329        };
1330        let Some(context_editor_view) =
1331            agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1332        else {
1333            return;
1334        };
1335        let Some(active_editor_view) = workspace
1336            .active_item(cx)
1337            .and_then(|item| item.act_as::<Editor>(cx))
1338        else {
1339            return;
1340        };
1341
1342        if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
1343            active_editor_view.update(cx, |editor, cx| {
1344                editor.insert(&text, window, cx);
1345                editor.focus_handle(cx).focus(window, cx);
1346            })
1347        }
1348    }
1349
1350    pub fn copy_code(
1351        workspace: &mut Workspace,
1352        _: &CopyCode,
1353        window: &mut Window,
1354        cx: &mut Context<Workspace>,
1355    ) {
1356        let result = maybe!({
1357            let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
1358            let context_editor_view =
1359                agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?;
1360            Self::get_selection_or_code_block(&context_editor_view, cx)
1361        });
1362        let Some((text, is_code_block)) = result else {
1363            return;
1364        };
1365
1366        cx.write_to_clipboard(ClipboardItem::new_string(text));
1367
1368        struct CopyToClipboardToast;
1369        workspace.show_toast(
1370            Toast::new(
1371                NotificationId::unique::<CopyToClipboardToast>(),
1372                format!(
1373                    "{} copied to clipboard.",
1374                    if is_code_block {
1375                        "Code block"
1376                    } else {
1377                        "Selection"
1378                    }
1379                ),
1380            )
1381            .autohide(),
1382            cx,
1383        );
1384    }
1385
1386    pub fn handle_insert_dragged_files(
1387        workspace: &mut Workspace,
1388        action: &InsertDraggedFiles,
1389        window: &mut Window,
1390        cx: &mut Context<Workspace>,
1391    ) {
1392        let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1393            return;
1394        };
1395        let Some(context_editor_view) =
1396            agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1397        else {
1398            return;
1399        };
1400
1401        let project = context_editor_view.read(cx).project.clone();
1402
1403        let paths = match action {
1404            InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
1405            InsertDraggedFiles::ExternalFiles(paths) => {
1406                let tasks = paths
1407                    .clone()
1408                    .into_iter()
1409                    .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
1410                    .collect::<Vec<_>>();
1411
1412                cx.background_spawn(async move {
1413                    let mut paths = vec![];
1414                    let mut worktrees = vec![];
1415
1416                    let opened_paths = futures::future::join_all(tasks).await;
1417
1418                    for entry in opened_paths {
1419                        if let Some((worktree, project_path)) = entry.log_err() {
1420                            worktrees.push(worktree);
1421                            paths.push(project_path);
1422                        }
1423                    }
1424
1425                    (paths, worktrees)
1426                })
1427            }
1428        };
1429
1430        context_editor_view.update(cx, |_, cx| {
1431            cx.spawn_in(window, async move |this, cx| {
1432                let (paths, dragged_file_worktrees) = paths.await;
1433                this.update_in(cx, |this, window, cx| {
1434                    this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
1435                })
1436                .ok();
1437            })
1438            .detach();
1439        })
1440    }
1441
1442    pub fn insert_dragged_files(
1443        &mut self,
1444        opened_paths: Vec<ProjectPath>,
1445        added_worktrees: Vec<Entity<Worktree>>,
1446        window: &mut Window,
1447        cx: &mut Context<Self>,
1448    ) {
1449        let mut file_slash_command_args = vec![];
1450        for project_path in opened_paths.into_iter() {
1451            let Some(worktree) = self
1452                .project
1453                .read(cx)
1454                .worktree_for_id(project_path.worktree_id, cx)
1455            else {
1456                continue;
1457            };
1458            let path_style = worktree.read(cx).path_style();
1459            let full_path = worktree
1460                .read(cx)
1461                .root_name()
1462                .join(&project_path.path)
1463                .display(path_style)
1464                .into_owned();
1465            file_slash_command_args.push(full_path);
1466        }
1467
1468        let cmd_name = FileSlashCommand.name();
1469
1470        let file_argument = file_slash_command_args.join(" ");
1471
1472        self.editor.update(cx, |editor, cx| {
1473            editor.insert("\n", window, cx);
1474            editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
1475        });
1476        self.confirm_command(&ConfirmCommand, window, cx);
1477        self.dragged_file_worktrees.extend(added_worktrees);
1478    }
1479
1480    pub fn quote_selection(
1481        workspace: &mut Workspace,
1482        _: &AddSelectionToThread,
1483        window: &mut Window,
1484        cx: &mut Context<Workspace>,
1485    ) {
1486        let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1487            return;
1488        };
1489
1490        let Some((selections, buffer)) = maybe!({
1491            let editor = workspace
1492                .active_item(cx)
1493                .and_then(|item| item.act_as::<Editor>(cx))?;
1494
1495            let buffer = editor.read(cx).buffer().clone();
1496            let snapshot = buffer.read(cx).snapshot(cx);
1497            let selections = editor.update(cx, |editor, cx| {
1498                editor
1499                    .selections
1500                    .all_adjusted(&editor.display_snapshot(cx))
1501                    .into_iter()
1502                    .filter_map(|s| {
1503                        (!s.is_empty())
1504                            .then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
1505                    })
1506                    .collect::<Vec<_>>()
1507            });
1508            Some((selections, buffer))
1509        }) else {
1510            return;
1511        };
1512
1513        if selections.is_empty() {
1514            return;
1515        }
1516
1517        agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
1518    }
1519
1520    /// Handles the SendReviewToAgent action from the ProjectDiff toolbar.
1521    /// Collects ALL stored review comments from ALL hunks and sends them
1522    /// to the Agent panel as creases.
1523    pub fn handle_send_review_to_agent(
1524        workspace: &mut Workspace,
1525        _: &SendReviewToAgent,
1526        window: &mut Window,
1527        cx: &mut Context<Workspace>,
1528    ) {
1529        use editor::{DiffHunkKey, StoredReviewComment};
1530        use git_ui::project_diff::ProjectDiff;
1531
1532        // Find the ProjectDiff item
1533        let Some(project_diff) = workspace.items_of_type::<ProjectDiff>(cx).next() else {
1534            workspace.show_toast(
1535                Toast::new(
1536                    NotificationId::unique::<SendReviewToAgent>(),
1537                    "No Project Diff panel found. Open it first to add review comments.",
1538                ),
1539                cx,
1540            );
1541            return;
1542        };
1543
1544        // Get the buffer reference first (before taking comments)
1545        let buffer = project_diff.update(cx, |project_diff, cx| {
1546            project_diff
1547                .editor()
1548                .read(cx)
1549                .primary_editor()
1550                .read(cx)
1551                .buffer()
1552                .clone()
1553        });
1554
1555        // Extract all stored comments from all hunks
1556        let all_comments: Vec<(DiffHunkKey, Vec<StoredReviewComment>)> =
1557            project_diff.update(cx, |project_diff, cx| {
1558                let editor = project_diff.editor().read(cx).primary_editor().clone();
1559                editor.update(cx, |editor, cx| editor.take_all_review_comments(cx))
1560            });
1561
1562        // Flatten: we have Vec<(DiffHunkKey, Vec<StoredReviewComment>)>
1563        // Convert to Vec<StoredReviewComment> for processing
1564        let comments: Vec<StoredReviewComment> = all_comments
1565            .into_iter()
1566            .flat_map(|(_, comments)| comments)
1567            .collect();
1568
1569        if comments.is_empty() {
1570            workspace.show_toast(
1571                Toast::new(
1572                    NotificationId::unique::<SendReviewToAgent>(),
1573                    "No review comments to send. Add comments using the + button in the diff view.",
1574                ),
1575                cx,
1576            );
1577            return;
1578        }
1579
1580        // Get or create the agent panel
1581        let Some(panel) = workspace.panel::<crate::AgentPanel>(cx) else {
1582            workspace.show_toast(
1583                Toast::new(
1584                    NotificationId::unique::<SendReviewToAgent>(),
1585                    "Agent panel is not available.",
1586                ),
1587                cx,
1588            );
1589            return;
1590        };
1591
1592        // Create a new thread if there isn't an active one (synchronous call)
1593        let has_active_thread = panel.read(cx).active_thread_view().is_some();
1594        if !has_active_thread {
1595            panel.update(cx, |panel, cx| {
1596                panel.new_agent_thread(AgentType::NativeAgent, window, cx);
1597            });
1598        }
1599
1600        // Focus the agent panel
1601        workspace.focus_panel::<crate::AgentPanel>(window, cx);
1602
1603        // Defer inserting creases until after the current update cycle completes,
1604        // allowing the newly created thread (if any) to fully initialize.
1605        cx.defer_in(window, move |workspace, window, cx| {
1606            let Some(panel) = workspace.panel::<crate::AgentPanel>(cx) else {
1607                workspace.show_toast(
1608                    Toast::new(
1609                        NotificationId::unique::<SendReviewToAgent>(),
1610                        "Agent panel closed unexpectedly.",
1611                    ),
1612                    cx,
1613                );
1614                return;
1615            };
1616
1617            let thread_view = panel.read(cx).active_thread_view().cloned();
1618            let Some(thread_view) = thread_view else {
1619                workspace.show_toast(
1620                    Toast::new(
1621                        NotificationId::unique::<SendReviewToAgent>(),
1622                        "No active thread view available after creating thread.",
1623                    ),
1624                    cx,
1625                );
1626                return;
1627            };
1628
1629            // Build creases for all comments, grouping by code snippet
1630            // so each snippet appears once with all its comments
1631            let snapshot = buffer.read(cx).snapshot(cx);
1632
1633            // Group comments by their point range (code snippet)
1634            let mut comments_by_range: std::collections::BTreeMap<
1635                (rope::Point, rope::Point),
1636                Vec<String>,
1637            > = std::collections::BTreeMap::new();
1638
1639            for comment in comments {
1640                let start = comment.anchor_range.start.to_point(&snapshot);
1641                let end = comment.anchor_range.end.to_point(&snapshot);
1642                comments_by_range
1643                    .entry((start, end))
1644                    .or_default()
1645                    .push(comment.comment);
1646            }
1647
1648            // Build one crease per unique code snippet with all its comments
1649            let mut all_creases = Vec::new();
1650            for ((start, end), comment_texts) in comments_by_range {
1651                let point_range = start..end;
1652
1653                let mut creases =
1654                    selections_creases(vec![point_range.clone()], snapshot.clone(), cx);
1655
1656                // Append all comments after the code snippet
1657                for (code_text, crease_title) in &mut creases {
1658                    let comments_section = comment_texts.join("\n\n");
1659                    *code_text = format!("{}\n\n{}", code_text, comments_section);
1660                    *crease_title = format!("Review: {}", crease_title);
1661                }
1662
1663                all_creases.extend(creases);
1664            }
1665
1666            // Insert all creases into the message editor
1667            thread_view.update(cx, |thread_view, cx| {
1668                thread_view.insert_code_crease(all_creases, window, cx);
1669            });
1670        });
1671    }
1672
1673    pub fn quote_ranges(
1674        &mut self,
1675        ranges: Vec<Range<Point>>,
1676        snapshot: MultiBufferSnapshot,
1677        window: &mut Window,
1678        cx: &mut Context<Self>,
1679    ) {
1680        let creases = selections_creases(ranges, snapshot, cx);
1681
1682        self.editor.update(cx, |editor, cx| {
1683            editor.insert("\n", window, cx);
1684            for (text, crease_title) in creases {
1685                let point = editor
1686                    .selections
1687                    .newest::<Point>(&editor.display_snapshot(cx))
1688                    .head();
1689                let start_row = MultiBufferRow(point.row);
1690
1691                editor.insert(&text, window, cx);
1692
1693                let snapshot = editor.buffer().read(cx).snapshot(cx);
1694                let anchor_before = snapshot.anchor_after(point);
1695                let anchor_after = editor
1696                    .selections
1697                    .newest_anchor()
1698                    .head()
1699                    .bias_left(&snapshot);
1700
1701                editor.insert("\n", window, cx);
1702
1703                let fold_placeholder =
1704                    quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1705                let crease = Crease::inline(
1706                    anchor_before..anchor_after,
1707                    fold_placeholder,
1708                    render_quote_selection_output_toggle,
1709                    |_, _, _, _| Empty.into_any(),
1710                );
1711                editor.insert_creases(vec![crease], cx);
1712                editor.fold_at(start_row, window, cx);
1713            }
1714        })
1715    }
1716
1717    fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
1718        if self.editor.read(cx).selections.count() == 1 {
1719            let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
1720            cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1721                copied_text,
1722                metadata,
1723            ));
1724            cx.stop_propagation();
1725            return;
1726        }
1727
1728        cx.propagate();
1729    }
1730
1731    fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context<Self>) {
1732        if self.editor.read(cx).selections.count() == 1 {
1733            let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
1734
1735            self.editor.update(cx, |editor, cx| {
1736                editor.transact(window, cx, |this, window, cx| {
1737                    this.change_selections(Default::default(), window, cx, |s| {
1738                        s.select(selections);
1739                    });
1740                    this.insert("", window, cx);
1741                    cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1742                        copied_text,
1743                        metadata,
1744                    ));
1745                });
1746            });
1747
1748            cx.stop_propagation();
1749            return;
1750        }
1751
1752        cx.propagate();
1753    }
1754
1755    fn get_clipboard_contents(
1756        &mut self,
1757        cx: &mut Context<Self>,
1758    ) -> (
1759        String,
1760        CopyMetadata,
1761        Vec<text::Selection<MultiBufferOffset>>,
1762    ) {
1763        let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
1764            let mut selection = editor
1765                .selections
1766                .newest_adjusted(&editor.display_snapshot(cx));
1767            let snapshot = editor.buffer().read(cx).snapshot(cx);
1768
1769            selection.goal = SelectionGoal::None;
1770
1771            let selection_start = snapshot.point_to_offset(selection.start);
1772
1773            (
1774                selection.map(|point| snapshot.point_to_offset(point)),
1775                editor.display_map.update(cx, |display_map, cx| {
1776                    display_map
1777                        .snapshot(cx)
1778                        .crease_snapshot
1779                        .creases_in_range(
1780                            MultiBufferRow(selection.start.row)
1781                                ..MultiBufferRow(selection.end.row + 1),
1782                            &snapshot,
1783                        )
1784                        .filter_map(|crease| {
1785                            if let Crease::Inline {
1786                                range, metadata, ..
1787                            } = &crease
1788                            {
1789                                let metadata = metadata.as_ref()?;
1790                                let start = range
1791                                    .start
1792                                    .to_offset(&snapshot)
1793                                    .saturating_sub(selection_start);
1794                                let end = range
1795                                    .end
1796                                    .to_offset(&snapshot)
1797                                    .saturating_sub(selection_start);
1798
1799                                let range_relative_to_selection = start..end;
1800                                if !range_relative_to_selection.is_empty() {
1801                                    return Some(SelectedCreaseMetadata {
1802                                        range_relative_to_selection,
1803                                        crease: metadata.clone(),
1804                                    });
1805                                }
1806                            }
1807                            None
1808                        })
1809                        .collect::<Vec<_>>()
1810                }),
1811            )
1812        });
1813
1814        let text_thread = self.text_thread.read(cx);
1815
1816        let mut text = String::new();
1817
1818        // If selection is empty, we want to copy the entire line
1819        if selection.range().is_empty() {
1820            let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1821            let point = snapshot.offset_to_point(selection.range().start);
1822            selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
1823            selection.end = snapshot
1824                .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
1825            for chunk in snapshot.text_for_range(selection.range()) {
1826                text.push_str(chunk);
1827            }
1828        } else {
1829            for message in text_thread.messages(cx) {
1830                if message.offset_range.start >= selection.range().end.0 {
1831                    break;
1832                } else if message.offset_range.end >= selection.range().start.0 {
1833                    let range = cmp::max(message.offset_range.start, selection.range().start.0)
1834                        ..cmp::min(message.offset_range.end, selection.range().end.0);
1835                    if !range.is_empty() {
1836                        for chunk in text_thread.buffer().read(cx).text_for_range(range) {
1837                            text.push_str(chunk);
1838                        }
1839                        if message.offset_range.end < selection.range().end.0 {
1840                            text.push('\n');
1841                        }
1842                    }
1843                }
1844            }
1845        }
1846        (text, CopyMetadata { creases }, vec![selection])
1847    }
1848
1849    fn paste(
1850        &mut self,
1851        action: &editor::actions::Paste,
1852        window: &mut Window,
1853        cx: &mut Context<Self>,
1854    ) {
1855        let Some(workspace) = self.workspace.upgrade() else {
1856            return;
1857        };
1858        let editor_clipboard_selections = cx
1859            .read_from_clipboard()
1860            .and_then(|item| item.entries().first().cloned())
1861            .and_then(|entry| match entry {
1862                ClipboardEntry::String(text) => {
1863                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
1864                }
1865                _ => None,
1866            });
1867
1868        // Insert creases for pasted clipboard selections that:
1869        // 1. Contain exactly one selection
1870        // 2. Have an associated file path
1871        // 3. Span multiple lines (not single-line selections)
1872        // 4. Belong to a file that exists in the current project
1873        let should_insert_creases = util::maybe!({
1874            let selections = editor_clipboard_selections.as_ref()?;
1875            if selections.len() > 1 {
1876                return Some(false);
1877            }
1878            let selection = selections.first()?;
1879            let file_path = selection.file_path.as_ref()?;
1880            let line_range = selection.line_range.as_ref()?;
1881
1882            if line_range.start() == line_range.end() {
1883                return Some(false);
1884            }
1885
1886            Some(
1887                workspace
1888                    .read(cx)
1889                    .project()
1890                    .read(cx)
1891                    .project_path_for_absolute_path(file_path, cx)
1892                    .is_some(),
1893            )
1894        })
1895        .unwrap_or(false);
1896
1897        if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
1898            if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
1899                if let Some(selections) = editor_clipboard_selections {
1900                    cx.stop_propagation();
1901
1902                    let text = clipboard_text.text();
1903                    self.editor.update(cx, |editor, cx| {
1904                        let mut current_offset = 0;
1905                        let weak_editor = cx.entity().downgrade();
1906
1907                        for selection in selections {
1908                            if let (Some(file_path), Some(line_range)) =
1909                                (selection.file_path, selection.line_range)
1910                            {
1911                                let selected_text =
1912                                    &text[current_offset..current_offset + selection.len];
1913                                let fence = assistant_slash_commands::codeblock_fence_for_path(
1914                                    file_path.to_str(),
1915                                    Some(line_range.clone()),
1916                                );
1917                                let formatted_text = format!("{fence}{selected_text}\n```");
1918
1919                                let insert_point = editor
1920                                    .selections
1921                                    .newest::<Point>(&editor.display_snapshot(cx))
1922                                    .head();
1923                                let start_row = MultiBufferRow(insert_point.row);
1924
1925                                editor.insert(&formatted_text, window, cx);
1926
1927                                let snapshot = editor.buffer().read(cx).snapshot(cx);
1928                                let anchor_before = snapshot.anchor_after(insert_point);
1929                                let anchor_after = editor
1930                                    .selections
1931                                    .newest_anchor()
1932                                    .head()
1933                                    .bias_left(&snapshot);
1934
1935                                editor.insert("\n", window, cx);
1936
1937                                let crease_text = acp_thread::selection_name(
1938                                    Some(file_path.as_ref()),
1939                                    &line_range,
1940                                );
1941
1942                                let fold_placeholder = quote_selection_fold_placeholder(
1943                                    crease_text,
1944                                    weak_editor.clone(),
1945                                );
1946                                let crease = Crease::inline(
1947                                    anchor_before..anchor_after,
1948                                    fold_placeholder,
1949                                    render_quote_selection_output_toggle,
1950                                    |_, _, _, _| Empty.into_any(),
1951                                );
1952                                editor.insert_creases(vec![crease], cx);
1953                                editor.fold_at(start_row, window, cx);
1954
1955                                current_offset += selection.len;
1956                                if !selection.is_entire_line && current_offset < text.len() {
1957                                    current_offset += 1;
1958                                }
1959                            }
1960                        }
1961                    });
1962                    return;
1963                }
1964            }
1965        }
1966
1967        cx.stop_propagation();
1968
1969        let mut images = if let Some(item) = cx.read_from_clipboard() {
1970            item.into_entries()
1971                .filter_map(|entry| {
1972                    if let ClipboardEntry::Image(image) = entry {
1973                        Some(image)
1974                    } else {
1975                        None
1976                    }
1977                })
1978                .collect()
1979        } else {
1980            Vec::new()
1981        };
1982
1983        if let Some(paths) = cx.read_from_clipboard() {
1984            for path in paths
1985                .into_entries()
1986                .filter_map(|entry| {
1987                    if let ClipboardEntry::ExternalPaths(paths) = entry {
1988                        Some(paths.paths().to_owned())
1989                    } else {
1990                        None
1991                    }
1992                })
1993                .flatten()
1994            {
1995                let Ok(content) = std::fs::read(path) else {
1996                    continue;
1997                };
1998                let Ok(format) = image::guess_format(&content) else {
1999                    continue;
2000                };
2001                images.push(gpui::Image::from_bytes(
2002                    match format {
2003                        image::ImageFormat::Png => gpui::ImageFormat::Png,
2004                        image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
2005                        image::ImageFormat::WebP => gpui::ImageFormat::Webp,
2006                        image::ImageFormat::Gif => gpui::ImageFormat::Gif,
2007                        image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
2008                        image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
2009                        image::ImageFormat::Ico => gpui::ImageFormat::Ico,
2010                        _ => continue,
2011                    },
2012                    content,
2013                ));
2014            }
2015        }
2016
2017        let metadata = if let Some(item) = cx.read_from_clipboard() {
2018            item.entries().first().and_then(|entry| {
2019                if let ClipboardEntry::String(text) = entry {
2020                    text.metadata_json::<CopyMetadata>()
2021                } else {
2022                    None
2023                }
2024            })
2025        } else {
2026            None
2027        };
2028
2029        if images.is_empty() {
2030            self.editor.update(cx, |editor, cx| {
2031                let paste_position = editor
2032                    .selections
2033                    .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
2034                    .head();
2035                editor.paste(action, window, cx);
2036
2037                if let Some(metadata) = metadata {
2038                    let buffer = editor.buffer().read(cx).snapshot(cx);
2039
2040                    let mut buffer_rows_to_fold = BTreeSet::new();
2041                    let weak_editor = cx.entity().downgrade();
2042                    editor.insert_creases(
2043                        metadata.creases.into_iter().map(|metadata| {
2044                            let start = buffer.anchor_after(
2045                                paste_position + metadata.range_relative_to_selection.start,
2046                            );
2047                            let end = buffer.anchor_before(
2048                                paste_position + metadata.range_relative_to_selection.end,
2049                            );
2050
2051                            let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2052                            buffer_rows_to_fold.insert(buffer_row);
2053                            Crease::inline(
2054                                start..end,
2055                                FoldPlaceholder {
2056                                    render: render_fold_icon_button(
2057                                        weak_editor.clone(),
2058                                        metadata.crease.icon_path.clone(),
2059                                        metadata.crease.label.clone(),
2060                                    ),
2061                                    ..Default::default()
2062                                },
2063                                render_slash_command_output_toggle,
2064                                |_, _, _, _| Empty.into_any(),
2065                            )
2066                            .with_metadata(metadata.crease)
2067                        }),
2068                        cx,
2069                    );
2070                    for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2071                        editor.fold_at(buffer_row, window, cx);
2072                    }
2073                }
2074            });
2075        } else {
2076            let mut image_positions = Vec::new();
2077            self.editor.update(cx, |editor, cx| {
2078                editor.transact(window, cx, |editor, _window, cx| {
2079                    let edits = editor
2080                        .selections
2081                        .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
2082                        .into_iter()
2083                        .map(|selection| (selection.start..selection.end, "\n"));
2084                    editor.edit(edits, cx);
2085
2086                    let snapshot = editor.buffer().read(cx).snapshot(cx);
2087                    for selection in editor
2088                        .selections
2089                        .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
2090                    {
2091                        image_positions.push(snapshot.anchor_before(selection.end));
2092                    }
2093                });
2094            });
2095
2096            self.text_thread.update(cx, |text_thread, cx| {
2097                for image in images {
2098                    let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
2099                    else {
2100                        continue;
2101                    };
2102                    let image_id = image.id();
2103                    let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
2104
2105                    for image_position in image_positions.iter() {
2106                        text_thread.insert_content(
2107                            Content::Image {
2108                                anchor: image_position.text_anchor,
2109                                image_id,
2110                                image: image_task.clone(),
2111                                render_image: render_image.clone(),
2112                            },
2113                            cx,
2114                        );
2115                    }
2116                }
2117            });
2118        }
2119    }
2120
2121    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
2122        self.editor.update(cx, |editor, cx| {
2123            editor.paste(&editor::actions::Paste, window, cx);
2124        });
2125    }
2126
2127    fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
2128        self.editor.update(cx, |editor, cx| {
2129            let buffer = editor.buffer().read(cx).snapshot(cx);
2130            let excerpt_id = *buffer.as_singleton().unwrap().0;
2131            let old_blocks = std::mem::take(&mut self.image_blocks);
2132            let new_blocks = self
2133                .text_thread
2134                .read(cx)
2135                .contents(cx)
2136                .map(
2137                    |Content::Image {
2138                         anchor,
2139                         render_image,
2140                         ..
2141                     }| (anchor, render_image),
2142                )
2143                .filter_map(|(anchor, render_image)| {
2144                    const MAX_HEIGHT_IN_LINES: u32 = 8;
2145                    let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
2146                    let image = render_image;
2147                    anchor.is_valid(&buffer).then(|| BlockProperties {
2148                        placement: BlockPlacement::Above(anchor),
2149                        height: Some(MAX_HEIGHT_IN_LINES),
2150                        style: BlockStyle::Sticky,
2151                        render: Arc::new(move |cx| {
2152                            let image_size = size_for_image(
2153                                &image,
2154                                size(
2155                                    cx.max_width - cx.margins.gutter.full_width(),
2156                                    MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
2157                                ),
2158                            );
2159                            h_flex()
2160                                .pl(cx.margins.gutter.full_width())
2161                                .child(
2162                                    img(image.clone())
2163                                        .object_fit(gpui::ObjectFit::ScaleDown)
2164                                        .w(image_size.width)
2165                                        .h(image_size.height),
2166                                )
2167                                .into_any_element()
2168                        }),
2169                        priority: 0,
2170                    })
2171                })
2172                .collect::<Vec<_>>();
2173
2174            editor.remove_blocks(old_blocks, None, cx);
2175            let ids = editor.insert_blocks(new_blocks, None, cx);
2176            self.image_blocks = HashSet::from_iter(ids);
2177        });
2178    }
2179
2180    fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
2181        self.text_thread.update(cx, |text_thread, cx| {
2182            let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
2183            for selection in selections.as_ref() {
2184                let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2185                let range = selection
2186                    .map(|endpoint| endpoint.to_offset(&buffer))
2187                    .range();
2188                text_thread.split_message(range.start.0..range.end.0, cx);
2189            }
2190        });
2191    }
2192
2193    fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
2194        self.text_thread.update(cx, |text_thread, cx| {
2195            text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2196        });
2197    }
2198
2199    pub fn title(&self, cx: &App) -> SharedString {
2200        self.text_thread.read(cx).summary().or_default()
2201    }
2202
2203    pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
2204        self.text_thread
2205            .update(cx, |text_thread, cx| text_thread.summarize(true, cx));
2206    }
2207
2208    fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
2209        let (token_count_color, token_count, max_token_count, tooltip) =
2210            match token_state(&self.text_thread, cx)? {
2211                TokenState::NoTokensLeft {
2212                    max_token_count,
2213                    token_count,
2214                } => (
2215                    Color::Error,
2216                    token_count,
2217                    max_token_count,
2218                    Some("Token Limit Reached"),
2219                ),
2220                TokenState::HasMoreTokens {
2221                    max_token_count,
2222                    token_count,
2223                    over_warn_threshold,
2224                } => {
2225                    let (color, tooltip) = if over_warn_threshold {
2226                        (Color::Warning, Some("Token Limit is Close to Exhaustion"))
2227                    } else {
2228                        (Color::Muted, None)
2229                    };
2230                    (color, token_count, max_token_count, tooltip)
2231                }
2232            };
2233
2234        Some(
2235            h_flex()
2236                .id("token-count")
2237                .gap_0p5()
2238                .child(
2239                    Label::new(humanize_token_count(token_count))
2240                        .size(LabelSize::Small)
2241                        .color(token_count_color),
2242                )
2243                .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2244                .child(
2245                    Label::new(humanize_token_count(max_token_count))
2246                        .size(LabelSize::Small)
2247                        .color(Color::Muted),
2248                )
2249                .when_some(tooltip, |element, tooltip| {
2250                    element.tooltip(Tooltip::text(tooltip))
2251                }),
2252        )
2253    }
2254
2255    fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2256        let focus_handle = self.focus_handle(cx);
2257
2258        let (style, tooltip) = match token_state(&self.text_thread, cx) {
2259            Some(TokenState::NoTokensLeft { .. }) => (
2260                ButtonStyle::Tinted(TintColor::Error),
2261                Some(Tooltip::text("Token limit reached")(window, cx)),
2262            ),
2263            Some(TokenState::HasMoreTokens {
2264                over_warn_threshold,
2265                ..
2266            }) => {
2267                let (style, tooltip) = if over_warn_threshold {
2268                    (
2269                        ButtonStyle::Tinted(TintColor::Warning),
2270                        Some(Tooltip::text("Token limit is close to exhaustion")(
2271                            window, cx,
2272                        )),
2273                    )
2274                } else {
2275                    (ButtonStyle::Filled, None)
2276                };
2277                (style, tooltip)
2278            }
2279            None => (ButtonStyle::Filled, None),
2280        };
2281
2282        Button::new("send_button", "Send")
2283            .label_size(LabelSize::Small)
2284            .disabled(self.sending_disabled(cx))
2285            .style(style)
2286            .when_some(tooltip, |button, tooltip| {
2287                button.tooltip(move |_, _| tooltip.clone())
2288            })
2289            .layer(ElevationIndex::ModalSurface)
2290            .key_binding(
2291                KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2292                    .map(|kb| kb.size(rems_from_px(12.))),
2293            )
2294            .on_click(move |_event, window, cx| {
2295                focus_handle.dispatch_action(&Assist, window, cx);
2296            })
2297    }
2298
2299    /// Whether or not we should allow messages to be sent.
2300    /// Will return false if the selected provided has a configuration error or
2301    /// if the user has not accepted the terms of service for this provider.
2302    fn sending_disabled(&self, cx: &mut Context<'_, TextThreadEditor>) -> bool {
2303        let model_registry = LanguageModelRegistry::read_global(cx);
2304        let Some(configuration_error) =
2305            model_registry.configuration_error(model_registry.default_model(), cx)
2306        else {
2307            return false;
2308        };
2309
2310        match configuration_error {
2311            ConfigurationError::NoProvider
2312            | ConfigurationError::ModelNotFound
2313            | ConfigurationError::ProviderNotAuthenticated(_) => true,
2314        }
2315    }
2316
2317    fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
2318        slash_command_picker::SlashCommandSelector::new(
2319            self.slash_commands.clone(),
2320            cx.entity().downgrade(),
2321            IconButton::new("trigger", IconName::Plus)
2322                .icon_size(IconSize::Small)
2323                .icon_color(Color::Muted)
2324                .selected_icon_color(Color::Accent)
2325                .selected_style(ButtonStyle::Filled),
2326            move |_window, cx| {
2327                Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx)
2328            },
2329        )
2330    }
2331
2332    fn render_language_model_selector(
2333        &self,
2334        window: &mut Window,
2335        cx: &mut Context<Self>,
2336    ) -> impl IntoElement {
2337        let active_model = LanguageModelRegistry::read_global(cx)
2338            .default_model()
2339            .map(|default| default.model);
2340        let model_name = match active_model {
2341            Some(model) => model.name().0,
2342            None => SharedString::from("Select Model"),
2343        };
2344
2345        let active_provider = LanguageModelRegistry::read_global(cx)
2346            .default_model()
2347            .map(|default| default.provider);
2348
2349        let provider_icon = active_provider
2350            .as_ref()
2351            .map(|p| p.icon())
2352            .unwrap_or(IconOrSvg::Icon(IconName::Ai));
2353
2354        let focus_handle = self.editor().focus_handle(cx);
2355
2356        let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
2357            (Color::Accent, IconName::ChevronUp)
2358        } else {
2359            (Color::Muted, IconName::ChevronDown)
2360        };
2361
2362        let provider_icon_element = match provider_icon {
2363            IconOrSvg::Svg(path) => Icon::from_external_svg(path),
2364            IconOrSvg::Icon(name) => Icon::new(name),
2365        }
2366        .color(color)
2367        .size(IconSize::XSmall);
2368
2369        let show_cycle_row = self
2370            .language_model_selector
2371            .read(cx)
2372            .delegate
2373            .favorites_count()
2374            > 1;
2375
2376        let tooltip = Tooltip::element({
2377            move |_, _cx| {
2378                ModelSelectorTooltip::new(focus_handle.clone())
2379                    .show_cycle_row(show_cycle_row)
2380                    .into_any_element()
2381            }
2382        });
2383
2384        PickerPopoverMenu::new(
2385            self.language_model_selector.clone(),
2386            ButtonLike::new("active-model")
2387                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
2388                .child(
2389                    h_flex()
2390                        .gap_0p5()
2391                        .child(provider_icon_element)
2392                        .child(
2393                            Label::new(model_name)
2394                                .color(color)
2395                                .size(LabelSize::Small)
2396                                .ml_0p5(),
2397                        )
2398                        .child(Icon::new(icon).color(color).size(IconSize::XSmall)),
2399                ),
2400            tooltip,
2401            gpui::Corner::BottomRight,
2402            cx,
2403        )
2404        .with_handle(self.language_model_selector_menu_handle.clone())
2405        .render(window, cx)
2406    }
2407
2408    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2409        let last_error = self.last_error.as_ref()?;
2410
2411        Some(
2412            div()
2413                .absolute()
2414                .right_3()
2415                .bottom_12()
2416                .max_w_96()
2417                .py_2()
2418                .px_3()
2419                .elevation_2(cx)
2420                .occlude()
2421                .child(match last_error {
2422                    AssistError::PaymentRequired => self.render_payment_required_error(cx),
2423                    AssistError::Message(error_message) => {
2424                        self.render_assist_error(error_message, cx)
2425                    }
2426                })
2427                .into_any(),
2428        )
2429    }
2430
2431    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2432        const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
2433
2434        v_flex()
2435            .gap_0p5()
2436            .child(
2437                h_flex()
2438                    .gap_1p5()
2439                    .items_center()
2440                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2441                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2442            )
2443            .child(
2444                div()
2445                    .id("error-message")
2446                    .max_h_24()
2447                    .overflow_y_scroll()
2448                    .child(Label::new(ERROR_MESSAGE)),
2449            )
2450            .child(
2451                h_flex()
2452                    .justify_end()
2453                    .mt_1()
2454                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2455                        |this, _, _window, cx| {
2456                            this.last_error = None;
2457                            cx.open_url(&zed_urls::account_url(cx));
2458                            cx.notify();
2459                        },
2460                    )))
2461                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2462                        |this, _, _window, cx| {
2463                            this.last_error = None;
2464                            cx.notify();
2465                        },
2466                    ))),
2467            )
2468            .into_any()
2469    }
2470
2471    fn render_assist_error(
2472        &self,
2473        error_message: &SharedString,
2474        cx: &mut Context<Self>,
2475    ) -> AnyElement {
2476        v_flex()
2477            .gap_0p5()
2478            .child(
2479                h_flex()
2480                    .gap_1p5()
2481                    .items_center()
2482                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2483                    .child(
2484                        Label::new("Error interacting with language model")
2485                            .weight(FontWeight::MEDIUM),
2486                    ),
2487            )
2488            .child(
2489                div()
2490                    .id("error-message")
2491                    .max_h_32()
2492                    .overflow_y_scroll()
2493                    .child(Label::new(error_message.clone())),
2494            )
2495            .child(
2496                h_flex()
2497                    .justify_end()
2498                    .mt_1()
2499                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2500                        |this, _, _window, cx| {
2501                            this.last_error = None;
2502                            cx.notify();
2503                        },
2504                    ))),
2505            )
2506            .into_any()
2507    }
2508}
2509
2510/// Returns the contents of the *outermost* fenced code block that contains the given offset.
2511fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
2512    const CODE_BLOCK_NODE: &str = "fenced_code_block";
2513    const CODE_BLOCK_CONTENT: &str = "code_fence_content";
2514
2515    let layer = snapshot.syntax_layers().next()?;
2516
2517    let root_node = layer.node();
2518    let mut cursor = root_node.walk();
2519
2520    // Go to the first child for the given offset
2521    while cursor.goto_first_child_for_byte(offset).is_some() {
2522        // If we're at the end of the node, go to the next one.
2523        // Example: if you have a fenced-code-block, and you're on the start of the line
2524        // right after the closing ```, you want to skip the fenced-code-block and
2525        // go to the next sibling.
2526        if cursor.node().end_byte() == offset {
2527            cursor.goto_next_sibling();
2528        }
2529
2530        if cursor.node().start_byte() > offset {
2531            break;
2532        }
2533
2534        // We found the fenced code block.
2535        if cursor.node().kind() == CODE_BLOCK_NODE {
2536            // Now we need to find the child node that contains the code.
2537            cursor.goto_first_child();
2538            loop {
2539                if cursor.node().kind() == CODE_BLOCK_CONTENT {
2540                    return Some(cursor.node().byte_range());
2541                }
2542                if !cursor.goto_next_sibling() {
2543                    break;
2544                }
2545            }
2546        }
2547    }
2548
2549    None
2550}
2551
2552fn render_thought_process_fold_icon_button(
2553    editor: WeakEntity<Editor>,
2554    status: ThoughtProcessStatus,
2555) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2556    Arc::new(move |fold_id, fold_range, _cx| {
2557        let editor = editor.clone();
2558
2559        let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
2560        let button = match status {
2561            ThoughtProcessStatus::Pending => button
2562                .child(
2563                    Icon::new(IconName::ToolThink)
2564                        .size(IconSize::Small)
2565                        .color(Color::Muted),
2566                )
2567                .child(
2568                    Label::new("Thinking…").color(Color::Muted).with_animation(
2569                        "pulsating-label",
2570                        Animation::new(Duration::from_secs(2))
2571                            .repeat()
2572                            .with_easing(pulsating_between(0.4, 0.8)),
2573                        |label, delta| label.alpha(delta),
2574                    ),
2575                ),
2576            ThoughtProcessStatus::Completed => button
2577                .style(ButtonStyle::Filled)
2578                .child(Icon::new(IconName::ToolThink).size(IconSize::Small))
2579                .child(Label::new("Thought Process").single_line()),
2580        };
2581
2582        button
2583            .on_click(move |_, window, cx| {
2584                editor
2585                    .update(cx, |editor, cx| {
2586                        let buffer_start = fold_range
2587                            .start
2588                            .to_point(&editor.buffer().read(cx).read(cx));
2589                        let buffer_row = MultiBufferRow(buffer_start.row);
2590                        editor.unfold_at(buffer_row, window, cx);
2591                    })
2592                    .ok();
2593            })
2594            .into_any_element()
2595    })
2596}
2597
2598fn render_fold_icon_button(
2599    editor: WeakEntity<Editor>,
2600    icon_path: SharedString,
2601    label: SharedString,
2602) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2603    Arc::new(move |fold_id, fold_range, _cx| {
2604        let editor = editor.clone();
2605        ButtonLike::new(fold_id)
2606            .style(ButtonStyle::Filled)
2607            .layer(ElevationIndex::ElevatedSurface)
2608            .child(Icon::from_path(icon_path.clone()))
2609            .child(Label::new(label.clone()).single_line())
2610            .on_click(move |_, window, cx| {
2611                editor
2612                    .update(cx, |editor, cx| {
2613                        let buffer_start = fold_range
2614                            .start
2615                            .to_point(&editor.buffer().read(cx).read(cx));
2616                        let buffer_row = MultiBufferRow(buffer_start.row);
2617                        editor.unfold_at(buffer_row, window, cx);
2618                    })
2619                    .ok();
2620            })
2621            .into_any_element()
2622    })
2623}
2624
2625type ToggleFold = Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>;
2626
2627fn render_slash_command_output_toggle(
2628    row: MultiBufferRow,
2629    is_folded: bool,
2630    fold: ToggleFold,
2631    _window: &mut Window,
2632    _cx: &mut App,
2633) -> AnyElement {
2634    Disclosure::new(
2635        ("slash-command-output-fold-indicator", row.0 as u64),
2636        !is_folded,
2637    )
2638    .toggle_state(is_folded)
2639    .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2640    .into_any_element()
2641}
2642
2643pub fn fold_toggle(
2644    name: &'static str,
2645) -> impl Fn(
2646    MultiBufferRow,
2647    bool,
2648    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
2649    &mut Window,
2650    &mut App,
2651) -> AnyElement {
2652    move |row, is_folded, fold, _window, _cx| {
2653        Disclosure::new((name, row.0 as u64), !is_folded)
2654            .toggle_state(is_folded)
2655            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2656            .into_any_element()
2657    }
2658}
2659
2660fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
2661    FoldPlaceholder {
2662        render: Arc::new({
2663            move |fold_id, fold_range, _cx| {
2664                let editor = editor.clone();
2665                ButtonLike::new(fold_id)
2666                    .style(ButtonStyle::Filled)
2667                    .layer(ElevationIndex::ElevatedSurface)
2668                    .child(Icon::new(IconName::TextSnippet))
2669                    .child(Label::new(title.clone()).single_line())
2670                    .on_click(move |_, window, cx| {
2671                        editor
2672                            .update(cx, |editor, cx| {
2673                                let buffer_start = fold_range
2674                                    .start
2675                                    .to_point(&editor.buffer().read(cx).read(cx));
2676                                let buffer_row = MultiBufferRow(buffer_start.row);
2677                                editor.unfold_at(buffer_row, window, cx);
2678                            })
2679                            .ok();
2680                    })
2681                    .into_any_element()
2682            }
2683        }),
2684        merge_adjacent: false,
2685        ..Default::default()
2686    }
2687}
2688
2689fn render_quote_selection_output_toggle(
2690    row: MultiBufferRow,
2691    is_folded: bool,
2692    fold: ToggleFold,
2693    _window: &mut Window,
2694    _cx: &mut App,
2695) -> AnyElement {
2696    Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
2697        .toggle_state(is_folded)
2698        .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2699        .into_any_element()
2700}
2701
2702fn render_pending_slash_command_gutter_decoration(
2703    row: MultiBufferRow,
2704    status: &PendingSlashCommandStatus,
2705    confirm_command: Arc<dyn Fn(&mut Window, &mut App)>,
2706) -> AnyElement {
2707    let mut icon = IconButton::new(
2708        ("slash-command-gutter-decoration", row.0),
2709        ui::IconName::TriangleRight,
2710    )
2711    .on_click(move |_e, window, cx| confirm_command(window, cx))
2712    .icon_size(ui::IconSize::Small)
2713    .size(ui::ButtonSize::None);
2714
2715    match status {
2716        PendingSlashCommandStatus::Idle => {
2717            icon = icon.icon_color(Color::Muted);
2718        }
2719        PendingSlashCommandStatus::Running { .. } => {
2720            icon = icon.toggle_state(true);
2721        }
2722        PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
2723    }
2724
2725    icon.into_any_element()
2726}
2727
2728#[derive(Debug, Clone, Serialize, Deserialize)]
2729struct CopyMetadata {
2730    creases: Vec<SelectedCreaseMetadata>,
2731}
2732
2733#[derive(Debug, Clone, Serialize, Deserialize)]
2734struct SelectedCreaseMetadata {
2735    range_relative_to_selection: Range<usize>,
2736    crease: CreaseMetadata,
2737}
2738
2739impl EventEmitter<EditorEvent> for TextThreadEditor {}
2740impl EventEmitter<SearchEvent> for TextThreadEditor {}
2741
2742impl Render for TextThreadEditor {
2743    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2744        let language_model_selector = self.language_model_selector_menu_handle.clone();
2745
2746        v_flex()
2747            .key_context("ContextEditor")
2748            .capture_action(cx.listener(TextThreadEditor::cancel))
2749            .capture_action(cx.listener(TextThreadEditor::save))
2750            .capture_action(cx.listener(TextThreadEditor::copy))
2751            .capture_action(cx.listener(TextThreadEditor::cut))
2752            .capture_action(cx.listener(TextThreadEditor::paste))
2753            .on_action(cx.listener(TextThreadEditor::paste_raw))
2754            .capture_action(cx.listener(TextThreadEditor::cycle_message_role))
2755            .capture_action(cx.listener(TextThreadEditor::confirm_command))
2756            .on_action(cx.listener(TextThreadEditor::assist))
2757            .on_action(cx.listener(TextThreadEditor::split))
2758            .on_action(move |_: &ToggleModelSelector, window, cx| {
2759                language_model_selector.toggle(window, cx);
2760            })
2761            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
2762                this.language_model_selector.update(cx, |selector, cx| {
2763                    selector.delegate.cycle_favorite_models(window, cx);
2764                });
2765            }))
2766            .size_full()
2767            .child(
2768                div()
2769                    .flex_grow()
2770                    .bg(cx.theme().colors().editor_background)
2771                    .child(self.editor.clone()),
2772            )
2773            .children(self.render_last_error(cx))
2774            .child(
2775                h_flex()
2776                    .relative()
2777                    .py_2()
2778                    .pl_1p5()
2779                    .pr_2()
2780                    .w_full()
2781                    .justify_between()
2782                    .border_t_1()
2783                    .border_color(cx.theme().colors().border_variant)
2784                    .bg(cx.theme().colors().editor_background)
2785                    .child(
2786                        h_flex()
2787                            .gap_0p5()
2788                            .child(self.render_inject_context_menu(cx)),
2789                    )
2790                    .child(
2791                        h_flex()
2792                            .gap_2p5()
2793                            .children(self.render_remaining_tokens(cx))
2794                            .child(
2795                                h_flex()
2796                                    .gap_1()
2797                                    .child(self.render_language_model_selector(window, cx))
2798                                    .child(self.render_send_button(window, cx)),
2799                            ),
2800                    ),
2801            )
2802    }
2803}
2804
2805impl Focusable for TextThreadEditor {
2806    fn focus_handle(&self, cx: &App) -> FocusHandle {
2807        self.editor.focus_handle(cx)
2808    }
2809}
2810
2811impl Item for TextThreadEditor {
2812    type Event = editor::EditorEvent;
2813
2814    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
2815        util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
2816    }
2817
2818    fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2819        match event {
2820            EditorEvent::Edited { .. } => {
2821                f(item::ItemEvent::Edit);
2822            }
2823            EditorEvent::TitleChanged => {
2824                f(item::ItemEvent::UpdateTab);
2825            }
2826            _ => {}
2827        }
2828    }
2829
2830    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
2831        Some(self.title(cx).to_string().into())
2832    }
2833
2834    fn as_searchable(
2835        &self,
2836        handle: &Entity<Self>,
2837        _: &App,
2838    ) -> Option<Box<dyn SearchableItemHandle>> {
2839        Some(Box::new(handle.clone()))
2840    }
2841
2842    fn set_nav_history(
2843        &mut self,
2844        nav_history: pane::ItemNavHistory,
2845        window: &mut Window,
2846        cx: &mut Context<Self>,
2847    ) {
2848        self.editor.update(cx, |editor, cx| {
2849            Item::set_nav_history(editor, nav_history, window, cx)
2850        })
2851    }
2852
2853    fn navigate(
2854        &mut self,
2855        data: Arc<dyn Any + Send>,
2856        window: &mut Window,
2857        cx: &mut Context<Self>,
2858    ) -> bool {
2859        self.editor
2860            .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
2861    }
2862
2863    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2864        self.editor
2865            .update(cx, |editor, cx| Item::deactivated(editor, window, cx))
2866    }
2867
2868    fn act_as_type<'a>(
2869        &'a self,
2870        type_id: TypeId,
2871        self_handle: &'a Entity<Self>,
2872        _: &'a App,
2873    ) -> Option<gpui::AnyEntity> {
2874        if type_id == TypeId::of::<Self>() {
2875            Some(self_handle.clone().into())
2876        } else if type_id == TypeId::of::<Editor>() {
2877            Some(self.editor.clone().into())
2878        } else {
2879            None
2880        }
2881    }
2882
2883    fn include_in_nav_history() -> bool {
2884        false
2885    }
2886}
2887
2888impl SearchableItem for TextThreadEditor {
2889    type Match = <Editor as SearchableItem>::Match;
2890
2891    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2892        self.editor.update(cx, |editor, cx| {
2893            editor.clear_matches(window, cx);
2894        });
2895    }
2896
2897    fn update_matches(
2898        &mut self,
2899        matches: &[Self::Match],
2900        active_match_index: Option<usize>,
2901        window: &mut Window,
2902        cx: &mut Context<Self>,
2903    ) {
2904        self.editor.update(cx, |editor, cx| {
2905            editor.update_matches(matches, active_match_index, window, cx)
2906        });
2907    }
2908
2909    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
2910        self.editor
2911            .update(cx, |editor, cx| editor.query_suggestion(window, cx))
2912    }
2913
2914    fn activate_match(
2915        &mut self,
2916        index: usize,
2917        matches: &[Self::Match],
2918        window: &mut Window,
2919        cx: &mut Context<Self>,
2920    ) {
2921        self.editor.update(cx, |editor, cx| {
2922            editor.activate_match(index, matches, window, cx);
2923        });
2924    }
2925
2926    fn select_matches(
2927        &mut self,
2928        matches: &[Self::Match],
2929        window: &mut Window,
2930        cx: &mut Context<Self>,
2931    ) {
2932        self.editor
2933            .update(cx, |editor, cx| editor.select_matches(matches, window, cx));
2934    }
2935
2936    fn replace(
2937        &mut self,
2938        identifier: &Self::Match,
2939        query: &project::search::SearchQuery,
2940        window: &mut Window,
2941        cx: &mut Context<Self>,
2942    ) {
2943        self.editor.update(cx, |editor, cx| {
2944            editor.replace(identifier, query, window, cx)
2945        });
2946    }
2947
2948    fn find_matches(
2949        &mut self,
2950        query: Arc<project::search::SearchQuery>,
2951        window: &mut Window,
2952        cx: &mut Context<Self>,
2953    ) -> Task<Vec<Self::Match>> {
2954        self.editor
2955            .update(cx, |editor, cx| editor.find_matches(query, window, cx))
2956    }
2957
2958    fn active_match_index(
2959        &mut self,
2960        direction: Direction,
2961        matches: &[Self::Match],
2962        window: &mut Window,
2963        cx: &mut Context<Self>,
2964    ) -> Option<usize> {
2965        self.editor.update(cx, |editor, cx| {
2966            editor.active_match_index(direction, matches, window, cx)
2967        })
2968    }
2969}
2970
2971impl FollowableItem for TextThreadEditor {
2972    fn remote_id(&self) -> Option<workspace::ViewId> {
2973        self.remote_id
2974    }
2975
2976    fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
2977        let text_thread = self.text_thread.read(cx);
2978        Some(proto::view::Variant::ContextEditor(
2979            proto::view::ContextEditor {
2980                context_id: text_thread.id().to_proto(),
2981                editor: if let Some(proto::view::Variant::Editor(proto)) =
2982                    self.editor.read(cx).to_state_proto(window, cx)
2983                {
2984                    Some(proto)
2985                } else {
2986                    None
2987                },
2988            },
2989        ))
2990    }
2991
2992    fn from_state_proto(
2993        workspace: Entity<Workspace>,
2994        id: workspace::ViewId,
2995        state: &mut Option<proto::view::Variant>,
2996        window: &mut Window,
2997        cx: &mut App,
2998    ) -> Option<Task<Result<Entity<Self>>>> {
2999        let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
3000            return None;
3001        };
3002        let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
3003            unreachable!()
3004        };
3005
3006        let text_thread_id = TextThreadId::from_proto(state.context_id);
3007        let editor_state = state.editor?;
3008
3009        let project = workspace.read(cx).project().clone();
3010        let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
3011
3012        let text_thread_editor_task = workspace.update(cx, |workspace, cx| {
3013            agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx)
3014        });
3015
3016        Some(window.spawn(cx, async move |cx| {
3017            let text_thread_editor = text_thread_editor_task.await?;
3018            text_thread_editor
3019                .update_in(cx, |text_thread_editor, window, cx| {
3020                    text_thread_editor.remote_id = Some(id);
3021                    text_thread_editor.editor.update(cx, |editor, cx| {
3022                        editor.apply_update_proto(
3023                            &project,
3024                            proto::update_view::Variant::Editor(proto::update_view::Editor {
3025                                selections: editor_state.selections,
3026                                pending_selection: editor_state.pending_selection,
3027                                scroll_top_anchor: editor_state.scroll_top_anchor,
3028                                scroll_x: editor_state.scroll_y,
3029                                scroll_y: editor_state.scroll_y,
3030                                ..Default::default()
3031                            }),
3032                            window,
3033                            cx,
3034                        )
3035                    })
3036                })?
3037                .await?;
3038            Ok(text_thread_editor)
3039        }))
3040    }
3041
3042    fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
3043        Editor::to_follow_event(event)
3044    }
3045
3046    fn add_event_to_update_proto(
3047        &self,
3048        event: &Self::Event,
3049        update: &mut Option<proto::update_view::Variant>,
3050        window: &Window,
3051        cx: &App,
3052    ) -> bool {
3053        self.editor
3054            .read(cx)
3055            .add_event_to_update_proto(event, update, window, cx)
3056    }
3057
3058    fn apply_update_proto(
3059        &mut self,
3060        project: &Entity<Project>,
3061        message: proto::update_view::Variant,
3062        window: &mut Window,
3063        cx: &mut Context<Self>,
3064    ) -> Task<Result<()>> {
3065        self.editor.update(cx, |editor, cx| {
3066            editor.apply_update_proto(project, message, window, cx)
3067        })
3068    }
3069
3070    fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
3071        true
3072    }
3073
3074    fn set_leader_id(
3075        &mut self,
3076        leader_id: Option<CollaboratorId>,
3077        window: &mut Window,
3078        cx: &mut Context<Self>,
3079    ) {
3080        self.editor
3081            .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
3082    }
3083
3084    fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
3085        if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() {
3086            Some(item::Dedup::KeepExisting)
3087        } else {
3088            None
3089        }
3090    }
3091}
3092
3093enum PendingSlashCommand {}
3094
3095fn invoked_slash_command_fold_placeholder(
3096    command_id: InvokedSlashCommandId,
3097    text_thread: WeakEntity<TextThread>,
3098) -> FoldPlaceholder {
3099    FoldPlaceholder {
3100        constrain_width: false,
3101        merge_adjacent: false,
3102        render: Arc::new(move |fold_id, _, cx| {
3103            let Some(text_thread) = text_thread.upgrade() else {
3104                return Empty.into_any();
3105            };
3106
3107            let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else {
3108                return Empty.into_any();
3109            };
3110
3111            h_flex()
3112                .id(fold_id)
3113                .px_1()
3114                .ml_6()
3115                .gap_2()
3116                .bg(cx.theme().colors().surface_background)
3117                .rounded_sm()
3118                .child(Label::new(format!("/{}", command.name)))
3119                .map(|parent| match &command.status {
3120                    InvokedSlashCommandStatus::Running(_) => {
3121                        parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
3122                    }
3123                    InvokedSlashCommandStatus::Error(message) => parent.child(
3124                        Label::new(format!("error: {message}"))
3125                            .single_line()
3126                            .color(Color::Error),
3127                    ),
3128                    InvokedSlashCommandStatus::Finished => parent,
3129                })
3130                .into_any_element()
3131        }),
3132        type_tag: Some(TypeId::of::<PendingSlashCommand>()),
3133    }
3134}
3135
3136enum TokenState {
3137    NoTokensLeft {
3138        max_token_count: u64,
3139        token_count: u64,
3140    },
3141    HasMoreTokens {
3142        max_token_count: u64,
3143        token_count: u64,
3144        over_warn_threshold: bool,
3145    },
3146}
3147
3148fn token_state(text_thread: &Entity<TextThread>, cx: &App) -> Option<TokenState> {
3149    const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3150
3151    let model = LanguageModelRegistry::read_global(cx)
3152        .default_model()?
3153        .model;
3154    let token_count = text_thread.read(cx).token_count()?;
3155    let max_token_count = model.max_token_count();
3156    let token_state = if max_token_count.saturating_sub(token_count) == 0 {
3157        TokenState::NoTokensLeft {
3158            max_token_count,
3159            token_count,
3160        }
3161    } else {
3162        let over_warn_threshold =
3163            token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3164        TokenState::HasMoreTokens {
3165            max_token_count,
3166            token_count,
3167            over_warn_threshold,
3168        }
3169    };
3170    Some(token_state)
3171}
3172
3173fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
3174    let image_size = data
3175        .size(0)
3176        .map(|dimension| Pixels::from(u32::from(dimension)));
3177    let image_ratio = image_size.width / image_size.height;
3178    let bounds_ratio = max_size.width / max_size.height;
3179
3180    if image_size.width > max_size.width || image_size.height > max_size.height {
3181        if bounds_ratio > image_ratio {
3182            size(
3183                image_size.width * (max_size.height / image_size.height),
3184                max_size.height,
3185            )
3186        } else {
3187            size(
3188                max_size.width,
3189                image_size.height * (max_size.width / image_size.width),
3190            )
3191        }
3192    } else {
3193        size(image_size.width, image_size.height)
3194    }
3195}
3196
3197pub fn humanize_token_count(count: u64) -> String {
3198    match count {
3199        0..=999 => count.to_string(),
3200        1000..=9999 => {
3201            let thousands = count / 1000;
3202            let hundreds = (count % 1000 + 50) / 100;
3203            if hundreds == 0 {
3204                format!("{}k", thousands)
3205            } else if hundreds == 10 {
3206                format!("{}k", thousands + 1)
3207            } else {
3208                format!("{}.{}k", thousands, hundreds)
3209            }
3210        }
3211        1_000_000..=9_999_999 => {
3212            let millions = count / 1_000_000;
3213            let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
3214            if hundred_thousands == 0 {
3215                format!("{}M", millions)
3216            } else if hundred_thousands == 10 {
3217                format!("{}M", millions + 1)
3218            } else {
3219                format!("{}.{}M", millions, hundred_thousands)
3220            }
3221        }
3222        10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
3223        _ => format!("{}k", (count + 500) / 1000),
3224    }
3225}
3226
3227pub fn make_lsp_adapter_delegate(
3228    project: &Entity<Project>,
3229    cx: &mut App,
3230) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
3231    project.update(cx, |project, cx| {
3232        // TODO: Find the right worktree.
3233        let Some(worktree) = project.worktrees(cx).next() else {
3234            return Ok(None::<Arc<dyn LspAdapterDelegate>>);
3235        };
3236        let http_client = project.client().http_client();
3237        project.lsp_store().update(cx, |_, cx| {
3238            Ok(Some(LocalLspAdapterDelegate::new(
3239                project.languages().clone(),
3240                project.environment(),
3241                cx.weak_entity(),
3242                &worktree,
3243                http_client,
3244                project.fs().clone(),
3245                cx,
3246            ) as Arc<dyn LspAdapterDelegate>))
3247        })
3248    })
3249}
3250
3251#[cfg(test)]
3252mod tests {
3253    use super::*;
3254    use editor::{MultiBufferOffset, SelectionEffects};
3255    use fs::FakeFs;
3256    use gpui::{App, TestAppContext, VisualTestContext};
3257    use indoc::indoc;
3258    use language::{Buffer, LanguageRegistry};
3259    use pretty_assertions::assert_eq;
3260    use prompt_store::PromptBuilder;
3261    use text::OffsetRangeExt;
3262    use unindent::Unindent;
3263    use util::path;
3264
3265    #[gpui::test]
3266    async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
3267        let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![
3268            (Role::User, "What is the Zed editor?"),
3269            (
3270                Role::Assistant,
3271                "Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
3272            ),
3273            (Role::User, ""),
3274        ],cx).await;
3275
3276        // Select & Copy whole user message
3277        assert_copy_paste_text_thread_editor(
3278            &text_thread_editor,
3279            message_range(&context, 0, &mut cx),
3280            indoc! {"
3281                What is the Zed editor?
3282                Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3283                What is the Zed editor?
3284            "},
3285            &mut cx,
3286        );
3287
3288        // Select & Copy whole assistant message
3289        assert_copy_paste_text_thread_editor(
3290            &text_thread_editor,
3291            message_range(&context, 1, &mut cx),
3292            indoc! {"
3293                What is the Zed editor?
3294                Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3295                What is the Zed editor?
3296                Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3297            "},
3298            &mut cx,
3299        );
3300    }
3301
3302    #[gpui::test]
3303    async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
3304        let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(
3305            vec![
3306                (Role::User, "user1"),
3307                (Role::Assistant, "assistant1"),
3308                (Role::Assistant, "assistant2"),
3309                (Role::User, ""),
3310            ],
3311            cx,
3312        )
3313        .await;
3314
3315        // Copy and paste first assistant message
3316        let message_2_range = message_range(&context, 1, &mut cx);
3317        assert_copy_paste_text_thread_editor(
3318            &text_thread_editor,
3319            message_2_range.start..message_2_range.start,
3320            indoc! {"
3321                user1
3322                assistant1
3323                assistant2
3324                assistant1
3325            "},
3326            &mut cx,
3327        );
3328
3329        // Copy and cut second assistant message
3330        let message_3_range = message_range(&context, 2, &mut cx);
3331        assert_copy_paste_text_thread_editor(
3332            &text_thread_editor,
3333            message_3_range.start..message_3_range.start,
3334            indoc! {"
3335                user1
3336                assistant1
3337                assistant2
3338                assistant1
3339                assistant2
3340            "},
3341            &mut cx,
3342        );
3343    }
3344
3345    #[gpui::test]
3346    fn test_find_code_blocks(cx: &mut App) {
3347        let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
3348
3349        let buffer = cx.new(|cx| {
3350            let text = r#"
3351                line 0
3352                line 1
3353                ```rust
3354                fn main() {}
3355                ```
3356                line 5
3357                line 6
3358                line 7
3359                ```go
3360                func main() {}
3361                ```
3362                line 11
3363                ```
3364                this is plain text code block
3365                ```
3366
3367                ```go
3368                func another() {}
3369                ```
3370                line 19
3371            "#
3372            .unindent();
3373            let mut buffer = Buffer::local(text, cx);
3374            buffer.set_language(Some(markdown.clone()), cx);
3375            buffer
3376        });
3377        let snapshot = buffer.read(cx).snapshot();
3378
3379        let code_blocks = vec![
3380            Point::new(3, 0)..Point::new(4, 0),
3381            Point::new(9, 0)..Point::new(10, 0),
3382            Point::new(13, 0)..Point::new(14, 0),
3383            Point::new(17, 0)..Point::new(18, 0),
3384        ]
3385        .into_iter()
3386        .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
3387        .collect::<Vec<_>>();
3388
3389        let expected_results = vec![
3390            (0, None),
3391            (1, None),
3392            (2, Some(code_blocks[0].clone())),
3393            (3, Some(code_blocks[0].clone())),
3394            (4, Some(code_blocks[0].clone())),
3395            (5, None),
3396            (6, None),
3397            (7, None),
3398            (8, Some(code_blocks[1].clone())),
3399            (9, Some(code_blocks[1].clone())),
3400            (10, Some(code_blocks[1].clone())),
3401            (11, None),
3402            (12, Some(code_blocks[2].clone())),
3403            (13, Some(code_blocks[2].clone())),
3404            (14, Some(code_blocks[2].clone())),
3405            (15, None),
3406            (16, Some(code_blocks[3].clone())),
3407            (17, Some(code_blocks[3].clone())),
3408            (18, Some(code_blocks[3].clone())),
3409            (19, None),
3410        ];
3411
3412        for (row, expected) in expected_results {
3413            let offset = snapshot.point_to_offset(Point::new(row, 0));
3414            let range = find_surrounding_code_block(&snapshot, offset);
3415            assert_eq!(range, expected, "unexpected result on row {:?}", row);
3416        }
3417    }
3418
3419    async fn setup_text_thread_editor_text(
3420        messages: Vec<(Role, &str)>,
3421        cx: &mut TestAppContext,
3422    ) -> (
3423        Entity<TextThread>,
3424        Entity<TextThreadEditor>,
3425        VisualTestContext,
3426    ) {
3427        cx.update(init_test);
3428
3429        let fs = FakeFs::new(cx.executor());
3430        let text_thread = create_text_thread_with_messages(messages, cx);
3431
3432        let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
3433        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3434        let workspace = window.root(cx).unwrap();
3435        let mut cx = VisualTestContext::from_window(*window, cx);
3436
3437        let text_thread_editor = window
3438            .update(&mut cx, |_, window, cx| {
3439                cx.new(|cx| {
3440                    TextThreadEditor::for_text_thread(
3441                        text_thread.clone(),
3442                        fs,
3443                        workspace.downgrade(),
3444                        project,
3445                        None,
3446                        window,
3447                        cx,
3448                    )
3449                })
3450            })
3451            .unwrap();
3452
3453        (text_thread, text_thread_editor, cx)
3454    }
3455
3456    fn message_range(
3457        text_thread: &Entity<TextThread>,
3458        message_ix: usize,
3459        cx: &mut TestAppContext,
3460    ) -> Range<MultiBufferOffset> {
3461        let range = text_thread.update(cx, |text_thread, cx| {
3462            text_thread
3463                .messages(cx)
3464                .nth(message_ix)
3465                .unwrap()
3466                .anchor_range
3467                .to_offset(&text_thread.buffer().read(cx).snapshot())
3468        });
3469        MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
3470    }
3471
3472    fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
3473        text_thread_editor: &Entity<TextThreadEditor>,
3474        range: Range<T>,
3475        expected_text: &str,
3476        cx: &mut VisualTestContext,
3477    ) {
3478        text_thread_editor.update_in(cx, |text_thread_editor, window, cx| {
3479            text_thread_editor.editor.update(cx, |editor, cx| {
3480                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3481                    s.select_ranges([range])
3482                });
3483            });
3484
3485            text_thread_editor.copy(&Default::default(), window, cx);
3486
3487            text_thread_editor.editor.update(cx, |editor, cx| {
3488                editor.move_to_end(&Default::default(), window, cx);
3489            });
3490
3491            text_thread_editor.paste(&Default::default(), window, cx);
3492
3493            text_thread_editor.editor.update(cx, |editor, cx| {
3494                assert_eq!(editor.text(cx), expected_text);
3495            });
3496        });
3497    }
3498
3499    fn create_text_thread_with_messages(
3500        mut messages: Vec<(Role, &str)>,
3501        cx: &mut TestAppContext,
3502    ) -> Entity<TextThread> {
3503        let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3504        let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
3505        cx.new(|cx| {
3506            let mut text_thread = TextThread::local(
3507                registry,
3508                prompt_builder.clone(),
3509                Arc::new(SlashCommandWorkingSet::default()),
3510                cx,
3511            );
3512            let mut message_1 = text_thread.messages(cx).next().unwrap();
3513            let (role, text) = messages.remove(0);
3514
3515            loop {
3516                if role == message_1.role {
3517                    text_thread.buffer().update(cx, |buffer, cx| {
3518                        buffer.edit([(message_1.offset_range, text)], None, cx);
3519                    });
3520                    break;
3521                }
3522                let mut ids = HashSet::default();
3523                ids.insert(message_1.id);
3524                text_thread.cycle_message_roles(ids, cx);
3525                message_1 = text_thread.messages(cx).next().unwrap();
3526            }
3527
3528            let mut last_message_id = message_1.id;
3529            for (role, text) in messages {
3530                text_thread.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
3531                let message = text_thread.messages(cx).last().unwrap();
3532                last_message_id = message.id;
3533                text_thread.buffer().update(cx, |buffer, cx| {
3534                    buffer.edit([(message.offset_range, text)], None, cx);
3535                })
3536            }
3537
3538            text_thread
3539        })
3540    }
3541
3542    fn init_test(cx: &mut App) {
3543        let settings_store = SettingsStore::test(cx);
3544        prompt_store::init(cx);
3545        LanguageModelRegistry::test(cx);
3546        cx.set_global(settings_store);
3547
3548        theme::init(theme::LoadThemes::JustBase, cx);
3549    }
3550}