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