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