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