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 feature_flags::{FeatureFlagAppExt, InlineAssistantUseToolFeatureFlag};
12use fs::Fs;
13use gpui::{
14 AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
15 Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,
16};
17use language_model::{LanguageModel, LanguageModelRegistry};
18use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
19use parking_lot::Mutex;
20use project::Project;
21use prompt_store::PromptStore;
22use settings::Settings;
23use std::cmp;
24use std::ops::Range;
25use std::rc::Rc;
26use std::sync::Arc;
27use theme::ThemeSettings;
28use ui::utils::WithRemSize;
29use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
30use uuid::Uuid;
31use workspace::notifications::NotificationId;
32use workspace::{Toast, Workspace};
33use zed_actions::agent::ToggleModelSelector;
34
35use crate::agent_model_selector::AgentModelSelector;
36use crate::buffer_codegen::{BufferCodegen, CodegenAlternative};
37use crate::completion_provider::{
38 PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
39};
40use crate::mention_set::paste_images_as_context;
41use crate::mention_set::{MentionSet, crease_for_mention};
42use crate::terminal_codegen::TerminalCodegen;
43use crate::{
44 CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
45};
46
47actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
48
49enum CompletionState {
50 Pending,
51 Generated { completion_text: Option<String> },
52 Rated,
53}
54
55struct SessionState {
56 session_id: Uuid,
57 completion: CompletionState,
58}
59
60pub struct PromptEditor<T> {
61 pub editor: Entity<Editor>,
62 mode: PromptEditorMode,
63 mention_set: Entity<MentionSet>,
64 history_store: Entity<HistoryStore>,
65 prompt_store: Option<Entity<PromptStore>>,
66 workspace: WeakEntity<Workspace>,
67 model_selector: Entity<AgentModelSelector>,
68 edited_since_done: bool,
69 prompt_history: VecDeque<String>,
70 prompt_history_ix: Option<usize>,
71 pending_prompt: String,
72 _codegen_subscription: Subscription,
73 editor_subscriptions: Vec<Subscription>,
74 show_rate_limit_notice: bool,
75 session_state: SessionState,
76 _phantom: std::marker::PhantomData<T>,
77}
78
79impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
80
81impl<T: 'static> Render for PromptEditor<T> {
82 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
83 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
84 let mut buttons = Vec::new();
85
86 const RIGHT_PADDING: Pixels = px(9.);
87
88 let (left_gutter_width, right_padding, explanation) = match &self.mode {
89 PromptEditorMode::Buffer {
90 id: _,
91 codegen,
92 editor_margins,
93 } => {
94 let codegen = codegen.read(cx);
95
96 if codegen.alternative_count(cx) > 1 {
97 buttons.push(self.render_cycle_controls(codegen, cx));
98 }
99
100 let editor_margins = editor_margins.lock();
101 let gutter = editor_margins.gutter;
102
103 let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
104 let right_padding = editor_margins.right + RIGHT_PADDING;
105
106 let active_alternative = codegen.active_alternative().read(cx);
107 let explanation = active_alternative
108 .description
109 .clone()
110 .or_else(|| active_alternative.failure.clone());
111
112 (left_gutter_width, right_padding, explanation)
113 }
114 PromptEditorMode::Terminal { .. } => {
115 // Give the equivalent of the same left-padding that we're using on the right
116 (Pixels::from(40.0), Pixels::from(24.), None)
117 }
118 };
119
120 let bottom_padding = match &self.mode {
121 PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
122 PromptEditorMode::Terminal { .. } => rems_from_px(4.0),
123 };
124
125 buttons.extend(self.render_buttons(window, cx));
126
127 let menu_visible = self.is_completions_menu_visible(cx);
128 let add_context_button = IconButton::new("add-context", IconName::AtSign)
129 .icon_size(IconSize::Small)
130 .icon_color(Color::Muted)
131 .when(!menu_visible, |this| {
132 this.tooltip(move |_window, cx| {
133 Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
134 })
135 })
136 .on_click(cx.listener(move |this, _, window, cx| {
137 this.trigger_completion_menu(window, cx);
138 }));
139
140 let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx));
141
142 if let Some(explanation) = &explanation {
143 markdown.update(cx, |markdown, cx| {
144 markdown.reset(SharedString::from(explanation), cx);
145 });
146 }
147
148 let explanation_label = self
149 .render_markdown(markdown, markdown_style(window, cx))
150 .into_any_element();
151
152 v_flex()
153 .key_context("InlineAssistant")
154 .capture_action(cx.listener(Self::paste))
155 .block_mouse_except_scroll()
156 .size_full()
157 .pt_0p5()
158 .pb(bottom_padding)
159 .pr(right_padding)
160 .gap_0p5()
161 .justify_center()
162 .border_y_1()
163 .border_color(cx.theme().colors().border)
164 .bg(cx.theme().colors().editor_background)
165 .child(
166 h_flex()
167 .on_action(cx.listener(Self::confirm))
168 .on_action(cx.listener(Self::cancel))
169 .on_action(cx.listener(Self::move_up))
170 .on_action(cx.listener(Self::move_down))
171 .on_action(cx.listener(Self::thumbs_up))
172 .on_action(cx.listener(Self::thumbs_down))
173 .capture_action(cx.listener(Self::cycle_prev))
174 .capture_action(cx.listener(Self::cycle_next))
175 .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
176 this.model_selector
177 .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
178 }))
179 .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
180 this.model_selector.update(cx, |model_selector, cx| {
181 model_selector.cycle_favorite_models(window, cx);
182 });
183 }))
184 .child(
185 WithRemSize::new(ui_font_size)
186 .h_full()
187 .w(left_gutter_width)
188 .flex()
189 .flex_row()
190 .flex_shrink_0()
191 .items_center()
192 .justify_center()
193 .gap_1()
194 .child(self.render_close_button(cx))
195 .map(|el| {
196 let CodegenStatus::Error(error) = self.codegen_status(cx) else {
197 return el;
198 };
199
200 let error_message = SharedString::from(error.to_string());
201 el.child(
202 div()
203 .id("error")
204 .tooltip(Tooltip::text(error_message))
205 .child(
206 Icon::new(IconName::XCircle)
207 .size(IconSize::Small)
208 .color(Color::Error),
209 ),
210 )
211 }),
212 )
213 .child(
214 h_flex()
215 .w_full()
216 .justify_between()
217 .child(div().flex_1().child(self.render_editor(window, cx)))
218 .child(
219 WithRemSize::new(ui_font_size)
220 .flex()
221 .flex_row()
222 .items_center()
223 .gap_1()
224 .child(add_context_button)
225 .child(self.model_selector.clone())
226 .children(buttons),
227 ),
228 ),
229 )
230 .when_some(explanation, |this, _| {
231 this.child(
232 h_flex()
233 .size_full()
234 .justify_center()
235 .child(div().w(left_gutter_width + px(6.)))
236 .child(
237 div()
238 .size_full()
239 .min_w_0()
240 .pt(rems_from_px(3.))
241 .pl_0p5()
242 .flex_1()
243 .border_t_1()
244 .border_color(cx.theme().colors().border_variant)
245 .child(explanation_label),
246 ),
247 )
248 })
249 }
250}
251
252fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
253 let theme_settings = ThemeSettings::get_global(cx);
254 let colors = cx.theme().colors();
255 let mut text_style = window.text_style();
256
257 text_style.refine(&TextStyleRefinement {
258 font_family: Some(theme_settings.ui_font.family.clone()),
259 color: Some(colors.text),
260 ..Default::default()
261 });
262
263 MarkdownStyle {
264 base_text_style: text_style.clone(),
265 syntax: cx.theme().syntax().clone(),
266 selection_background_color: colors.element_selection_background,
267 heading_level_styles: Some(HeadingLevelStyles {
268 h1: Some(TextStyleRefinement {
269 font_size: Some(rems(1.15).into()),
270 ..Default::default()
271 }),
272 h2: Some(TextStyleRefinement {
273 font_size: Some(rems(1.1).into()),
274 ..Default::default()
275 }),
276 h3: Some(TextStyleRefinement {
277 font_size: Some(rems(1.05).into()),
278 ..Default::default()
279 }),
280 h4: Some(TextStyleRefinement {
281 font_size: Some(rems(1.).into()),
282 ..Default::default()
283 }),
284 h5: Some(TextStyleRefinement {
285 font_size: Some(rems(0.95).into()),
286 ..Default::default()
287 }),
288 h6: Some(TextStyleRefinement {
289 font_size: Some(rems(0.875).into()),
290 ..Default::default()
291 }),
292 }),
293 inline_code: TextStyleRefinement {
294 font_family: Some(theme_settings.buffer_font.family.clone()),
295 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
296 font_features: Some(theme_settings.buffer_font.features.clone()),
297 background_color: Some(colors.editor_foreground.opacity(0.08)),
298 ..Default::default()
299 },
300 ..Default::default()
301 }
302}
303
304impl<T: 'static> Focusable for PromptEditor<T> {
305 fn focus_handle(&self, cx: &App) -> FocusHandle {
306 self.editor.focus_handle(cx)
307 }
308}
309
310impl<T: 'static> PromptEditor<T> {
311 const MAX_LINES: u8 = 8;
312
313 fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
314 match &self.mode {
315 PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
316 PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
317 }
318 }
319
320 fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
321 self.editor_subscriptions.clear();
322 self.editor_subscriptions.push(cx.subscribe_in(
323 &self.editor,
324 window,
325 Self::handle_prompt_editor_events,
326 ));
327 }
328
329 fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
330 self.editor.update(cx, |editor, cx| {
331 editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
332 PromptEditorCompletionProviderDelegate,
333 cx.weak_entity(),
334 self.mention_set.clone(),
335 self.history_store.clone(),
336 self.prompt_store.clone(),
337 self.workspace.clone(),
338 ))));
339 });
340 }
341
342 pub fn set_show_cursor_when_unfocused(
343 &mut self,
344 show_cursor_when_unfocused: bool,
345 cx: &mut Context<Self>,
346 ) {
347 self.editor.update(cx, |editor, cx| {
348 editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
349 });
350 }
351
352 pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
353 let prompt = self.prompt(cx);
354 let existing_creases = self.editor.update(cx, |editor, cx| {
355 extract_message_creases(editor, &self.mention_set, window, cx)
356 });
357 let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
358 let mut creases = vec![];
359 self.editor = cx.new(|cx| {
360 let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
361 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
362 editor.set_placeholder_text("Add a prompt…", window, cx);
363 editor.set_text(prompt, window, cx);
364 creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
365
366 if focus {
367 window.focus(&editor.focus_handle(cx), cx);
368 }
369 editor
370 });
371
372 self.mention_set.update(cx, |mention_set, _cx| {
373 debug_assert_eq!(
374 creases.len(),
375 mention_set.creases().len(),
376 "Missing creases"
377 );
378
379 let mentions = mention_set
380 .clear()
381 .zip(creases)
382 .map(|((_, value), id)| (id, value))
383 .collect::<HashMap<_, _>>();
384 mention_set.set_mentions(mentions);
385 });
386
387 self.assign_completion_provider(cx);
388 self.subscribe_to_editor(window, cx);
389 }
390
391 pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
392 let action = match mode {
393 PromptEditorMode::Buffer { codegen, .. } => {
394 if codegen.read(cx).is_insertion {
395 "Generate"
396 } else {
397 "Transform"
398 }
399 }
400 PromptEditorMode::Terminal { .. } => "Generate",
401 };
402
403 let agent_panel_keybinding =
404 ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
405 .map(|keybinding| format!("{keybinding} to chat"))
406 .unwrap_or_default();
407
408 format!("{action}… ({agent_panel_keybinding} ― ↓↑ for history — @ to include context)")
409 }
410
411 pub fn prompt(&self, cx: &App) -> String {
412 self.editor.read(cx).text(cx)
413 }
414
415 fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
416 if inline_assistant_model_supports_images(cx)
417 && let Some(task) =
418 paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
419 {
420 task.detach();
421 }
422 }
423
424 fn handle_prompt_editor_events(
425 &mut self,
426 editor: &Entity<Editor>,
427 event: &EditorEvent,
428 window: &mut Window,
429 cx: &mut Context<Self>,
430 ) {
431 match event {
432 EditorEvent::Edited { .. } => {
433 let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
434
435 self.mention_set
436 .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
437
438 if let Some(workspace) = window.root::<Workspace>().flatten() {
439 workspace.update(cx, |workspace, cx| {
440 let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
441
442 workspace
443 .client()
444 .telemetry()
445 .log_edit_event("inline assist", is_via_ssh);
446 });
447 }
448 let prompt = snapshot.text();
449 if self
450 .prompt_history_ix
451 .is_none_or(|ix| self.prompt_history[ix] != prompt)
452 {
453 self.prompt_history_ix.take();
454 self.pending_prompt = prompt;
455 }
456
457 self.edited_since_done = true;
458 self.session_state.completion = CompletionState::Pending;
459 cx.notify();
460 }
461 EditorEvent::Blurred => {
462 if self.show_rate_limit_notice {
463 self.show_rate_limit_notice = false;
464 cx.notify();
465 }
466 }
467 _ => {}
468 }
469 }
470
471 pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
472 self.editor
473 .read(cx)
474 .context_menu()
475 .borrow()
476 .as_ref()
477 .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
478 }
479
480 pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
481 self.editor.update(cx, |editor, cx| {
482 let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
483 matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
484 });
485
486 let has_at_sign = {
487 let snapshot = editor.display_snapshot(cx);
488 let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
489 let offset = cursor.to_offset(&snapshot);
490 if offset.0 > 0 {
491 snapshot
492 .buffer_snapshot()
493 .reversed_chars_at(offset)
494 .next()
495 .map(|sign| sign == '@')
496 .unwrap_or(false)
497 } else {
498 false
499 }
500 };
501
502 if menu_is_open && has_at_sign {
503 return;
504 }
505
506 editor.insert("@", window, cx);
507 editor.show_completions(&editor::actions::ShowCompletions, window, cx);
508 });
509 }
510
511 fn cancel(
512 &mut self,
513 _: &editor::actions::Cancel,
514 _window: &mut Window,
515 cx: &mut Context<Self>,
516 ) {
517 match self.codegen_status(cx) {
518 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
519 cx.emit(PromptEditorEvent::CancelRequested);
520 }
521 CodegenStatus::Pending => {
522 cx.emit(PromptEditorEvent::StopRequested);
523 }
524 }
525 }
526
527 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
528 match self.codegen_status(cx) {
529 CodegenStatus::Idle => {
530 self.fire_started_telemetry(cx);
531 cx.emit(PromptEditorEvent::StartRequested);
532 }
533 CodegenStatus::Pending => {}
534 CodegenStatus::Done => {
535 if self.edited_since_done {
536 self.fire_started_telemetry(cx);
537 cx.emit(PromptEditorEvent::StartRequested);
538 } else {
539 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
540 }
541 }
542 CodegenStatus::Error(_) => {
543 self.fire_started_telemetry(cx);
544 cx.emit(PromptEditorEvent::StartRequested);
545 }
546 }
547 }
548
549 fn fire_started_telemetry(&self, cx: &Context<Self>) {
550 let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() else {
551 return;
552 };
553
554 let model_telemetry_id = model.model.telemetry_id();
555 let model_provider_id = model.provider.id().to_string();
556
557 let (kind, language_name) = match &self.mode {
558 PromptEditorMode::Buffer { codegen, .. } => {
559 let codegen = codegen.read(cx);
560 (
561 "inline",
562 codegen.language_name(cx).map(|name| name.to_string()),
563 )
564 }
565 PromptEditorMode::Terminal { .. } => ("inline_terminal", None),
566 };
567
568 telemetry::event!(
569 "Assistant Started",
570 session_id = self.session_state.session_id.to_string(),
571 kind = kind,
572 phase = "started",
573 model = model_telemetry_id,
574 model_provider = model_provider_id,
575 language_name = language_name,
576 );
577 }
578
579 fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context<Self>) {
580 match &self.session_state.completion {
581 CompletionState::Pending => {
582 self.toast("Can't rate, still generating...", None, cx);
583 return;
584 }
585 CompletionState::Rated => {
586 self.toast(
587 "Already rated this completion",
588 Some(self.session_state.session_id),
589 cx,
590 );
591 return;
592 }
593 CompletionState::Generated { completion_text } => {
594 let model_info = self.model_selector.read(cx).active_model(cx);
595 let (model_id, use_streaming_tools) = {
596 let Some(configured_model) = model_info else {
597 self.toast("No configured model", None, cx);
598 return;
599 };
600 (
601 configured_model.model.telemetry_id(),
602 CodegenAlternative::use_streaming_tools(
603 configured_model.model.as_ref(),
604 cx,
605 ),
606 )
607 };
608
609 let selected_text = match &self.mode {
610 PromptEditorMode::Buffer { codegen, .. } => {
611 codegen.read(cx).selected_text(cx).map(|s| s.to_string())
612 }
613 PromptEditorMode::Terminal { .. } => None,
614 };
615
616 let prompt = self.editor.read(cx).text(cx);
617
618 let kind = match &self.mode {
619 PromptEditorMode::Buffer { .. } => "inline",
620 PromptEditorMode::Terminal { .. } => "inline_terminal",
621 };
622
623 telemetry::event!(
624 "Inline Assistant Rated",
625 rating = "positive",
626 session_id = self.session_state.session_id.to_string(),
627 kind = kind,
628 model = model_id,
629 prompt = prompt,
630 completion = completion_text,
631 selected_text = selected_text,
632 use_streaming_tools
633 );
634
635 self.session_state.completion = CompletionState::Rated;
636
637 cx.notify();
638 }
639 }
640 }
641
642 fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context<Self>) {
643 match &self.session_state.completion {
644 CompletionState::Pending => {
645 self.toast("Can't rate, still generating...", None, cx);
646 return;
647 }
648 CompletionState::Rated => {
649 self.toast(
650 "Already rated this completion",
651 Some(self.session_state.session_id),
652 cx,
653 );
654 return;
655 }
656 CompletionState::Generated { completion_text } => {
657 let model_info = self.model_selector.read(cx).active_model(cx);
658 let (model_telemetry_id, use_streaming_tools) = {
659 let Some(configured_model) = model_info else {
660 self.toast("No configured model", None, cx);
661 return;
662 };
663 (
664 configured_model.model.telemetry_id(),
665 CodegenAlternative::use_streaming_tools(
666 configured_model.model.as_ref(),
667 cx,
668 ),
669 )
670 };
671
672 let selected_text = match &self.mode {
673 PromptEditorMode::Buffer { codegen, .. } => {
674 codegen.read(cx).selected_text(cx).map(|s| s.to_string())
675 }
676 PromptEditorMode::Terminal { .. } => None,
677 };
678
679 let prompt = self.editor.read(cx).text(cx);
680
681 let kind = match &self.mode {
682 PromptEditorMode::Buffer { .. } => "inline",
683 PromptEditorMode::Terminal { .. } => "inline_terminal",
684 };
685
686 telemetry::event!(
687 "Inline Assistant Rated",
688 rating = "negative",
689 session_id = self.session_state.session_id.to_string(),
690 kind = kind,
691 model = model_telemetry_id,
692 prompt = prompt,
693 completion = completion_text,
694 selected_text = selected_text,
695 use_streaming_tools
696 );
697
698 self.session_state.completion = CompletionState::Rated;
699
700 cx.notify();
701 }
702 }
703 }
704
705 fn toast(&mut self, msg: &str, uuid: Option<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
706 self.workspace
707 .update(cx, |workspace, cx| {
708 enum InlinePromptRating {}
709 workspace.show_toast(
710 {
711 let mut toast = Toast::new(
712 NotificationId::unique::<InlinePromptRating>(),
713 msg.to_string(),
714 )
715 .autohide();
716
717 if let Some(uuid) = uuid {
718 toast = toast.on_click("Click to copy rating ID", move |_, cx| {
719 cx.write_to_clipboard(ClipboardItem::new_string(uuid.to_string()));
720 });
721 };
722
723 toast
724 },
725 cx,
726 );
727 })
728 .ok();
729 }
730
731 fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
732 if let Some(ix) = self.prompt_history_ix {
733 if ix > 0 {
734 self.prompt_history_ix = Some(ix - 1);
735 let prompt = self.prompt_history[ix - 1].as_str();
736 self.editor.update(cx, |editor, cx| {
737 editor.set_text(prompt, window, cx);
738 editor.move_to_beginning(&Default::default(), window, cx);
739 });
740 }
741 } else if !self.prompt_history.is_empty() {
742 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
743 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
744 self.editor.update(cx, |editor, cx| {
745 editor.set_text(prompt, window, cx);
746 editor.move_to_beginning(&Default::default(), window, cx);
747 });
748 }
749 }
750
751 fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
752 if let Some(ix) = self.prompt_history_ix {
753 if ix < self.prompt_history.len() - 1 {
754 self.prompt_history_ix = Some(ix + 1);
755 let prompt = self.prompt_history[ix + 1].as_str();
756 self.editor.update(cx, |editor, cx| {
757 editor.set_text(prompt, window, cx);
758 editor.move_to_end(&Default::default(), window, cx)
759 });
760 } else {
761 self.prompt_history_ix = None;
762 let prompt = self.pending_prompt.as_str();
763 self.editor.update(cx, |editor, cx| {
764 editor.set_text(prompt, window, cx);
765 editor.move_to_end(&Default::default(), window, cx)
766 });
767 }
768 }
769 }
770
771 fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
772 let mode = match &self.mode {
773 PromptEditorMode::Buffer { codegen, .. } => {
774 let codegen = codegen.read(cx);
775 if codegen.is_insertion {
776 GenerationMode::Generate
777 } else {
778 GenerationMode::Transform
779 }
780 }
781 PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
782 };
783
784 let codegen_status = self.codegen_status(cx);
785
786 match codegen_status {
787 CodegenStatus::Idle => {
788 vec![
789 Button::new("start", mode.start_label())
790 .label_size(LabelSize::Small)
791 .icon(IconName::Return)
792 .icon_size(IconSize::XSmall)
793 .icon_color(Color::Muted)
794 .on_click(
795 cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
796 )
797 .into_any_element(),
798 ]
799 }
800 CodegenStatus::Pending => vec![
801 IconButton::new("stop", IconName::Stop)
802 .icon_color(Color::Error)
803 .shape(IconButtonShape::Square)
804 .tooltip(move |_window, cx| {
805 Tooltip::with_meta(
806 mode.tooltip_interrupt(),
807 Some(&menu::Cancel),
808 "Changes won't be discarded",
809 cx,
810 )
811 })
812 .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
813 .into_any_element(),
814 ],
815 CodegenStatus::Done | CodegenStatus::Error(_) => {
816 let has_error = matches!(codegen_status, CodegenStatus::Error(_));
817 if has_error || self.edited_since_done {
818 vec![
819 IconButton::new("restart", IconName::RotateCw)
820 .icon_color(Color::Info)
821 .shape(IconButtonShape::Square)
822 .tooltip(move |_window, cx| {
823 Tooltip::with_meta(
824 mode.tooltip_restart(),
825 Some(&menu::Confirm),
826 "Changes will be discarded",
827 cx,
828 )
829 })
830 .on_click(cx.listener(|_, _, _, cx| {
831 cx.emit(PromptEditorEvent::StartRequested);
832 }))
833 .into_any_element(),
834 ]
835 } else {
836 let show_rating_buttons = cx.has_flag::<InlineAssistantUseToolFeatureFlag>();
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 show_rating_buttons {
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 history_store: Entity<HistoryStore>,
1221 prompt_store: Option<Entity<PromptStore>>,
1222 project: WeakEntity<Project>,
1223 workspace: WeakEntity<Workspace>,
1224 window: &mut Window,
1225 cx: &mut Context<PromptEditor<BufferCodegen>>,
1226 ) -> PromptEditor<BufferCodegen> {
1227 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1228 let mode = PromptEditorMode::Buffer {
1229 id,
1230 codegen,
1231 editor_margins,
1232 };
1233
1234 let prompt_editor = cx.new(|cx| {
1235 let mut editor = Editor::new(
1236 EditorMode::AutoHeight {
1237 min_lines: 1,
1238 max_lines: Some(Self::MAX_LINES as usize),
1239 },
1240 prompt_buffer,
1241 None,
1242 window,
1243 cx,
1244 );
1245 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1246 // Since the prompt editors for all inline assistants are linked,
1247 // always show the cursor (even when it isn't focused) because
1248 // typing in one will make what you typed appear in all of them.
1249 editor.set_show_cursor_when_unfocused(true, cx);
1250 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1251 editor.set_context_menu_options(ContextMenuOptions {
1252 min_entries_visible: 12,
1253 max_entries_visible: 12,
1254 placement: None,
1255 });
1256
1257 editor
1258 });
1259
1260 let mention_set =
1261 cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
1262
1263 let model_selector_menu_handle = PopoverMenuHandle::default();
1264
1265 let mut this: PromptEditor<BufferCodegen> = PromptEditor {
1266 editor: prompt_editor.clone(),
1267 mention_set,
1268 history_store,
1269 prompt_store,
1270 workspace,
1271 model_selector: cx.new(|cx| {
1272 AgentModelSelector::new(
1273 fs,
1274 model_selector_menu_handle,
1275 prompt_editor.focus_handle(cx),
1276 ModelUsageContext::InlineAssistant,
1277 window,
1278 cx,
1279 )
1280 }),
1281 edited_since_done: false,
1282 prompt_history,
1283 prompt_history_ix: None,
1284 pending_prompt: String::new(),
1285 _codegen_subscription: codegen_subscription,
1286 editor_subscriptions: Vec::new(),
1287 show_rate_limit_notice: false,
1288 mode,
1289 session_state: SessionState {
1290 session_id,
1291 completion: CompletionState::Pending,
1292 },
1293 _phantom: Default::default(),
1294 };
1295
1296 this.assign_completion_provider(cx);
1297 this.subscribe_to_editor(window, cx);
1298 this
1299 }
1300
1301 fn handle_codegen_changed(
1302 &mut self,
1303 codegen: Entity<BufferCodegen>,
1304 cx: &mut Context<PromptEditor<BufferCodegen>>,
1305 ) {
1306 match self.codegen_status(cx) {
1307 CodegenStatus::Idle => {
1308 self.editor
1309 .update(cx, |editor, _| editor.set_read_only(false));
1310 }
1311 CodegenStatus::Pending => {
1312 self.session_state.completion = CompletionState::Pending;
1313 self.editor
1314 .update(cx, |editor, _| editor.set_read_only(true));
1315 }
1316 CodegenStatus::Done => {
1317 let completion = codegen.read(cx).active_completion(cx);
1318 self.session_state.completion = CompletionState::Generated {
1319 completion_text: completion,
1320 };
1321 self.edited_since_done = false;
1322 self.editor
1323 .update(cx, |editor, _| editor.set_read_only(false));
1324 }
1325 CodegenStatus::Error(_error) => {
1326 self.edited_since_done = false;
1327 self.editor
1328 .update(cx, |editor, _| editor.set_read_only(false));
1329 }
1330 }
1331 }
1332
1333 pub fn id(&self) -> InlineAssistId {
1334 match &self.mode {
1335 PromptEditorMode::Buffer { id, .. } => *id,
1336 PromptEditorMode::Terminal { .. } => unreachable!(),
1337 }
1338 }
1339
1340 pub fn codegen(&self) -> &Entity<BufferCodegen> {
1341 match &self.mode {
1342 PromptEditorMode::Buffer { codegen, .. } => codegen,
1343 PromptEditorMode::Terminal { .. } => unreachable!(),
1344 }
1345 }
1346
1347 pub fn mention_set(&self) -> &Entity<MentionSet> {
1348 &self.mention_set
1349 }
1350
1351 pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
1352 match &self.mode {
1353 PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
1354 PromptEditorMode::Terminal { .. } => unreachable!(),
1355 }
1356 }
1357}
1358
1359#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1360pub struct TerminalInlineAssistId(pub usize);
1361
1362impl TerminalInlineAssistId {
1363 pub fn post_inc(&mut self) -> TerminalInlineAssistId {
1364 let id = *self;
1365 self.0 += 1;
1366 id
1367 }
1368}
1369
1370impl PromptEditor<TerminalCodegen> {
1371 pub fn new_terminal(
1372 id: TerminalInlineAssistId,
1373 prompt_history: VecDeque<String>,
1374 prompt_buffer: Entity<MultiBuffer>,
1375 codegen: Entity<TerminalCodegen>,
1376 session_id: Uuid,
1377 fs: Arc<dyn Fs>,
1378 history_store: Entity<HistoryStore>,
1379 prompt_store: Option<Entity<PromptStore>>,
1380 project: WeakEntity<Project>,
1381 workspace: WeakEntity<Workspace>,
1382 window: &mut Window,
1383 cx: &mut Context<Self>,
1384 ) -> Self {
1385 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1386 let mode = PromptEditorMode::Terminal {
1387 id,
1388 codegen,
1389 height_in_lines: 1,
1390 };
1391
1392 let prompt_editor = cx.new(|cx| {
1393 let mut editor = Editor::new(
1394 EditorMode::AutoHeight {
1395 min_lines: 1,
1396 max_lines: Some(Self::MAX_LINES as usize),
1397 },
1398 prompt_buffer,
1399 None,
1400 window,
1401 cx,
1402 );
1403 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1404 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1405 editor.set_context_menu_options(ContextMenuOptions {
1406 min_entries_visible: 12,
1407 max_entries_visible: 12,
1408 placement: None,
1409 });
1410 editor
1411 });
1412
1413 let mention_set =
1414 cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
1415
1416 let model_selector_menu_handle = PopoverMenuHandle::default();
1417
1418 let mut this = Self {
1419 editor: prompt_editor.clone(),
1420 mention_set,
1421 history_store,
1422 prompt_store,
1423 workspace,
1424 model_selector: cx.new(|cx| {
1425 AgentModelSelector::new(
1426 fs,
1427 model_selector_menu_handle.clone(),
1428 prompt_editor.focus_handle(cx),
1429 ModelUsageContext::InlineAssistant,
1430 window,
1431 cx,
1432 )
1433 }),
1434 edited_since_done: false,
1435 prompt_history,
1436 prompt_history_ix: None,
1437 pending_prompt: String::new(),
1438 _codegen_subscription: codegen_subscription,
1439 editor_subscriptions: Vec::new(),
1440 mode,
1441 show_rate_limit_notice: false,
1442 session_state: SessionState {
1443 session_id,
1444 completion: CompletionState::Pending,
1445 },
1446 _phantom: Default::default(),
1447 };
1448 this.count_lines(cx);
1449 this.assign_completion_provider(cx);
1450 this.subscribe_to_editor(window, cx);
1451 this
1452 }
1453
1454 fn count_lines(&mut self, cx: &mut Context<Self>) {
1455 let height_in_lines = cmp::max(
1456 2, // Make the editor at least two lines tall, to account for padding and buttons.
1457 cmp::min(
1458 self.editor
1459 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1460 Self::MAX_LINES as u32,
1461 ),
1462 ) as u8;
1463
1464 match &mut self.mode {
1465 PromptEditorMode::Terminal {
1466 height_in_lines: current_height,
1467 ..
1468 } => {
1469 if height_in_lines != *current_height {
1470 *current_height = height_in_lines;
1471 cx.emit(PromptEditorEvent::Resized { height_in_lines });
1472 }
1473 }
1474 PromptEditorMode::Buffer { .. } => unreachable!(),
1475 }
1476 }
1477
1478 fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1479 match &self.codegen().read(cx).status {
1480 CodegenStatus::Idle => {
1481 self.editor
1482 .update(cx, |editor, _| editor.set_read_only(false));
1483 }
1484 CodegenStatus::Pending => {
1485 self.session_state.completion = CompletionState::Pending;
1486 self.editor
1487 .update(cx, |editor, _| editor.set_read_only(true));
1488 }
1489 CodegenStatus::Done | CodegenStatus::Error(_) => {
1490 self.session_state.completion = CompletionState::Generated {
1491 completion_text: codegen.read(cx).completion(),
1492 };
1493 self.edited_since_done = false;
1494 self.editor
1495 .update(cx, |editor, _| editor.set_read_only(false));
1496 }
1497 }
1498 }
1499
1500 pub fn mention_set(&self) -> &Entity<MentionSet> {
1501 &self.mention_set
1502 }
1503
1504 pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1505 match &self.mode {
1506 PromptEditorMode::Buffer { .. } => unreachable!(),
1507 PromptEditorMode::Terminal { codegen, .. } => codegen,
1508 }
1509 }
1510
1511 pub fn id(&self) -> TerminalInlineAssistId {
1512 match &self.mode {
1513 PromptEditorMode::Buffer { .. } => unreachable!(),
1514 PromptEditorMode::Terminal { id, .. } => *id,
1515 }
1516 }
1517}
1518
1519pub enum CodegenStatus {
1520 Idle,
1521 Pending,
1522 Done,
1523 Error(anyhow::Error),
1524}
1525
1526/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1527#[derive(Copy, Clone)]
1528pub enum CancelButtonState {
1529 Idle,
1530 Pending,
1531 Done,
1532 Error,
1533}
1534
1535impl Into<CancelButtonState> for &CodegenStatus {
1536 fn into(self) -> CancelButtonState {
1537 match self {
1538 CodegenStatus::Idle => CancelButtonState::Idle,
1539 CodegenStatus::Pending => CancelButtonState::Pending,
1540 CodegenStatus::Done => CancelButtonState::Done,
1541 CodegenStatus::Error(_) => CancelButtonState::Error,
1542 }
1543 }
1544}
1545
1546#[derive(Copy, Clone)]
1547pub enum GenerationMode {
1548 Generate,
1549 Transform,
1550}
1551
1552impl GenerationMode {
1553 fn start_label(self) -> &'static str {
1554 match self {
1555 GenerationMode::Generate => "Generate",
1556 GenerationMode::Transform => "Transform",
1557 }
1558 }
1559 fn tooltip_interrupt(self) -> &'static str {
1560 match self {
1561 GenerationMode::Generate => "Interrupt Generation",
1562 GenerationMode::Transform => "Interrupt Transform",
1563 }
1564 }
1565
1566 fn tooltip_restart(self) -> &'static str {
1567 match self {
1568 GenerationMode::Generate => "Restart Generation",
1569 GenerationMode::Transform => "Restart Transform",
1570 }
1571 }
1572
1573 fn tooltip_accept(self) -> &'static str {
1574 match self {
1575 GenerationMode::Generate => "Accept Generation",
1576 GenerationMode::Transform => "Accept Transform",
1577 }
1578 }
1579}
1580
1581/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1582#[derive(Clone, Debug)]
1583struct MessageCrease {
1584 range: Range<MultiBufferOffset>,
1585 icon_path: SharedString,
1586 label: SharedString,
1587}
1588
1589fn extract_message_creases(
1590 editor: &mut Editor,
1591 mention_set: &Entity<MentionSet>,
1592 window: &mut Window,
1593 cx: &mut Context<'_, Editor>,
1594) -> Vec<MessageCrease> {
1595 let creases = mention_set.read(cx).creases();
1596 let snapshot = editor.snapshot(window, cx);
1597 snapshot
1598 .crease_snapshot
1599 .creases()
1600 .filter(|(id, _)| creases.contains(id))
1601 .filter_map(|(_, crease)| {
1602 let metadata = crease.metadata()?.clone();
1603 Some(MessageCrease {
1604 range: crease.range().to_offset(snapshot.buffer()),
1605 label: metadata.label,
1606 icon_path: metadata.icon_path,
1607 })
1608 })
1609 .collect()
1610}
1611
1612fn insert_message_creases(
1613 editor: &mut Editor,
1614 message_creases: &[MessageCrease],
1615 window: &mut Window,
1616 cx: &mut Context<'_, Editor>,
1617) -> Vec<CreaseId> {
1618 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1619 let creases = message_creases
1620 .iter()
1621 .map(|crease| {
1622 let start = buffer_snapshot.anchor_after(crease.range.start);
1623 let end = buffer_snapshot.anchor_before(crease.range.end);
1624 crease_for_mention(
1625 crease.label.clone(),
1626 crease.icon_path.clone(),
1627 start..end,
1628 cx.weak_entity(),
1629 )
1630 })
1631 .collect::<Vec<_>>();
1632 let ids = editor.insert_creases(creases.clone(), cx);
1633 editor.fold_creases(creases, false, window, cx);
1634 ids
1635}