1use crate::context_store::ContextStore;
2use agent::HistoryStore;
3use collections::VecDeque;
4use editor::actions::Paste;
5use editor::display_map::EditorMargins;
6use editor::{
7 ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
8 actions::{MoveDown, MoveUp},
9};
10use fs::Fs;
11use gpui::{
12 AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
13 Focusable, Subscription, TextStyle, WeakEntity, Window,
14};
15use language_model::{LanguageModel, LanguageModelRegistry};
16use parking_lot::Mutex;
17use prompt_store::PromptStore;
18use settings::Settings;
19use std::cmp;
20use std::rc::Rc;
21use std::sync::Arc;
22use theme::ThemeSettings;
23use ui::utils::WithRemSize;
24use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
25use workspace::Workspace;
26use zed_actions::agent::ToggleModelSelector;
27
28use crate::agent_model_selector::AgentModelSelector;
29use crate::buffer_codegen::BufferCodegen;
30use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
31use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
32use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
33use crate::terminal_codegen::TerminalCodegen;
34use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
35use crate::{RemoveAllContext, ToggleContextPicker};
36
37pub struct PromptEditor<T> {
38 pub editor: Entity<Editor>,
39 mode: PromptEditorMode,
40 context_store: Entity<ContextStore>,
41 context_strip: Entity<ContextStrip>,
42 context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
43 model_selector: Entity<AgentModelSelector>,
44 edited_since_done: bool,
45 prompt_history: VecDeque<String>,
46 prompt_history_ix: Option<usize>,
47 pending_prompt: String,
48 _codegen_subscription: Subscription,
49 editor_subscriptions: Vec<Subscription>,
50 _context_strip_subscription: Subscription,
51 show_rate_limit_notice: bool,
52 _phantom: std::marker::PhantomData<T>,
53}
54
55impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
56
57impl<T: 'static> Render for PromptEditor<T> {
58 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
59 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
60 let mut buttons = Vec::new();
61
62 const RIGHT_PADDING: Pixels = px(9.);
63
64 let (left_gutter_width, right_padding) = match &self.mode {
65 PromptEditorMode::Buffer {
66 id: _,
67 codegen,
68 editor_margins,
69 } => {
70 let codegen = codegen.read(cx);
71
72 if codegen.alternative_count(cx) > 1 {
73 buttons.push(self.render_cycle_controls(codegen, cx));
74 }
75
76 let editor_margins = editor_margins.lock();
77 let gutter = editor_margins.gutter;
78
79 let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
80 let right_padding = editor_margins.right + RIGHT_PADDING;
81
82 (left_gutter_width, right_padding)
83 }
84 PromptEditorMode::Terminal { .. } => {
85 // Give the equivalent of the same left-padding that we're using on the right
86 (Pixels::from(40.0), Pixels::from(24.))
87 }
88 };
89
90 let bottom_padding = match &self.mode {
91 PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
92 PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
93 };
94
95 buttons.extend(self.render_buttons(window, cx));
96
97 v_flex()
98 .key_context("PromptEditor")
99 .capture_action(cx.listener(Self::paste))
100 .bg(cx.theme().colors().editor_background)
101 .block_mouse_except_scroll()
102 .gap_0p5()
103 .border_y_1()
104 .border_color(cx.theme().status().info_border)
105 .size_full()
106 .pt_0p5()
107 .pb(bottom_padding)
108 .pr(right_padding)
109 .child(
110 h_flex()
111 .items_start()
112 .cursor(CursorStyle::Arrow)
113 .on_action(cx.listener(Self::toggle_context_picker))
114 .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
115 this.model_selector
116 .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
117 }))
118 .on_action(cx.listener(Self::confirm))
119 .on_action(cx.listener(Self::cancel))
120 .on_action(cx.listener(Self::move_up))
121 .on_action(cx.listener(Self::move_down))
122 .on_action(cx.listener(Self::remove_all_context))
123 .capture_action(cx.listener(Self::cycle_prev))
124 .capture_action(cx.listener(Self::cycle_next))
125 .child(
126 WithRemSize::new(ui_font_size)
127 .flex()
128 .flex_row()
129 .flex_shrink_0()
130 .items_center()
131 .h_full()
132 .w(left_gutter_width)
133 .justify_center()
134 .gap_2()
135 .child(self.render_close_button(cx))
136 .map(|el| {
137 let CodegenStatus::Error(error) = self.codegen_status(cx) else {
138 return el;
139 };
140
141 let error_message = SharedString::from(error.to_string());
142 el.child(
143 div()
144 .id("error")
145 .tooltip(Tooltip::text(error_message))
146 .child(
147 Icon::new(IconName::XCircle)
148 .size(IconSize::Small)
149 .color(Color::Error),
150 ),
151 )
152 }),
153 )
154 .child(
155 h_flex()
156 .w_full()
157 .justify_between()
158 .child(div().flex_1().child(self.render_editor(window, cx)))
159 .child(
160 WithRemSize::new(ui_font_size)
161 .flex()
162 .flex_row()
163 .items_center()
164 .gap_1()
165 .children(buttons),
166 ),
167 ),
168 )
169 .child(
170 WithRemSize::new(ui_font_size)
171 .flex()
172 .flex_row()
173 .items_center()
174 .child(h_flex().flex_shrink_0().w(left_gutter_width))
175 .child(
176 h_flex()
177 .w_full()
178 .pl_1()
179 .items_start()
180 .justify_between()
181 .child(self.context_strip.clone())
182 .child(self.model_selector.clone()),
183 ),
184 )
185 }
186}
187
188impl<T: 'static> Focusable for PromptEditor<T> {
189 fn focus_handle(&self, cx: &App) -> FocusHandle {
190 self.editor.focus_handle(cx)
191 }
192}
193
194impl<T: 'static> PromptEditor<T> {
195 const MAX_LINES: u8 = 8;
196
197 fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
198 match &self.mode {
199 PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
200 PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
201 }
202 }
203
204 fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
205 self.editor_subscriptions.clear();
206 self.editor_subscriptions.push(cx.subscribe_in(
207 &self.editor,
208 window,
209 Self::handle_prompt_editor_events,
210 ));
211 }
212
213 pub fn set_show_cursor_when_unfocused(
214 &mut self,
215 show_cursor_when_unfocused: bool,
216 cx: &mut Context<Self>,
217 ) {
218 self.editor.update(cx, |editor, cx| {
219 editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
220 });
221 }
222
223 pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
224 let prompt = self.prompt(cx);
225 let existing_creases = self.editor.update(cx, extract_message_creases);
226
227 let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
228 self.editor = cx.new(|cx| {
229 let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
230 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
231 editor.set_placeholder_text("Add a prompt…", window, cx);
232 editor.set_text(prompt, window, cx);
233 insert_message_creases(
234 &mut editor,
235 &existing_creases,
236 &self.context_store,
237 window,
238 cx,
239 );
240
241 if focus {
242 window.focus(&editor.focus_handle(cx));
243 }
244 editor
245 });
246 self.subscribe_to_editor(window, cx);
247 }
248
249 pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
250 let action = match mode {
251 PromptEditorMode::Buffer { codegen, .. } => {
252 if codegen.read(cx).is_insertion {
253 "Generate"
254 } else {
255 "Transform"
256 }
257 }
258 PromptEditorMode::Terminal { .. } => "Generate",
259 };
260
261 let agent_panel_keybinding =
262 ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
263 .map(|keybinding| format!("{keybinding} to chat ― "))
264 .unwrap_or_default();
265
266 format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
267 }
268
269 pub fn prompt(&self, cx: &App) -> String {
270 self.editor.read(cx).text(cx)
271 }
272
273 fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
274 let images = cx
275 .read_from_clipboard()
276 .map(|item| {
277 item.into_entries()
278 .filter_map(|entry| {
279 if let ClipboardEntry::Image(image) = entry {
280 Some(image)
281 } else {
282 None
283 }
284 })
285 .collect::<Vec<_>>()
286 })
287 .unwrap_or_default();
288
289 if images.is_empty() {
290 return;
291 }
292 cx.stop_propagation();
293
294 self.context_store.update(cx, |store, cx| {
295 for image in images {
296 store.add_image_instance(Arc::new(image), cx);
297 }
298 });
299 }
300
301 fn handle_prompt_editor_events(
302 &mut self,
303 _: &Entity<Editor>,
304 event: &EditorEvent,
305 window: &mut Window,
306 cx: &mut Context<Self>,
307 ) {
308 match event {
309 EditorEvent::Edited { .. } => {
310 if let Some(workspace) = window.root::<Workspace>().flatten() {
311 workspace.update(cx, |workspace, cx| {
312 let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
313
314 workspace
315 .client()
316 .telemetry()
317 .log_edit_event("inline assist", is_via_ssh);
318 });
319 }
320 let prompt = self.editor.read(cx).text(cx);
321 if self
322 .prompt_history_ix
323 .is_none_or(|ix| self.prompt_history[ix] != prompt)
324 {
325 self.prompt_history_ix.take();
326 self.pending_prompt = prompt;
327 }
328
329 self.edited_since_done = true;
330 cx.notify();
331 }
332 EditorEvent::Blurred => {
333 if self.show_rate_limit_notice {
334 self.show_rate_limit_notice = false;
335 cx.notify();
336 }
337 }
338 _ => {}
339 }
340 }
341
342 fn toggle_context_picker(
343 &mut self,
344 _: &ToggleContextPicker,
345 window: &mut Window,
346 cx: &mut Context<Self>,
347 ) {
348 self.context_picker_menu_handle.toggle(window, cx);
349 }
350
351 pub fn remove_all_context(
352 &mut self,
353 _: &RemoveAllContext,
354 _window: &mut Window,
355 cx: &mut Context<Self>,
356 ) {
357 self.context_store.update(cx, |store, cx| store.clear(cx));
358 cx.notify();
359 }
360
361 fn cancel(
362 &mut self,
363 _: &editor::actions::Cancel,
364 _window: &mut Window,
365 cx: &mut Context<Self>,
366 ) {
367 match self.codegen_status(cx) {
368 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
369 cx.emit(PromptEditorEvent::CancelRequested);
370 }
371 CodegenStatus::Pending => {
372 cx.emit(PromptEditorEvent::StopRequested);
373 }
374 }
375 }
376
377 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
378 match self.codegen_status(cx) {
379 CodegenStatus::Idle => {
380 cx.emit(PromptEditorEvent::StartRequested);
381 }
382 CodegenStatus::Pending => {}
383 CodegenStatus::Done => {
384 if self.edited_since_done {
385 cx.emit(PromptEditorEvent::StartRequested);
386 } else {
387 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
388 }
389 }
390 CodegenStatus::Error(_) => {
391 cx.emit(PromptEditorEvent::StartRequested);
392 }
393 }
394 }
395
396 fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
397 if let Some(ix) = self.prompt_history_ix {
398 if ix > 0 {
399 self.prompt_history_ix = Some(ix - 1);
400 let prompt = self.prompt_history[ix - 1].as_str();
401 self.editor.update(cx, |editor, cx| {
402 editor.set_text(prompt, window, cx);
403 editor.move_to_beginning(&Default::default(), window, cx);
404 });
405 }
406 } else if !self.prompt_history.is_empty() {
407 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
408 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
409 self.editor.update(cx, |editor, cx| {
410 editor.set_text(prompt, window, cx);
411 editor.move_to_beginning(&Default::default(), window, cx);
412 });
413 }
414 }
415
416 fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
417 if let Some(ix) = self.prompt_history_ix {
418 if ix < self.prompt_history.len() - 1 {
419 self.prompt_history_ix = Some(ix + 1);
420 let prompt = self.prompt_history[ix + 1].as_str();
421 self.editor.update(cx, |editor, cx| {
422 editor.set_text(prompt, window, cx);
423 editor.move_to_end(&Default::default(), window, cx)
424 });
425 } else {
426 self.prompt_history_ix = None;
427 let prompt = self.pending_prompt.as_str();
428 self.editor.update(cx, |editor, cx| {
429 editor.set_text(prompt, window, cx);
430 editor.move_to_end(&Default::default(), window, cx)
431 });
432 }
433 } else if self.context_strip.read(cx).has_context_items(cx) {
434 self.context_strip.focus_handle(cx).focus(window);
435 }
436 }
437
438 fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
439 let mode = match &self.mode {
440 PromptEditorMode::Buffer { codegen, .. } => {
441 let codegen = codegen.read(cx);
442 if codegen.is_insertion {
443 GenerationMode::Generate
444 } else {
445 GenerationMode::Transform
446 }
447 }
448 PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
449 };
450
451 let codegen_status = self.codegen_status(cx);
452
453 match codegen_status {
454 CodegenStatus::Idle => {
455 vec![
456 Button::new("start", mode.start_label())
457 .label_size(LabelSize::Small)
458 .icon(IconName::Return)
459 .icon_size(IconSize::XSmall)
460 .icon_color(Color::Muted)
461 .on_click(
462 cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
463 )
464 .into_any_element(),
465 ]
466 }
467 CodegenStatus::Pending => vec![
468 IconButton::new("stop", IconName::Stop)
469 .icon_color(Color::Error)
470 .shape(IconButtonShape::Square)
471 .tooltip(move |window, cx| {
472 Tooltip::with_meta(
473 mode.tooltip_interrupt(),
474 Some(&menu::Cancel),
475 "Changes won't be discarded",
476 window,
477 cx,
478 )
479 })
480 .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
481 .into_any_element(),
482 ],
483 CodegenStatus::Done | CodegenStatus::Error(_) => {
484 let has_error = matches!(codegen_status, CodegenStatus::Error(_));
485 if has_error || self.edited_since_done {
486 vec![
487 IconButton::new("restart", IconName::RotateCw)
488 .icon_color(Color::Info)
489 .shape(IconButtonShape::Square)
490 .tooltip(move |window, cx| {
491 Tooltip::with_meta(
492 mode.tooltip_restart(),
493 Some(&menu::Confirm),
494 "Changes will be discarded",
495 window,
496 cx,
497 )
498 })
499 .on_click(cx.listener(|_, _, _, cx| {
500 cx.emit(PromptEditorEvent::StartRequested);
501 }))
502 .into_any_element(),
503 ]
504 } else {
505 let accept = IconButton::new("accept", IconName::Check)
506 .icon_color(Color::Info)
507 .shape(IconButtonShape::Square)
508 .tooltip(move |window, cx| {
509 Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx)
510 })
511 .on_click(cx.listener(|_, _, _, cx| {
512 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
513 }))
514 .into_any_element();
515
516 match &self.mode {
517 PromptEditorMode::Terminal { .. } => vec![
518 accept,
519 IconButton::new("confirm", IconName::PlayFilled)
520 .icon_color(Color::Info)
521 .shape(IconButtonShape::Square)
522 .tooltip(|window, cx| {
523 Tooltip::for_action(
524 "Execute Generated Command",
525 &menu::SecondaryConfirm,
526 window,
527 cx,
528 )
529 })
530 .on_click(cx.listener(|_, _, _, cx| {
531 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
532 }))
533 .into_any_element(),
534 ],
535 PromptEditorMode::Buffer { .. } => vec![accept],
536 }
537 }
538 }
539 }
540 }
541
542 fn cycle_prev(
543 &mut self,
544 _: &CyclePreviousInlineAssist,
545 _: &mut Window,
546 cx: &mut Context<Self>,
547 ) {
548 match &self.mode {
549 PromptEditorMode::Buffer { codegen, .. } => {
550 codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
551 }
552 PromptEditorMode::Terminal { .. } => {
553 // no cycle buttons in terminal mode
554 }
555 }
556 }
557
558 fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
559 match &self.mode {
560 PromptEditorMode::Buffer { codegen, .. } => {
561 codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
562 }
563 PromptEditorMode::Terminal { .. } => {
564 // no cycle buttons in terminal mode
565 }
566 }
567 }
568
569 fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
570 IconButton::new("cancel", IconName::Close)
571 .icon_color(Color::Muted)
572 .shape(IconButtonShape::Square)
573 .tooltip(Tooltip::text("Close Assistant"))
574 .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
575 .into_any_element()
576 }
577
578 fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
579 let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
580
581 let model_registry = LanguageModelRegistry::read_global(cx);
582 let default_model = model_registry.default_model().map(|default| default.model);
583 let alternative_models = model_registry.inline_alternative_models();
584
585 let get_model_name = |index: usize| -> String {
586 let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
587
588 match index {
589 0 => default_model.as_ref().map_or_else(String::new, name),
590 index if index <= alternative_models.len() => alternative_models
591 .get(index - 1)
592 .map_or_else(String::new, name),
593 _ => String::new(),
594 }
595 };
596
597 let total_models = alternative_models.len() + 1;
598
599 if total_models <= 1 {
600 return div().into_any_element();
601 }
602
603 let current_index = codegen.active_alternative;
604 let prev_index = (current_index + total_models - 1) % total_models;
605 let next_index = (current_index + 1) % total_models;
606
607 let prev_model_name = get_model_name(prev_index);
608 let next_model_name = get_model_name(next_index);
609
610 h_flex()
611 .child(
612 IconButton::new("previous", IconName::ChevronLeft)
613 .icon_color(Color::Muted)
614 .disabled(disabled || current_index == 0)
615 .shape(IconButtonShape::Square)
616 .tooltip({
617 let focus_handle = self.editor.focus_handle(cx);
618 move |window, cx| {
619 cx.new(|cx| {
620 let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
621 KeyBinding::for_action_in(
622 &CyclePreviousInlineAssist,
623 &focus_handle,
624 window,
625 cx,
626 ),
627 );
628 if !disabled && current_index != 0 {
629 tooltip = tooltip.meta(prev_model_name.clone());
630 }
631 tooltip
632 })
633 .into()
634 }
635 })
636 .on_click(cx.listener(|this, _, window, cx| {
637 this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
638 })),
639 )
640 .child(
641 Label::new(format!(
642 "{}/{}",
643 codegen.active_alternative + 1,
644 codegen.alternative_count(cx)
645 ))
646 .size(LabelSize::Small)
647 .color(if disabled {
648 Color::Disabled
649 } else {
650 Color::Muted
651 }),
652 )
653 .child(
654 IconButton::new("next", IconName::ChevronRight)
655 .icon_color(Color::Muted)
656 .disabled(disabled || current_index == total_models - 1)
657 .shape(IconButtonShape::Square)
658 .tooltip({
659 let focus_handle = self.editor.focus_handle(cx);
660 move |window, cx| {
661 cx.new(|cx| {
662 let mut tooltip = Tooltip::new("Next Alternative").key_binding(
663 KeyBinding::for_action_in(
664 &CycleNextInlineAssist,
665 &focus_handle,
666 window,
667 cx,
668 ),
669 );
670 if !disabled && current_index != total_models - 1 {
671 tooltip = tooltip.meta(next_model_name.clone());
672 }
673 tooltip
674 })
675 .into()
676 }
677 })
678 .on_click(cx.listener(|this, _, window, cx| {
679 this.cycle_next(&CycleNextInlineAssist, window, cx)
680 })),
681 )
682 .into_any_element()
683 }
684
685 fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
686 let colors = cx.theme().colors();
687
688 div()
689 .key_context("InlineAssistEditor")
690 .size_full()
691 .p_2()
692 .pl_1()
693 .bg(colors.editor_background)
694 .child({
695 let settings = ThemeSettings::get_global(cx);
696 let font_size = settings.buffer_font_size(cx);
697 let line_height = font_size * 1.2;
698
699 let text_style = TextStyle {
700 color: colors.editor_foreground,
701 font_family: settings.buffer_font.family.clone(),
702 font_features: settings.buffer_font.features.clone(),
703 font_size: font_size.into(),
704 line_height: line_height.into(),
705 ..Default::default()
706 };
707
708 EditorElement::new(
709 &self.editor,
710 EditorStyle {
711 background: colors.editor_background,
712 local_player: cx.theme().players().local(),
713 text: text_style,
714 ..Default::default()
715 },
716 )
717 })
718 .into_any_element()
719 }
720
721 fn handle_context_strip_event(
722 &mut self,
723 _context_strip: &Entity<ContextStrip>,
724 event: &ContextStripEvent,
725 window: &mut Window,
726 cx: &mut Context<Self>,
727 ) {
728 match event {
729 ContextStripEvent::PickerDismissed
730 | ContextStripEvent::BlurredEmpty
731 | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
732 ContextStripEvent::BlurredDown => {}
733 }
734 }
735}
736
737pub enum PromptEditorMode {
738 Buffer {
739 id: InlineAssistId,
740 codegen: Entity<BufferCodegen>,
741 editor_margins: Arc<Mutex<EditorMargins>>,
742 },
743 Terminal {
744 id: TerminalInlineAssistId,
745 codegen: Entity<TerminalCodegen>,
746 height_in_lines: u8,
747 },
748}
749
750pub enum PromptEditorEvent {
751 StartRequested,
752 StopRequested,
753 ConfirmRequested { execute: bool },
754 CancelRequested,
755 Resized { height_in_lines: u8 },
756}
757
758#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
759pub struct InlineAssistId(pub usize);
760
761impl InlineAssistId {
762 pub const fn post_inc(&mut self) -> InlineAssistId {
763 let id = *self;
764 self.0 += 1;
765 id
766 }
767}
768
769impl PromptEditor<BufferCodegen> {
770 pub fn new_buffer(
771 id: InlineAssistId,
772 editor_margins: Arc<Mutex<EditorMargins>>,
773 prompt_history: VecDeque<String>,
774 prompt_buffer: Entity<MultiBuffer>,
775 codegen: Entity<BufferCodegen>,
776 fs: Arc<dyn Fs>,
777 context_store: Entity<ContextStore>,
778 workspace: WeakEntity<Workspace>,
779 thread_store: Option<WeakEntity<HistoryStore>>,
780 prompt_store: Option<WeakEntity<PromptStore>>,
781 window: &mut Window,
782 cx: &mut Context<PromptEditor<BufferCodegen>>,
783 ) -> PromptEditor<BufferCodegen> {
784 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
785 let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
786 let mode = PromptEditorMode::Buffer {
787 id,
788 codegen,
789 editor_margins,
790 };
791
792 let prompt_editor = cx.new(|cx| {
793 let mut editor = Editor::new(
794 EditorMode::AutoHeight {
795 min_lines: 1,
796 max_lines: Some(Self::MAX_LINES as usize),
797 },
798 prompt_buffer,
799 None,
800 window,
801 cx,
802 );
803 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
804 // Since the prompt editors for all inline assistants are linked,
805 // always show the cursor (even when it isn't focused) because
806 // typing in one will make what you typed appear in all of them.
807 editor.set_show_cursor_when_unfocused(true, cx);
808 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
809 editor.register_addon(ContextCreasesAddon::new());
810 editor.set_context_menu_options(ContextMenuOptions {
811 min_entries_visible: 12,
812 max_entries_visible: 12,
813 placement: None,
814 });
815
816 editor
817 });
818
819 let prompt_editor_entity = prompt_editor.downgrade();
820 prompt_editor.update(cx, |editor, _| {
821 editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
822 workspace.clone(),
823 context_store.downgrade(),
824 thread_store.clone(),
825 prompt_store.clone(),
826 prompt_editor_entity,
827 codegen_buffer.as_ref().map(Entity::downgrade),
828 ))));
829 });
830
831 let context_picker_menu_handle = PopoverMenuHandle::default();
832 let model_selector_menu_handle = PopoverMenuHandle::default();
833
834 let context_strip = cx.new(|cx| {
835 ContextStrip::new(
836 context_store.clone(),
837 workspace.clone(),
838 thread_store.clone(),
839 prompt_store,
840 context_picker_menu_handle.clone(),
841 SuggestContextKind::Thread,
842 ModelUsageContext::InlineAssistant,
843 window,
844 cx,
845 )
846 });
847
848 let context_strip_subscription =
849 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
850
851 let mut this: PromptEditor<BufferCodegen> = PromptEditor {
852 editor: prompt_editor.clone(),
853 context_store,
854 context_strip,
855 context_picker_menu_handle,
856 model_selector: cx.new(|cx| {
857 AgentModelSelector::new(
858 fs,
859 model_selector_menu_handle,
860 prompt_editor.focus_handle(cx),
861 ModelUsageContext::InlineAssistant,
862 window,
863 cx,
864 )
865 }),
866 edited_since_done: false,
867 prompt_history,
868 prompt_history_ix: None,
869 pending_prompt: String::new(),
870 _codegen_subscription: codegen_subscription,
871 editor_subscriptions: Vec::new(),
872 _context_strip_subscription: context_strip_subscription,
873 show_rate_limit_notice: false,
874 mode,
875 _phantom: Default::default(),
876 };
877
878 this.subscribe_to_editor(window, cx);
879 this
880 }
881
882 fn handle_codegen_changed(
883 &mut self,
884 _: Entity<BufferCodegen>,
885 cx: &mut Context<PromptEditor<BufferCodegen>>,
886 ) {
887 match self.codegen_status(cx) {
888 CodegenStatus::Idle => {
889 self.editor
890 .update(cx, |editor, _| editor.set_read_only(false));
891 }
892 CodegenStatus::Pending => {
893 self.editor
894 .update(cx, |editor, _| editor.set_read_only(true));
895 }
896 CodegenStatus::Done => {
897 self.edited_since_done = false;
898 self.editor
899 .update(cx, |editor, _| editor.set_read_only(false));
900 }
901 CodegenStatus::Error(_error) => {
902 self.edited_since_done = false;
903 self.editor
904 .update(cx, |editor, _| editor.set_read_only(false));
905 }
906 }
907 }
908
909 pub fn id(&self) -> InlineAssistId {
910 match &self.mode {
911 PromptEditorMode::Buffer { id, .. } => *id,
912 PromptEditorMode::Terminal { .. } => unreachable!(),
913 }
914 }
915
916 pub fn codegen(&self) -> &Entity<BufferCodegen> {
917 match &self.mode {
918 PromptEditorMode::Buffer { codegen, .. } => codegen,
919 PromptEditorMode::Terminal { .. } => unreachable!(),
920 }
921 }
922
923 pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
924 match &self.mode {
925 PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
926 PromptEditorMode::Terminal { .. } => unreachable!(),
927 }
928 }
929}
930
931#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
932pub struct TerminalInlineAssistId(pub usize);
933
934impl TerminalInlineAssistId {
935 pub const fn post_inc(&mut self) -> TerminalInlineAssistId {
936 let id = *self;
937 self.0 += 1;
938 id
939 }
940}
941
942impl PromptEditor<TerminalCodegen> {
943 pub fn new_terminal(
944 id: TerminalInlineAssistId,
945 prompt_history: VecDeque<String>,
946 prompt_buffer: Entity<MultiBuffer>,
947 codegen: Entity<TerminalCodegen>,
948 fs: Arc<dyn Fs>,
949 context_store: Entity<ContextStore>,
950 workspace: WeakEntity<Workspace>,
951 thread_store: Option<WeakEntity<HistoryStore>>,
952 prompt_store: Option<WeakEntity<PromptStore>>,
953 window: &mut Window,
954 cx: &mut Context<Self>,
955 ) -> Self {
956 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
957 let mode = PromptEditorMode::Terminal {
958 id,
959 codegen,
960 height_in_lines: 1,
961 };
962
963 let prompt_editor = cx.new(|cx| {
964 let mut editor = Editor::new(
965 EditorMode::AutoHeight {
966 min_lines: 1,
967 max_lines: Some(Self::MAX_LINES as usize),
968 },
969 prompt_buffer,
970 None,
971 window,
972 cx,
973 );
974 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
975 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
976 editor.set_context_menu_options(ContextMenuOptions {
977 min_entries_visible: 12,
978 max_entries_visible: 12,
979 placement: None,
980 });
981 editor
982 });
983
984 let prompt_editor_entity = prompt_editor.downgrade();
985 prompt_editor.update(cx, |editor, _| {
986 editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
987 workspace.clone(),
988 context_store.downgrade(),
989 thread_store.clone(),
990 prompt_store.clone(),
991 prompt_editor_entity,
992 None,
993 ))));
994 });
995
996 let context_picker_menu_handle = PopoverMenuHandle::default();
997 let model_selector_menu_handle = PopoverMenuHandle::default();
998
999 let context_strip = cx.new(|cx| {
1000 ContextStrip::new(
1001 context_store.clone(),
1002 workspace.clone(),
1003 thread_store.clone(),
1004 prompt_store.clone(),
1005 context_picker_menu_handle.clone(),
1006 SuggestContextKind::Thread,
1007 ModelUsageContext::InlineAssistant,
1008 window,
1009 cx,
1010 )
1011 });
1012
1013 let context_strip_subscription =
1014 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1015
1016 let mut this = Self {
1017 editor: prompt_editor.clone(),
1018 context_store,
1019 context_strip,
1020 context_picker_menu_handle,
1021 model_selector: cx.new(|cx| {
1022 AgentModelSelector::new(
1023 fs,
1024 model_selector_menu_handle.clone(),
1025 prompt_editor.focus_handle(cx),
1026 ModelUsageContext::InlineAssistant,
1027 window,
1028 cx,
1029 )
1030 }),
1031 edited_since_done: false,
1032 prompt_history,
1033 prompt_history_ix: None,
1034 pending_prompt: String::new(),
1035 _codegen_subscription: codegen_subscription,
1036 editor_subscriptions: Vec::new(),
1037 _context_strip_subscription: context_strip_subscription,
1038 mode,
1039 show_rate_limit_notice: false,
1040 _phantom: Default::default(),
1041 };
1042 this.count_lines(cx);
1043 this.subscribe_to_editor(window, cx);
1044 this
1045 }
1046
1047 fn count_lines(&mut self, cx: &mut Context<Self>) {
1048 let height_in_lines = cmp::max(
1049 2, // Make the editor at least two lines tall, to account for padding and buttons.
1050 cmp::min(
1051 self.editor
1052 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1053 Self::MAX_LINES as u32,
1054 ),
1055 ) as u8;
1056
1057 match &mut self.mode {
1058 PromptEditorMode::Terminal {
1059 height_in_lines: current_height,
1060 ..
1061 } => {
1062 if height_in_lines != *current_height {
1063 *current_height = height_in_lines;
1064 cx.emit(PromptEditorEvent::Resized { height_in_lines });
1065 }
1066 }
1067 PromptEditorMode::Buffer { .. } => unreachable!(),
1068 }
1069 }
1070
1071 fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1072 match &self.codegen().read(cx).status {
1073 CodegenStatus::Idle => {
1074 self.editor
1075 .update(cx, |editor, _| editor.set_read_only(false));
1076 }
1077 CodegenStatus::Pending => {
1078 self.editor
1079 .update(cx, |editor, _| editor.set_read_only(true));
1080 }
1081 CodegenStatus::Done | CodegenStatus::Error(_) => {
1082 self.edited_since_done = false;
1083 self.editor
1084 .update(cx, |editor, _| editor.set_read_only(false));
1085 }
1086 }
1087 }
1088
1089 pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1090 match &self.mode {
1091 PromptEditorMode::Buffer { .. } => unreachable!(),
1092 PromptEditorMode::Terminal { codegen, .. } => codegen,
1093 }
1094 }
1095
1096 pub fn id(&self) -> TerminalInlineAssistId {
1097 match &self.mode {
1098 PromptEditorMode::Buffer { .. } => unreachable!(),
1099 PromptEditorMode::Terminal { id, .. } => *id,
1100 }
1101 }
1102}
1103
1104pub enum CodegenStatus {
1105 Idle,
1106 Pending,
1107 Done,
1108 Error(anyhow::Error),
1109}
1110
1111/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1112#[derive(Copy, Clone)]
1113pub enum CancelButtonState {
1114 Idle,
1115 Pending,
1116 Done,
1117 Error,
1118}
1119
1120impl Into<CancelButtonState> for &CodegenStatus {
1121 fn into(self) -> CancelButtonState {
1122 match self {
1123 CodegenStatus::Idle => CancelButtonState::Idle,
1124 CodegenStatus::Pending => CancelButtonState::Pending,
1125 CodegenStatus::Done => CancelButtonState::Done,
1126 CodegenStatus::Error(_) => CancelButtonState::Error,
1127 }
1128 }
1129}
1130
1131#[derive(Copy, Clone)]
1132pub enum GenerationMode {
1133 Generate,
1134 Transform,
1135}
1136
1137impl GenerationMode {
1138 const fn start_label(self) -> &'static str {
1139 match self {
1140 GenerationMode::Generate => "Generate",
1141 GenerationMode::Transform => "Transform",
1142 }
1143 }
1144 const fn tooltip_interrupt(self) -> &'static str {
1145 match self {
1146 GenerationMode::Generate => "Interrupt Generation",
1147 GenerationMode::Transform => "Interrupt Transform",
1148 }
1149 }
1150
1151 const fn tooltip_restart(self) -> &'static str {
1152 match self {
1153 GenerationMode::Generate => "Restart Generation",
1154 GenerationMode::Transform => "Restart Transform",
1155 }
1156 }
1157
1158 const fn tooltip_accept(self) -> &'static str {
1159 match self {
1160 GenerationMode::Generate => "Accept Generation",
1161 GenerationMode::Transform => "Accept Transform",
1162 }
1163 }
1164}