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