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 _, ZedProFeatureFlag};
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::{ModelType, 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::<ZedProFeatureFlag>()
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.default_model().map(|default| default.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 ModelType::InlineAssistant,
894 window,
895 cx,
896 )
897 }),
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::<ZedProFeatureFlag>()
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 pub fn new_terminal(
986 id: TerminalInlineAssistId,
987 prompt_history: VecDeque<String>,
988 prompt_buffer: Entity<MultiBuffer>,
989 codegen: Entity<TerminalCodegen>,
990 fs: Arc<dyn Fs>,
991 context_store: Entity<ContextStore>,
992 workspace: WeakEntity<Workspace>,
993 thread_store: Option<WeakEntity<ThreadStore>>,
994 window: &mut Window,
995 cx: &mut Context<Self>,
996 ) -> Self {
997 let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
998 let mode = PromptEditorMode::Terminal {
999 id,
1000 codegen,
1001 height_in_lines: 1,
1002 };
1003
1004 let prompt_editor = cx.new(|cx| {
1005 let mut editor = Editor::new(
1006 EditorMode::AutoHeight {
1007 max_lines: Self::MAX_LINES as usize,
1008 },
1009 prompt_buffer,
1010 None,
1011 window,
1012 cx,
1013 );
1014 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1015 editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
1016 editor
1017 });
1018 let context_picker_menu_handle = PopoverMenuHandle::default();
1019 let model_selector_menu_handle = PopoverMenuHandle::default();
1020
1021 let context_strip = cx.new(|cx| {
1022 ContextStrip::new(
1023 context_store.clone(),
1024 workspace.clone(),
1025 thread_store.clone(),
1026 context_picker_menu_handle.clone(),
1027 SuggestContextKind::Thread,
1028 window,
1029 cx,
1030 )
1031 });
1032
1033 let context_strip_subscription =
1034 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1035
1036 let mut this = Self {
1037 editor: prompt_editor.clone(),
1038 context_store,
1039 context_strip,
1040 context_picker_menu_handle,
1041 model_selector: cx.new(|cx| {
1042 AssistantModelSelector::new(
1043 fs,
1044 model_selector_menu_handle.clone(),
1045 prompt_editor.focus_handle(cx),
1046 ModelType::InlineAssistant,
1047 window,
1048 cx,
1049 )
1050 }),
1051 edited_since_done: false,
1052 prompt_history,
1053 prompt_history_ix: None,
1054 pending_prompt: String::new(),
1055 _codegen_subscription: codegen_subscription,
1056 editor_subscriptions: Vec::new(),
1057 _context_strip_subscription: context_strip_subscription,
1058 mode,
1059 show_rate_limit_notice: false,
1060 _phantom: Default::default(),
1061 };
1062 this.count_lines(cx);
1063 this.subscribe_to_editor(window, cx);
1064 this
1065 }
1066
1067 fn count_lines(&mut self, cx: &mut Context<Self>) {
1068 let height_in_lines = cmp::max(
1069 2, // Make the editor at least two lines tall, to account for padding and buttons.
1070 cmp::min(
1071 self.editor
1072 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1073 Self::MAX_LINES as u32,
1074 ),
1075 ) as u8;
1076
1077 match &mut self.mode {
1078 PromptEditorMode::Terminal {
1079 height_in_lines: current_height,
1080 ..
1081 } => {
1082 if height_in_lines != *current_height {
1083 *current_height = height_in_lines;
1084 cx.emit(PromptEditorEvent::Resized { height_in_lines });
1085 }
1086 }
1087 PromptEditorMode::Buffer { .. } => unreachable!(),
1088 }
1089 }
1090
1091 fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1092 match &self.codegen().read(cx).status {
1093 CodegenStatus::Idle => {
1094 self.editor
1095 .update(cx, |editor, _| editor.set_read_only(false));
1096 }
1097 CodegenStatus::Pending => {
1098 self.editor
1099 .update(cx, |editor, _| editor.set_read_only(true));
1100 }
1101 CodegenStatus::Done | CodegenStatus::Error(_) => {
1102 self.edited_since_done = false;
1103 self.editor
1104 .update(cx, |editor, _| editor.set_read_only(false));
1105 }
1106 }
1107 }
1108
1109 pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1110 match &self.mode {
1111 PromptEditorMode::Buffer { .. } => unreachable!(),
1112 PromptEditorMode::Terminal { codegen, .. } => codegen,
1113 }
1114 }
1115
1116 pub fn id(&self) -> TerminalInlineAssistId {
1117 match &self.mode {
1118 PromptEditorMode::Buffer { .. } => unreachable!(),
1119 PromptEditorMode::Terminal { id, .. } => *id,
1120 }
1121 }
1122}
1123
1124const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
1125
1126fn dismissed_rate_limit_notice() -> bool {
1127 db::kvp::KEY_VALUE_STORE
1128 .read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
1129 .log_err()
1130 .map_or(false, |s| s.is_some())
1131}
1132
1133fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
1134 db::write_and_log(cx, move || async move {
1135 if is_dismissed {
1136 db::kvp::KEY_VALUE_STORE
1137 .write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
1138 .await
1139 } else {
1140 db::kvp::KEY_VALUE_STORE
1141 .delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
1142 .await
1143 }
1144 })
1145}
1146
1147pub enum CodegenStatus {
1148 Idle,
1149 Pending,
1150 Done,
1151 Error(anyhow::Error),
1152}
1153
1154/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1155#[derive(Copy, Clone)]
1156pub enum CancelButtonState {
1157 Idle,
1158 Pending,
1159 Done,
1160 Error,
1161}
1162
1163impl Into<CancelButtonState> for &CodegenStatus {
1164 fn into(self) -> CancelButtonState {
1165 match self {
1166 CodegenStatus::Idle => CancelButtonState::Idle,
1167 CodegenStatus::Pending => CancelButtonState::Pending,
1168 CodegenStatus::Done => CancelButtonState::Done,
1169 CodegenStatus::Error(_) => CancelButtonState::Error,
1170 }
1171 }
1172}
1173
1174#[derive(Copy, Clone)]
1175pub enum GenerationMode {
1176 Generate,
1177 Transform,
1178}
1179
1180impl GenerationMode {
1181 fn start_label(self) -> &'static str {
1182 match self {
1183 GenerationMode::Generate { .. } => "Generate",
1184 GenerationMode::Transform => "Transform",
1185 }
1186 }
1187 fn tooltip_interrupt(self) -> &'static str {
1188 match self {
1189 GenerationMode::Generate { .. } => "Interrupt Generation",
1190 GenerationMode::Transform => "Interrupt Transform",
1191 }
1192 }
1193
1194 fn tooltip_restart(self) -> &'static str {
1195 match self {
1196 GenerationMode::Generate { .. } => "Restart Generation",
1197 GenerationMode::Transform => "Restart Transform",
1198 }
1199 }
1200
1201 fn tooltip_accept(self) -> &'static str {
1202 match self {
1203 GenerationMode::Generate { .. } => "Accept Generation",
1204 GenerationMode::Transform => "Accept Transform",
1205 }
1206 }
1207}