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