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                            cx,
 477                        )
 478                    })
 479                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 480                    .into_any_element(),
 481            ],
 482            CodegenStatus::Done | CodegenStatus::Error(_) => {
 483                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 484                if has_error || self.edited_since_done {
 485                    vec![
 486                        IconButton::new("restart", IconName::RotateCw)
 487                            .icon_color(Color::Info)
 488                            .shape(IconButtonShape::Square)
 489                            .tooltip(move |_window, cx| {
 490                                Tooltip::with_meta(
 491                                    mode.tooltip_restart(),
 492                                    Some(&menu::Confirm),
 493                                    "Changes will be discarded",
 494                                    cx,
 495                                )
 496                            })
 497                            .on_click(cx.listener(|_, _, _, cx| {
 498                                cx.emit(PromptEditorEvent::StartRequested);
 499                            }))
 500                            .into_any_element(),
 501                    ]
 502                } else {
 503                    let accept = IconButton::new("accept", IconName::Check)
 504                        .icon_color(Color::Info)
 505                        .shape(IconButtonShape::Square)
 506                        .tooltip(move |_window, cx| {
 507                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
 508                        })
 509                        .on_click(cx.listener(|_, _, _, cx| {
 510                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 511                        }))
 512                        .into_any_element();
 513
 514                    match &self.mode {
 515                        PromptEditorMode::Terminal { .. } => vec![
 516                            accept,
 517                            IconButton::new("confirm", IconName::PlayFilled)
 518                                .icon_color(Color::Info)
 519                                .shape(IconButtonShape::Square)
 520                                .tooltip(|_window, cx| {
 521                                    Tooltip::for_action(
 522                                        "Execute Generated Command",
 523                                        &menu::SecondaryConfirm,
 524                                        cx,
 525                                    )
 526                                })
 527                                .on_click(cx.listener(|_, _, _, cx| {
 528                                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 529                                }))
 530                                .into_any_element(),
 531                        ],
 532                        PromptEditorMode::Buffer { .. } => vec![accept],
 533                    }
 534                }
 535            }
 536        }
 537    }
 538
 539    fn cycle_prev(
 540        &mut self,
 541        _: &CyclePreviousInlineAssist,
 542        _: &mut Window,
 543        cx: &mut Context<Self>,
 544    ) {
 545        match &self.mode {
 546            PromptEditorMode::Buffer { codegen, .. } => {
 547                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 548            }
 549            PromptEditorMode::Terminal { .. } => {
 550                // no cycle buttons in terminal mode
 551            }
 552        }
 553    }
 554
 555    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 556        match &self.mode {
 557            PromptEditorMode::Buffer { codegen, .. } => {
 558                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 559            }
 560            PromptEditorMode::Terminal { .. } => {
 561                // no cycle buttons in terminal mode
 562            }
 563        }
 564    }
 565
 566    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 567        IconButton::new("cancel", IconName::Close)
 568            .icon_color(Color::Muted)
 569            .shape(IconButtonShape::Square)
 570            .tooltip(Tooltip::text("Close Assistant"))
 571            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 572            .into_any_element()
 573    }
 574
 575    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
 576        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
 577
 578        let model_registry = LanguageModelRegistry::read_global(cx);
 579        let default_model = model_registry.default_model().map(|default| default.model);
 580        let alternative_models = model_registry.inline_alternative_models();
 581
 582        let get_model_name = |index: usize| -> String {
 583            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
 584
 585            match index {
 586                0 => default_model.as_ref().map_or_else(String::new, name),
 587                index if index <= alternative_models.len() => alternative_models
 588                    .get(index - 1)
 589                    .map_or_else(String::new, name),
 590                _ => String::new(),
 591            }
 592        };
 593
 594        let total_models = alternative_models.len() + 1;
 595
 596        if total_models <= 1 {
 597            return div().into_any_element();
 598        }
 599
 600        let current_index = codegen.active_alternative;
 601        let prev_index = (current_index + total_models - 1) % total_models;
 602        let next_index = (current_index + 1) % total_models;
 603
 604        let prev_model_name = get_model_name(prev_index);
 605        let next_model_name = get_model_name(next_index);
 606
 607        h_flex()
 608            .child(
 609                IconButton::new("previous", IconName::ChevronLeft)
 610                    .icon_color(Color::Muted)
 611                    .disabled(disabled || current_index == 0)
 612                    .shape(IconButtonShape::Square)
 613                    .tooltip({
 614                        let focus_handle = self.editor.focus_handle(cx);
 615                        move |_window, cx| {
 616                            cx.new(|cx| {
 617                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
 618                                    KeyBinding::for_action_in(
 619                                        &CyclePreviousInlineAssist,
 620                                        &focus_handle,
 621                                        cx,
 622                                    ),
 623                                );
 624                                if !disabled && current_index != 0 {
 625                                    tooltip = tooltip.meta(prev_model_name.clone());
 626                                }
 627                                tooltip
 628                            })
 629                            .into()
 630                        }
 631                    })
 632                    .on_click(cx.listener(|this, _, window, cx| {
 633                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
 634                    })),
 635            )
 636            .child(
 637                Label::new(format!(
 638                    "{}/{}",
 639                    codegen.active_alternative + 1,
 640                    codegen.alternative_count(cx)
 641                ))
 642                .size(LabelSize::Small)
 643                .color(if disabled {
 644                    Color::Disabled
 645                } else {
 646                    Color::Muted
 647                }),
 648            )
 649            .child(
 650                IconButton::new("next", IconName::ChevronRight)
 651                    .icon_color(Color::Muted)
 652                    .disabled(disabled || current_index == total_models - 1)
 653                    .shape(IconButtonShape::Square)
 654                    .tooltip({
 655                        let focus_handle = self.editor.focus_handle(cx);
 656                        move |_window, cx| {
 657                            cx.new(|cx| {
 658                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
 659                                    KeyBinding::for_action_in(
 660                                        &CycleNextInlineAssist,
 661                                        &focus_handle,
 662                                        cx,
 663                                    ),
 664                                );
 665                                if !disabled && current_index != total_models - 1 {
 666                                    tooltip = tooltip.meta(next_model_name.clone());
 667                                }
 668                                tooltip
 669                            })
 670                            .into()
 671                        }
 672                    })
 673                    .on_click(cx.listener(|this, _, window, cx| {
 674                        this.cycle_next(&CycleNextInlineAssist, window, cx)
 675                    })),
 676            )
 677            .into_any_element()
 678    }
 679
 680    fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 681        let colors = cx.theme().colors();
 682
 683        div()
 684            .key_context("InlineAssistEditor")
 685            .size_full()
 686            .p_2()
 687            .pl_1()
 688            .bg(colors.editor_background)
 689            .child({
 690                let settings = ThemeSettings::get_global(cx);
 691                let font_size = settings.buffer_font_size(cx);
 692                let line_height = font_size * 1.2;
 693
 694                let text_style = TextStyle {
 695                    color: colors.editor_foreground,
 696                    font_family: settings.buffer_font.family.clone(),
 697                    font_features: settings.buffer_font.features.clone(),
 698                    font_size: font_size.into(),
 699                    line_height: line_height.into(),
 700                    ..Default::default()
 701                };
 702
 703                EditorElement::new(
 704                    &self.editor,
 705                    EditorStyle {
 706                        background: colors.editor_background,
 707                        local_player: cx.theme().players().local(),
 708                        text: text_style,
 709                        ..Default::default()
 710                    },
 711                )
 712            })
 713            .into_any_element()
 714    }
 715
 716    fn handle_context_strip_event(
 717        &mut self,
 718        _context_strip: &Entity<ContextStrip>,
 719        event: &ContextStripEvent,
 720        window: &mut Window,
 721        cx: &mut Context<Self>,
 722    ) {
 723        match event {
 724            ContextStripEvent::PickerDismissed
 725            | ContextStripEvent::BlurredEmpty
 726            | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
 727            ContextStripEvent::BlurredDown => {}
 728        }
 729    }
 730}
 731
 732pub enum PromptEditorMode {
 733    Buffer {
 734        id: InlineAssistId,
 735        codegen: Entity<BufferCodegen>,
 736        editor_margins: Arc<Mutex<EditorMargins>>,
 737    },
 738    Terminal {
 739        id: TerminalInlineAssistId,
 740        codegen: Entity<TerminalCodegen>,
 741        height_in_lines: u8,
 742    },
 743}
 744
 745pub enum PromptEditorEvent {
 746    StartRequested,
 747    StopRequested,
 748    ConfirmRequested { execute: bool },
 749    CancelRequested,
 750    Resized { height_in_lines: u8 },
 751}
 752
 753#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 754pub struct InlineAssistId(pub usize);
 755
 756impl InlineAssistId {
 757    pub fn post_inc(&mut self) -> InlineAssistId {
 758        let id = *self;
 759        self.0 += 1;
 760        id
 761    }
 762}
 763
 764impl PromptEditor<BufferCodegen> {
 765    pub fn new_buffer(
 766        id: InlineAssistId,
 767        editor_margins: Arc<Mutex<EditorMargins>>,
 768        prompt_history: VecDeque<String>,
 769        prompt_buffer: Entity<MultiBuffer>,
 770        codegen: Entity<BufferCodegen>,
 771        fs: Arc<dyn Fs>,
 772        context_store: Entity<ContextStore>,
 773        workspace: WeakEntity<Workspace>,
 774        thread_store: Option<WeakEntity<HistoryStore>>,
 775        prompt_store: Option<WeakEntity<PromptStore>>,
 776        window: &mut Window,
 777        cx: &mut Context<PromptEditor<BufferCodegen>>,
 778    ) -> PromptEditor<BufferCodegen> {
 779        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 780        let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
 781        let mode = PromptEditorMode::Buffer {
 782            id,
 783            codegen,
 784            editor_margins,
 785        };
 786
 787        let prompt_editor = cx.new(|cx| {
 788            let mut editor = Editor::new(
 789                EditorMode::AutoHeight {
 790                    min_lines: 1,
 791                    max_lines: Some(Self::MAX_LINES as usize),
 792                },
 793                prompt_buffer,
 794                None,
 795                window,
 796                cx,
 797            );
 798            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 799            // Since the prompt editors for all inline assistants are linked,
 800            // always show the cursor (even when it isn't focused) because
 801            // typing in one will make what you typed appear in all of them.
 802            editor.set_show_cursor_when_unfocused(true, cx);
 803            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
 804            editor.register_addon(ContextCreasesAddon::new());
 805            editor.set_context_menu_options(ContextMenuOptions {
 806                min_entries_visible: 12,
 807                max_entries_visible: 12,
 808                placement: None,
 809            });
 810
 811            editor
 812        });
 813
 814        let prompt_editor_entity = prompt_editor.downgrade();
 815        prompt_editor.update(cx, |editor, _| {
 816            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 817                workspace.clone(),
 818                context_store.downgrade(),
 819                thread_store.clone(),
 820                prompt_store.clone(),
 821                prompt_editor_entity,
 822                codegen_buffer.as_ref().map(Entity::downgrade),
 823            ))));
 824        });
 825
 826        let context_picker_menu_handle = PopoverMenuHandle::default();
 827        let model_selector_menu_handle = PopoverMenuHandle::default();
 828
 829        let context_strip = cx.new(|cx| {
 830            ContextStrip::new(
 831                context_store.clone(),
 832                workspace.clone(),
 833                thread_store.clone(),
 834                prompt_store,
 835                context_picker_menu_handle.clone(),
 836                SuggestContextKind::Thread,
 837                ModelUsageContext::InlineAssistant,
 838                window,
 839                cx,
 840            )
 841        });
 842
 843        let context_strip_subscription =
 844            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
 845
 846        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
 847            editor: prompt_editor.clone(),
 848            context_store,
 849            context_strip,
 850            context_picker_menu_handle,
 851            model_selector: cx.new(|cx| {
 852                AgentModelSelector::new(
 853                    fs,
 854                    model_selector_menu_handle,
 855                    prompt_editor.focus_handle(cx),
 856                    ModelUsageContext::InlineAssistant,
 857                    window,
 858                    cx,
 859                )
 860            }),
 861            edited_since_done: false,
 862            prompt_history,
 863            prompt_history_ix: None,
 864            pending_prompt: String::new(),
 865            _codegen_subscription: codegen_subscription,
 866            editor_subscriptions: Vec::new(),
 867            _context_strip_subscription: context_strip_subscription,
 868            show_rate_limit_notice: false,
 869            mode,
 870            _phantom: Default::default(),
 871        };
 872
 873        this.subscribe_to_editor(window, cx);
 874        this
 875    }
 876
 877    fn handle_codegen_changed(
 878        &mut self,
 879        _: Entity<BufferCodegen>,
 880        cx: &mut Context<PromptEditor<BufferCodegen>>,
 881    ) {
 882        match self.codegen_status(cx) {
 883            CodegenStatus::Idle => {
 884                self.editor
 885                    .update(cx, |editor, _| editor.set_read_only(false));
 886            }
 887            CodegenStatus::Pending => {
 888                self.editor
 889                    .update(cx, |editor, _| editor.set_read_only(true));
 890            }
 891            CodegenStatus::Done => {
 892                self.edited_since_done = false;
 893                self.editor
 894                    .update(cx, |editor, _| editor.set_read_only(false));
 895            }
 896            CodegenStatus::Error(_error) => {
 897                self.edited_since_done = false;
 898                self.editor
 899                    .update(cx, |editor, _| editor.set_read_only(false));
 900            }
 901        }
 902    }
 903
 904    pub fn id(&self) -> InlineAssistId {
 905        match &self.mode {
 906            PromptEditorMode::Buffer { id, .. } => *id,
 907            PromptEditorMode::Terminal { .. } => unreachable!(),
 908        }
 909    }
 910
 911    pub fn codegen(&self) -> &Entity<BufferCodegen> {
 912        match &self.mode {
 913            PromptEditorMode::Buffer { codegen, .. } => codegen,
 914            PromptEditorMode::Terminal { .. } => unreachable!(),
 915        }
 916    }
 917
 918    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
 919        match &self.mode {
 920            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
 921            PromptEditorMode::Terminal { .. } => unreachable!(),
 922        }
 923    }
 924}
 925
 926#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 927pub struct TerminalInlineAssistId(pub usize);
 928
 929impl TerminalInlineAssistId {
 930    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
 931        let id = *self;
 932        self.0 += 1;
 933        id
 934    }
 935}
 936
 937impl PromptEditor<TerminalCodegen> {
 938    pub fn new_terminal(
 939        id: TerminalInlineAssistId,
 940        prompt_history: VecDeque<String>,
 941        prompt_buffer: Entity<MultiBuffer>,
 942        codegen: Entity<TerminalCodegen>,
 943        fs: Arc<dyn Fs>,
 944        context_store: Entity<ContextStore>,
 945        workspace: WeakEntity<Workspace>,
 946        thread_store: Option<WeakEntity<HistoryStore>>,
 947        prompt_store: Option<WeakEntity<PromptStore>>,
 948        window: &mut Window,
 949        cx: &mut Context<Self>,
 950    ) -> Self {
 951        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 952        let mode = PromptEditorMode::Terminal {
 953            id,
 954            codegen,
 955            height_in_lines: 1,
 956        };
 957
 958        let prompt_editor = cx.new(|cx| {
 959            let mut editor = Editor::new(
 960                EditorMode::AutoHeight {
 961                    min_lines: 1,
 962                    max_lines: Some(Self::MAX_LINES as usize),
 963                },
 964                prompt_buffer,
 965                None,
 966                window,
 967                cx,
 968            );
 969            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 970            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
 971            editor.set_context_menu_options(ContextMenuOptions {
 972                min_entries_visible: 12,
 973                max_entries_visible: 12,
 974                placement: None,
 975            });
 976            editor
 977        });
 978
 979        let prompt_editor_entity = prompt_editor.downgrade();
 980        prompt_editor.update(cx, |editor, _| {
 981            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 982                workspace.clone(),
 983                context_store.downgrade(),
 984                thread_store.clone(),
 985                prompt_store.clone(),
 986                prompt_editor_entity,
 987                None,
 988            ))));
 989        });
 990
 991        let context_picker_menu_handle = PopoverMenuHandle::default();
 992        let model_selector_menu_handle = PopoverMenuHandle::default();
 993
 994        let context_strip = cx.new(|cx| {
 995            ContextStrip::new(
 996                context_store.clone(),
 997                workspace.clone(),
 998                thread_store.clone(),
 999                prompt_store.clone(),
1000                context_picker_menu_handle.clone(),
1001                SuggestContextKind::Thread,
1002                ModelUsageContext::InlineAssistant,
1003                window,
1004                cx,
1005            )
1006        });
1007
1008        let context_strip_subscription =
1009            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1010
1011        let mut this = Self {
1012            editor: prompt_editor.clone(),
1013            context_store,
1014            context_strip,
1015            context_picker_menu_handle,
1016            model_selector: cx.new(|cx| {
1017                AgentModelSelector::new(
1018                    fs,
1019                    model_selector_menu_handle.clone(),
1020                    prompt_editor.focus_handle(cx),
1021                    ModelUsageContext::InlineAssistant,
1022                    window,
1023                    cx,
1024                )
1025            }),
1026            edited_since_done: false,
1027            prompt_history,
1028            prompt_history_ix: None,
1029            pending_prompt: String::new(),
1030            _codegen_subscription: codegen_subscription,
1031            editor_subscriptions: Vec::new(),
1032            _context_strip_subscription: context_strip_subscription,
1033            mode,
1034            show_rate_limit_notice: false,
1035            _phantom: Default::default(),
1036        };
1037        this.count_lines(cx);
1038        this.subscribe_to_editor(window, cx);
1039        this
1040    }
1041
1042    fn count_lines(&mut self, cx: &mut Context<Self>) {
1043        let height_in_lines = cmp::max(
1044            2, // Make the editor at least two lines tall, to account for padding and buttons.
1045            cmp::min(
1046                self.editor
1047                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1048                Self::MAX_LINES as u32,
1049            ),
1050        ) as u8;
1051
1052        match &mut self.mode {
1053            PromptEditorMode::Terminal {
1054                height_in_lines: current_height,
1055                ..
1056            } => {
1057                if height_in_lines != *current_height {
1058                    *current_height = height_in_lines;
1059                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1060                }
1061            }
1062            PromptEditorMode::Buffer { .. } => unreachable!(),
1063        }
1064    }
1065
1066    fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1067        match &self.codegen().read(cx).status {
1068            CodegenStatus::Idle => {
1069                self.editor
1070                    .update(cx, |editor, _| editor.set_read_only(false));
1071            }
1072            CodegenStatus::Pending => {
1073                self.editor
1074                    .update(cx, |editor, _| editor.set_read_only(true));
1075            }
1076            CodegenStatus::Done | CodegenStatus::Error(_) => {
1077                self.edited_since_done = false;
1078                self.editor
1079                    .update(cx, |editor, _| editor.set_read_only(false));
1080            }
1081        }
1082    }
1083
1084    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1085        match &self.mode {
1086            PromptEditorMode::Buffer { .. } => unreachable!(),
1087            PromptEditorMode::Terminal { codegen, .. } => codegen,
1088        }
1089    }
1090
1091    pub fn id(&self) -> TerminalInlineAssistId {
1092        match &self.mode {
1093            PromptEditorMode::Buffer { .. } => unreachable!(),
1094            PromptEditorMode::Terminal { id, .. } => *id,
1095        }
1096    }
1097}
1098
1099pub enum CodegenStatus {
1100    Idle,
1101    Pending,
1102    Done,
1103    Error(anyhow::Error),
1104}
1105
1106/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1107#[derive(Copy, Clone)]
1108pub enum CancelButtonState {
1109    Idle,
1110    Pending,
1111    Done,
1112    Error,
1113}
1114
1115impl Into<CancelButtonState> for &CodegenStatus {
1116    fn into(self) -> CancelButtonState {
1117        match self {
1118            CodegenStatus::Idle => CancelButtonState::Idle,
1119            CodegenStatus::Pending => CancelButtonState::Pending,
1120            CodegenStatus::Done => CancelButtonState::Done,
1121            CodegenStatus::Error(_) => CancelButtonState::Error,
1122        }
1123    }
1124}
1125
1126#[derive(Copy, Clone)]
1127pub enum GenerationMode {
1128    Generate,
1129    Transform,
1130}
1131
1132impl GenerationMode {
1133    fn start_label(self) -> &'static str {
1134        match self {
1135            GenerationMode::Generate => "Generate",
1136            GenerationMode::Transform => "Transform",
1137        }
1138    }
1139    fn tooltip_interrupt(self) -> &'static str {
1140        match self {
1141            GenerationMode::Generate => "Interrupt Generation",
1142            GenerationMode::Transform => "Interrupt Transform",
1143        }
1144    }
1145
1146    fn tooltip_restart(self) -> &'static str {
1147        match self {
1148            GenerationMode::Generate => "Restart Generation",
1149            GenerationMode::Transform => "Restart Transform",
1150        }
1151    }
1152
1153    fn tooltip_accept(self) -> &'static str {
1154        match self {
1155            GenerationMode::Generate => "Accept Generation",
1156            GenerationMode::Transform => "Accept Transform",
1157        }
1158    }
1159}