inline_prompt_editor.rs

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