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