1use crate::acp::AcpThreadHistory;
2use agent::ThreadStore;
3use collections::{HashMap, VecDeque};
4use editor::actions::Paste;
5use editor::code_context_menus::CodeContextMenu;
6use editor::display_map::{CreaseId, EditorMargins};
7use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
8use editor::{
9 ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
10 actions::{MoveDown, MoveUp},
11};
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: WeakEntity<AcpThreadHistory>,
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.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 rated = matches!(self.session_state.completion, CompletionState::Rated);
837
838 let accept = IconButton::new("accept", IconName::Check)
839 .icon_color(Color::Info)
840 .shape(IconButtonShape::Square)
841 .tooltip(move |_window, cx| {
842 Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
843 })
844 .on_click(cx.listener(|_, _, _, cx| {
845 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
846 }))
847 .into_any_element();
848
849 let mut buttons = Vec::new();
850
851 buttons.push(
852 h_flex()
853 .pl_1()
854 .gap_1()
855 .border_l_1()
856 .border_color(cx.theme().colors().border_variant)
857 .child(
858 IconButton::new("thumbs-up", IconName::ThumbsUp)
859 .shape(IconButtonShape::Square)
860 .map(|this| {
861 if rated {
862 this.disabled(true).icon_color(Color::Disabled).tooltip(
863 move |_, cx| {
864 Tooltip::with_meta(
865 "Good Result",
866 None,
867 "You already rated this result",
868 cx,
869 )
870 },
871 )
872 } else {
873 this.icon_color(Color::Muted).tooltip(move |_, cx| {
874 Tooltip::for_action(
875 "Good Result",
876 &ThumbsUpResult,
877 cx,
878 )
879 })
880 }
881 })
882 .on_click(cx.listener(|this, _, window, cx| {
883 this.thumbs_up(&ThumbsUpResult, window, cx);
884 })),
885 )
886 .child(
887 IconButton::new("thumbs-down", IconName::ThumbsDown)
888 .shape(IconButtonShape::Square)
889 .map(|this| {
890 if rated {
891 this.disabled(true).icon_color(Color::Disabled).tooltip(
892 move |_, cx| {
893 Tooltip::with_meta(
894 "Bad Result",
895 None,
896 "You already rated this result",
897 cx,
898 )
899 },
900 )
901 } else {
902 this.icon_color(Color::Muted).tooltip(move |_, cx| {
903 Tooltip::for_action(
904 "Bad Result",
905 &ThumbsDownResult,
906 cx,
907 )
908 })
909 }
910 })
911 .on_click(cx.listener(|this, _, window, cx| {
912 this.thumbs_down(&ThumbsDownResult, window, cx);
913 })),
914 )
915 .into_any_element(),
916 );
917
918 buttons.push(accept);
919
920 match &self.mode {
921 PromptEditorMode::Terminal { .. } => {
922 buttons.push(
923 IconButton::new("confirm", IconName::PlayFilled)
924 .icon_color(Color::Info)
925 .shape(IconButtonShape::Square)
926 .tooltip(|_window, cx| {
927 Tooltip::for_action(
928 "Execute Generated Command",
929 &menu::SecondaryConfirm,
930 cx,
931 )
932 })
933 .on_click(cx.listener(|_, _, _, cx| {
934 cx.emit(PromptEditorEvent::ConfirmRequested {
935 execute: true,
936 });
937 }))
938 .into_any_element(),
939 );
940 buttons
941 }
942 PromptEditorMode::Buffer { .. } => buttons,
943 }
944 }
945 }
946 }
947 }
948
949 fn cycle_prev(
950 &mut self,
951 _: &CyclePreviousInlineAssist,
952 _: &mut Window,
953 cx: &mut Context<Self>,
954 ) {
955 match &self.mode {
956 PromptEditorMode::Buffer { codegen, .. } => {
957 codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
958 }
959 PromptEditorMode::Terminal { .. } => {
960 // no cycle buttons in terminal mode
961 }
962 }
963 }
964
965 fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
966 match &self.mode {
967 PromptEditorMode::Buffer { codegen, .. } => {
968 codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
969 }
970 PromptEditorMode::Terminal { .. } => {
971 // no cycle buttons in terminal mode
972 }
973 }
974 }
975
976 fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
977 let focus_handle = self.editor.focus_handle(cx);
978
979 IconButton::new("cancel", IconName::Close)
980 .icon_color(Color::Muted)
981 .shape(IconButtonShape::Square)
982 .tooltip({
983 move |_window, cx| {
984 Tooltip::for_action_in(
985 "Close Assistant",
986 &editor::actions::Cancel,
987 &focus_handle,
988 cx,
989 )
990 }
991 })
992 .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
993 .into_any_element()
994 }
995
996 fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
997 let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
998
999 let model_registry = LanguageModelRegistry::read_global(cx);
1000 let default_model = model_registry.default_model().map(|default| default.model);
1001 let alternative_models = model_registry.inline_alternative_models();
1002
1003 let get_model_name = |index: usize| -> String {
1004 let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
1005
1006 match index {
1007 0 => default_model.as_ref().map_or_else(String::new, name),
1008 index if index <= alternative_models.len() => alternative_models
1009 .get(index - 1)
1010 .map_or_else(String::new, name),
1011 _ => String::new(),
1012 }
1013 };
1014
1015 let total_models = alternative_models.len() + 1;
1016
1017 if total_models <= 1 {
1018 return div().into_any_element();
1019 }
1020
1021 let current_index = codegen.active_alternative;
1022 let prev_index = (current_index + total_models - 1) % total_models;
1023 let next_index = (current_index + 1) % total_models;
1024
1025 let prev_model_name = get_model_name(prev_index);
1026 let next_model_name = get_model_name(next_index);
1027
1028 h_flex()
1029 .child(
1030 IconButton::new("previous", IconName::ChevronLeft)
1031 .icon_color(Color::Muted)
1032 .disabled(disabled || current_index == 0)
1033 .shape(IconButtonShape::Square)
1034 .tooltip({
1035 let focus_handle = self.editor.focus_handle(cx);
1036 move |_window, cx| {
1037 cx.new(|cx| {
1038 let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
1039 KeyBinding::for_action_in(
1040 &CyclePreviousInlineAssist,
1041 &focus_handle,
1042 cx,
1043 ),
1044 );
1045 if !disabled && current_index != 0 {
1046 tooltip = tooltip.meta(prev_model_name.clone());
1047 }
1048 tooltip
1049 })
1050 .into()
1051 }
1052 })
1053 .on_click(cx.listener(|this, _, window, cx| {
1054 this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
1055 })),
1056 )
1057 .child(
1058 Label::new(format!(
1059 "{}/{}",
1060 codegen.active_alternative + 1,
1061 codegen.alternative_count(cx)
1062 ))
1063 .size(LabelSize::Small)
1064 .color(if disabled {
1065 Color::Disabled
1066 } else {
1067 Color::Muted
1068 }),
1069 )
1070 .child(
1071 IconButton::new("next", IconName::ChevronRight)
1072 .icon_color(Color::Muted)
1073 .disabled(disabled || current_index == total_models - 1)
1074 .shape(IconButtonShape::Square)
1075 .tooltip({
1076 let focus_handle = self.editor.focus_handle(cx);
1077 move |_window, cx| {
1078 cx.new(|cx| {
1079 let mut tooltip = Tooltip::new("Next Alternative").key_binding(
1080 KeyBinding::for_action_in(
1081 &CycleNextInlineAssist,
1082 &focus_handle,
1083 cx,
1084 ),
1085 );
1086 if !disabled && current_index != total_models - 1 {
1087 tooltip = tooltip.meta(next_model_name.clone());
1088 }
1089 tooltip
1090 })
1091 .into()
1092 }
1093 })
1094 .on_click(cx.listener(|this, _, window, cx| {
1095 this.cycle_next(&CycleNextInlineAssist, window, cx)
1096 })),
1097 )
1098 .into_any_element()
1099 }
1100
1101 fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1102 let colors = cx.theme().colors();
1103
1104 div()
1105 .size_full()
1106 .p_2()
1107 .pl_1()
1108 .bg(colors.editor_background)
1109 .child({
1110 let settings = ThemeSettings::get_global(cx);
1111 let font_size = settings.buffer_font_size(cx);
1112 let line_height = font_size * 1.2;
1113
1114 let text_style = TextStyle {
1115 color: colors.editor_foreground,
1116 font_family: settings.buffer_font.family.clone(),
1117 font_features: settings.buffer_font.features.clone(),
1118 font_size: font_size.into(),
1119 line_height: line_height.into(),
1120 ..Default::default()
1121 };
1122
1123 EditorElement::new(
1124 &self.editor,
1125 EditorStyle {
1126 background: colors.editor_background,
1127 local_player: cx.theme().players().local(),
1128 syntax: cx.theme().syntax().clone(),
1129 text: text_style,
1130 ..Default::default()
1131 },
1132 )
1133 })
1134 .into_any_element()
1135 }
1136
1137 fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1138 MarkdownElement::new(markdown, style)
1139 }
1140}
1141
1142pub enum PromptEditorMode {
1143 Buffer {
1144 id: InlineAssistId,
1145 codegen: Entity<BufferCodegen>,
1146 editor_margins: Arc<Mutex<EditorMargins>>,
1147 },
1148 Terminal {
1149 id: TerminalInlineAssistId,
1150 codegen: Entity<TerminalCodegen>,
1151 height_in_lines: u8,
1152 },
1153}
1154
1155pub enum PromptEditorEvent {
1156 StartRequested,
1157 StopRequested,
1158 ConfirmRequested { execute: bool },
1159 CancelRequested,
1160 Resized { height_in_lines: u8 },
1161}
1162
1163#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1164pub struct InlineAssistId(pub usize);
1165
1166impl InlineAssistId {
1167 pub fn post_inc(&mut self) -> InlineAssistId {
1168 let id = *self;
1169 self.0 += 1;
1170 id
1171 }
1172}
1173
1174struct PromptEditorCompletionProviderDelegate;
1175
1176fn inline_assistant_model_supports_images(cx: &App) -> bool {
1177 LanguageModelRegistry::read_global(cx)
1178 .inline_assistant_model()
1179 .map_or(false, |m| m.model.supports_images())
1180}
1181
1182impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
1183 fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
1184 vec![
1185 PromptContextType::File,
1186 PromptContextType::Symbol,
1187 PromptContextType::Thread,
1188 PromptContextType::Fetch,
1189 PromptContextType::Rules,
1190 ]
1191 }
1192
1193 fn supports_images(&self, cx: &App) -> bool {
1194 inline_assistant_model_supports_images(cx)
1195 }
1196
1197 fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
1198 Vec::new()
1199 }
1200
1201 fn confirm_command(&self, _cx: &mut App) {}
1202}
1203
1204impl PromptEditor<BufferCodegen> {
1205 pub fn new_buffer(
1206 id: InlineAssistId,
1207 editor_margins: Arc<Mutex<EditorMargins>>,
1208 prompt_history: VecDeque<String>,
1209 prompt_buffer: Entity<MultiBuffer>,
1210 codegen: Entity<BufferCodegen>,
1211 session_id: Uuid,
1212 fs: Arc<dyn Fs>,
1213 thread_store: Entity<ThreadStore>,
1214 prompt_store: Option<Entity<PromptStore>>,
1215 history: WeakEntity<AcpThreadHistory>,
1216 project: WeakEntity<Project>,
1217 workspace: WeakEntity<Workspace>,
1218 window: &mut Window,
1219 cx: &mut Context<PromptEditor<BufferCodegen>>,
1220 ) -> PromptEditor<BufferCodegen> {
1221 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1222 let mode = PromptEditorMode::Buffer {
1223 id,
1224 codegen,
1225 editor_margins,
1226 };
1227
1228 let prompt_editor = cx.new(|cx| {
1229 let mut editor = Editor::new(
1230 EditorMode::AutoHeight {
1231 min_lines: 1,
1232 max_lines: Some(Self::MAX_LINES as usize),
1233 },
1234 prompt_buffer,
1235 None,
1236 window,
1237 cx,
1238 );
1239 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1240 // Since the prompt editors for all inline assistants are linked,
1241 // always show the cursor (even when it isn't focused) because
1242 // typing in one will make what you typed appear in all of them.
1243 editor.set_show_cursor_when_unfocused(true, cx);
1244 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1245 editor.set_context_menu_options(ContextMenuOptions {
1246 min_entries_visible: 12,
1247 max_entries_visible: 12,
1248 placement: None,
1249 });
1250
1251 editor
1252 });
1253
1254 let mention_set = cx
1255 .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1256
1257 let model_selector_menu_handle = PopoverMenuHandle::default();
1258
1259 let mut this: PromptEditor<BufferCodegen> = PromptEditor {
1260 editor: prompt_editor.clone(),
1261 mention_set,
1262 history,
1263 prompt_store,
1264 workspace,
1265 model_selector: cx.new(|cx| {
1266 AgentModelSelector::new(
1267 fs,
1268 model_selector_menu_handle,
1269 prompt_editor.focus_handle(cx),
1270 ModelUsageContext::InlineAssistant,
1271 window,
1272 cx,
1273 )
1274 }),
1275 edited_since_done: false,
1276 prompt_history,
1277 prompt_history_ix: None,
1278 pending_prompt: String::new(),
1279 _codegen_subscription: codegen_subscription,
1280 editor_subscriptions: Vec::new(),
1281 show_rate_limit_notice: false,
1282 mode,
1283 session_state: SessionState {
1284 session_id,
1285 completion: CompletionState::Pending,
1286 },
1287 _phantom: Default::default(),
1288 };
1289
1290 this.assign_completion_provider(cx);
1291 this.subscribe_to_editor(window, cx);
1292 this
1293 }
1294
1295 fn handle_codegen_changed(
1296 &mut self,
1297 codegen: Entity<BufferCodegen>,
1298 cx: &mut Context<PromptEditor<BufferCodegen>>,
1299 ) {
1300 match self.codegen_status(cx) {
1301 CodegenStatus::Idle => {
1302 self.editor
1303 .update(cx, |editor, _| editor.set_read_only(false));
1304 }
1305 CodegenStatus::Pending => {
1306 self.session_state.completion = CompletionState::Pending;
1307 self.editor
1308 .update(cx, |editor, _| editor.set_read_only(true));
1309 }
1310 CodegenStatus::Done => {
1311 let completion = codegen.read(cx).active_completion(cx);
1312 self.session_state.completion = CompletionState::Generated {
1313 completion_text: completion,
1314 };
1315 self.edited_since_done = false;
1316 self.editor
1317 .update(cx, |editor, _| editor.set_read_only(false));
1318 }
1319 CodegenStatus::Error(_error) => {
1320 self.edited_since_done = false;
1321 self.editor
1322 .update(cx, |editor, _| editor.set_read_only(false));
1323 }
1324 }
1325 }
1326
1327 pub fn id(&self) -> InlineAssistId {
1328 match &self.mode {
1329 PromptEditorMode::Buffer { id, .. } => *id,
1330 PromptEditorMode::Terminal { .. } => unreachable!(),
1331 }
1332 }
1333
1334 pub fn codegen(&self) -> &Entity<BufferCodegen> {
1335 match &self.mode {
1336 PromptEditorMode::Buffer { codegen, .. } => codegen,
1337 PromptEditorMode::Terminal { .. } => unreachable!(),
1338 }
1339 }
1340
1341 pub fn mention_set(&self) -> &Entity<MentionSet> {
1342 &self.mention_set
1343 }
1344
1345 pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
1346 match &self.mode {
1347 PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
1348 PromptEditorMode::Terminal { .. } => unreachable!(),
1349 }
1350 }
1351}
1352
1353#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1354pub struct TerminalInlineAssistId(pub usize);
1355
1356impl TerminalInlineAssistId {
1357 pub fn post_inc(&mut self) -> TerminalInlineAssistId {
1358 let id = *self;
1359 self.0 += 1;
1360 id
1361 }
1362}
1363
1364impl PromptEditor<TerminalCodegen> {
1365 pub fn new_terminal(
1366 id: TerminalInlineAssistId,
1367 prompt_history: VecDeque<String>,
1368 prompt_buffer: Entity<MultiBuffer>,
1369 codegen: Entity<TerminalCodegen>,
1370 session_id: Uuid,
1371 fs: Arc<dyn Fs>,
1372 thread_store: Entity<ThreadStore>,
1373 prompt_store: Option<Entity<PromptStore>>,
1374 history: WeakEntity<AcpThreadHistory>,
1375 project: WeakEntity<Project>,
1376 workspace: WeakEntity<Workspace>,
1377 window: &mut Window,
1378 cx: &mut Context<Self>,
1379 ) -> Self {
1380 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1381 let mode = PromptEditorMode::Terminal {
1382 id,
1383 codegen,
1384 height_in_lines: 1,
1385 };
1386
1387 let prompt_editor = cx.new(|cx| {
1388 let mut editor = Editor::new(
1389 EditorMode::AutoHeight {
1390 min_lines: 1,
1391 max_lines: Some(Self::MAX_LINES as usize),
1392 },
1393 prompt_buffer,
1394 None,
1395 window,
1396 cx,
1397 );
1398 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1399 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1400 editor.set_context_menu_options(ContextMenuOptions {
1401 min_entries_visible: 12,
1402 max_entries_visible: 12,
1403 placement: None,
1404 });
1405 editor
1406 });
1407
1408 let mention_set = cx
1409 .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1410
1411 let model_selector_menu_handle = PopoverMenuHandle::default();
1412
1413 let mut this = Self {
1414 editor: prompt_editor.clone(),
1415 mention_set,
1416 history,
1417 prompt_store,
1418 workspace,
1419 model_selector: cx.new(|cx| {
1420 AgentModelSelector::new(
1421 fs,
1422 model_selector_menu_handle.clone(),
1423 prompt_editor.focus_handle(cx),
1424 ModelUsageContext::InlineAssistant,
1425 window,
1426 cx,
1427 )
1428 }),
1429 edited_since_done: false,
1430 prompt_history,
1431 prompt_history_ix: None,
1432 pending_prompt: String::new(),
1433 _codegen_subscription: codegen_subscription,
1434 editor_subscriptions: Vec::new(),
1435 mode,
1436 show_rate_limit_notice: false,
1437 session_state: SessionState {
1438 session_id,
1439 completion: CompletionState::Pending,
1440 },
1441 _phantom: Default::default(),
1442 };
1443 this.count_lines(cx);
1444 this.assign_completion_provider(cx);
1445 this.subscribe_to_editor(window, cx);
1446 this
1447 }
1448
1449 fn count_lines(&mut self, cx: &mut Context<Self>) {
1450 let height_in_lines = cmp::max(
1451 2, // Make the editor at least two lines tall, to account for padding and buttons.
1452 cmp::min(
1453 self.editor
1454 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1455 Self::MAX_LINES as u32,
1456 ),
1457 ) as u8;
1458
1459 match &mut self.mode {
1460 PromptEditorMode::Terminal {
1461 height_in_lines: current_height,
1462 ..
1463 } => {
1464 if height_in_lines != *current_height {
1465 *current_height = height_in_lines;
1466 cx.emit(PromptEditorEvent::Resized { height_in_lines });
1467 }
1468 }
1469 PromptEditorMode::Buffer { .. } => unreachable!(),
1470 }
1471 }
1472
1473 fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1474 match &self.codegen().read(cx).status {
1475 CodegenStatus::Idle => {
1476 self.editor
1477 .update(cx, |editor, _| editor.set_read_only(false));
1478 }
1479 CodegenStatus::Pending => {
1480 self.session_state.completion = CompletionState::Pending;
1481 self.editor
1482 .update(cx, |editor, _| editor.set_read_only(true));
1483 }
1484 CodegenStatus::Done | CodegenStatus::Error(_) => {
1485 self.session_state.completion = CompletionState::Generated {
1486 completion_text: codegen.read(cx).completion(),
1487 };
1488 self.edited_since_done = false;
1489 self.editor
1490 .update(cx, |editor, _| editor.set_read_only(false));
1491 }
1492 }
1493 }
1494
1495 pub fn mention_set(&self) -> &Entity<MentionSet> {
1496 &self.mention_set
1497 }
1498
1499 pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1500 match &self.mode {
1501 PromptEditorMode::Buffer { .. } => unreachable!(),
1502 PromptEditorMode::Terminal { codegen, .. } => codegen,
1503 }
1504 }
1505
1506 pub fn id(&self) -> TerminalInlineAssistId {
1507 match &self.mode {
1508 PromptEditorMode::Buffer { .. } => unreachable!(),
1509 PromptEditorMode::Terminal { id, .. } => *id,
1510 }
1511 }
1512}
1513
1514pub enum CodegenStatus {
1515 Idle,
1516 Pending,
1517 Done,
1518 Error(anyhow::Error),
1519}
1520
1521/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1522#[derive(Copy, Clone)]
1523pub enum CancelButtonState {
1524 Idle,
1525 Pending,
1526 Done,
1527 Error,
1528}
1529
1530impl Into<CancelButtonState> for &CodegenStatus {
1531 fn into(self) -> CancelButtonState {
1532 match self {
1533 CodegenStatus::Idle => CancelButtonState::Idle,
1534 CodegenStatus::Pending => CancelButtonState::Pending,
1535 CodegenStatus::Done => CancelButtonState::Done,
1536 CodegenStatus::Error(_) => CancelButtonState::Error,
1537 }
1538 }
1539}
1540
1541#[derive(Copy, Clone)]
1542pub enum GenerationMode {
1543 Generate,
1544 Transform,
1545}
1546
1547impl GenerationMode {
1548 fn start_label(self) -> &'static str {
1549 match self {
1550 GenerationMode::Generate => "Generate",
1551 GenerationMode::Transform => "Transform",
1552 }
1553 }
1554 fn tooltip_interrupt(self) -> &'static str {
1555 match self {
1556 GenerationMode::Generate => "Interrupt Generation",
1557 GenerationMode::Transform => "Interrupt Transform",
1558 }
1559 }
1560
1561 fn tooltip_restart(self) -> &'static str {
1562 match self {
1563 GenerationMode::Generate => "Restart Generation",
1564 GenerationMode::Transform => "Restart Transform",
1565 }
1566 }
1567
1568 fn tooltip_accept(self) -> &'static str {
1569 match self {
1570 GenerationMode::Generate => "Accept Generation",
1571 GenerationMode::Transform => "Accept Transform",
1572 }
1573 }
1574}
1575
1576/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1577#[derive(Clone, Debug)]
1578struct MessageCrease {
1579 range: Range<MultiBufferOffset>,
1580 icon_path: SharedString,
1581 label: SharedString,
1582}
1583
1584fn extract_message_creases(
1585 editor: &mut Editor,
1586 mention_set: &Entity<MentionSet>,
1587 window: &mut Window,
1588 cx: &mut Context<'_, Editor>,
1589) -> Vec<MessageCrease> {
1590 let creases = mention_set.read(cx).creases();
1591 let snapshot = editor.snapshot(window, cx);
1592 snapshot
1593 .crease_snapshot
1594 .creases()
1595 .filter(|(id, _)| creases.contains(id))
1596 .filter_map(|(_, crease)| {
1597 let metadata = crease.metadata()?.clone();
1598 Some(MessageCrease {
1599 range: crease.range().to_offset(snapshot.buffer()),
1600 label: metadata.label,
1601 icon_path: metadata.icon_path,
1602 })
1603 })
1604 .collect()
1605}
1606
1607fn insert_message_creases(
1608 editor: &mut Editor,
1609 message_creases: &[MessageCrease],
1610 window: &mut Window,
1611 cx: &mut Context<'_, Editor>,
1612) -> Vec<CreaseId> {
1613 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1614 let creases = message_creases
1615 .iter()
1616 .map(|crease| {
1617 let start = buffer_snapshot.anchor_after(crease.range.start);
1618 let end = buffer_snapshot.anchor_before(crease.range.end);
1619 crease_for_mention(
1620 crease.label.clone(),
1621 crease.icon_path.clone(),
1622 start..end,
1623 cx.weak_entity(),
1624 )
1625 })
1626 .collect::<Vec<_>>();
1627 let ids = editor.insert_creases(creases.clone(), cx);
1628 editor.fold_creases(creases, false, window, cx);
1629 ids
1630}