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