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