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