inline_prompt_editor.rs

   1use agent::HistoryStore;
   2use collections::{HashMap, VecDeque};
   3use editor::actions::Paste;
   4use editor::code_context_menus::CodeContextMenu;
   5use editor::display_map::{CreaseId, 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                cx.weak_entity(),
 233                self.mention_set.clone(),
 234                self.history_store.clone(),
 235                self.prompt_store.clone(),
 236                self.workspace.clone(),
 237            ))));
 238        });
 239    }
 240
 241    pub fn set_show_cursor_when_unfocused(
 242        &mut self,
 243        show_cursor_when_unfocused: bool,
 244        cx: &mut Context<Self>,
 245    ) {
 246        self.editor.update(cx, |editor, cx| {
 247            editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
 248        });
 249    }
 250
 251    pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 252        let prompt = self.prompt(cx);
 253        let existing_creases = self.editor.update(cx, |editor, cx| {
 254            extract_message_creases(editor, &self.mention_set, window, cx)
 255        });
 256        let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
 257        let mut creases = vec![];
 258        self.editor = cx.new(|cx| {
 259            let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
 260            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 261            editor.set_placeholder_text("Add a prompt…", window, cx);
 262            editor.set_text(prompt, window, cx);
 263            creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
 264
 265            if focus {
 266                window.focus(&editor.focus_handle(cx));
 267            }
 268            editor
 269        });
 270
 271        self.mention_set.update(cx, |mention_set, _cx| {
 272            debug_assert_eq!(
 273                creases.len(),
 274                mention_set.creases().len(),
 275                "Missing creases"
 276            );
 277
 278            let mentions = mention_set
 279                .clear()
 280                .zip(creases)
 281                .map(|((_, value), id)| (id, value))
 282                .collect::<HashMap<_, _>>();
 283            mention_set.set_mentions(mentions);
 284        });
 285
 286        self.assign_completion_provider(cx);
 287        self.subscribe_to_editor(window, cx);
 288    }
 289
 290    pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
 291        let action = match mode {
 292            PromptEditorMode::Buffer { codegen, .. } => {
 293                if codegen.read(cx).is_insertion {
 294                    "Generate"
 295                } else {
 296                    "Transform"
 297                }
 298            }
 299            PromptEditorMode::Terminal { .. } => "Generate",
 300        };
 301
 302        let agent_panel_keybinding =
 303            ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
 304                .map(|keybinding| format!("{keybinding} to chat"))
 305                .unwrap_or_default();
 306
 307        format!("{action}… ({agent_panel_keybinding} ― ↓↑ for history — @ to include context)")
 308    }
 309
 310    pub fn prompt(&self, cx: &App) -> String {
 311        self.editor.read(cx).text(cx)
 312    }
 313
 314    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 315        if inline_assistant_model_supports_images(cx)
 316            && let Some(task) =
 317                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
 318        {
 319            task.detach();
 320        }
 321    }
 322
 323    fn handle_prompt_editor_events(
 324        &mut self,
 325        editor: &Entity<Editor>,
 326        event: &EditorEvent,
 327        window: &mut Window,
 328        cx: &mut Context<Self>,
 329    ) {
 330        match event {
 331            EditorEvent::Edited { .. } => {
 332                let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 333
 334                self.mention_set
 335                    .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 336
 337                if let Some(workspace) = window.root::<Workspace>().flatten() {
 338                    workspace.update(cx, |workspace, cx| {
 339                        let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
 340
 341                        workspace
 342                            .client()
 343                            .telemetry()
 344                            .log_edit_event("inline assist", is_via_ssh);
 345                    });
 346                }
 347                let prompt = snapshot.text();
 348                if self
 349                    .prompt_history_ix
 350                    .is_none_or(|ix| self.prompt_history[ix] != prompt)
 351                {
 352                    self.prompt_history_ix.take();
 353                    self.pending_prompt = prompt;
 354                }
 355
 356                self.edited_since_done = true;
 357                cx.notify();
 358            }
 359            EditorEvent::Blurred => {
 360                if self.show_rate_limit_notice {
 361                    self.show_rate_limit_notice = false;
 362                    cx.notify();
 363                }
 364            }
 365            _ => {}
 366        }
 367    }
 368
 369    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 370        self.editor
 371            .read(cx)
 372            .context_menu()
 373            .borrow()
 374            .as_ref()
 375            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 376    }
 377
 378    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 379        self.editor.update(cx, |editor, cx| {
 380            let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 381                matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 382            });
 383
 384            let has_at_sign = {
 385                let snapshot = editor.display_snapshot(cx);
 386                let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 387                let offset = cursor.to_offset(&snapshot);
 388                if offset.0 > 0 {
 389                    snapshot
 390                        .buffer_snapshot()
 391                        .reversed_chars_at(offset)
 392                        .next()
 393                        .map(|sign| sign == '@')
 394                        .unwrap_or(false)
 395                } else {
 396                    false
 397                }
 398            };
 399
 400            if menu_is_open && has_at_sign {
 401                return;
 402            }
 403
 404            editor.insert("@", window, cx);
 405            editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 406        });
 407    }
 408
 409    fn cancel(
 410        &mut self,
 411        _: &editor::actions::Cancel,
 412        _window: &mut Window,
 413        cx: &mut Context<Self>,
 414    ) {
 415        match self.codegen_status(cx) {
 416            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 417                cx.emit(PromptEditorEvent::CancelRequested);
 418            }
 419            CodegenStatus::Pending => {
 420                cx.emit(PromptEditorEvent::StopRequested);
 421            }
 422        }
 423    }
 424
 425    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 426        match self.codegen_status(cx) {
 427            CodegenStatus::Idle => {
 428                cx.emit(PromptEditorEvent::StartRequested);
 429            }
 430            CodegenStatus::Pending => {}
 431            CodegenStatus::Done => {
 432                if self.edited_since_done {
 433                    cx.emit(PromptEditorEvent::StartRequested);
 434                } else {
 435                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 436                }
 437            }
 438            CodegenStatus::Error(_) => {
 439                cx.emit(PromptEditorEvent::StartRequested);
 440            }
 441        }
 442    }
 443
 444    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 445        if let Some(ix) = self.prompt_history_ix {
 446            if ix > 0 {
 447                self.prompt_history_ix = Some(ix - 1);
 448                let prompt = self.prompt_history[ix - 1].as_str();
 449                self.editor.update(cx, |editor, cx| {
 450                    editor.set_text(prompt, window, cx);
 451                    editor.move_to_beginning(&Default::default(), window, cx);
 452                });
 453            }
 454        } else if !self.prompt_history.is_empty() {
 455            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 456            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 457            self.editor.update(cx, |editor, cx| {
 458                editor.set_text(prompt, window, cx);
 459                editor.move_to_beginning(&Default::default(), window, cx);
 460            });
 461        }
 462    }
 463
 464    fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 465        if let Some(ix) = self.prompt_history_ix {
 466            if ix < self.prompt_history.len() - 1 {
 467                self.prompt_history_ix = Some(ix + 1);
 468                let prompt = self.prompt_history[ix + 1].as_str();
 469                self.editor.update(cx, |editor, cx| {
 470                    editor.set_text(prompt, window, cx);
 471                    editor.move_to_end(&Default::default(), window, cx)
 472                });
 473            } else {
 474                self.prompt_history_ix = None;
 475                let prompt = self.pending_prompt.as_str();
 476                self.editor.update(cx, |editor, cx| {
 477                    editor.set_text(prompt, window, cx);
 478                    editor.move_to_end(&Default::default(), window, cx)
 479                });
 480            }
 481        }
 482    }
 483
 484    fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
 485        let mode = match &self.mode {
 486            PromptEditorMode::Buffer { codegen, .. } => {
 487                let codegen = codegen.read(cx);
 488                if codegen.is_insertion {
 489                    GenerationMode::Generate
 490                } else {
 491                    GenerationMode::Transform
 492                }
 493            }
 494            PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
 495        };
 496
 497        let codegen_status = self.codegen_status(cx);
 498
 499        match codegen_status {
 500            CodegenStatus::Idle => {
 501                vec![
 502                    Button::new("start", mode.start_label())
 503                        .label_size(LabelSize::Small)
 504                        .icon(IconName::Return)
 505                        .icon_size(IconSize::XSmall)
 506                        .icon_color(Color::Muted)
 507                        .on_click(
 508                            cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
 509                        )
 510                        .into_any_element(),
 511                ]
 512            }
 513            CodegenStatus::Pending => vec![
 514                IconButton::new("stop", IconName::Stop)
 515                    .icon_color(Color::Error)
 516                    .shape(IconButtonShape::Square)
 517                    .tooltip(move |_window, cx| {
 518                        Tooltip::with_meta(
 519                            mode.tooltip_interrupt(),
 520                            Some(&menu::Cancel),
 521                            "Changes won't be discarded",
 522                            cx,
 523                        )
 524                    })
 525                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 526                    .into_any_element(),
 527            ],
 528            CodegenStatus::Done | CodegenStatus::Error(_) => {
 529                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 530                if has_error || self.edited_since_done {
 531                    vec![
 532                        IconButton::new("restart", IconName::RotateCw)
 533                            .icon_color(Color::Info)
 534                            .shape(IconButtonShape::Square)
 535                            .tooltip(move |_window, cx| {
 536                                Tooltip::with_meta(
 537                                    mode.tooltip_restart(),
 538                                    Some(&menu::Confirm),
 539                                    "Changes will be discarded",
 540                                    cx,
 541                                )
 542                            })
 543                            .on_click(cx.listener(|_, _, _, cx| {
 544                                cx.emit(PromptEditorEvent::StartRequested);
 545                            }))
 546                            .into_any_element(),
 547                    ]
 548                } else {
 549                    let accept = IconButton::new("accept", IconName::Check)
 550                        .icon_color(Color::Info)
 551                        .shape(IconButtonShape::Square)
 552                        .tooltip(move |_window, cx| {
 553                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
 554                        })
 555                        .on_click(cx.listener(|_, _, _, cx| {
 556                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 557                        }))
 558                        .into_any_element();
 559
 560                    match &self.mode {
 561                        PromptEditorMode::Terminal { .. } => vec![
 562                            accept,
 563                            IconButton::new("confirm", IconName::PlayFilled)
 564                                .icon_color(Color::Info)
 565                                .shape(IconButtonShape::Square)
 566                                .tooltip(|_window, cx| {
 567                                    Tooltip::for_action(
 568                                        "Execute Generated Command",
 569                                        &menu::SecondaryConfirm,
 570                                        cx,
 571                                    )
 572                                })
 573                                .on_click(cx.listener(|_, _, _, cx| {
 574                                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 575                                }))
 576                                .into_any_element(),
 577                        ],
 578                        PromptEditorMode::Buffer { .. } => vec![accept],
 579                    }
 580                }
 581            }
 582        }
 583    }
 584
 585    fn cycle_prev(
 586        &mut self,
 587        _: &CyclePreviousInlineAssist,
 588        _: &mut Window,
 589        cx: &mut Context<Self>,
 590    ) {
 591        match &self.mode {
 592            PromptEditorMode::Buffer { codegen, .. } => {
 593                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 594            }
 595            PromptEditorMode::Terminal { .. } => {
 596                // no cycle buttons in terminal mode
 597            }
 598        }
 599    }
 600
 601    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 602        match &self.mode {
 603            PromptEditorMode::Buffer { codegen, .. } => {
 604                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 605            }
 606            PromptEditorMode::Terminal { .. } => {
 607                // no cycle buttons in terminal mode
 608            }
 609        }
 610    }
 611
 612    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 613        IconButton::new("cancel", IconName::Close)
 614            .icon_color(Color::Muted)
 615            .shape(IconButtonShape::Square)
 616            .tooltip(Tooltip::text("Close Assistant"))
 617            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 618            .into_any_element()
 619    }
 620
 621    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
 622        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
 623
 624        let model_registry = LanguageModelRegistry::read_global(cx);
 625        let default_model = model_registry.default_model().map(|default| default.model);
 626        let alternative_models = model_registry.inline_alternative_models();
 627
 628        let get_model_name = |index: usize| -> String {
 629            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
 630
 631            match index {
 632                0 => default_model.as_ref().map_or_else(String::new, name),
 633                index if index <= alternative_models.len() => alternative_models
 634                    .get(index - 1)
 635                    .map_or_else(String::new, name),
 636                _ => String::new(),
 637            }
 638        };
 639
 640        let total_models = alternative_models.len() + 1;
 641
 642        if total_models <= 1 {
 643            return div().into_any_element();
 644        }
 645
 646        let current_index = codegen.active_alternative;
 647        let prev_index = (current_index + total_models - 1) % total_models;
 648        let next_index = (current_index + 1) % total_models;
 649
 650        let prev_model_name = get_model_name(prev_index);
 651        let next_model_name = get_model_name(next_index);
 652
 653        h_flex()
 654            .child(
 655                IconButton::new("previous", IconName::ChevronLeft)
 656                    .icon_color(Color::Muted)
 657                    .disabled(disabled || current_index == 0)
 658                    .shape(IconButtonShape::Square)
 659                    .tooltip({
 660                        let focus_handle = self.editor.focus_handle(cx);
 661                        move |_window, cx| {
 662                            cx.new(|cx| {
 663                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
 664                                    KeyBinding::for_action_in(
 665                                        &CyclePreviousInlineAssist,
 666                                        &focus_handle,
 667                                        cx,
 668                                    ),
 669                                );
 670                                if !disabled && current_index != 0 {
 671                                    tooltip = tooltip.meta(prev_model_name.clone());
 672                                }
 673                                tooltip
 674                            })
 675                            .into()
 676                        }
 677                    })
 678                    .on_click(cx.listener(|this, _, window, cx| {
 679                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
 680                    })),
 681            )
 682            .child(
 683                Label::new(format!(
 684                    "{}/{}",
 685                    codegen.active_alternative + 1,
 686                    codegen.alternative_count(cx)
 687                ))
 688                .size(LabelSize::Small)
 689                .color(if disabled {
 690                    Color::Disabled
 691                } else {
 692                    Color::Muted
 693                }),
 694            )
 695            .child(
 696                IconButton::new("next", IconName::ChevronRight)
 697                    .icon_color(Color::Muted)
 698                    .disabled(disabled || current_index == total_models - 1)
 699                    .shape(IconButtonShape::Square)
 700                    .tooltip({
 701                        let focus_handle = self.editor.focus_handle(cx);
 702                        move |_window, cx| {
 703                            cx.new(|cx| {
 704                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
 705                                    KeyBinding::for_action_in(
 706                                        &CycleNextInlineAssist,
 707                                        &focus_handle,
 708                                        cx,
 709                                    ),
 710                                );
 711                                if !disabled && current_index != total_models - 1 {
 712                                    tooltip = tooltip.meta(next_model_name.clone());
 713                                }
 714                                tooltip
 715                            })
 716                            .into()
 717                        }
 718                    })
 719                    .on_click(cx.listener(|this, _, window, cx| {
 720                        this.cycle_next(&CycleNextInlineAssist, window, cx)
 721                    })),
 722            )
 723            .into_any_element()
 724    }
 725
 726    fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 727        let colors = cx.theme().colors();
 728
 729        div()
 730            .key_context("InlineAssistEditor")
 731            .size_full()
 732            .p_2()
 733            .pl_1()
 734            .bg(colors.editor_background)
 735            .child({
 736                let settings = ThemeSettings::get_global(cx);
 737                let font_size = settings.buffer_font_size(cx);
 738                let line_height = font_size * 1.2;
 739
 740                let text_style = TextStyle {
 741                    color: colors.editor_foreground,
 742                    font_family: settings.buffer_font.family.clone(),
 743                    font_features: settings.buffer_font.features.clone(),
 744                    font_size: font_size.into(),
 745                    line_height: line_height.into(),
 746                    ..Default::default()
 747                };
 748
 749                EditorElement::new(
 750                    &self.editor,
 751                    EditorStyle {
 752                        background: colors.editor_background,
 753                        local_player: cx.theme().players().local(),
 754                        syntax: cx.theme().syntax().clone(),
 755                        text: text_style,
 756                        ..Default::default()
 757                    },
 758                )
 759            })
 760            .into_any_element()
 761    }
 762}
 763
 764pub enum PromptEditorMode {
 765    Buffer {
 766        id: InlineAssistId,
 767        codegen: Entity<BufferCodegen>,
 768        editor_margins: Arc<Mutex<EditorMargins>>,
 769    },
 770    Terminal {
 771        id: TerminalInlineAssistId,
 772        codegen: Entity<TerminalCodegen>,
 773        height_in_lines: u8,
 774    },
 775}
 776
 777pub enum PromptEditorEvent {
 778    StartRequested,
 779    StopRequested,
 780    ConfirmRequested { execute: bool },
 781    CancelRequested,
 782    Resized { height_in_lines: u8 },
 783}
 784
 785#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 786pub struct InlineAssistId(pub usize);
 787
 788impl InlineAssistId {
 789    pub fn post_inc(&mut self) -> InlineAssistId {
 790        let id = *self;
 791        self.0 += 1;
 792        id
 793    }
 794}
 795
 796struct PromptEditorCompletionProviderDelegate;
 797
 798fn inline_assistant_model_supports_images(cx: &App) -> bool {
 799    LanguageModelRegistry::read_global(cx)
 800        .inline_assistant_model()
 801        .map_or(false, |m| m.model.supports_images())
 802}
 803
 804impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
 805    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
 806        vec![
 807            PromptContextType::File,
 808            PromptContextType::Symbol,
 809            PromptContextType::Thread,
 810            PromptContextType::Fetch,
 811            PromptContextType::Rules,
 812        ]
 813    }
 814
 815    fn supports_images(&self, cx: &App) -> bool {
 816        inline_assistant_model_supports_images(cx)
 817    }
 818
 819    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
 820        Vec::new()
 821    }
 822
 823    fn confirm_command(&self, _cx: &mut App) {}
 824}
 825
 826impl PromptEditor<BufferCodegen> {
 827    pub fn new_buffer(
 828        id: InlineAssistId,
 829        editor_margins: Arc<Mutex<EditorMargins>>,
 830        prompt_history: VecDeque<String>,
 831        prompt_buffer: Entity<MultiBuffer>,
 832        codegen: Entity<BufferCodegen>,
 833        fs: Arc<dyn Fs>,
 834        history_store: Entity<HistoryStore>,
 835        prompt_store: Option<Entity<PromptStore>>,
 836        project: WeakEntity<Project>,
 837        workspace: WeakEntity<Workspace>,
 838        window: &mut Window,
 839        cx: &mut Context<PromptEditor<BufferCodegen>>,
 840    ) -> PromptEditor<BufferCodegen> {
 841        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 842        let mode = PromptEditorMode::Buffer {
 843            id,
 844            codegen,
 845            editor_margins,
 846        };
 847
 848        let prompt_editor = cx.new(|cx| {
 849            let mut editor = Editor::new(
 850                EditorMode::AutoHeight {
 851                    min_lines: 1,
 852                    max_lines: Some(Self::MAX_LINES as usize),
 853                },
 854                prompt_buffer,
 855                None,
 856                window,
 857                cx,
 858            );
 859            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 860            // Since the prompt editors for all inline assistants are linked,
 861            // always show the cursor (even when it isn't focused) because
 862            // typing in one will make what you typed appear in all of them.
 863            editor.set_show_cursor_when_unfocused(true, cx);
 864            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
 865            editor.set_context_menu_options(ContextMenuOptions {
 866                min_entries_visible: 12,
 867                max_entries_visible: 12,
 868                placement: None,
 869            });
 870
 871            editor
 872        });
 873
 874        let mention_set =
 875            cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
 876
 877        let model_selector_menu_handle = PopoverMenuHandle::default();
 878
 879        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
 880            editor: prompt_editor.clone(),
 881            mention_set,
 882            history_store,
 883            prompt_store,
 884            workspace,
 885            model_selector: cx.new(|cx| {
 886                AgentModelSelector::new(
 887                    fs,
 888                    model_selector_menu_handle,
 889                    prompt_editor.focus_handle(cx),
 890                    ModelUsageContext::InlineAssistant,
 891                    window,
 892                    cx,
 893                )
 894            }),
 895            edited_since_done: false,
 896            prompt_history,
 897            prompt_history_ix: None,
 898            pending_prompt: String::new(),
 899            _codegen_subscription: codegen_subscription,
 900            editor_subscriptions: Vec::new(),
 901            show_rate_limit_notice: false,
 902            mode,
 903            _phantom: Default::default(),
 904        };
 905
 906        this.assign_completion_provider(cx);
 907        this.subscribe_to_editor(window, cx);
 908        this
 909    }
 910
 911    fn handle_codegen_changed(
 912        &mut self,
 913        _: Entity<BufferCodegen>,
 914        cx: &mut Context<PromptEditor<BufferCodegen>>,
 915    ) {
 916        match self.codegen_status(cx) {
 917            CodegenStatus::Idle => {
 918                self.editor
 919                    .update(cx, |editor, _| editor.set_read_only(false));
 920            }
 921            CodegenStatus::Pending => {
 922                self.editor
 923                    .update(cx, |editor, _| editor.set_read_only(true));
 924            }
 925            CodegenStatus::Done => {
 926                self.edited_since_done = false;
 927                self.editor
 928                    .update(cx, |editor, _| editor.set_read_only(false));
 929            }
 930            CodegenStatus::Error(_error) => {
 931                self.edited_since_done = false;
 932                self.editor
 933                    .update(cx, |editor, _| editor.set_read_only(false));
 934            }
 935        }
 936    }
 937
 938    pub fn id(&self) -> InlineAssistId {
 939        match &self.mode {
 940            PromptEditorMode::Buffer { id, .. } => *id,
 941            PromptEditorMode::Terminal { .. } => unreachable!(),
 942        }
 943    }
 944
 945    pub fn codegen(&self) -> &Entity<BufferCodegen> {
 946        match &self.mode {
 947            PromptEditorMode::Buffer { codegen, .. } => codegen,
 948            PromptEditorMode::Terminal { .. } => unreachable!(),
 949        }
 950    }
 951
 952    pub fn mention_set(&self) -> &Entity<MentionSet> {
 953        &self.mention_set
 954    }
 955
 956    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
 957        match &self.mode {
 958            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
 959            PromptEditorMode::Terminal { .. } => unreachable!(),
 960        }
 961    }
 962}
 963
 964#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 965pub struct TerminalInlineAssistId(pub usize);
 966
 967impl TerminalInlineAssistId {
 968    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
 969        let id = *self;
 970        self.0 += 1;
 971        id
 972    }
 973}
 974
 975impl PromptEditor<TerminalCodegen> {
 976    pub fn new_terminal(
 977        id: TerminalInlineAssistId,
 978        prompt_history: VecDeque<String>,
 979        prompt_buffer: Entity<MultiBuffer>,
 980        codegen: Entity<TerminalCodegen>,
 981        fs: Arc<dyn Fs>,
 982        history_store: Entity<HistoryStore>,
 983        prompt_store: Option<Entity<PromptStore>>,
 984        project: WeakEntity<Project>,
 985        workspace: WeakEntity<Workspace>,
 986        window: &mut Window,
 987        cx: &mut Context<Self>,
 988    ) -> Self {
 989        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 990        let mode = PromptEditorMode::Terminal {
 991            id,
 992            codegen,
 993            height_in_lines: 1,
 994        };
 995
 996        let prompt_editor = cx.new(|cx| {
 997            let mut editor = Editor::new(
 998                EditorMode::AutoHeight {
 999                    min_lines: 1,
1000                    max_lines: Some(Self::MAX_LINES as usize),
1001                },
1002                prompt_buffer,
1003                None,
1004                window,
1005                cx,
1006            );
1007            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1008            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1009            editor.set_context_menu_options(ContextMenuOptions {
1010                min_entries_visible: 12,
1011                max_entries_visible: 12,
1012                placement: None,
1013            });
1014            editor
1015        });
1016
1017        let mention_set =
1018            cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
1019
1020        let model_selector_menu_handle = PopoverMenuHandle::default();
1021
1022        let mut this = Self {
1023            editor: prompt_editor.clone(),
1024            mention_set,
1025            history_store,
1026            prompt_store,
1027            workspace,
1028            model_selector: cx.new(|cx| {
1029                AgentModelSelector::new(
1030                    fs,
1031                    model_selector_menu_handle.clone(),
1032                    prompt_editor.focus_handle(cx),
1033                    ModelUsageContext::InlineAssistant,
1034                    window,
1035                    cx,
1036                )
1037            }),
1038            edited_since_done: false,
1039            prompt_history,
1040            prompt_history_ix: None,
1041            pending_prompt: String::new(),
1042            _codegen_subscription: codegen_subscription,
1043            editor_subscriptions: Vec::new(),
1044            mode,
1045            show_rate_limit_notice: false,
1046            _phantom: Default::default(),
1047        };
1048        this.count_lines(cx);
1049        this.assign_completion_provider(cx);
1050        this.subscribe_to_editor(window, cx);
1051        this
1052    }
1053
1054    fn count_lines(&mut self, cx: &mut Context<Self>) {
1055        let height_in_lines = cmp::max(
1056            2, // Make the editor at least two lines tall, to account for padding and buttons.
1057            cmp::min(
1058                self.editor
1059                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1060                Self::MAX_LINES as u32,
1061            ),
1062        ) as u8;
1063
1064        match &mut self.mode {
1065            PromptEditorMode::Terminal {
1066                height_in_lines: current_height,
1067                ..
1068            } => {
1069                if height_in_lines != *current_height {
1070                    *current_height = height_in_lines;
1071                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1072                }
1073            }
1074            PromptEditorMode::Buffer { .. } => unreachable!(),
1075        }
1076    }
1077
1078    fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1079        match &self.codegen().read(cx).status {
1080            CodegenStatus::Idle => {
1081                self.editor
1082                    .update(cx, |editor, _| editor.set_read_only(false));
1083            }
1084            CodegenStatus::Pending => {
1085                self.editor
1086                    .update(cx, |editor, _| editor.set_read_only(true));
1087            }
1088            CodegenStatus::Done | CodegenStatus::Error(_) => {
1089                self.edited_since_done = false;
1090                self.editor
1091                    .update(cx, |editor, _| editor.set_read_only(false));
1092            }
1093        }
1094    }
1095
1096    pub fn mention_set(&self) -> &Entity<MentionSet> {
1097        &self.mention_set
1098    }
1099
1100    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1101        match &self.mode {
1102            PromptEditorMode::Buffer { .. } => unreachable!(),
1103            PromptEditorMode::Terminal { codegen, .. } => codegen,
1104        }
1105    }
1106
1107    pub fn id(&self) -> TerminalInlineAssistId {
1108        match &self.mode {
1109            PromptEditorMode::Buffer { .. } => unreachable!(),
1110            PromptEditorMode::Terminal { id, .. } => *id,
1111        }
1112    }
1113}
1114
1115pub enum CodegenStatus {
1116    Idle,
1117    Pending,
1118    Done,
1119    Error(anyhow::Error),
1120}
1121
1122/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1123#[derive(Copy, Clone)]
1124pub enum CancelButtonState {
1125    Idle,
1126    Pending,
1127    Done,
1128    Error,
1129}
1130
1131impl Into<CancelButtonState> for &CodegenStatus {
1132    fn into(self) -> CancelButtonState {
1133        match self {
1134            CodegenStatus::Idle => CancelButtonState::Idle,
1135            CodegenStatus::Pending => CancelButtonState::Pending,
1136            CodegenStatus::Done => CancelButtonState::Done,
1137            CodegenStatus::Error(_) => CancelButtonState::Error,
1138        }
1139    }
1140}
1141
1142#[derive(Copy, Clone)]
1143pub enum GenerationMode {
1144    Generate,
1145    Transform,
1146}
1147
1148impl GenerationMode {
1149    fn start_label(self) -> &'static str {
1150        match self {
1151            GenerationMode::Generate => "Generate",
1152            GenerationMode::Transform => "Transform",
1153        }
1154    }
1155    fn tooltip_interrupt(self) -> &'static str {
1156        match self {
1157            GenerationMode::Generate => "Interrupt Generation",
1158            GenerationMode::Transform => "Interrupt Transform",
1159        }
1160    }
1161
1162    fn tooltip_restart(self) -> &'static str {
1163        match self {
1164            GenerationMode::Generate => "Restart Generation",
1165            GenerationMode::Transform => "Restart Transform",
1166        }
1167    }
1168
1169    fn tooltip_accept(self) -> &'static str {
1170        match self {
1171            GenerationMode::Generate => "Accept Generation",
1172            GenerationMode::Transform => "Accept Transform",
1173        }
1174    }
1175}
1176
1177/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1178#[derive(Clone, Debug)]
1179struct MessageCrease {
1180    range: Range<MultiBufferOffset>,
1181    icon_path: SharedString,
1182    label: SharedString,
1183}
1184
1185fn extract_message_creases(
1186    editor: &mut Editor,
1187    mention_set: &Entity<MentionSet>,
1188    window: &mut Window,
1189    cx: &mut Context<'_, Editor>,
1190) -> Vec<MessageCrease> {
1191    let creases = mention_set.read(cx).creases();
1192    let snapshot = editor.snapshot(window, cx);
1193    snapshot
1194        .crease_snapshot
1195        .creases()
1196        .filter(|(id, _)| creases.contains(id))
1197        .filter_map(|(_, crease)| {
1198            let metadata = crease.metadata()?.clone();
1199            Some(MessageCrease {
1200                range: crease.range().to_offset(snapshot.buffer()),
1201                label: metadata.label,
1202                icon_path: metadata.icon_path,
1203            })
1204        })
1205        .collect()
1206}
1207
1208fn insert_message_creases(
1209    editor: &mut Editor,
1210    message_creases: &[MessageCrease],
1211    window: &mut Window,
1212    cx: &mut Context<'_, Editor>,
1213) -> Vec<CreaseId> {
1214    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1215    let creases = message_creases
1216        .iter()
1217        .map(|crease| {
1218            let start = buffer_snapshot.anchor_after(crease.range.start);
1219            let end = buffer_snapshot.anchor_before(crease.range.end);
1220            crease_for_mention(
1221                crease.label.clone(),
1222                crease.icon_path.clone(),
1223                start..end,
1224                cx.weak_entity(),
1225            )
1226        })
1227        .collect::<Vec<_>>();
1228    let ids = editor.insert_creases(creases.clone(), cx);
1229    editor.fold_creases(creases, false, window, cx);
1230    ids
1231}