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