text_thread_editor.rs

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