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