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