inline_prompt_editor.rs

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