text_thread_editor.rs

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