inline_prompt_editor.rs

   1use agent::HistoryStore;
   2use collections::{HashMap, VecDeque};
   3use editor::actions::Paste;
   4use editor::code_context_menus::CodeContextMenu;
   5use editor::display_map::{CreaseId, EditorMargins};
   6use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
   7use editor::{
   8    ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
   9    actions::{MoveDown, MoveUp},
  10};
  11use feature_flags::{FeatureFlag, FeatureFlagAppExt};
  12use fs::Fs;
  13use gpui::{
  14    AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
  15    Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,
  16};
  17use language_model::{LanguageModel, LanguageModelRegistry};
  18use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  19use parking_lot::Mutex;
  20use project::Project;
  21use prompt_store::PromptStore;
  22use settings::Settings;
  23use std::ops::Range;
  24use std::rc::Rc;
  25use std::sync::Arc;
  26use std::{cmp, mem};
  27use theme::ThemeSettings;
  28use ui::utils::WithRemSize;
  29use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
  30use uuid::Uuid;
  31use workspace::notifications::NotificationId;
  32use workspace::{Toast, Workspace};
  33use zed_actions::agent::ToggleModelSelector;
  34
  35use crate::agent_model_selector::AgentModelSelector;
  36use crate::buffer_codegen::BufferCodegen;
  37use crate::completion_provider::{
  38    PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
  39};
  40use crate::mention_set::paste_images_as_context;
  41use crate::mention_set::{MentionSet, crease_for_mention};
  42use crate::terminal_codegen::TerminalCodegen;
  43use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
  44
  45actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
  46
  47pub struct InlineAssistRatingFeatureFlag;
  48
  49impl FeatureFlag for InlineAssistRatingFeatureFlag {
  50    const NAME: &'static str = "inline-assist-rating";
  51
  52    fn enabled_for_staff() -> bool {
  53        false
  54    }
  55}
  56
  57enum RatingState {
  58    Pending,
  59    GeneratedCompletion(Option<String>),
  60    Rated(Uuid),
  61}
  62
  63impl RatingState {
  64    fn is_pending(&self) -> bool {
  65        matches!(self, RatingState::Pending)
  66    }
  67
  68    fn rating_id(&self) -> Option<Uuid> {
  69        match self {
  70            RatingState::Pending => None,
  71            RatingState::GeneratedCompletion(_) => None,
  72            RatingState::Rated(id) => Some(*id),
  73        }
  74    }
  75
  76    fn rate(&mut self) -> (Uuid, Option<String>) {
  77        let id = Uuid::new_v4();
  78        let old_state = mem::replace(self, RatingState::Rated(id));
  79        let completion = match old_state {
  80            RatingState::Pending => None,
  81            RatingState::GeneratedCompletion(completion) => completion,
  82            RatingState::Rated(_) => None,
  83        };
  84
  85        (id, completion)
  86    }
  87
  88    fn reset(&mut self) {
  89        *self = RatingState::Pending;
  90    }
  91
  92    fn generated_completion(&mut self, generated_completion: Option<String>) {
  93        *self = RatingState::GeneratedCompletion(generated_completion);
  94    }
  95}
  96
  97pub struct PromptEditor<T> {
  98    pub editor: Entity<Editor>,
  99    mode: PromptEditorMode,
 100    mention_set: Entity<MentionSet>,
 101    history_store: Entity<HistoryStore>,
 102    prompt_store: Option<Entity<PromptStore>>,
 103    workspace: WeakEntity<Workspace>,
 104    model_selector: Entity<AgentModelSelector>,
 105    edited_since_done: bool,
 106    prompt_history: VecDeque<String>,
 107    prompt_history_ix: Option<usize>,
 108    pending_prompt: String,
 109    _codegen_subscription: Subscription,
 110    editor_subscriptions: Vec<Subscription>,
 111    show_rate_limit_notice: bool,
 112    rated: RatingState,
 113    _phantom: std::marker::PhantomData<T>,
 114}
 115
 116impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
 117
 118impl<T: 'static> Render for PromptEditor<T> {
 119    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 120        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
 121        let mut buttons = Vec::new();
 122
 123        const RIGHT_PADDING: Pixels = px(9.);
 124
 125        let (left_gutter_width, right_padding, explanation) = match &self.mode {
 126            PromptEditorMode::Buffer {
 127                id: _,
 128                codegen,
 129                editor_margins,
 130            } => {
 131                let codegen = codegen.read(cx);
 132
 133                if codegen.alternative_count(cx) > 1 {
 134                    buttons.push(self.render_cycle_controls(codegen, cx));
 135                }
 136
 137                let editor_margins = editor_margins.lock();
 138                let gutter = editor_margins.gutter;
 139
 140                let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
 141                let right_padding = editor_margins.right + RIGHT_PADDING;
 142
 143                let explanation = codegen
 144                    .active_alternative()
 145                    .read(cx)
 146                    .model_explanation
 147                    .clone();
 148
 149                (left_gutter_width, right_padding, explanation)
 150            }
 151            PromptEditorMode::Terminal { .. } => {
 152                // Give the equivalent of the same left-padding that we're using on the right
 153                (Pixels::from(40.0), Pixels::from(24.), None)
 154            }
 155        };
 156
 157        let bottom_padding = match &self.mode {
 158            PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
 159            PromptEditorMode::Terminal { .. } => rems_from_px(4.0),
 160        };
 161
 162        buttons.extend(self.render_buttons(window, cx));
 163
 164        let menu_visible = self.is_completions_menu_visible(cx);
 165        let add_context_button = IconButton::new("add-context", IconName::AtSign)
 166            .icon_size(IconSize::Small)
 167            .icon_color(Color::Muted)
 168            .when(!menu_visible, |this| {
 169                this.tooltip(move |_window, cx| {
 170                    Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
 171                })
 172            })
 173            .on_click(cx.listener(move |this, _, window, cx| {
 174                this.trigger_completion_menu(window, cx);
 175            }));
 176
 177        let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx));
 178
 179        if let Some(explanation) = &explanation {
 180            markdown.update(cx, |markdown, cx| {
 181                markdown.reset(explanation.clone(), cx);
 182            });
 183        }
 184
 185        let explanation_label = self
 186            .render_markdown(markdown, markdown_style(window, cx))
 187            .into_any_element();
 188
 189        v_flex()
 190            .key_context("PromptEditor")
 191            .capture_action(cx.listener(Self::paste))
 192            .block_mouse_except_scroll()
 193            .size_full()
 194            .pt_0p5()
 195            .pb(bottom_padding)
 196            .pr(right_padding)
 197            .gap_0p5()
 198            .justify_center()
 199            .border_y_1()
 200            .border_color(cx.theme().colors().border)
 201            .bg(cx.theme().colors().editor_background)
 202            .child(
 203                h_flex()
 204                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 205                        this.model_selector
 206                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 207                    }))
 208                    .on_action(cx.listener(Self::confirm))
 209                    .on_action(cx.listener(Self::cancel))
 210                    .on_action(cx.listener(Self::move_up))
 211                    .on_action(cx.listener(Self::move_down))
 212                    .on_action(cx.listener(Self::thumbs_up))
 213                    .on_action(cx.listener(Self::thumbs_down))
 214                    .capture_action(cx.listener(Self::cycle_prev))
 215                    .capture_action(cx.listener(Self::cycle_next))
 216                    .child(
 217                        WithRemSize::new(ui_font_size)
 218                            .h_full()
 219                            .w(left_gutter_width)
 220                            .flex()
 221                            .flex_row()
 222                            .flex_shrink_0()
 223                            .items_center()
 224                            .justify_center()
 225                            .gap_1()
 226                            .child(self.render_close_button(cx))
 227                            .map(|el| {
 228                                let CodegenStatus::Error(error) = self.codegen_status(cx) else {
 229                                    return el;
 230                                };
 231
 232                                let error_message = SharedString::from(error.to_string());
 233                                el.child(
 234                                    div()
 235                                        .id("error")
 236                                        .tooltip(Tooltip::text(error_message))
 237                                        .child(
 238                                            Icon::new(IconName::XCircle)
 239                                                .size(IconSize::Small)
 240                                                .color(Color::Error),
 241                                        ),
 242                                )
 243                            }),
 244                    )
 245                    .child(
 246                        h_flex()
 247                            .w_full()
 248                            .justify_between()
 249                            .child(div().flex_1().child(self.render_editor(window, cx)))
 250                            .child(
 251                                WithRemSize::new(ui_font_size)
 252                                    .flex()
 253                                    .flex_row()
 254                                    .items_center()
 255                                    .gap_1()
 256                                    .child(add_context_button)
 257                                    .child(self.model_selector.clone())
 258                                    .children(buttons),
 259                            ),
 260                    ),
 261            )
 262            .when_some(explanation, |this, _| {
 263                this.child(
 264                    h_flex()
 265                        .size_full()
 266                        .justify_center()
 267                        .child(div().w(left_gutter_width + px(6.)))
 268                        .child(
 269                            div()
 270                                .size_full()
 271                                .min_w_0()
 272                                .pt(rems_from_px(3.))
 273                                .pl_0p5()
 274                                .flex_1()
 275                                .border_t_1()
 276                                .border_color(cx.theme().colors().border_variant)
 277                                .child(explanation_label),
 278                        ),
 279                )
 280            })
 281    }
 282}
 283
 284fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 285    let theme_settings = ThemeSettings::get_global(cx);
 286    let colors = cx.theme().colors();
 287    let mut text_style = window.text_style();
 288
 289    text_style.refine(&TextStyleRefinement {
 290        font_family: Some(theme_settings.ui_font.family.clone()),
 291        color: Some(colors.text),
 292        ..Default::default()
 293    });
 294
 295    MarkdownStyle {
 296        base_text_style: text_style.clone(),
 297        syntax: cx.theme().syntax().clone(),
 298        selection_background_color: colors.element_selection_background,
 299        heading_level_styles: Some(HeadingLevelStyles {
 300            h1: Some(TextStyleRefinement {
 301                font_size: Some(rems(1.15).into()),
 302                ..Default::default()
 303            }),
 304            h2: Some(TextStyleRefinement {
 305                font_size: Some(rems(1.1).into()),
 306                ..Default::default()
 307            }),
 308            h3: Some(TextStyleRefinement {
 309                font_size: Some(rems(1.05).into()),
 310                ..Default::default()
 311            }),
 312            h4: Some(TextStyleRefinement {
 313                font_size: Some(rems(1.).into()),
 314                ..Default::default()
 315            }),
 316            h5: Some(TextStyleRefinement {
 317                font_size: Some(rems(0.95).into()),
 318                ..Default::default()
 319            }),
 320            h6: Some(TextStyleRefinement {
 321                font_size: Some(rems(0.875).into()),
 322                ..Default::default()
 323            }),
 324        }),
 325        inline_code: TextStyleRefinement {
 326            font_family: Some(theme_settings.buffer_font.family.clone()),
 327            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 328            font_features: Some(theme_settings.buffer_font.features.clone()),
 329            background_color: Some(colors.editor_foreground.opacity(0.08)),
 330            ..Default::default()
 331        },
 332        ..Default::default()
 333    }
 334}
 335
 336impl<T: 'static> Focusable for PromptEditor<T> {
 337    fn focus_handle(&self, cx: &App) -> FocusHandle {
 338        self.editor.focus_handle(cx)
 339    }
 340}
 341
 342impl<T: 'static> PromptEditor<T> {
 343    const MAX_LINES: u8 = 8;
 344
 345    fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
 346        match &self.mode {
 347            PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
 348            PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
 349        }
 350    }
 351
 352    fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 353        self.editor_subscriptions.clear();
 354        self.editor_subscriptions.push(cx.subscribe_in(
 355            &self.editor,
 356            window,
 357            Self::handle_prompt_editor_events,
 358        ));
 359    }
 360
 361    fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
 362        self.editor.update(cx, |editor, cx| {
 363            editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
 364                PromptEditorCompletionProviderDelegate,
 365                cx.weak_entity(),
 366                self.mention_set.clone(),
 367                self.history_store.clone(),
 368                self.prompt_store.clone(),
 369                self.workspace.clone(),
 370            ))));
 371        });
 372    }
 373
 374    pub fn set_show_cursor_when_unfocused(
 375        &mut self,
 376        show_cursor_when_unfocused: bool,
 377        cx: &mut Context<Self>,
 378    ) {
 379        self.editor.update(cx, |editor, cx| {
 380            editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
 381        });
 382    }
 383
 384    pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 385        let prompt = self.prompt(cx);
 386        let existing_creases = self.editor.update(cx, |editor, cx| {
 387            extract_message_creases(editor, &self.mention_set, window, cx)
 388        });
 389        let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
 390        let mut creases = vec![];
 391        self.editor = cx.new(|cx| {
 392            let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
 393            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 394            editor.set_placeholder_text("Add a prompt…", window, cx);
 395            editor.set_text(prompt, window, cx);
 396            creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
 397
 398            if focus {
 399                window.focus(&editor.focus_handle(cx));
 400            }
 401            editor
 402        });
 403
 404        self.mention_set.update(cx, |mention_set, _cx| {
 405            debug_assert_eq!(
 406                creases.len(),
 407                mention_set.creases().len(),
 408                "Missing creases"
 409            );
 410
 411            let mentions = mention_set
 412                .clear()
 413                .zip(creases)
 414                .map(|((_, value), id)| (id, value))
 415                .collect::<HashMap<_, _>>();
 416            mention_set.set_mentions(mentions);
 417        });
 418
 419        self.assign_completion_provider(cx);
 420        self.subscribe_to_editor(window, cx);
 421    }
 422
 423    pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
 424        let action = match mode {
 425            PromptEditorMode::Buffer { codegen, .. } => {
 426                if codegen.read(cx).is_insertion {
 427                    "Generate"
 428                } else {
 429                    "Transform"
 430                }
 431            }
 432            PromptEditorMode::Terminal { .. } => "Generate",
 433        };
 434
 435        let agent_panel_keybinding =
 436            ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
 437                .map(|keybinding| format!("{keybinding} to chat"))
 438                .unwrap_or_default();
 439
 440        format!("{action}… ({agent_panel_keybinding} ― ↓↑ for history — @ to include context)")
 441    }
 442
 443    pub fn prompt(&self, cx: &App) -> String {
 444        self.editor.read(cx).text(cx)
 445    }
 446
 447    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 448        if inline_assistant_model_supports_images(cx)
 449            && let Some(task) =
 450                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
 451        {
 452            task.detach();
 453        }
 454    }
 455
 456    fn handle_prompt_editor_events(
 457        &mut self,
 458        editor: &Entity<Editor>,
 459        event: &EditorEvent,
 460        window: &mut Window,
 461        cx: &mut Context<Self>,
 462    ) {
 463        match event {
 464            EditorEvent::Edited { .. } => {
 465                let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 466
 467                self.mention_set
 468                    .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 469
 470                if let Some(workspace) = window.root::<Workspace>().flatten() {
 471                    workspace.update(cx, |workspace, cx| {
 472                        let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
 473
 474                        workspace
 475                            .client()
 476                            .telemetry()
 477                            .log_edit_event("inline assist", is_via_ssh);
 478                    });
 479                }
 480                let prompt = snapshot.text();
 481                if self
 482                    .prompt_history_ix
 483                    .is_none_or(|ix| self.prompt_history[ix] != prompt)
 484                {
 485                    self.prompt_history_ix.take();
 486                    self.pending_prompt = prompt;
 487                }
 488
 489                self.edited_since_done = true;
 490                self.rated.reset();
 491                cx.notify();
 492            }
 493            EditorEvent::Blurred => {
 494                if self.show_rate_limit_notice {
 495                    self.show_rate_limit_notice = false;
 496                    cx.notify();
 497                }
 498            }
 499            _ => {}
 500        }
 501    }
 502
 503    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 504        self.editor
 505            .read(cx)
 506            .context_menu()
 507            .borrow()
 508            .as_ref()
 509            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 510    }
 511
 512    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 513        self.editor.update(cx, |editor, cx| {
 514            let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 515                matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 516            });
 517
 518            let has_at_sign = {
 519                let snapshot = editor.display_snapshot(cx);
 520                let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 521                let offset = cursor.to_offset(&snapshot);
 522                if offset.0 > 0 {
 523                    snapshot
 524                        .buffer_snapshot()
 525                        .reversed_chars_at(offset)
 526                        .next()
 527                        .map(|sign| sign == '@')
 528                        .unwrap_or(false)
 529                } else {
 530                    false
 531                }
 532            };
 533
 534            if menu_is_open && has_at_sign {
 535                return;
 536            }
 537
 538            editor.insert("@", window, cx);
 539            editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 540        });
 541    }
 542
 543    fn cancel(
 544        &mut self,
 545        _: &editor::actions::Cancel,
 546        _window: &mut Window,
 547        cx: &mut Context<Self>,
 548    ) {
 549        match self.codegen_status(cx) {
 550            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 551                cx.emit(PromptEditorEvent::CancelRequested);
 552            }
 553            CodegenStatus::Pending => {
 554                cx.emit(PromptEditorEvent::StopRequested);
 555            }
 556        }
 557    }
 558
 559    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 560        match self.codegen_status(cx) {
 561            CodegenStatus::Idle => {
 562                cx.emit(PromptEditorEvent::StartRequested);
 563            }
 564            CodegenStatus::Pending => {}
 565            CodegenStatus::Done => {
 566                if self.edited_since_done {
 567                    cx.emit(PromptEditorEvent::StartRequested);
 568                } else {
 569                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 570                }
 571            }
 572            CodegenStatus::Error(_) => {
 573                cx.emit(PromptEditorEvent::StartRequested);
 574            }
 575        }
 576    }
 577
 578    fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context<Self>) {
 579        if self.rated.is_pending() {
 580            self.toast("Still generating...", None, cx);
 581            return;
 582        }
 583
 584        if let Some(rating_id) = self.rated.rating_id() {
 585            self.toast("Already rated this completion", Some(rating_id), cx);
 586            return;
 587        }
 588
 589        let (rating_id, completion) = self.rated.rate();
 590
 591        let selected_text = match &self.mode {
 592            PromptEditorMode::Buffer { codegen, .. } => {
 593                codegen.read(cx).selected_text(cx).map(|s| s.to_string())
 594            }
 595            PromptEditorMode::Terminal { .. } => None,
 596        };
 597
 598        let model_info = self.model_selector.read(cx).active_model(cx);
 599        let model_id = {
 600            let Some(configured_model) = model_info else {
 601                self.toast("No configured model", None, cx);
 602                return;
 603            };
 604
 605            configured_model.model.telemetry_id()
 606        };
 607
 608        let prompt = self.editor.read(cx).text(cx);
 609
 610        telemetry::event!(
 611            "Inline Assistant Rated",
 612            rating = "positive",
 613            model = model_id,
 614            prompt = prompt,
 615            completion = completion,
 616            selected_text = selected_text,
 617            rating_id = rating_id.to_string()
 618        );
 619
 620        cx.notify();
 621    }
 622
 623    fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context<Self>) {
 624        if self.rated.is_pending() {
 625            self.toast("Still generating...", None, cx);
 626            return;
 627        }
 628        if let Some(rating_id) = self.rated.rating_id() {
 629            self.toast("Already rated this completion", Some(rating_id), cx);
 630            return;
 631        }
 632
 633        let (rating_id, completion) = self.rated.rate();
 634
 635        let selected_text = match &self.mode {
 636            PromptEditorMode::Buffer { codegen, .. } => {
 637                codegen.read(cx).selected_text(cx).map(|s| s.to_string())
 638            }
 639            PromptEditorMode::Terminal { .. } => None,
 640        };
 641
 642        let model_info = self.model_selector.read(cx).active_model(cx);
 643        let model_telemetry_id = {
 644            let Some(configured_model) = model_info else {
 645                self.toast("No configured model", None, cx);
 646                return;
 647            };
 648
 649            configured_model.model.telemetry_id()
 650        };
 651
 652        let prompt = self.editor.read(cx).text(cx);
 653
 654        telemetry::event!(
 655            "Inline Assistant Rated",
 656            rating = "negative",
 657            model = model_telemetry_id,
 658            prompt = prompt,
 659            completion = completion,
 660            selected_text = selected_text,
 661            rating_id = rating_id.to_string()
 662        );
 663
 664        cx.notify();
 665    }
 666
 667    fn toast(&mut self, msg: &str, uuid: Option<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
 668        self.workspace
 669            .update(cx, |workspace, cx| {
 670                enum InlinePromptRating {}
 671                workspace.show_toast(
 672                    {
 673                        let mut toast = Toast::new(
 674                            NotificationId::unique::<InlinePromptRating>(),
 675                            msg.to_string(),
 676                        )
 677                        .autohide();
 678
 679                        if let Some(uuid) = uuid {
 680                            toast = toast.on_click("Click to copy rating ID", move |_, cx| {
 681                                cx.write_to_clipboard(ClipboardItem::new_string(uuid.to_string()));
 682                            });
 683                        };
 684
 685                        toast
 686                    },
 687                    cx,
 688                );
 689            })
 690            .ok();
 691    }
 692
 693    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 694        if let Some(ix) = self.prompt_history_ix {
 695            if ix > 0 {
 696                self.prompt_history_ix = Some(ix - 1);
 697                let prompt = self.prompt_history[ix - 1].as_str();
 698                self.editor.update(cx, |editor, cx| {
 699                    editor.set_text(prompt, window, cx);
 700                    editor.move_to_beginning(&Default::default(), window, cx);
 701                });
 702            }
 703        } else if !self.prompt_history.is_empty() {
 704            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 705            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 706            self.editor.update(cx, |editor, cx| {
 707                editor.set_text(prompt, window, cx);
 708                editor.move_to_beginning(&Default::default(), window, cx);
 709            });
 710        }
 711    }
 712
 713    fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 714        if let Some(ix) = self.prompt_history_ix {
 715            if ix < self.prompt_history.len() - 1 {
 716                self.prompt_history_ix = Some(ix + 1);
 717                let prompt = self.prompt_history[ix + 1].as_str();
 718                self.editor.update(cx, |editor, cx| {
 719                    editor.set_text(prompt, window, cx);
 720                    editor.move_to_end(&Default::default(), window, cx)
 721                });
 722            } else {
 723                self.prompt_history_ix = None;
 724                let prompt = self.pending_prompt.as_str();
 725                self.editor.update(cx, |editor, cx| {
 726                    editor.set_text(prompt, window, cx);
 727                    editor.move_to_end(&Default::default(), window, cx)
 728                });
 729            }
 730        }
 731    }
 732
 733    fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
 734        let mode = match &self.mode {
 735            PromptEditorMode::Buffer { codegen, .. } => {
 736                let codegen = codegen.read(cx);
 737                if codegen.is_insertion {
 738                    GenerationMode::Generate
 739                } else {
 740                    GenerationMode::Transform
 741                }
 742            }
 743            PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
 744        };
 745
 746        let codegen_status = self.codegen_status(cx);
 747
 748        match codegen_status {
 749            CodegenStatus::Idle => {
 750                vec![
 751                    Button::new("start", mode.start_label())
 752                        .label_size(LabelSize::Small)
 753                        .icon(IconName::Return)
 754                        .icon_size(IconSize::XSmall)
 755                        .icon_color(Color::Muted)
 756                        .on_click(
 757                            cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
 758                        )
 759                        .into_any_element(),
 760                ]
 761            }
 762            CodegenStatus::Pending => vec![
 763                IconButton::new("stop", IconName::Stop)
 764                    .icon_color(Color::Error)
 765                    .shape(IconButtonShape::Square)
 766                    .tooltip(move |_window, cx| {
 767                        Tooltip::with_meta(
 768                            mode.tooltip_interrupt(),
 769                            Some(&menu::Cancel),
 770                            "Changes won't be discarded",
 771                            cx,
 772                        )
 773                    })
 774                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 775                    .into_any_element(),
 776            ],
 777            CodegenStatus::Done | CodegenStatus::Error(_) => {
 778                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 779                if has_error || self.edited_since_done {
 780                    vec![
 781                        IconButton::new("restart", IconName::RotateCw)
 782                            .icon_color(Color::Info)
 783                            .shape(IconButtonShape::Square)
 784                            .tooltip(move |_window, cx| {
 785                                Tooltip::with_meta(
 786                                    mode.tooltip_restart(),
 787                                    Some(&menu::Confirm),
 788                                    "Changes will be discarded",
 789                                    cx,
 790                                )
 791                            })
 792                            .on_click(cx.listener(|_, _, _, cx| {
 793                                cx.emit(PromptEditorEvent::StartRequested);
 794                            }))
 795                            .into_any_element(),
 796                    ]
 797                } else {
 798                    let show_rating_buttons = cx.has_flag::<InlineAssistRatingFeatureFlag>();
 799                    let rated = self.rated.rating_id().is_some();
 800
 801                    let accept = IconButton::new("accept", IconName::Check)
 802                        .icon_color(Color::Info)
 803                        .shape(IconButtonShape::Square)
 804                        .tooltip(move |_window, cx| {
 805                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
 806                        })
 807                        .on_click(cx.listener(|_, _, _, cx| {
 808                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 809                        }))
 810                        .into_any_element();
 811
 812                    let mut buttons = Vec::new();
 813
 814                    if show_rating_buttons {
 815                        buttons.push(
 816                            IconButton::new("thumbs-down", IconName::ThumbsDown)
 817                                .icon_color(if rated { Color::Muted } else { Color::Default })
 818                                .shape(IconButtonShape::Square)
 819                                .disabled(rated)
 820                                .tooltip(Tooltip::text("Bad result"))
 821                                .on_click(cx.listener(|this, _, window, cx| {
 822                                    this.thumbs_down(&ThumbsDownResult, window, cx);
 823                                }))
 824                                .into_any_element(),
 825                        );
 826
 827                        buttons.push(
 828                            IconButton::new("thumbs-up", IconName::ThumbsUp)
 829                                .icon_color(if rated { Color::Muted } else { Color::Default })
 830                                .shape(IconButtonShape::Square)
 831                                .disabled(rated)
 832                                .tooltip(Tooltip::text("Good result"))
 833                                .on_click(cx.listener(|this, _, window, cx| {
 834                                    this.thumbs_up(&ThumbsUpResult, window, cx);
 835                                }))
 836                                .into_any_element(),
 837                        );
 838                    }
 839
 840                    buttons.push(accept);
 841
 842                    match &self.mode {
 843                        PromptEditorMode::Terminal { .. } => {
 844                            buttons.push(
 845                                IconButton::new("confirm", IconName::PlayFilled)
 846                                    .icon_color(Color::Info)
 847                                    .shape(IconButtonShape::Square)
 848                                    .tooltip(|_window, cx| {
 849                                        Tooltip::for_action(
 850                                            "Execute Generated Command",
 851                                            &menu::SecondaryConfirm,
 852                                            cx,
 853                                        )
 854                                    })
 855                                    .on_click(cx.listener(|_, _, _, cx| {
 856                                        cx.emit(PromptEditorEvent::ConfirmRequested {
 857                                            execute: true,
 858                                        });
 859                                    }))
 860                                    .into_any_element(),
 861                            );
 862                            buttons
 863                        }
 864                        PromptEditorMode::Buffer { .. } => buttons,
 865                    }
 866                }
 867            }
 868        }
 869    }
 870
 871    fn cycle_prev(
 872        &mut self,
 873        _: &CyclePreviousInlineAssist,
 874        _: &mut Window,
 875        cx: &mut Context<Self>,
 876    ) {
 877        match &self.mode {
 878            PromptEditorMode::Buffer { codegen, .. } => {
 879                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 880            }
 881            PromptEditorMode::Terminal { .. } => {
 882                // no cycle buttons in terminal mode
 883            }
 884        }
 885    }
 886
 887    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 888        match &self.mode {
 889            PromptEditorMode::Buffer { codegen, .. } => {
 890                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 891            }
 892            PromptEditorMode::Terminal { .. } => {
 893                // no cycle buttons in terminal mode
 894            }
 895        }
 896    }
 897
 898    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 899        IconButton::new("cancel", IconName::Close)
 900            .icon_color(Color::Muted)
 901            .shape(IconButtonShape::Square)
 902            .tooltip(Tooltip::text("Close Assistant"))
 903            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 904            .into_any_element()
 905    }
 906
 907    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
 908        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
 909
 910        let model_registry = LanguageModelRegistry::read_global(cx);
 911        let default_model = model_registry.default_model().map(|default| default.model);
 912        let alternative_models = model_registry.inline_alternative_models();
 913
 914        let get_model_name = |index: usize| -> String {
 915            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
 916
 917            match index {
 918                0 => default_model.as_ref().map_or_else(String::new, name),
 919                index if index <= alternative_models.len() => alternative_models
 920                    .get(index - 1)
 921                    .map_or_else(String::new, name),
 922                _ => String::new(),
 923            }
 924        };
 925
 926        let total_models = alternative_models.len() + 1;
 927
 928        if total_models <= 1 {
 929            return div().into_any_element();
 930        }
 931
 932        let current_index = codegen.active_alternative;
 933        let prev_index = (current_index + total_models - 1) % total_models;
 934        let next_index = (current_index + 1) % total_models;
 935
 936        let prev_model_name = get_model_name(prev_index);
 937        let next_model_name = get_model_name(next_index);
 938
 939        h_flex()
 940            .child(
 941                IconButton::new("previous", IconName::ChevronLeft)
 942                    .icon_color(Color::Muted)
 943                    .disabled(disabled || current_index == 0)
 944                    .shape(IconButtonShape::Square)
 945                    .tooltip({
 946                        let focus_handle = self.editor.focus_handle(cx);
 947                        move |_window, cx| {
 948                            cx.new(|cx| {
 949                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
 950                                    KeyBinding::for_action_in(
 951                                        &CyclePreviousInlineAssist,
 952                                        &focus_handle,
 953                                        cx,
 954                                    ),
 955                                );
 956                                if !disabled && current_index != 0 {
 957                                    tooltip = tooltip.meta(prev_model_name.clone());
 958                                }
 959                                tooltip
 960                            })
 961                            .into()
 962                        }
 963                    })
 964                    .on_click(cx.listener(|this, _, window, cx| {
 965                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
 966                    })),
 967            )
 968            .child(
 969                Label::new(format!(
 970                    "{}/{}",
 971                    codegen.active_alternative + 1,
 972                    codegen.alternative_count(cx)
 973                ))
 974                .size(LabelSize::Small)
 975                .color(if disabled {
 976                    Color::Disabled
 977                } else {
 978                    Color::Muted
 979                }),
 980            )
 981            .child(
 982                IconButton::new("next", IconName::ChevronRight)
 983                    .icon_color(Color::Muted)
 984                    .disabled(disabled || current_index == total_models - 1)
 985                    .shape(IconButtonShape::Square)
 986                    .tooltip({
 987                        let focus_handle = self.editor.focus_handle(cx);
 988                        move |_window, cx| {
 989                            cx.new(|cx| {
 990                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
 991                                    KeyBinding::for_action_in(
 992                                        &CycleNextInlineAssist,
 993                                        &focus_handle,
 994                                        cx,
 995                                    ),
 996                                );
 997                                if !disabled && current_index != total_models - 1 {
 998                                    tooltip = tooltip.meta(next_model_name.clone());
 999                                }
1000                                tooltip
1001                            })
1002                            .into()
1003                        }
1004                    })
1005                    .on_click(cx.listener(|this, _, window, cx| {
1006                        this.cycle_next(&CycleNextInlineAssist, window, cx)
1007                    })),
1008            )
1009            .into_any_element()
1010    }
1011
1012    fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1013        let colors = cx.theme().colors();
1014
1015        div()
1016            .key_context("InlineAssistEditor")
1017            .size_full()
1018            .p_2()
1019            .pl_1()
1020            .bg(colors.editor_background)
1021            .child({
1022                let settings = ThemeSettings::get_global(cx);
1023                let font_size = settings.buffer_font_size(cx);
1024                let line_height = font_size * 1.2;
1025
1026                let text_style = TextStyle {
1027                    color: colors.editor_foreground,
1028                    font_family: settings.buffer_font.family.clone(),
1029                    font_features: settings.buffer_font.features.clone(),
1030                    font_size: font_size.into(),
1031                    line_height: line_height.into(),
1032                    ..Default::default()
1033                };
1034
1035                EditorElement::new(
1036                    &self.editor,
1037                    EditorStyle {
1038                        background: colors.editor_background,
1039                        local_player: cx.theme().players().local(),
1040                        syntax: cx.theme().syntax().clone(),
1041                        text: text_style,
1042                        ..Default::default()
1043                    },
1044                )
1045            })
1046            .into_any_element()
1047    }
1048
1049    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1050        MarkdownElement::new(markdown, style)
1051    }
1052}
1053
1054pub enum PromptEditorMode {
1055    Buffer {
1056        id: InlineAssistId,
1057        codegen: Entity<BufferCodegen>,
1058        editor_margins: Arc<Mutex<EditorMargins>>,
1059    },
1060    Terminal {
1061        id: TerminalInlineAssistId,
1062        codegen: Entity<TerminalCodegen>,
1063        height_in_lines: u8,
1064    },
1065}
1066
1067pub enum PromptEditorEvent {
1068    StartRequested,
1069    StopRequested,
1070    ConfirmRequested { execute: bool },
1071    CancelRequested,
1072    Resized { height_in_lines: u8 },
1073}
1074
1075#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1076pub struct InlineAssistId(pub usize);
1077
1078impl InlineAssistId {
1079    pub fn post_inc(&mut self) -> InlineAssistId {
1080        let id = *self;
1081        self.0 += 1;
1082        id
1083    }
1084}
1085
1086struct PromptEditorCompletionProviderDelegate;
1087
1088fn inline_assistant_model_supports_images(cx: &App) -> bool {
1089    LanguageModelRegistry::read_global(cx)
1090        .inline_assistant_model()
1091        .map_or(false, |m| m.model.supports_images())
1092}
1093
1094impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
1095    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
1096        vec![
1097            PromptContextType::File,
1098            PromptContextType::Symbol,
1099            PromptContextType::Thread,
1100            PromptContextType::Fetch,
1101            PromptContextType::Rules,
1102        ]
1103    }
1104
1105    fn supports_images(&self, cx: &App) -> bool {
1106        inline_assistant_model_supports_images(cx)
1107    }
1108
1109    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
1110        Vec::new()
1111    }
1112
1113    fn confirm_command(&self, _cx: &mut App) {}
1114}
1115
1116impl PromptEditor<BufferCodegen> {
1117    pub fn new_buffer(
1118        id: InlineAssistId,
1119        editor_margins: Arc<Mutex<EditorMargins>>,
1120        prompt_history: VecDeque<String>,
1121        prompt_buffer: Entity<MultiBuffer>,
1122        codegen: Entity<BufferCodegen>,
1123        fs: Arc<dyn Fs>,
1124        history_store: Entity<HistoryStore>,
1125        prompt_store: Option<Entity<PromptStore>>,
1126        project: WeakEntity<Project>,
1127        workspace: WeakEntity<Workspace>,
1128        window: &mut Window,
1129        cx: &mut Context<PromptEditor<BufferCodegen>>,
1130    ) -> PromptEditor<BufferCodegen> {
1131        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1132        let mode = PromptEditorMode::Buffer {
1133            id,
1134            codegen,
1135            editor_margins,
1136        };
1137
1138        let prompt_editor = cx.new(|cx| {
1139            let mut editor = Editor::new(
1140                EditorMode::AutoHeight {
1141                    min_lines: 1,
1142                    max_lines: Some(Self::MAX_LINES as usize),
1143                },
1144                prompt_buffer,
1145                None,
1146                window,
1147                cx,
1148            );
1149            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1150            // Since the prompt editors for all inline assistants are linked,
1151            // always show the cursor (even when it isn't focused) because
1152            // typing in one will make what you typed appear in all of them.
1153            editor.set_show_cursor_when_unfocused(true, cx);
1154            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1155            editor.set_context_menu_options(ContextMenuOptions {
1156                min_entries_visible: 12,
1157                max_entries_visible: 12,
1158                placement: None,
1159            });
1160
1161            editor
1162        });
1163
1164        let mention_set =
1165            cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
1166
1167        let model_selector_menu_handle = PopoverMenuHandle::default();
1168
1169        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
1170            editor: prompt_editor.clone(),
1171            mention_set,
1172            history_store,
1173            prompt_store,
1174            workspace,
1175            model_selector: cx.new(|cx| {
1176                AgentModelSelector::new(
1177                    fs,
1178                    model_selector_menu_handle,
1179                    prompt_editor.focus_handle(cx),
1180                    ModelUsageContext::InlineAssistant,
1181                    window,
1182                    cx,
1183                )
1184            }),
1185            edited_since_done: false,
1186            prompt_history,
1187            prompt_history_ix: None,
1188            pending_prompt: String::new(),
1189            _codegen_subscription: codegen_subscription,
1190            editor_subscriptions: Vec::new(),
1191            show_rate_limit_notice: false,
1192            mode,
1193            rated: RatingState::Pending,
1194            _phantom: Default::default(),
1195        };
1196
1197        this.assign_completion_provider(cx);
1198        this.subscribe_to_editor(window, cx);
1199        this
1200    }
1201
1202    fn handle_codegen_changed(
1203        &mut self,
1204        codegen: Entity<BufferCodegen>,
1205        cx: &mut Context<PromptEditor<BufferCodegen>>,
1206    ) {
1207        match self.codegen_status(cx) {
1208            CodegenStatus::Idle => {
1209                self.editor
1210                    .update(cx, |editor, _| editor.set_read_only(false));
1211            }
1212            CodegenStatus::Pending => {
1213                self.rated.reset();
1214                self.editor
1215                    .update(cx, |editor, _| editor.set_read_only(true));
1216            }
1217            CodegenStatus::Done => {
1218                let completion = codegen.read(cx).active_completion(cx);
1219                self.rated.generated_completion(completion);
1220                self.edited_since_done = false;
1221                self.editor
1222                    .update(cx, |editor, _| editor.set_read_only(false));
1223            }
1224            CodegenStatus::Error(_error) => {
1225                self.edited_since_done = false;
1226                self.editor
1227                    .update(cx, |editor, _| editor.set_read_only(false));
1228            }
1229        }
1230    }
1231
1232    pub fn id(&self) -> InlineAssistId {
1233        match &self.mode {
1234            PromptEditorMode::Buffer { id, .. } => *id,
1235            PromptEditorMode::Terminal { .. } => unreachable!(),
1236        }
1237    }
1238
1239    pub fn codegen(&self) -> &Entity<BufferCodegen> {
1240        match &self.mode {
1241            PromptEditorMode::Buffer { codegen, .. } => codegen,
1242            PromptEditorMode::Terminal { .. } => unreachable!(),
1243        }
1244    }
1245
1246    pub fn mention_set(&self) -> &Entity<MentionSet> {
1247        &self.mention_set
1248    }
1249
1250    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
1251        match &self.mode {
1252            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
1253            PromptEditorMode::Terminal { .. } => unreachable!(),
1254        }
1255    }
1256}
1257
1258#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1259pub struct TerminalInlineAssistId(pub usize);
1260
1261impl TerminalInlineAssistId {
1262    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
1263        let id = *self;
1264        self.0 += 1;
1265        id
1266    }
1267}
1268
1269impl PromptEditor<TerminalCodegen> {
1270    pub fn new_terminal(
1271        id: TerminalInlineAssistId,
1272        prompt_history: VecDeque<String>,
1273        prompt_buffer: Entity<MultiBuffer>,
1274        codegen: Entity<TerminalCodegen>,
1275        fs: Arc<dyn Fs>,
1276        history_store: Entity<HistoryStore>,
1277        prompt_store: Option<Entity<PromptStore>>,
1278        project: WeakEntity<Project>,
1279        workspace: WeakEntity<Workspace>,
1280        window: &mut Window,
1281        cx: &mut Context<Self>,
1282    ) -> Self {
1283        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1284        let mode = PromptEditorMode::Terminal {
1285            id,
1286            codegen,
1287            height_in_lines: 1,
1288        };
1289
1290        let prompt_editor = cx.new(|cx| {
1291            let mut editor = Editor::new(
1292                EditorMode::AutoHeight {
1293                    min_lines: 1,
1294                    max_lines: Some(Self::MAX_LINES as usize),
1295                },
1296                prompt_buffer,
1297                None,
1298                window,
1299                cx,
1300            );
1301            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1302            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1303            editor.set_context_menu_options(ContextMenuOptions {
1304                min_entries_visible: 12,
1305                max_entries_visible: 12,
1306                placement: None,
1307            });
1308            editor
1309        });
1310
1311        let mention_set =
1312            cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
1313
1314        let model_selector_menu_handle = PopoverMenuHandle::default();
1315
1316        let mut this = Self {
1317            editor: prompt_editor.clone(),
1318            mention_set,
1319            history_store,
1320            prompt_store,
1321            workspace,
1322            model_selector: cx.new(|cx| {
1323                AgentModelSelector::new(
1324                    fs,
1325                    model_selector_menu_handle.clone(),
1326                    prompt_editor.focus_handle(cx),
1327                    ModelUsageContext::InlineAssistant,
1328                    window,
1329                    cx,
1330                )
1331            }),
1332            edited_since_done: false,
1333            prompt_history,
1334            prompt_history_ix: None,
1335            pending_prompt: String::new(),
1336            _codegen_subscription: codegen_subscription,
1337            editor_subscriptions: Vec::new(),
1338            mode,
1339            show_rate_limit_notice: false,
1340            rated: RatingState::Pending,
1341            _phantom: Default::default(),
1342        };
1343        this.count_lines(cx);
1344        this.assign_completion_provider(cx);
1345        this.subscribe_to_editor(window, cx);
1346        this
1347    }
1348
1349    fn count_lines(&mut self, cx: &mut Context<Self>) {
1350        let height_in_lines = cmp::max(
1351            2, // Make the editor at least two lines tall, to account for padding and buttons.
1352            cmp::min(
1353                self.editor
1354                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1355                Self::MAX_LINES as u32,
1356            ),
1357        ) as u8;
1358
1359        match &mut self.mode {
1360            PromptEditorMode::Terminal {
1361                height_in_lines: current_height,
1362                ..
1363            } => {
1364                if height_in_lines != *current_height {
1365                    *current_height = height_in_lines;
1366                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1367                }
1368            }
1369            PromptEditorMode::Buffer { .. } => unreachable!(),
1370        }
1371    }
1372
1373    fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1374        match &self.codegen().read(cx).status {
1375            CodegenStatus::Idle => {
1376                self.editor
1377                    .update(cx, |editor, _| editor.set_read_only(false));
1378            }
1379            CodegenStatus::Pending => {
1380                self.rated = RatingState::Pending;
1381                self.editor
1382                    .update(cx, |editor, _| editor.set_read_only(true));
1383            }
1384            CodegenStatus::Done | CodegenStatus::Error(_) => {
1385                self.rated
1386                    .generated_completion(codegen.read(cx).completion());
1387                self.edited_since_done = false;
1388                self.editor
1389                    .update(cx, |editor, _| editor.set_read_only(false));
1390            }
1391        }
1392    }
1393
1394    pub fn mention_set(&self) -> &Entity<MentionSet> {
1395        &self.mention_set
1396    }
1397
1398    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1399        match &self.mode {
1400            PromptEditorMode::Buffer { .. } => unreachable!(),
1401            PromptEditorMode::Terminal { codegen, .. } => codegen,
1402        }
1403    }
1404
1405    pub fn id(&self) -> TerminalInlineAssistId {
1406        match &self.mode {
1407            PromptEditorMode::Buffer { .. } => unreachable!(),
1408            PromptEditorMode::Terminal { id, .. } => *id,
1409        }
1410    }
1411}
1412
1413pub enum CodegenStatus {
1414    Idle,
1415    Pending,
1416    Done,
1417    Error(anyhow::Error),
1418}
1419
1420/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1421#[derive(Copy, Clone)]
1422pub enum CancelButtonState {
1423    Idle,
1424    Pending,
1425    Done,
1426    Error,
1427}
1428
1429impl Into<CancelButtonState> for &CodegenStatus {
1430    fn into(self) -> CancelButtonState {
1431        match self {
1432            CodegenStatus::Idle => CancelButtonState::Idle,
1433            CodegenStatus::Pending => CancelButtonState::Pending,
1434            CodegenStatus::Done => CancelButtonState::Done,
1435            CodegenStatus::Error(_) => CancelButtonState::Error,
1436        }
1437    }
1438}
1439
1440#[derive(Copy, Clone)]
1441pub enum GenerationMode {
1442    Generate,
1443    Transform,
1444}
1445
1446impl GenerationMode {
1447    fn start_label(self) -> &'static str {
1448        match self {
1449            GenerationMode::Generate => "Generate",
1450            GenerationMode::Transform => "Transform",
1451        }
1452    }
1453    fn tooltip_interrupt(self) -> &'static str {
1454        match self {
1455            GenerationMode::Generate => "Interrupt Generation",
1456            GenerationMode::Transform => "Interrupt Transform",
1457        }
1458    }
1459
1460    fn tooltip_restart(self) -> &'static str {
1461        match self {
1462            GenerationMode::Generate => "Restart Generation",
1463            GenerationMode::Transform => "Restart Transform",
1464        }
1465    }
1466
1467    fn tooltip_accept(self) -> &'static str {
1468        match self {
1469            GenerationMode::Generate => "Accept Generation",
1470            GenerationMode::Transform => "Accept Transform",
1471        }
1472    }
1473}
1474
1475/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1476#[derive(Clone, Debug)]
1477struct MessageCrease {
1478    range: Range<MultiBufferOffset>,
1479    icon_path: SharedString,
1480    label: SharedString,
1481}
1482
1483fn extract_message_creases(
1484    editor: &mut Editor,
1485    mention_set: &Entity<MentionSet>,
1486    window: &mut Window,
1487    cx: &mut Context<'_, Editor>,
1488) -> Vec<MessageCrease> {
1489    let creases = mention_set.read(cx).creases();
1490    let snapshot = editor.snapshot(window, cx);
1491    snapshot
1492        .crease_snapshot
1493        .creases()
1494        .filter(|(id, _)| creases.contains(id))
1495        .filter_map(|(_, crease)| {
1496            let metadata = crease.metadata()?.clone();
1497            Some(MessageCrease {
1498                range: crease.range().to_offset(snapshot.buffer()),
1499                label: metadata.label,
1500                icon_path: metadata.icon_path,
1501            })
1502        })
1503        .collect()
1504}
1505
1506fn insert_message_creases(
1507    editor: &mut Editor,
1508    message_creases: &[MessageCrease],
1509    window: &mut Window,
1510    cx: &mut Context<'_, Editor>,
1511) -> Vec<CreaseId> {
1512    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1513    let creases = message_creases
1514        .iter()
1515        .map(|crease| {
1516            let start = buffer_snapshot.anchor_after(crease.range.start);
1517            let end = buffer_snapshot.anchor_before(crease.range.end);
1518            crease_for_mention(
1519                crease.label.clone(),
1520                crease.icon_path.clone(),
1521                start..end,
1522                cx.weak_entity(),
1523            )
1524        })
1525        .collect::<Vec<_>>();
1526    let ids = editor.insert_creases(creases.clone(), cx);
1527    editor.fold_creases(creases, false, window, cx);
1528    ids
1529}