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