inline_prompt_editor.rs

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