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 cx,
477 )
478 })
479 .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
480 .into_any_element(),
481 ],
482 CodegenStatus::Done | CodegenStatus::Error(_) => {
483 let has_error = matches!(codegen_status, CodegenStatus::Error(_));
484 if has_error || self.edited_since_done {
485 vec![
486 IconButton::new("restart", IconName::RotateCw)
487 .icon_color(Color::Info)
488 .shape(IconButtonShape::Square)
489 .tooltip(move |_window, cx| {
490 Tooltip::with_meta(
491 mode.tooltip_restart(),
492 Some(&menu::Confirm),
493 "Changes will be discarded",
494 cx,
495 )
496 })
497 .on_click(cx.listener(|_, _, _, cx| {
498 cx.emit(PromptEditorEvent::StartRequested);
499 }))
500 .into_any_element(),
501 ]
502 } else {
503 let accept = IconButton::new("accept", IconName::Check)
504 .icon_color(Color::Info)
505 .shape(IconButtonShape::Square)
506 .tooltip(move |_window, cx| {
507 Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
508 })
509 .on_click(cx.listener(|_, _, _, cx| {
510 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
511 }))
512 .into_any_element();
513
514 match &self.mode {
515 PromptEditorMode::Terminal { .. } => vec![
516 accept,
517 IconButton::new("confirm", IconName::PlayFilled)
518 .icon_color(Color::Info)
519 .shape(IconButtonShape::Square)
520 .tooltip(|_window, cx| {
521 Tooltip::for_action(
522 "Execute Generated Command",
523 &menu::SecondaryConfirm,
524 cx,
525 )
526 })
527 .on_click(cx.listener(|_, _, _, cx| {
528 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
529 }))
530 .into_any_element(),
531 ],
532 PromptEditorMode::Buffer { .. } => vec![accept],
533 }
534 }
535 }
536 }
537 }
538
539 fn cycle_prev(
540 &mut self,
541 _: &CyclePreviousInlineAssist,
542 _: &mut Window,
543 cx: &mut Context<Self>,
544 ) {
545 match &self.mode {
546 PromptEditorMode::Buffer { codegen, .. } => {
547 codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
548 }
549 PromptEditorMode::Terminal { .. } => {
550 // no cycle buttons in terminal mode
551 }
552 }
553 }
554
555 fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
556 match &self.mode {
557 PromptEditorMode::Buffer { codegen, .. } => {
558 codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
559 }
560 PromptEditorMode::Terminal { .. } => {
561 // no cycle buttons in terminal mode
562 }
563 }
564 }
565
566 fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
567 IconButton::new("cancel", IconName::Close)
568 .icon_color(Color::Muted)
569 .shape(IconButtonShape::Square)
570 .tooltip(Tooltip::text("Close Assistant"))
571 .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
572 .into_any_element()
573 }
574
575 fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
576 let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
577
578 let model_registry = LanguageModelRegistry::read_global(cx);
579 let default_model = model_registry.default_model().map(|default| default.model);
580 let alternative_models = model_registry.inline_alternative_models();
581
582 let get_model_name = |index: usize| -> String {
583 let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
584
585 match index {
586 0 => default_model.as_ref().map_or_else(String::new, name),
587 index if index <= alternative_models.len() => alternative_models
588 .get(index - 1)
589 .map_or_else(String::new, name),
590 _ => String::new(),
591 }
592 };
593
594 let total_models = alternative_models.len() + 1;
595
596 if total_models <= 1 {
597 return div().into_any_element();
598 }
599
600 let current_index = codegen.active_alternative;
601 let prev_index = (current_index + total_models - 1) % total_models;
602 let next_index = (current_index + 1) % total_models;
603
604 let prev_model_name = get_model_name(prev_index);
605 let next_model_name = get_model_name(next_index);
606
607 h_flex()
608 .child(
609 IconButton::new("previous", IconName::ChevronLeft)
610 .icon_color(Color::Muted)
611 .disabled(disabled || current_index == 0)
612 .shape(IconButtonShape::Square)
613 .tooltip({
614 let focus_handle = self.editor.focus_handle(cx);
615 move |_window, cx| {
616 cx.new(|cx| {
617 let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
618 KeyBinding::for_action_in(
619 &CyclePreviousInlineAssist,
620 &focus_handle,
621 cx,
622 ),
623 );
624 if !disabled && current_index != 0 {
625 tooltip = tooltip.meta(prev_model_name.clone());
626 }
627 tooltip
628 })
629 .into()
630 }
631 })
632 .on_click(cx.listener(|this, _, window, cx| {
633 this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
634 })),
635 )
636 .child(
637 Label::new(format!(
638 "{}/{}",
639 codegen.active_alternative + 1,
640 codegen.alternative_count(cx)
641 ))
642 .size(LabelSize::Small)
643 .color(if disabled {
644 Color::Disabled
645 } else {
646 Color::Muted
647 }),
648 )
649 .child(
650 IconButton::new("next", IconName::ChevronRight)
651 .icon_color(Color::Muted)
652 .disabled(disabled || current_index == total_models - 1)
653 .shape(IconButtonShape::Square)
654 .tooltip({
655 let focus_handle = self.editor.focus_handle(cx);
656 move |_window, cx| {
657 cx.new(|cx| {
658 let mut tooltip = Tooltip::new("Next Alternative").key_binding(
659 KeyBinding::for_action_in(
660 &CycleNextInlineAssist,
661 &focus_handle,
662 cx,
663 ),
664 );
665 if !disabled && current_index != total_models - 1 {
666 tooltip = tooltip.meta(next_model_name.clone());
667 }
668 tooltip
669 })
670 .into()
671 }
672 })
673 .on_click(cx.listener(|this, _, window, cx| {
674 this.cycle_next(&CycleNextInlineAssist, window, cx)
675 })),
676 )
677 .into_any_element()
678 }
679
680 fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
681 let colors = cx.theme().colors();
682
683 div()
684 .key_context("InlineAssistEditor")
685 .size_full()
686 .p_2()
687 .pl_1()
688 .bg(colors.editor_background)
689 .child({
690 let settings = ThemeSettings::get_global(cx);
691 let font_size = settings.buffer_font_size(cx);
692 let line_height = font_size * 1.2;
693
694 let text_style = TextStyle {
695 color: colors.editor_foreground,
696 font_family: settings.buffer_font.family.clone(),
697 font_features: settings.buffer_font.features.clone(),
698 font_size: font_size.into(),
699 line_height: line_height.into(),
700 ..Default::default()
701 };
702
703 EditorElement::new(
704 &self.editor,
705 EditorStyle {
706 background: colors.editor_background,
707 local_player: cx.theme().players().local(),
708 text: text_style,
709 ..Default::default()
710 },
711 )
712 })
713 .into_any_element()
714 }
715
716 fn handle_context_strip_event(
717 &mut self,
718 _context_strip: &Entity<ContextStrip>,
719 event: &ContextStripEvent,
720 window: &mut Window,
721 cx: &mut Context<Self>,
722 ) {
723 match event {
724 ContextStripEvent::PickerDismissed
725 | ContextStripEvent::BlurredEmpty
726 | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
727 ContextStripEvent::BlurredDown => {}
728 }
729 }
730}
731
732pub enum PromptEditorMode {
733 Buffer {
734 id: InlineAssistId,
735 codegen: Entity<BufferCodegen>,
736 editor_margins: Arc<Mutex<EditorMargins>>,
737 },
738 Terminal {
739 id: TerminalInlineAssistId,
740 codegen: Entity<TerminalCodegen>,
741 height_in_lines: u8,
742 },
743}
744
745pub enum PromptEditorEvent {
746 StartRequested,
747 StopRequested,
748 ConfirmRequested { execute: bool },
749 CancelRequested,
750 Resized { height_in_lines: u8 },
751}
752
753#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
754pub struct InlineAssistId(pub usize);
755
756impl InlineAssistId {
757 pub fn post_inc(&mut self) -> InlineAssistId {
758 let id = *self;
759 self.0 += 1;
760 id
761 }
762}
763
764impl PromptEditor<BufferCodegen> {
765 pub fn new_buffer(
766 id: InlineAssistId,
767 editor_margins: Arc<Mutex<EditorMargins>>,
768 prompt_history: VecDeque<String>,
769 prompt_buffer: Entity<MultiBuffer>,
770 codegen: Entity<BufferCodegen>,
771 fs: Arc<dyn Fs>,
772 context_store: Entity<ContextStore>,
773 workspace: WeakEntity<Workspace>,
774 thread_store: Option<WeakEntity<HistoryStore>>,
775 prompt_store: Option<WeakEntity<PromptStore>>,
776 window: &mut Window,
777 cx: &mut Context<PromptEditor<BufferCodegen>>,
778 ) -> PromptEditor<BufferCodegen> {
779 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
780 let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
781 let mode = PromptEditorMode::Buffer {
782 id,
783 codegen,
784 editor_margins,
785 };
786
787 let prompt_editor = cx.new(|cx| {
788 let mut editor = Editor::new(
789 EditorMode::AutoHeight {
790 min_lines: 1,
791 max_lines: Some(Self::MAX_LINES as usize),
792 },
793 prompt_buffer,
794 None,
795 window,
796 cx,
797 );
798 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
799 // Since the prompt editors for all inline assistants are linked,
800 // always show the cursor (even when it isn't focused) because
801 // typing in one will make what you typed appear in all of them.
802 editor.set_show_cursor_when_unfocused(true, cx);
803 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
804 editor.register_addon(ContextCreasesAddon::new());
805 editor.set_context_menu_options(ContextMenuOptions {
806 min_entries_visible: 12,
807 max_entries_visible: 12,
808 placement: None,
809 });
810
811 editor
812 });
813
814 let prompt_editor_entity = prompt_editor.downgrade();
815 prompt_editor.update(cx, |editor, _| {
816 editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
817 workspace.clone(),
818 context_store.downgrade(),
819 thread_store.clone(),
820 prompt_store.clone(),
821 prompt_editor_entity,
822 codegen_buffer.as_ref().map(Entity::downgrade),
823 ))));
824 });
825
826 let context_picker_menu_handle = PopoverMenuHandle::default();
827 let model_selector_menu_handle = PopoverMenuHandle::default();
828
829 let context_strip = cx.new(|cx| {
830 ContextStrip::new(
831 context_store.clone(),
832 workspace.clone(),
833 thread_store.clone(),
834 prompt_store,
835 context_picker_menu_handle.clone(),
836 SuggestContextKind::Thread,
837 ModelUsageContext::InlineAssistant,
838 window,
839 cx,
840 )
841 });
842
843 let context_strip_subscription =
844 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
845
846 let mut this: PromptEditor<BufferCodegen> = PromptEditor {
847 editor: prompt_editor.clone(),
848 context_store,
849 context_strip,
850 context_picker_menu_handle,
851 model_selector: cx.new(|cx| {
852 AgentModelSelector::new(
853 fs,
854 model_selector_menu_handle,
855 prompt_editor.focus_handle(cx),
856 ModelUsageContext::InlineAssistant,
857 window,
858 cx,
859 )
860 }),
861 edited_since_done: false,
862 prompt_history,
863 prompt_history_ix: None,
864 pending_prompt: String::new(),
865 _codegen_subscription: codegen_subscription,
866 editor_subscriptions: Vec::new(),
867 _context_strip_subscription: context_strip_subscription,
868 show_rate_limit_notice: false,
869 mode,
870 _phantom: Default::default(),
871 };
872
873 this.subscribe_to_editor(window, cx);
874 this
875 }
876
877 fn handle_codegen_changed(
878 &mut self,
879 _: Entity<BufferCodegen>,
880 cx: &mut Context<PromptEditor<BufferCodegen>>,
881 ) {
882 match self.codegen_status(cx) {
883 CodegenStatus::Idle => {
884 self.editor
885 .update(cx, |editor, _| editor.set_read_only(false));
886 }
887 CodegenStatus::Pending => {
888 self.editor
889 .update(cx, |editor, _| editor.set_read_only(true));
890 }
891 CodegenStatus::Done => {
892 self.edited_since_done = false;
893 self.editor
894 .update(cx, |editor, _| editor.set_read_only(false));
895 }
896 CodegenStatus::Error(_error) => {
897 self.edited_since_done = false;
898 self.editor
899 .update(cx, |editor, _| editor.set_read_only(false));
900 }
901 }
902 }
903
904 pub fn id(&self) -> InlineAssistId {
905 match &self.mode {
906 PromptEditorMode::Buffer { id, .. } => *id,
907 PromptEditorMode::Terminal { .. } => unreachable!(),
908 }
909 }
910
911 pub fn codegen(&self) -> &Entity<BufferCodegen> {
912 match &self.mode {
913 PromptEditorMode::Buffer { codegen, .. } => codegen,
914 PromptEditorMode::Terminal { .. } => unreachable!(),
915 }
916 }
917
918 pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
919 match &self.mode {
920 PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
921 PromptEditorMode::Terminal { .. } => unreachable!(),
922 }
923 }
924}
925
926#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
927pub struct TerminalInlineAssistId(pub usize);
928
929impl TerminalInlineAssistId {
930 pub fn post_inc(&mut self) -> TerminalInlineAssistId {
931 let id = *self;
932 self.0 += 1;
933 id
934 }
935}
936
937impl PromptEditor<TerminalCodegen> {
938 pub fn new_terminal(
939 id: TerminalInlineAssistId,
940 prompt_history: VecDeque<String>,
941 prompt_buffer: Entity<MultiBuffer>,
942 codegen: Entity<TerminalCodegen>,
943 fs: Arc<dyn Fs>,
944 context_store: Entity<ContextStore>,
945 workspace: WeakEntity<Workspace>,
946 thread_store: Option<WeakEntity<HistoryStore>>,
947 prompt_store: Option<WeakEntity<PromptStore>>,
948 window: &mut Window,
949 cx: &mut Context<Self>,
950 ) -> Self {
951 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
952 let mode = PromptEditorMode::Terminal {
953 id,
954 codegen,
955 height_in_lines: 1,
956 };
957
958 let prompt_editor = cx.new(|cx| {
959 let mut editor = Editor::new(
960 EditorMode::AutoHeight {
961 min_lines: 1,
962 max_lines: Some(Self::MAX_LINES as usize),
963 },
964 prompt_buffer,
965 None,
966 window,
967 cx,
968 );
969 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
970 editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
971 editor.set_context_menu_options(ContextMenuOptions {
972 min_entries_visible: 12,
973 max_entries_visible: 12,
974 placement: None,
975 });
976 editor
977 });
978
979 let prompt_editor_entity = prompt_editor.downgrade();
980 prompt_editor.update(cx, |editor, _| {
981 editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
982 workspace.clone(),
983 context_store.downgrade(),
984 thread_store.clone(),
985 prompt_store.clone(),
986 prompt_editor_entity,
987 None,
988 ))));
989 });
990
991 let context_picker_menu_handle = PopoverMenuHandle::default();
992 let model_selector_menu_handle = PopoverMenuHandle::default();
993
994 let context_strip = cx.new(|cx| {
995 ContextStrip::new(
996 context_store.clone(),
997 workspace.clone(),
998 thread_store.clone(),
999 prompt_store.clone(),
1000 context_picker_menu_handle.clone(),
1001 SuggestContextKind::Thread,
1002 ModelUsageContext::InlineAssistant,
1003 window,
1004 cx,
1005 )
1006 });
1007
1008 let context_strip_subscription =
1009 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1010
1011 let mut this = Self {
1012 editor: prompt_editor.clone(),
1013 context_store,
1014 context_strip,
1015 context_picker_menu_handle,
1016 model_selector: cx.new(|cx| {
1017 AgentModelSelector::new(
1018 fs,
1019 model_selector_menu_handle.clone(),
1020 prompt_editor.focus_handle(cx),
1021 ModelUsageContext::InlineAssistant,
1022 window,
1023 cx,
1024 )
1025 }),
1026 edited_since_done: false,
1027 prompt_history,
1028 prompt_history_ix: None,
1029 pending_prompt: String::new(),
1030 _codegen_subscription: codegen_subscription,
1031 editor_subscriptions: Vec::new(),
1032 _context_strip_subscription: context_strip_subscription,
1033 mode,
1034 show_rate_limit_notice: false,
1035 _phantom: Default::default(),
1036 };
1037 this.count_lines(cx);
1038 this.subscribe_to_editor(window, cx);
1039 this
1040 }
1041
1042 fn count_lines(&mut self, cx: &mut Context<Self>) {
1043 let height_in_lines = cmp::max(
1044 2, // Make the editor at least two lines tall, to account for padding and buttons.
1045 cmp::min(
1046 self.editor
1047 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1048 Self::MAX_LINES as u32,
1049 ),
1050 ) as u8;
1051
1052 match &mut self.mode {
1053 PromptEditorMode::Terminal {
1054 height_in_lines: current_height,
1055 ..
1056 } => {
1057 if height_in_lines != *current_height {
1058 *current_height = height_in_lines;
1059 cx.emit(PromptEditorEvent::Resized { height_in_lines });
1060 }
1061 }
1062 PromptEditorMode::Buffer { .. } => unreachable!(),
1063 }
1064 }
1065
1066 fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1067 match &self.codegen().read(cx).status {
1068 CodegenStatus::Idle => {
1069 self.editor
1070 .update(cx, |editor, _| editor.set_read_only(false));
1071 }
1072 CodegenStatus::Pending => {
1073 self.editor
1074 .update(cx, |editor, _| editor.set_read_only(true));
1075 }
1076 CodegenStatus::Done | CodegenStatus::Error(_) => {
1077 self.edited_since_done = false;
1078 self.editor
1079 .update(cx, |editor, _| editor.set_read_only(false));
1080 }
1081 }
1082 }
1083
1084 pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1085 match &self.mode {
1086 PromptEditorMode::Buffer { .. } => unreachable!(),
1087 PromptEditorMode::Terminal { codegen, .. } => codegen,
1088 }
1089 }
1090
1091 pub fn id(&self) -> TerminalInlineAssistId {
1092 match &self.mode {
1093 PromptEditorMode::Buffer { .. } => unreachable!(),
1094 PromptEditorMode::Terminal { id, .. } => *id,
1095 }
1096 }
1097}
1098
1099pub enum CodegenStatus {
1100 Idle,
1101 Pending,
1102 Done,
1103 Error(anyhow::Error),
1104}
1105
1106/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1107#[derive(Copy, Clone)]
1108pub enum CancelButtonState {
1109 Idle,
1110 Pending,
1111 Done,
1112 Error,
1113}
1114
1115impl Into<CancelButtonState> for &CodegenStatus {
1116 fn into(self) -> CancelButtonState {
1117 match self {
1118 CodegenStatus::Idle => CancelButtonState::Idle,
1119 CodegenStatus::Pending => CancelButtonState::Pending,
1120 CodegenStatus::Done => CancelButtonState::Done,
1121 CodegenStatus::Error(_) => CancelButtonState::Error,
1122 }
1123 }
1124}
1125
1126#[derive(Copy, Clone)]
1127pub enum GenerationMode {
1128 Generate,
1129 Transform,
1130}
1131
1132impl GenerationMode {
1133 fn start_label(self) -> &'static str {
1134 match self {
1135 GenerationMode::Generate => "Generate",
1136 GenerationMode::Transform => "Transform",
1137 }
1138 }
1139 fn tooltip_interrupt(self) -> &'static str {
1140 match self {
1141 GenerationMode::Generate => "Interrupt Generation",
1142 GenerationMode::Transform => "Interrupt Transform",
1143 }
1144 }
1145
1146 fn tooltip_restart(self) -> &'static str {
1147 match self {
1148 GenerationMode::Generate => "Restart Generation",
1149 GenerationMode::Transform => "Restart Transform",
1150 }
1151 }
1152
1153 fn tooltip_accept(self) -> &'static str {
1154 match self {
1155 GenerationMode::Generate => "Accept Generation",
1156 GenerationMode::Transform => "Accept Transform",
1157 }
1158 }
1159}