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