inline_prompt_editor.rs

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