inline_prompt_editor.rs

   1use crate::assistant_model_selector::AssistantModelSelector;
   2use crate::buffer_codegen::BufferCodegen;
   3use crate::context_picker::ContextPicker;
   4use crate::context_store::ContextStore;
   5use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
   6use crate::terminal_codegen::TerminalCodegen;
   7use crate::thread_store::ThreadStore;
   8use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
   9use crate::{RemoveAllContext, ToggleContextPicker};
  10use client::ErrorExt;
  11use collections::VecDeque;
  12use editor::{
  13    actions::{MoveDown, MoveUp},
  14    Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
  15};
  16use feature_flags::{FeatureFlagAppExt as _, ZedPro};
  17use fs::Fs;
  18use gpui::{
  19    anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
  20    EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
  21};
  22use language_model::{LanguageModel, LanguageModelRegistry};
  23use parking_lot::Mutex;
  24use settings::Settings;
  25use std::cmp;
  26use std::sync::Arc;
  27use theme::ThemeSettings;
  28use ui::utils::WithRemSize;
  29use ui::{
  30    prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
  31};
  32use util::ResultExt;
  33use workspace::Workspace;
  34
  35pub struct PromptEditor<T> {
  36    pub editor: Entity<Editor>,
  37    mode: PromptEditorMode,
  38    context_store: Entity<ContextStore>,
  39    context_strip: Entity<ContextStrip>,
  40    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
  41    model_selector: Entity<AssistantModelSelector>,
  42    edited_since_done: bool,
  43    prompt_history: VecDeque<String>,
  44    prompt_history_ix: Option<usize>,
  45    pending_prompt: String,
  46    _codegen_subscription: Subscription,
  47    editor_subscriptions: Vec<Subscription>,
  48    _context_strip_subscription: Subscription,
  49    show_rate_limit_notice: bool,
  50    _phantom: std::marker::PhantomData<T>,
  51}
  52
  53impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
  54
  55impl<T: 'static> Render for PromptEditor<T> {
  56    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
  57        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
  58        let mut buttons = Vec::new();
  59
  60        let left_gutter_width = match &self.mode {
  61            PromptEditorMode::Buffer {
  62                id: _,
  63                codegen,
  64                gutter_dimensions,
  65            } => {
  66                let codegen = codegen.read(cx);
  67
  68                if codegen.alternative_count(cx) > 1 {
  69                    buttons.push(self.render_cycle_controls(&codegen, cx));
  70                }
  71
  72                let gutter_dimensions = gutter_dimensions.lock();
  73
  74                gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
  75            }
  76            PromptEditorMode::Terminal { .. } => {
  77                // Give the equivalent of the same left-padding that we're using on the right
  78                Pixels::from(40.0)
  79            }
  80        };
  81
  82        let bottom_padding = match &self.mode {
  83            PromptEditorMode::Buffer { .. } => Pixels::from(0.),
  84            PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
  85        };
  86
  87        buttons.extend(self.render_buttons(window, cx));
  88
  89        v_flex()
  90            .key_context("PromptEditor")
  91            .bg(cx.theme().colors().editor_background)
  92            .block_mouse_down()
  93            .gap_0p5()
  94            .border_y_1()
  95            .border_color(cx.theme().status().info_border)
  96            .size_full()
  97            .pt_0p5()
  98            .pb(bottom_padding)
  99            .pr_6()
 100            .child(
 101                h_flex()
 102                    .items_start()
 103                    .cursor(CursorStyle::Arrow)
 104                    .on_action(cx.listener(Self::toggle_context_picker))
 105                    .on_action(cx.listener(|this, action, window, cx| {
 106                        let selector = this.model_selector.read(cx).selector.clone();
 107                        selector.update(cx, |selector, cx| {
 108                            selector.toggle_model_selector(action, window, cx);
 109                        })
 110                    }))
 111                    .on_action(cx.listener(Self::confirm))
 112                    .on_action(cx.listener(Self::cancel))
 113                    .on_action(cx.listener(Self::move_up))
 114                    .on_action(cx.listener(Self::move_down))
 115                    .on_action(cx.listener(Self::remove_all_context))
 116                    .capture_action(cx.listener(Self::cycle_prev))
 117                    .capture_action(cx.listener(Self::cycle_next))
 118                    .child(
 119                        WithRemSize::new(ui_font_size)
 120                            .flex()
 121                            .flex_row()
 122                            .flex_shrink_0()
 123                            .items_center()
 124                            .h_full()
 125                            .w(left_gutter_width)
 126                            .justify_center()
 127                            .gap_2()
 128                            .child(self.render_close_button(cx))
 129                            .map(|el| {
 130                                let CodegenStatus::Error(error) = self.codegen_status(cx) else {
 131                                    return el;
 132                                };
 133
 134                                let error_message = SharedString::from(error.to_string());
 135                                if error.error_code() == proto::ErrorCode::RateLimitExceeded
 136                                    && cx.has_flag::<ZedPro>()
 137                                {
 138                                    el.child(
 139                                        v_flex()
 140                                            .child(
 141                                                IconButton::new(
 142                                                    "rate-limit-error",
 143                                                    IconName::XCircle,
 144                                                )
 145                                                .toggle_state(self.show_rate_limit_notice)
 146                                                .shape(IconButtonShape::Square)
 147                                                .icon_size(IconSize::Small)
 148                                                .on_click(
 149                                                    cx.listener(Self::toggle_rate_limit_notice),
 150                                                ),
 151                                            )
 152                                            .children(self.show_rate_limit_notice.then(|| {
 153                                                deferred(
 154                                                    anchored()
 155                                                        .position_mode(
 156                                                            gpui::AnchoredPositionMode::Local,
 157                                                        )
 158                                                        .position(point(px(0.), px(24.)))
 159                                                        .anchor(gpui::Corner::TopLeft)
 160                                                        .child(self.render_rate_limit_notice(cx)),
 161                                                )
 162                                            })),
 163                                    )
 164                                } else {
 165                                    el.child(
 166                                        div()
 167                                            .id("error")
 168                                            .tooltip(Tooltip::text(error_message))
 169                                            .child(
 170                                                Icon::new(IconName::XCircle)
 171                                                    .size(IconSize::Small)
 172                                                    .color(Color::Error),
 173                                            ),
 174                                    )
 175                                }
 176                            }),
 177                    )
 178                    .child(
 179                        h_flex()
 180                            .w_full()
 181                            .justify_between()
 182                            .child(div().flex_1().child(self.render_editor(window, cx)))
 183                            .child(
 184                                WithRemSize::new(ui_font_size)
 185                                    .flex()
 186                                    .flex_row()
 187                                    .items_center()
 188                                    .gap_1()
 189                                    .children(buttons),
 190                            ),
 191                    ),
 192            )
 193            .child(
 194                WithRemSize::new(ui_font_size)
 195                    .flex()
 196                    .flex_row()
 197                    .items_center()
 198                    .child(h_flex().flex_shrink_0().w(left_gutter_width))
 199                    .child(
 200                        h_flex()
 201                            .w_full()
 202                            .pl_1()
 203                            .items_start()
 204                            .justify_between()
 205                            .child(self.context_strip.clone())
 206                            .child(self.model_selector.clone()),
 207                    ),
 208            )
 209    }
 210}
 211
 212impl<T: 'static> Focusable for PromptEditor<T> {
 213    fn focus_handle(&self, cx: &App) -> FocusHandle {
 214        self.editor.focus_handle(cx)
 215    }
 216}
 217
 218impl<T: 'static> PromptEditor<T> {
 219    const MAX_LINES: u8 = 8;
 220
 221    fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
 222        match &self.mode {
 223            PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
 224            PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
 225        }
 226    }
 227
 228    fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 229        self.editor_subscriptions.clear();
 230        self.editor_subscriptions.push(cx.subscribe_in(
 231            &self.editor,
 232            window,
 233            Self::handle_prompt_editor_events,
 234        ));
 235    }
 236
 237    pub fn set_show_cursor_when_unfocused(
 238        &mut self,
 239        show_cursor_when_unfocused: bool,
 240        cx: &mut Context<Self>,
 241    ) {
 242        self.editor.update(cx, |editor, cx| {
 243            editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
 244        });
 245    }
 246
 247    pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 248        let prompt = self.prompt(cx);
 249        let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
 250        self.editor = cx.new(|cx| {
 251            let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
 252            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 253            editor.set_placeholder_text(Self::placeholder_text(&self.mode, window, cx), cx);
 254            editor.set_placeholder_text("Add a prompt…", cx);
 255            editor.set_text(prompt, window, cx);
 256            if focus {
 257                window.focus(&editor.focus_handle(cx));
 258            }
 259            editor
 260        });
 261        self.subscribe_to_editor(window, cx);
 262    }
 263
 264    pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
 265        let action = match mode {
 266            PromptEditorMode::Buffer { codegen, .. } => {
 267                if codegen.read(cx).is_insertion {
 268                    "Generate"
 269                } else {
 270                    "Transform"
 271                }
 272            }
 273            PromptEditorMode::Terminal { .. } => "Generate",
 274        };
 275
 276        let assistant_panel_keybinding =
 277            ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
 278                .map(|keybinding| format!("{keybinding} to chat ― "))
 279                .unwrap_or_default();
 280
 281        format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
 282    }
 283
 284    pub fn prompt(&self, cx: &App) -> String {
 285        self.editor.read(cx).text(cx)
 286    }
 287
 288    fn toggle_rate_limit_notice(
 289        &mut self,
 290        _: &ClickEvent,
 291        window: &mut Window,
 292        cx: &mut Context<Self>,
 293    ) {
 294        self.show_rate_limit_notice = !self.show_rate_limit_notice;
 295        if self.show_rate_limit_notice {
 296            window.focus(&self.editor.focus_handle(cx));
 297        }
 298        cx.notify();
 299    }
 300
 301    fn handle_prompt_editor_events(
 302        &mut self,
 303        _: &Entity<Editor>,
 304        event: &EditorEvent,
 305        window: &mut Window,
 306        cx: &mut Context<Self>,
 307    ) {
 308        match event {
 309            EditorEvent::Edited { .. } => {
 310                if let Some(workspace) = window.root::<Workspace>().flatten() {
 311                    workspace.update(cx, |workspace, cx| {
 312                        let is_via_ssh = workspace
 313                            .project()
 314                            .update(cx, |project, _| project.is_via_ssh());
 315
 316                        workspace
 317                            .client()
 318                            .telemetry()
 319                            .log_edit_event("inline assist", is_via_ssh);
 320                    });
 321                }
 322                let prompt = self.editor.read(cx).text(cx);
 323                if self
 324                    .prompt_history_ix
 325                    .map_or(true, |ix| self.prompt_history[ix] != prompt)
 326                {
 327                    self.prompt_history_ix.take();
 328                    self.pending_prompt = prompt;
 329                }
 330
 331                self.edited_since_done = true;
 332                cx.notify();
 333            }
 334            EditorEvent::Blurred => {
 335                if self.show_rate_limit_notice {
 336                    self.show_rate_limit_notice = false;
 337                    cx.notify();
 338                }
 339            }
 340            _ => {}
 341        }
 342    }
 343
 344    fn toggle_context_picker(
 345        &mut self,
 346        _: &ToggleContextPicker,
 347        window: &mut Window,
 348        cx: &mut Context<Self>,
 349    ) {
 350        self.context_picker_menu_handle.toggle(window, cx);
 351    }
 352
 353    pub fn remove_all_context(
 354        &mut self,
 355        _: &RemoveAllContext,
 356        _window: &mut Window,
 357        cx: &mut Context<Self>,
 358    ) {
 359        self.context_store.update(cx, |store, _cx| store.clear());
 360        cx.notify();
 361    }
 362
 363    fn cancel(
 364        &mut self,
 365        _: &editor::actions::Cancel,
 366        _window: &mut Window,
 367        cx: &mut Context<Self>,
 368    ) {
 369        match self.codegen_status(cx) {
 370            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 371                cx.emit(PromptEditorEvent::CancelRequested);
 372            }
 373            CodegenStatus::Pending => {
 374                cx.emit(PromptEditorEvent::StopRequested);
 375            }
 376        }
 377    }
 378
 379    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 380        match self.codegen_status(cx) {
 381            CodegenStatus::Idle => {
 382                cx.emit(PromptEditorEvent::StartRequested);
 383            }
 384            CodegenStatus::Pending => {
 385                cx.emit(PromptEditorEvent::DismissRequested);
 386            }
 387            CodegenStatus::Done => {
 388                if self.edited_since_done {
 389                    cx.emit(PromptEditorEvent::StartRequested);
 390                } else {
 391                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 392                }
 393            }
 394            CodegenStatus::Error(_) => {
 395                cx.emit(PromptEditorEvent::StartRequested);
 396            }
 397        }
 398    }
 399
 400    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 401        if let Some(ix) = self.prompt_history_ix {
 402            if ix > 0 {
 403                self.prompt_history_ix = Some(ix - 1);
 404                let prompt = self.prompt_history[ix - 1].as_str();
 405                self.editor.update(cx, |editor, cx| {
 406                    editor.set_text(prompt, window, cx);
 407                    editor.move_to_beginning(&Default::default(), window, cx);
 408                });
 409            }
 410        } else if !self.prompt_history.is_empty() {
 411            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 412            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 413            self.editor.update(cx, |editor, cx| {
 414                editor.set_text(prompt, window, cx);
 415                editor.move_to_beginning(&Default::default(), window, cx);
 416            });
 417        }
 418    }
 419
 420    fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 421        if let Some(ix) = self.prompt_history_ix {
 422            if ix < self.prompt_history.len() - 1 {
 423                self.prompt_history_ix = Some(ix + 1);
 424                let prompt = self.prompt_history[ix + 1].as_str();
 425                self.editor.update(cx, |editor, cx| {
 426                    editor.set_text(prompt, window, cx);
 427                    editor.move_to_end(&Default::default(), window, cx)
 428                });
 429            } else {
 430                self.prompt_history_ix = None;
 431                let prompt = self.pending_prompt.as_str();
 432                self.editor.update(cx, |editor, cx| {
 433                    editor.set_text(prompt, window, cx);
 434                    editor.move_to_end(&Default::default(), window, cx)
 435                });
 436            }
 437        } else {
 438            self.context_strip.focus_handle(cx).focus(window);
 439        }
 440    }
 441
 442    fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
 443        let mode = match &self.mode {
 444            PromptEditorMode::Buffer { codegen, .. } => {
 445                let codegen = codegen.read(cx);
 446                if codegen.is_insertion {
 447                    GenerationMode::Generate
 448                } else {
 449                    GenerationMode::Transform
 450                }
 451            }
 452            PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
 453        };
 454
 455        let codegen_status = self.codegen_status(cx);
 456
 457        match codegen_status {
 458            CodegenStatus::Idle => {
 459                vec![Button::new("start", mode.start_label())
 460                    .label_size(LabelSize::Small)
 461                    .icon(IconName::Return)
 462                    .icon_size(IconSize::XSmall)
 463                    .icon_color(Color::Muted)
 464                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
 465                    .into_any_element()]
 466            }
 467            CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
 468                .icon_color(Color::Error)
 469                .shape(IconButtonShape::Square)
 470                .tooltip(move |window, cx| {
 471                    Tooltip::with_meta(
 472                        mode.tooltip_interrupt(),
 473                        Some(&menu::Cancel),
 474                        "Changes won't be discarded",
 475                        window,
 476                        cx,
 477                    )
 478                })
 479                .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 480                .into_any_element()],
 481            CodegenStatus::Done | CodegenStatus::Error(_) => {
 482                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 483                if has_error || self.edited_since_done {
 484                    vec![IconButton::new("restart", IconName::RotateCw)
 485                        .icon_color(Color::Info)
 486                        .shape(IconButtonShape::Square)
 487                        .tooltip(move |window, cx| {
 488                            Tooltip::with_meta(
 489                                mode.tooltip_restart(),
 490                                Some(&menu::Confirm),
 491                                "Changes will be discarded",
 492                                window,
 493                                cx,
 494                            )
 495                        })
 496                        .on_click(cx.listener(|_, _, _, cx| {
 497                            cx.emit(PromptEditorEvent::StartRequested);
 498                        }))
 499                        .into_any_element()]
 500                } else {
 501                    let accept = IconButton::new("accept", IconName::Check)
 502                        .icon_color(Color::Info)
 503                        .shape(IconButtonShape::Square)
 504                        .tooltip(move |window, cx| {
 505                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx)
 506                        })
 507                        .on_click(cx.listener(|_, _, _, cx| {
 508                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 509                        }))
 510                        .into_any_element();
 511
 512                    match &self.mode {
 513                        PromptEditorMode::Terminal { .. } => vec![
 514                            accept,
 515                            IconButton::new("confirm", IconName::Play)
 516                                .icon_color(Color::Info)
 517                                .shape(IconButtonShape::Square)
 518                                .tooltip(|window, cx| {
 519                                    Tooltip::for_action(
 520                                        "Execute Generated Command",
 521                                        &menu::SecondaryConfirm,
 522                                        window,
 523                                        cx,
 524                                    )
 525                                })
 526                                .on_click(cx.listener(|_, _, _, cx| {
 527                                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 528                                }))
 529                                .into_any_element(),
 530                        ],
 531                        PromptEditorMode::Buffer { .. } => vec![accept],
 532                    }
 533                }
 534            }
 535        }
 536    }
 537
 538    fn cycle_prev(
 539        &mut self,
 540        _: &CyclePreviousInlineAssist,
 541        _: &mut Window,
 542        cx: &mut Context<Self>,
 543    ) {
 544        match &self.mode {
 545            PromptEditorMode::Buffer { codegen, .. } => {
 546                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 547            }
 548            PromptEditorMode::Terminal { .. } => {
 549                // no cycle buttons in terminal mode
 550            }
 551        }
 552    }
 553
 554    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 555        match &self.mode {
 556            PromptEditorMode::Buffer { codegen, .. } => {
 557                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 558            }
 559            PromptEditorMode::Terminal { .. } => {
 560                // no cycle buttons in terminal mode
 561            }
 562        }
 563    }
 564
 565    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 566        IconButton::new("cancel", IconName::Close)
 567            .icon_color(Color::Muted)
 568            .shape(IconButtonShape::Square)
 569            .tooltip(Tooltip::text("Close Assistant"))
 570            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 571            .into_any_element()
 572    }
 573
 574    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
 575        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
 576
 577        let model_registry = LanguageModelRegistry::read_global(cx);
 578        let default_model = model_registry.active_model();
 579        let alternative_models = model_registry.inline_alternative_models();
 580
 581        let get_model_name = |index: usize| -> String {
 582            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
 583
 584            match index {
 585                0 => default_model.as_ref().map_or_else(String::new, name),
 586                index if index <= alternative_models.len() => alternative_models
 587                    .get(index - 1)
 588                    .map_or_else(String::new, name),
 589                _ => String::new(),
 590            }
 591        };
 592
 593        let total_models = alternative_models.len() + 1;
 594
 595        if total_models <= 1 {
 596            return div().into_any_element();
 597        }
 598
 599        let current_index = codegen.active_alternative;
 600        let prev_index = (current_index + total_models - 1) % total_models;
 601        let next_index = (current_index + 1) % total_models;
 602
 603        let prev_model_name = get_model_name(prev_index);
 604        let next_model_name = get_model_name(next_index);
 605
 606        h_flex()
 607            .child(
 608                IconButton::new("previous", IconName::ChevronLeft)
 609                    .icon_color(Color::Muted)
 610                    .disabled(disabled || current_index == 0)
 611                    .shape(IconButtonShape::Square)
 612                    .tooltip({
 613                        let focus_handle = self.editor.focus_handle(cx);
 614                        move |window, cx| {
 615                            cx.new(|cx| {
 616                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
 617                                    KeyBinding::for_action_in(
 618                                        &CyclePreviousInlineAssist,
 619                                        &focus_handle,
 620                                        window,
 621                                        cx,
 622                                    ),
 623                                );
 624                                if !disabled && current_index != 0 {
 625                                    tooltip = tooltip.meta(prev_model_name.clone());
 626                                }
 627                                tooltip
 628                            })
 629                            .into()
 630                        }
 631                    })
 632                    .on_click(cx.listener(|this, _, window, cx| {
 633                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
 634                    })),
 635            )
 636            .child(
 637                Label::new(format!(
 638                    "{}/{}",
 639                    codegen.active_alternative + 1,
 640                    codegen.alternative_count(cx)
 641                ))
 642                .size(LabelSize::Small)
 643                .color(if disabled {
 644                    Color::Disabled
 645                } else {
 646                    Color::Muted
 647                }),
 648            )
 649            .child(
 650                IconButton::new("next", IconName::ChevronRight)
 651                    .icon_color(Color::Muted)
 652                    .disabled(disabled || current_index == total_models - 1)
 653                    .shape(IconButtonShape::Square)
 654                    .tooltip({
 655                        let focus_handle = self.editor.focus_handle(cx);
 656                        move |window, cx| {
 657                            cx.new(|cx| {
 658                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
 659                                    KeyBinding::for_action_in(
 660                                        &CycleNextInlineAssist,
 661                                        &focus_handle,
 662                                        window,
 663                                        cx,
 664                                    ),
 665                                );
 666                                if !disabled && current_index != total_models - 1 {
 667                                    tooltip = tooltip.meta(next_model_name.clone());
 668                                }
 669                                tooltip
 670                            })
 671                            .into()
 672                        }
 673                    })
 674                    .on_click(cx.listener(|this, _, window, cx| {
 675                        this.cycle_next(&CycleNextInlineAssist, window, cx)
 676                    })),
 677            )
 678            .into_any_element()
 679    }
 680
 681    fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement {
 682        Popover::new().child(
 683            v_flex()
 684                .occlude()
 685                .p_2()
 686                .child(
 687                    Label::new("Out of Tokens")
 688                        .size(LabelSize::Small)
 689                        .weight(FontWeight::BOLD),
 690                )
 691                .child(Label::new(
 692                    "Try Zed Pro for higher limits, a wider range of models, and more.",
 693                ))
 694                .child(
 695                    h_flex()
 696                        .justify_between()
 697                        .child(CheckboxWithLabel::new(
 698                            "dont-show-again",
 699                            Label::new("Don't show again"),
 700                            if dismissed_rate_limit_notice() {
 701                                ui::ToggleState::Selected
 702                            } else {
 703                                ui::ToggleState::Unselected
 704                            },
 705                            |selection, _, cx| {
 706                                let is_dismissed = match selection {
 707                                    ui::ToggleState::Unselected => false,
 708                                    ui::ToggleState::Indeterminate => return,
 709                                    ui::ToggleState::Selected => true,
 710                                };
 711
 712                                set_rate_limit_notice_dismissed(is_dismissed, cx)
 713                            },
 714                        ))
 715                        .child(
 716                            h_flex()
 717                                .gap_2()
 718                                .child(
 719                                    Button::new("dismiss", "Dismiss")
 720                                        .style(ButtonStyle::Transparent)
 721                                        .on_click(cx.listener(Self::toggle_rate_limit_notice)),
 722                                )
 723                                .child(Button::new("more-info", "More Info").on_click(
 724                                    |_event, window, cx| {
 725                                        window.dispatch_action(
 726                                            Box::new(zed_actions::OpenAccountSettings),
 727                                            cx,
 728                                        )
 729                                    },
 730                                )),
 731                        ),
 732                ),
 733        )
 734    }
 735
 736    fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 737        let font_size = TextSize::Default.rems(cx);
 738        let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
 739
 740        div()
 741            .key_context("InlineAssistEditor")
 742            .size_full()
 743            .p_2()
 744            .pl_1()
 745            .bg(cx.theme().colors().editor_background)
 746            .child({
 747                let settings = ThemeSettings::get_global(cx);
 748                let text_style = TextStyle {
 749                    color: cx.theme().colors().editor_foreground,
 750                    font_family: settings.buffer_font.family.clone(),
 751                    font_features: settings.buffer_font.features.clone(),
 752                    font_size: font_size.into(),
 753                    line_height: line_height.into(),
 754                    ..Default::default()
 755                };
 756
 757                EditorElement::new(
 758                    &self.editor,
 759                    EditorStyle {
 760                        background: cx.theme().colors().editor_background,
 761                        local_player: cx.theme().players().local(),
 762                        text: text_style,
 763                        ..Default::default()
 764                    },
 765                )
 766            })
 767            .into_any_element()
 768    }
 769
 770    fn handle_context_strip_event(
 771        &mut self,
 772        _context_strip: &Entity<ContextStrip>,
 773        event: &ContextStripEvent,
 774        window: &mut Window,
 775        cx: &mut Context<Self>,
 776    ) {
 777        match event {
 778            ContextStripEvent::PickerDismissed
 779            | ContextStripEvent::BlurredEmpty
 780            | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
 781            ContextStripEvent::BlurredDown => {}
 782        }
 783    }
 784}
 785
 786pub enum PromptEditorMode {
 787    Buffer {
 788        id: InlineAssistId,
 789        codegen: Entity<BufferCodegen>,
 790        gutter_dimensions: Arc<Mutex<GutterDimensions>>,
 791    },
 792    Terminal {
 793        id: TerminalInlineAssistId,
 794        codegen: Entity<TerminalCodegen>,
 795        height_in_lines: u8,
 796    },
 797}
 798
 799pub enum PromptEditorEvent {
 800    StartRequested,
 801    StopRequested,
 802    ConfirmRequested { execute: bool },
 803    CancelRequested,
 804    DismissRequested,
 805    Resized { height_in_lines: u8 },
 806}
 807
 808#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 809pub struct InlineAssistId(pub usize);
 810
 811impl InlineAssistId {
 812    pub fn post_inc(&mut self) -> InlineAssistId {
 813        let id = *self;
 814        self.0 += 1;
 815        id
 816    }
 817}
 818
 819impl PromptEditor<BufferCodegen> {
 820    #[allow(clippy::too_many_arguments)]
 821    pub fn new_buffer(
 822        id: InlineAssistId,
 823        gutter_dimensions: Arc<Mutex<GutterDimensions>>,
 824        prompt_history: VecDeque<String>,
 825        prompt_buffer: Entity<MultiBuffer>,
 826        codegen: Entity<BufferCodegen>,
 827        fs: Arc<dyn Fs>,
 828        context_store: Entity<ContextStore>,
 829        workspace: WeakEntity<Workspace>,
 830        thread_store: Option<WeakEntity<ThreadStore>>,
 831        window: &mut Window,
 832        cx: &mut Context<PromptEditor<BufferCodegen>>,
 833    ) -> PromptEditor<BufferCodegen> {
 834        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 835        let mode = PromptEditorMode::Buffer {
 836            id,
 837            codegen,
 838            gutter_dimensions,
 839        };
 840
 841        let prompt_editor = cx.new(|cx| {
 842            let mut editor = Editor::new(
 843                EditorMode::AutoHeight {
 844                    max_lines: Self::MAX_LINES as usize,
 845                },
 846                prompt_buffer,
 847                None,
 848                false,
 849                window,
 850                cx,
 851            );
 852            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 853            // Since the prompt editors for all inline assistants are linked,
 854            // always show the cursor (even when it isn't focused) because
 855            // typing in one will make what you typed appear in all of them.
 856            editor.set_show_cursor_when_unfocused(true, cx);
 857            editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
 858            editor
 859        });
 860        let context_picker_menu_handle = PopoverMenuHandle::default();
 861
 862        let context_strip = cx.new(|cx| {
 863            ContextStrip::new(
 864                context_store.clone(),
 865                workspace.clone(),
 866                prompt_editor.downgrade(),
 867                thread_store.clone(),
 868                context_picker_menu_handle.clone(),
 869                SuggestContextKind::Thread,
 870                window,
 871                cx,
 872            )
 873        });
 874
 875        let context_strip_subscription =
 876            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
 877
 878        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
 879            editor: prompt_editor.clone(),
 880            context_store,
 881            context_strip,
 882            context_picker_menu_handle,
 883            model_selector: cx.new(|cx| {
 884                AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
 885            }),
 886            edited_since_done: false,
 887            prompt_history,
 888            prompt_history_ix: None,
 889            pending_prompt: String::new(),
 890            _codegen_subscription: codegen_subscription,
 891            editor_subscriptions: Vec::new(),
 892            _context_strip_subscription: context_strip_subscription,
 893            show_rate_limit_notice: false,
 894            mode,
 895            _phantom: Default::default(),
 896        };
 897
 898        this.subscribe_to_editor(window, cx);
 899        this
 900    }
 901
 902    fn handle_codegen_changed(
 903        &mut self,
 904        _: Entity<BufferCodegen>,
 905        cx: &mut Context<PromptEditor<BufferCodegen>>,
 906    ) {
 907        match self.codegen_status(cx) {
 908            CodegenStatus::Idle => {
 909                self.editor
 910                    .update(cx, |editor, _| editor.set_read_only(false));
 911            }
 912            CodegenStatus::Pending => {
 913                self.editor
 914                    .update(cx, |editor, _| editor.set_read_only(true));
 915            }
 916            CodegenStatus::Done => {
 917                self.edited_since_done = false;
 918                self.editor
 919                    .update(cx, |editor, _| editor.set_read_only(false));
 920            }
 921            CodegenStatus::Error(error) => {
 922                if cx.has_flag::<ZedPro>()
 923                    && error.error_code() == proto::ErrorCode::RateLimitExceeded
 924                    && !dismissed_rate_limit_notice()
 925                {
 926                    self.show_rate_limit_notice = true;
 927                    cx.notify();
 928                }
 929
 930                self.edited_since_done = false;
 931                self.editor
 932                    .update(cx, |editor, _| editor.set_read_only(false));
 933            }
 934        }
 935    }
 936
 937    pub fn id(&self) -> InlineAssistId {
 938        match &self.mode {
 939            PromptEditorMode::Buffer { id, .. } => *id,
 940            PromptEditorMode::Terminal { .. } => unreachable!(),
 941        }
 942    }
 943
 944    pub fn codegen(&self) -> &Entity<BufferCodegen> {
 945        match &self.mode {
 946            PromptEditorMode::Buffer { codegen, .. } => codegen,
 947            PromptEditorMode::Terminal { .. } => unreachable!(),
 948        }
 949    }
 950
 951    pub fn gutter_dimensions(&self) -> &Arc<Mutex<GutterDimensions>> {
 952        match &self.mode {
 953            PromptEditorMode::Buffer {
 954                gutter_dimensions, ..
 955            } => gutter_dimensions,
 956            PromptEditorMode::Terminal { .. } => unreachable!(),
 957        }
 958    }
 959}
 960
 961#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 962pub struct TerminalInlineAssistId(pub usize);
 963
 964impl TerminalInlineAssistId {
 965    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
 966        let id = *self;
 967        self.0 += 1;
 968        id
 969    }
 970}
 971
 972impl PromptEditor<TerminalCodegen> {
 973    #[allow(clippy::too_many_arguments)]
 974    pub fn new_terminal(
 975        id: TerminalInlineAssistId,
 976        prompt_history: VecDeque<String>,
 977        prompt_buffer: Entity<MultiBuffer>,
 978        codegen: Entity<TerminalCodegen>,
 979        fs: Arc<dyn Fs>,
 980        context_store: Entity<ContextStore>,
 981        workspace: WeakEntity<Workspace>,
 982        thread_store: Option<WeakEntity<ThreadStore>>,
 983        window: &mut Window,
 984        cx: &mut Context<Self>,
 985    ) -> Self {
 986        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 987        let mode = PromptEditorMode::Terminal {
 988            id,
 989            codegen,
 990            height_in_lines: 1,
 991        };
 992
 993        let prompt_editor = cx.new(|cx| {
 994            let mut editor = Editor::new(
 995                EditorMode::AutoHeight {
 996                    max_lines: Self::MAX_LINES as usize,
 997                },
 998                prompt_buffer,
 999                None,
1000                false,
1001                window,
1002                cx,
1003            );
1004            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1005            editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
1006            editor
1007        });
1008        let context_picker_menu_handle = PopoverMenuHandle::default();
1009
1010        let context_strip = cx.new(|cx| {
1011            ContextStrip::new(
1012                context_store.clone(),
1013                workspace.clone(),
1014                prompt_editor.downgrade(),
1015                thread_store.clone(),
1016                context_picker_menu_handle.clone(),
1017                SuggestContextKind::Thread,
1018                window,
1019                cx,
1020            )
1021        });
1022
1023        let context_strip_subscription =
1024            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1025
1026        let mut this = Self {
1027            editor: prompt_editor.clone(),
1028            context_store,
1029            context_strip,
1030            context_picker_menu_handle,
1031            model_selector: cx.new(|cx| {
1032                AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
1033            }),
1034            edited_since_done: false,
1035            prompt_history,
1036            prompt_history_ix: None,
1037            pending_prompt: String::new(),
1038            _codegen_subscription: codegen_subscription,
1039            editor_subscriptions: Vec::new(),
1040            _context_strip_subscription: context_strip_subscription,
1041            mode,
1042            show_rate_limit_notice: false,
1043            _phantom: Default::default(),
1044        };
1045        this.count_lines(cx);
1046        this.subscribe_to_editor(window, cx);
1047        this
1048    }
1049
1050    fn count_lines(&mut self, cx: &mut Context<Self>) {
1051        let height_in_lines = cmp::max(
1052            2, // Make the editor at least two lines tall, to account for padding and buttons.
1053            cmp::min(
1054                self.editor
1055                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1056                Self::MAX_LINES as u32,
1057            ),
1058        ) as u8;
1059
1060        match &mut self.mode {
1061            PromptEditorMode::Terminal {
1062                height_in_lines: current_height,
1063                ..
1064            } => {
1065                if height_in_lines != *current_height {
1066                    *current_height = height_in_lines;
1067                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1068                }
1069            }
1070            PromptEditorMode::Buffer { .. } => unreachable!(),
1071        }
1072    }
1073
1074    fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1075        match &self.codegen().read(cx).status {
1076            CodegenStatus::Idle => {
1077                self.editor
1078                    .update(cx, |editor, _| editor.set_read_only(false));
1079            }
1080            CodegenStatus::Pending => {
1081                self.editor
1082                    .update(cx, |editor, _| editor.set_read_only(true));
1083            }
1084            CodegenStatus::Done | CodegenStatus::Error(_) => {
1085                self.edited_since_done = false;
1086                self.editor
1087                    .update(cx, |editor, _| editor.set_read_only(false));
1088            }
1089        }
1090    }
1091
1092    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1093        match &self.mode {
1094            PromptEditorMode::Buffer { .. } => unreachable!(),
1095            PromptEditorMode::Terminal { codegen, .. } => codegen,
1096        }
1097    }
1098
1099    pub fn id(&self) -> TerminalInlineAssistId {
1100        match &self.mode {
1101            PromptEditorMode::Buffer { .. } => unreachable!(),
1102            PromptEditorMode::Terminal { id, .. } => *id,
1103        }
1104    }
1105}
1106
1107const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
1108
1109fn dismissed_rate_limit_notice() -> bool {
1110    db::kvp::KEY_VALUE_STORE
1111        .read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
1112        .log_err()
1113        .map_or(false, |s| s.is_some())
1114}
1115
1116fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
1117    db::write_and_log(cx, move || async move {
1118        if is_dismissed {
1119            db::kvp::KEY_VALUE_STORE
1120                .write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
1121                .await
1122        } else {
1123            db::kvp::KEY_VALUE_STORE
1124                .delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
1125                .await
1126        }
1127    })
1128}
1129
1130pub enum CodegenStatus {
1131    Idle,
1132    Pending,
1133    Done,
1134    Error(anyhow::Error),
1135}
1136
1137/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1138#[derive(Copy, Clone)]
1139pub enum CancelButtonState {
1140    Idle,
1141    Pending,
1142    Done,
1143    Error,
1144}
1145
1146impl Into<CancelButtonState> for &CodegenStatus {
1147    fn into(self) -> CancelButtonState {
1148        match self {
1149            CodegenStatus::Idle => CancelButtonState::Idle,
1150            CodegenStatus::Pending => CancelButtonState::Pending,
1151            CodegenStatus::Done => CancelButtonState::Done,
1152            CodegenStatus::Error(_) => CancelButtonState::Error,
1153        }
1154    }
1155}
1156
1157#[derive(Copy, Clone)]
1158pub enum GenerationMode {
1159    Generate,
1160    Transform,
1161}
1162
1163impl GenerationMode {
1164    fn start_label(self) -> &'static str {
1165        match self {
1166            GenerationMode::Generate { .. } => "Generate",
1167            GenerationMode::Transform => "Transform",
1168        }
1169    }
1170    fn tooltip_interrupt(self) -> &'static str {
1171        match self {
1172            GenerationMode::Generate { .. } => "Interrupt Generation",
1173            GenerationMode::Transform => "Interrupt Transform",
1174        }
1175    }
1176
1177    fn tooltip_restart(self) -> &'static str {
1178        match self {
1179            GenerationMode::Generate { .. } => "Restart Generation",
1180            GenerationMode::Transform => "Restart Transform",
1181        }
1182    }
1183
1184    fn tooltip_accept(self) -> &'static str {
1185        match self {
1186            GenerationMode::Generate { .. } => "Accept Generation",
1187            GenerationMode::Transform => "Accept Transform",
1188        }
1189    }
1190}