inline_prompt_editor.rs

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