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