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