use crate::ThreadHistory;use agent::ThreadStore;use agent_settings::AgentSettings;use collections::{HashMap, VecDeque};use editor::actions::Paste;use editor::code_context_menus::CodeContextMenu;use editor::display_map::{CreaseId, EditorMargins};use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};use editor::{ ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,};use fs::Fs;use gpui::{ AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,};use language_model::{LanguageModel, LanguageModelRegistry};use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};use parking_lot::Mutex;use project::Project;use prompt_store::PromptStore;use settings::Settings;use std::cmp;use std::ops::Range;use std::rc::Rc;use std::sync::Arc;use theme_settings::ThemeSettings;use ui::utils::WithRemSize;use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};use uuid::Uuid;use workspace::notifications::NotificationId;use workspace::{Toast, Workspace};use zed_actions::{ agent::ToggleModelSelector, editor::{MoveDown, MoveUp},}; use crate::agent_model_selector::AgentModelSelector;use crate::buffer_codegen::{BufferCodegen, CodegenAlternative};use crate::completion_provider::{ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,};use crate::mention_set::paste_images_as_context;use crate::mention_set::{MentionSet, crease_for_mention};use crate::terminal_codegen::TerminalCodegen;use crate::{ CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,}; actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]); enum CompletionState { Pending, Generated { completion_text: Option<String> }, Rated,} struct SessionState { session_id: Uuid, completion: CompletionState,} pub struct PromptEditor<T> { pub editor: Entity<Editor>, mode: PromptEditorMode, mention_set: Entity<MentionSet>, history: Option<WeakEntity<ThreadHistory>>, prompt_store: Option<Entity<PromptStore>>, workspace: WeakEntity<Workspace>, model_selector: Entity<AgentModelSelector>, edited_since_done: bool, prompt_history: VecDeque<String>, prompt_history_ix: Option<usize>, pending_prompt: String, _codegen_subscription: Subscription, editor_subscriptions: Vec<Subscription>, show_rate_limit_notice: bool, session_state: SessionState, _phantom: std::marker::PhantomData<T>,} impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {} impl<T: 'static> Render for PromptEditor<T> { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let mut buttons = Vec::new(); const RIGHT_PADDING: Pixels = px(9.); let (left_gutter_width, right_padding, explanation) = match &self.mode { PromptEditorMode::Buffer { id: _, codegen, editor_margins, } => { let codegen = codegen.read(cx); if codegen.alternative_count(cx) > 1 { buttons.push(self.render_cycle_controls(codegen, cx)); } let editor_margins = editor_margins.lock(); let gutter = editor_margins.gutter; let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0); let right_padding = editor_margins.right + RIGHT_PADDING; let active_alternative = codegen.active_alternative().read(cx); let explanation = active_alternative .description .clone() .or_else(|| active_alternative.failure.clone()); (left_gutter_width, right_padding, explanation) } PromptEditorMode::Terminal { .. } => { // Give the equivalent of the same left-padding that we're using on the right (Pixels::from(40.0), Pixels::from(24.), None) } }; let bottom_padding = match &self.mode { PromptEditorMode::Buffer { .. } => rems_from_px(2.0), PromptEditorMode::Terminal { .. } => rems_from_px(4.0), }; buttons.extend(self.render_buttons(window, cx)); let menu_visible = self.is_completions_menu_visible(cx); let add_context_button = IconButton::new("add-context", IconName::AtSign) .icon_size(IconSize::Small) .icon_color(Color::Muted) .when(!menu_visible, |this| { this.tooltip(move |_window, cx| { Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx) }) }) .on_click(cx.listener(move |this, _, window, cx| { this.trigger_completion_menu(window, cx); })); let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx)); if let Some(explanation) = &explanation { markdown.update(cx, |markdown, cx| { markdown.reset(SharedString::from(explanation), cx); }); } let explanation_label = self .render_markdown(markdown, markdown_style(window, cx)) .into_any_element(); v_flex() .key_context("InlineAssistant") .capture_action(cx.listener(Self::paste)) .block_mouse_except_scroll() .size_full() .pt_0p5() .pb(bottom_padding) .pr(right_padding) .gap_0p5() .justify_center() .border_y_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) .child( h_flex() .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_down)) .on_action(cx.listener(Self::thumbs_up)) .on_action(cx.listener(Self::thumbs_down)) .capture_action(cx.listener(Self::cycle_prev)) .capture_action(cx.listener(Self::cycle_next)) .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { this.model_selector .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); })) .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| { this.model_selector.update(cx, |model_selector, cx| { model_selector.cycle_favorite_models(window, cx); }); })) .child( WithRemSize::new(ui_font_size) .h_full() .w(left_gutter_width) .flex() .flex_row() .flex_shrink_0() .items_center() .justify_center() .gap_1() .child(self.render_close_button(cx)) .map(|el| { let CodegenStatus::Error(error) = self.codegen_status(cx) else { return el; }; let error_message = SharedString::from(error.to_string()); el.child( div() .id("error") .tooltip(Tooltip::text(error_message)) .child( Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ), ) }), ) .child( h_flex() .w_full() .justify_between() .child(div().flex_1().child(self.render_editor(window, cx))) .child( WithRemSize::new(ui_font_size) .flex() .flex_row() .items_center() .gap_1() .child(add_context_button) .child(self.model_selector.clone()) .children(buttons), ), ), ) .when_some(explanation, |this, _| { this.child( h_flex() .size_full() .justify_center() .child(div().w(left_gutter_width + px(6.))) .child( div() .size_full() .min_w_0() .pt(rems_from_px(3.)) .pl_0p5() .flex_1() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(explanation_label), ), ) }) }} fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); let mut text_style = window.text_style(); text_style.refine(&TextStyleRefinement { font_family: Some(theme_settings.ui_font.family.clone()), color: Some(colors.text), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), selection_background_color: colors.element_selection_background, heading_level_styles: Some(HeadingLevelStyles { h1: Some(TextStyleRefinement { font_size: Some(rems(1.15).into()), ..Default::default() }), h2: Some(TextStyleRefinement { font_size: Some(rems(1.1).into()), ..Default::default() }), h3: Some(TextStyleRefinement { font_size: Some(rems(1.05).into()), ..Default::default() }), h4: Some(TextStyleRefinement { font_size: Some(rems(1.).into()), ..Default::default() }), h5: Some(TextStyleRefinement { font_size: Some(rems(0.95).into()), ..Default::default() }), h6: Some(TextStyleRefinement { font_size: Some(rems(0.875).into()), ..Default::default() }), }), inline_code: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), background_color: Some(colors.editor_foreground.opacity(0.08)), ..Default::default() }, ..Default::default() }} impl<T: 'static> Focusable for PromptEditor<T> { fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) }} impl<T: 'static> PromptEditor<T> { const MAX_LINES: u8 = 8; fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus { match &self.mode { PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx), PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status, } } fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor_subscriptions.clear(); self.editor_subscriptions.push(cx.subscribe_in( &self.editor, window, Self::handle_prompt_editor_events, )); } fn assign_completion_provider(&mut self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new( PromptEditorCompletionProviderDelegate, cx.weak_entity(), self.mention_set.clone(), self.history.clone(), self.prompt_store.clone(), self.workspace.clone(), )))); }); } pub fn set_show_cursor_when_unfocused( &mut self, show_cursor_when_unfocused: bool, cx: &mut Context<Self>, ) { self.editor.update(cx, |editor, cx| { editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx) }); } pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) { let prompt = self.prompt(cx); let existing_creases = self.editor.update(cx, |editor, cx| { extract_message_creases(editor, &self.mention_set, window, cx) }); let focus = self.editor.focus_handle(cx).contains_focused(window, cx); let mut creases = vec![]; self.editor = cx.new(|cx| { let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text("Add a prompt…", window, cx); editor.set_text(prompt, window, cx); creases = insert_message_creases(&mut editor, &existing_creases, window, cx); if focus { window.focus(&editor.focus_handle(cx), cx); } editor }); self.mention_set.update(cx, |mention_set, _cx| { debug_assert_eq!( creases.len(), mention_set.creases().len(), "Missing creases" ); let mentions = mention_set .clear() .zip(creases) .map(|((_, value), id)| (id, value)) .collect::<HashMap<_, _>>(); mention_set.set_mentions(mentions); }); self.assign_completion_provider(cx); self.subscribe_to_editor(window, cx); } pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String { let action = match mode { PromptEditorMode::Buffer { codegen, .. } => { if codegen.read(cx).is_insertion { "Generate" } else { "Transform" } } PromptEditorMode::Terminal { .. } => "Generate", }; let agent_panel_keybinding = ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx) .map(|keybinding| format!("{keybinding} to chat")) .unwrap_or_default(); format!("{action}… ({agent_panel_keybinding} ― ↓↑ for history — @ to include context)") } pub fn prompt(&self, cx: &App) -> String { self.editor.read(cx).text(cx) } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { if inline_assistant_model_supports_images(cx) && let Some(task) = paste_images_as_context( self.editor.clone(), self.mention_set.clone(), self.workspace.clone(), window, cx, ) { task.detach(); } } fn handle_prompt_editor_events( &mut self, editor: &Entity<Editor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::Edited { .. } => { let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); self.mention_set .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); workspace .client() .telemetry() .log_edit_event("inline assist", is_via_ssh); }); } let prompt = snapshot.text(); if self .prompt_history_ix .is_none_or(|ix| self.prompt_history[ix] != prompt) { self.prompt_history_ix.take(); self.pending_prompt = prompt; } self.edited_since_done = true; self.session_state.completion = CompletionState::Pending; cx.notify(); } EditorEvent::Blurred => { if self.show_rate_limit_notice { self.show_rate_limit_notice = false; cx.notify(); } } _ => {} } } pub fn is_completions_menu_visible(&self, cx: &App) -> bool { self.editor .read(cx) .context_menu() .borrow() .as_ref() .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) } pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| { matches!(menu, CodeContextMenu::Completions(_)) && menu.visible() }); let has_at_sign = { let snapshot = editor.display_snapshot(cx); let cursor = editor.selections.newest::<text::Point>(&snapshot).head(); let offset = cursor.to_offset(&snapshot); if offset.0 > 0 { snapshot .buffer_snapshot()Lines 1–500 of 1646. Load more
use crate::ThreadHistory; use agent::ThreadStore; use agent_settings::AgentSettings; use collections::{HashMap, VecDeque}; use editor::actions::Paste; use editor::code_context_menus::CodeContextMenu; use editor::display_map::{CreaseId, EditorMargins}; use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _}; use editor::{ ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, }; use fs::Fs; use gpui::{ AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions, }; use language_model::{LanguageModel, LanguageModelRegistry}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::Project; use prompt_store::PromptStore; use settings::Settings; use std::cmp; use std::ops::Range; use std::rc::Rc; use std::sync::Arc; use theme_settings::ThemeSettings; use ui::utils::WithRemSize; use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; use uuid::Uuid; use workspace::notifications::NotificationId; use workspace::{Toast, Workspace}; use zed_actions::{ agent::ToggleModelSelector, editor::{MoveDown, MoveUp}, }; use crate::agent_model_selector::AgentModelSelector; use crate::buffer_codegen::{BufferCodegen, CodegenAlternative}; use crate::completion_provider::{ PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType, }; use crate::mention_set::paste_images_as_context; use crate::mention_set::{MentionSet, crease_for_mention}; use crate::terminal_codegen::TerminalCodegen; use crate::{ CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, }; actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]); enum CompletionState { Pending, Generated { completion_text: Option<String> }, Rated, } struct SessionState { session_id: Uuid, completion: CompletionState, } pub struct PromptEditor<T> { pub editor: Entity<Editor>, mode: PromptEditorMode, mention_set: Entity<MentionSet>, history: Option<WeakEntity<ThreadHistory>>, prompt_store: Option<Entity<PromptStore>>, workspace: WeakEntity<Workspace>, model_selector: Entity<AgentModelSelector>, edited_since_done: bool, prompt_history: VecDeque<String>, prompt_history_ix: Option<usize>, pending_prompt: String, _codegen_subscription: Subscription, editor_subscriptions: Vec<Subscription>, show_rate_limit_notice: bool, session_state: SessionState, _phantom: std::marker::PhantomData<T>, } impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {} impl<T: 'static> Render for PromptEditor<T> { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let mut buttons = Vec::new(); const RIGHT_PADDING: Pixels = px(9.); let (left_gutter_width, right_padding, explanation) = match &self.mode { PromptEditorMode::Buffer { id: _, codegen, editor_margins, } => { let codegen = codegen.read(cx); if codegen.alternative_count(cx) > 1 { buttons.push(self.render_cycle_controls(codegen, cx)); } let editor_margins = editor_margins.lock(); let gutter = editor_margins.gutter; let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0); let right_padding = editor_margins.right + RIGHT_PADDING; let active_alternative = codegen.active_alternative().read(cx); let explanation = active_alternative .description .clone() .or_else(|| active_alternative.failure.clone()); (left_gutter_width, right_padding, explanation) } PromptEditorMode::Terminal { .. } => { // Give the equivalent of the same left-padding that we're using on the right (Pixels::from(40.0), Pixels::from(24.), None) } }; let bottom_padding = match &self.mode { PromptEditorMode::Buffer { .. } => rems_from_px(2.0), PromptEditorMode::Terminal { .. } => rems_from_px(4.0), }; buttons.extend(self.render_buttons(window, cx)); let menu_visible = self.is_completions_menu_visible(cx); let add_context_button = IconButton::new("add-context", IconName::AtSign) .icon_size(IconSize::Small) .icon_color(Color::Muted) .when(!menu_visible, |this| { this.tooltip(move |_window, cx| { Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx) }) }) .on_click(cx.listener(move |this, _, window, cx| { this.trigger_completion_menu(window, cx); })); let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx)); if let Some(explanation) = &explanation { markdown.update(cx, |markdown, cx| { markdown.reset(SharedString::from(explanation), cx); }); } let explanation_label = self .render_markdown(markdown, markdown_style(window, cx)) .into_any_element(); v_flex() .key_context("InlineAssistant") .capture_action(cx.listener(Self::paste)) .block_mouse_except_scroll() .size_full() .pt_0p5() .pb(bottom_padding) .pr(right_padding) .gap_0p5() .justify_center() .border_y_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) .child( h_flex() .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_down)) .on_action(cx.listener(Self::thumbs_up)) .on_action(cx.listener(Self::thumbs_down)) .capture_action(cx.listener(Self::cycle_prev)) .capture_action(cx.listener(Self::cycle_next)) .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { this.model_selector .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); })) .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| { this.model_selector.update(cx, |model_selector, cx| { model_selector.cycle_favorite_models(window, cx); }); })) .child( WithRemSize::new(ui_font_size) .h_full() .w(left_gutter_width) .flex() .flex_row() .flex_shrink_0() .items_center() .justify_center() .gap_1() .child(self.render_close_button(cx)) .map(|el| { let CodegenStatus::Error(error) = self.codegen_status(cx) else { return el; }; let error_message = SharedString::from(error.to_string()); el.child( div() .id("error") .tooltip(Tooltip::text(error_message)) .child( Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ), ) }), ) .child( h_flex() .w_full() .justify_between() .child(div().flex_1().child(self.render_editor(window, cx))) .child( WithRemSize::new(ui_font_size) .flex() .flex_row() .items_center() .gap_1() .child(add_context_button) .child(self.model_selector.clone()) .children(buttons), ), ), ) .when_some(explanation, |this, _| { this.child( h_flex() .size_full() .justify_center() .child(div().w(left_gutter_width + px(6.))) .child( div() .size_full() .min_w_0() .pt(rems_from_px(3.)) .pl_0p5() .flex_1() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(explanation_label), ), ) }) } } fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); let mut text_style = window.text_style(); text_style.refine(&TextStyleRefinement { font_family: Some(theme_settings.ui_font.family.clone()), color: Some(colors.text), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), selection_background_color: colors.element_selection_background, heading_level_styles: Some(HeadingLevelStyles { h1: Some(TextStyleRefinement { font_size: Some(rems(1.15).into()), ..Default::default() }), h2: Some(TextStyleRefinement { font_size: Some(rems(1.1).into()), ..Default::default() }), h3: Some(TextStyleRefinement { font_size: Some(rems(1.05).into()), ..Default::default() }), h4: Some(TextStyleRefinement { font_size: Some(rems(1.).into()), ..Default::default() }), h5: Some(TextStyleRefinement { font_size: Some(rems(0.95).into()), ..Default::default() }), h6: Some(TextStyleRefinement { font_size: Some(rems(0.875).into()), ..Default::default() }), }), inline_code: TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), font_features: Some(theme_settings.buffer_font.features.clone()), background_color: Some(colors.editor_foreground.opacity(0.08)), ..Default::default() }, ..Default::default() } } impl<T: 'static> Focusable for PromptEditor<T> { fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) } } impl<T: 'static> PromptEditor<T> { const MAX_LINES: u8 = 8; fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus { match &self.mode { PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx), PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status, } } fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor_subscriptions.clear(); self.editor_subscriptions.push(cx.subscribe_in( &self.editor, window, Self::handle_prompt_editor_events, )); } fn assign_completion_provider(&mut self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new( PromptEditorCompletionProviderDelegate, cx.weak_entity(), self.mention_set.clone(), self.history.clone(), self.prompt_store.clone(), self.workspace.clone(), )))); }); } pub fn set_show_cursor_when_unfocused( &mut self, show_cursor_when_unfocused: bool, cx: &mut Context<Self>, ) { self.editor.update(cx, |editor, cx| { editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx) }); } pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) { let prompt = self.prompt(cx); let existing_creases = self.editor.update(cx, |editor, cx| { extract_message_creases(editor, &self.mention_set, window, cx) }); let focus = self.editor.focus_handle(cx).contains_focused(window, cx); let mut creases = vec![]; self.editor = cx.new(|cx| { let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text("Add a prompt…", window, cx); editor.set_text(prompt, window, cx); creases = insert_message_creases(&mut editor, &existing_creases, window, cx); if focus { window.focus(&editor.focus_handle(cx), cx); } editor }); self.mention_set.update(cx, |mention_set, _cx| { debug_assert_eq!( creases.len(), mention_set.creases().len(), "Missing creases" ); let mentions = mention_set .clear() .zip(creases) .map(|((_, value), id)| (id, value)) .collect::<HashMap<_, _>>(); mention_set.set_mentions(mentions); }); self.assign_completion_provider(cx); self.subscribe_to_editor(window, cx); } pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String { let action = match mode { PromptEditorMode::Buffer { codegen, .. } => { if codegen.read(cx).is_insertion { "Generate" } else { "Transform" } } PromptEditorMode::Terminal { .. } => "Generate", }; let agent_panel_keybinding = ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx) .map(|keybinding| format!("{keybinding} to chat")) .unwrap_or_default(); format!("{action}… ({agent_panel_keybinding} ― ↓↑ for history — @ to include context)") } pub fn prompt(&self, cx: &App) -> String { self.editor.read(cx).text(cx) } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { if inline_assistant_model_supports_images(cx) && let Some(task) = paste_images_as_context( self.editor.clone(), self.mention_set.clone(), self.workspace.clone(), window, cx, ) { task.detach(); } } fn handle_prompt_editor_events( &mut self, editor: &Entity<Editor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::Edited { .. } => { let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); self.mention_set .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); workspace .client() .telemetry() .log_edit_event("inline assist", is_via_ssh); }); } let prompt = snapshot.text(); if self .prompt_history_ix .is_none_or(|ix| self.prompt_history[ix] != prompt) { self.prompt_history_ix.take(); self.pending_prompt = prompt; } self.edited_since_done = true; self.session_state.completion = CompletionState::Pending; cx.notify(); } EditorEvent::Blurred => { if self.show_rate_limit_notice { self.show_rate_limit_notice = false; cx.notify(); } } _ => {} } } pub fn is_completions_menu_visible(&self, cx: &App) -> bool { self.editor .read(cx) .context_menu() .borrow() .as_ref() .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) } pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| { matches!(menu, CodeContextMenu::Completions(_)) && menu.visible() }); let has_at_sign = { let snapshot = editor.display_snapshot(cx); let cursor = editor.selections.newest::<text::Point>(&snapshot).head(); let offset = cursor.to_offset(&snapshot); if offset.0 > 0 { snapshot .buffer_snapshot()