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