1635 lines · 61.0 KB · Rust
RawDownload
use agent::HistoryStore;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,    actions::{MoveDown, MoveUp},};use feature_flags::{FeatureFlagAppExt, InlineAssistantUseToolFeatureFlag};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::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; 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_store: Entity<HistoryStore>,    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_store.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(), 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) = window.root::<Workspace>().flatten() {                    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()                        .reversed_chars_at(offset)                        .next()                        .map(|sign| sign == '@')                        .unwrap_or(false)                } else {                    false                }            };

Lines 1–500 of 1635.