1148 lines · 49.0 KB · Rust
RawDownload
use std::collections::BTreeMap;use std::sync::Arc; use crate::assistant_model_selector::ModelType;use crate::context::{AssistantContext, format_context_as_string};use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};use buffer_diff::BufferDiff;use collections::HashSet;use editor::actions::{MoveUp, Paste};use editor::{    ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,    EditorStyle, MultiBuffer,};use file_icons::FileIcons;use fs::Fs;use gpui::{    Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,    Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,};use language::{Buffer, Language};use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};use language_model_selector::ToggleModelSelector;use multi_buffer;use project::Project;use settings::Settings;use std::time::Duration;use theme::ThemeSettings;use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};use util::ResultExt as _;use workspace::Workspace; use crate::assistant_model_selector::AssistantModelSelector;use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};use crate::context_store::{ContextStore, refresh_context_store_text};use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};use crate::profile_selector::ProfileSelector;use crate::thread::{Thread, TokenUsageRatio};use crate::thread_store::ThreadStore;use crate::{    AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,    ToggleContextPicker, ToggleProfileSelector,}; pub struct MessageEditor {    thread: Entity<Thread>,    incompatible_tools_state: Entity<IncompatibleToolsState>,    editor: Entity<Editor>,    #[allow(dead_code)]    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    context_store: Entity<ContextStore>,    context_strip: Entity<ContextStrip>,    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,    model_selector: Entity<AssistantModelSelector>,    profile_selector: Entity<ProfileSelector>,    edits_expanded: bool,    editor_is_expanded: bool,    waiting_for_summaries_to_send: bool,    last_estimated_token_count: Option<usize>,    update_token_count_task: Option<Task<anyhow::Result<()>>>,    _subscriptions: Vec<Subscription>,} const MAX_EDITOR_LINES: usize = 8; impl MessageEditor {    pub fn new(        fs: Arc<dyn Fs>,        workspace: WeakEntity<Workspace>,        context_store: Entity<ContextStore>,        thread_store: WeakEntity<ThreadStore>,        thread: Entity<Thread>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let context_picker_menu_handle = PopoverMenuHandle::default();        let model_selector_menu_handle = PopoverMenuHandle::default();         let language = Language::new(            language::LanguageConfig {                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),                ..Default::default()            },            None,        );         let editor = cx.new(|cx| {            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));            let mut editor = Editor::new(                editor::EditorMode::AutoHeight {                    max_lines: MAX_EDITOR_LINES,                },                buffer,                None,                window,                cx,            );            editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);            editor.set_show_indent_guides(false, cx);            editor.set_soft_wrap();            editor.set_context_menu_options(ContextMenuOptions {                min_entries_visible: 12,                max_entries_visible: 12,                placement: Some(ContextMenuPlacement::Above),            });            editor        });         let editor_entity = editor.downgrade();        editor.update(cx, |editor, _| {            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(                workspace.clone(),                context_store.downgrade(),                Some(thread_store.clone()),                editor_entity,            ))));        });         let context_strip = cx.new(|cx| {            ContextStrip::new(                context_store.clone(),                workspace.clone(),                Some(thread_store.clone()),                context_picker_menu_handle.clone(),                SuggestContextKind::File,                window,                cx,            )        });         let incompatible_tools =            cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));         let subscriptions = vec![            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),            cx.subscribe(&editor, |this, _, event, cx| match event {                EditorEvent::BufferEdited => {                    this.message_or_context_changed(true, cx);                }                _ => {}            }),            cx.observe(&context_store, |this, _, cx| {                this.message_or_context_changed(false, cx);            }),        ];         Self {            editor: editor.clone(),            project: thread.read(cx).project().clone(),            thread,            incompatible_tools_state: incompatible_tools.clone(),            workspace,            context_store,            context_strip,            context_picker_menu_handle,            model_selector: cx.new(|cx| {                AssistantModelSelector::new(                    fs.clone(),                    model_selector_menu_handle,                    editor.focus_handle(cx),                    ModelType::Default,                    window,                    cx,                )            }),            edits_expanded: false,            editor_is_expanded: false,            waiting_for_summaries_to_send: false,            profile_selector: cx                .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),            last_estimated_token_count: None,            update_token_count_task: None,            _subscriptions: subscriptions,        }    }     fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {        cx.notify();    }     pub fn expand_message_editor(        &mut self,        _: &ExpandMessageEditor,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        self.set_editor_is_expanded(!self.editor_is_expanded, cx);    }     fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {        self.editor_is_expanded = is_expanded;        self.editor.update(cx, |editor, _| {            if self.editor_is_expanded {                editor.set_mode(EditorMode::Full {                    scale_ui_elements_with_buffer_font_size: false,                    show_active_line_background: false,                })            } else {                editor.set_mode(EditorMode::AutoHeight {                    max_lines: MAX_EDITOR_LINES,                })            }        });        cx.notify();    }     fn toggle_context_picker(        &mut self,        _: &ToggleContextPicker,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.context_picker_menu_handle.toggle(window, cx);    }    pub fn remove_all_context(        &mut self,        _: &RemoveAllContext,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        self.context_store.update(cx, |store, _cx| store.clear());        cx.notify();    }     fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {        if self.is_editor_empty(cx) {            return;        }         if self.thread.read(cx).is_generating() {            self.stop_current_and_send_new_message(window, cx);            return;        }         self.set_editor_is_expanded(false, cx);        self.send_to_model(window, cx);         cx.notify();    }     fn is_editor_empty(&self, cx: &App) -> bool {        self.editor.read(cx).text(cx).trim().is_empty()    }     fn is_model_selected(&self, cx: &App) -> bool {        LanguageModelRegistry::read_global(cx)            .default_model()            .is_some()    }     fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {        let model_registry = LanguageModelRegistry::read_global(cx);        let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {            return;        };         if provider.must_accept_terms(cx) {            cx.notify();            return;        }         let user_message = self.editor.update(cx, |editor, cx| {            let text = editor.text(cx);            editor.clear(window, cx);            text        });         self.last_estimated_token_count.take();        cx.emit(MessageEditorEvent::EstimatedTokenCount);         let refresh_task =            refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);        let wait_for_images = self.context_store.read(cx).wait_for_images(cx);         let thread = self.thread.clone();        let context_store = self.context_store.clone();        let git_store = self.project.read(cx).git_store().clone();        let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));         cx.spawn(async move |this, cx| {            let checkpoint = checkpoint.await.ok();            refresh_task.await;            wait_for_images.await;             thread                .update(cx, |thread, cx| {                    let context = context_store.read(cx).context().clone();                    thread.insert_user_message(user_message, context, checkpoint, cx);                })                .log_err();             context_store                .update(cx, |context_store, cx| {                    let excerpt_ids = context_store                        .context()                        .iter()                        .filter(|ctx| {                            matches!(                                ctx,                                AssistantContext::Selection(_) | AssistantContext::Image(_)                            )                        })                        .map(|ctx| ctx.id())                        .collect::<Vec<_>>();                     for id in excerpt_ids {                        context_store.remove_context(id, cx);                    }                })                .log_err();             if let Some(wait_for_summaries) = context_store                .update(cx, |context_store, cx| context_store.wait_for_summaries(cx))                .log_err()            {                this.update(cx, |this, cx| {                    this.waiting_for_summaries_to_send = true;                    cx.notify();                })                .log_err();                 wait_for_summaries.await;                 this.update(cx, |this, cx| {                    this.waiting_for_summaries_to_send = false;                    cx.notify();                })                .log_err();            }             // Send to model after summaries are done            thread                .update(cx, |thread, cx| {                    thread.advance_prompt_id();                    thread.send_to_model(model, cx);                })                .log_err();        })        .detach();    }     fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {        let cancelled = self            .thread            .update(cx, |thread, cx| thread.cancel_last_completion(cx));         if cancelled {            self.set_editor_is_expanded(false, cx);            self.send_to_model(window, cx);        }    }     fn handle_context_strip_event(        &mut self,        _context_strip: &Entity<ContextStrip>,        event: &ContextStripEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        match event {            ContextStripEvent::PickerDismissed            | ContextStripEvent::BlurredEmpty            | ContextStripEvent::BlurredDown => {                let editor_focus_handle = self.editor.focus_handle(cx);                window.focus(&editor_focus_handle);            }            ContextStripEvent::BlurredUp => {}        }    }     fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {        if self.context_picker_menu_handle.is_deployed() {            cx.propagate();        } else {            self.context_strip.focus_handle(cx).focus(window);        }    }     fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {        let images = cx            .read_from_clipboard()            .map(|item| {                item.into_entries()                    .filter_map(|entry| {                        if let ClipboardEntry::Image(image) = entry {                            Some(image)                        } else {                            None                        }                    })                    .collect::<Vec<_>>()            })            .unwrap_or_default();         if images.is_empty() {            return;        }        cx.stop_propagation();         self.context_store.update(cx, |store, cx| {            for image in images {                store.add_image(Arc::new(image), cx);            }        });    }     fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.edits_expanded = true;        AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();        cx.notify();    }     fn handle_file_click(        &self,        buffer: Entity<Buffer>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if let Ok(diff) = AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx)        {            let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);            diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));        }    }     fn render_editor(        &self,        font_size: Rems,        line_height: Pixels,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Div {        let thread = self.thread.read(cx);         let editor_bg_color = cx.theme().colors().editor_background;        let is_generating = thread.is_generating();        let focus_handle = self.editor.focus_handle(cx);         let is_model_selected = self.is_model_selected(cx);        let is_editor_empty = self.is_editor_empty(cx);         let model = LanguageModelRegistry::read_global(cx)            .default_model()            .map(|default| default.model.clone());         let incompatible_tools = model            .as_ref()            .map(|model| {                self.incompatible_tools_state.update(cx, |state, cx| {                    state                        .incompatible_tools(model, cx)                        .iter()                        .cloned()                        .collect::<Vec<_>>()                })            })            .unwrap_or_default();         let is_editor_expanded = self.editor_is_expanded;        let expand_icon = if is_editor_expanded {            IconName::Minimize        } else {            IconName::Maximize        };         v_flex()            .key_context("MessageEditor")            .on_action(cx.listener(Self::chat))            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {                this.profile_selector                    .read(cx)                    .menu_handle()                    .toggle(window, cx);            }))            .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(Self::toggle_context_picker))            .on_action(cx.listener(Self::remove_all_context))            .on_action(cx.listener(Self::move_up))            .on_action(cx.listener(Self::toggle_chat_mode))            .on_action(cx.listener(Self::expand_message_editor))            .capture_action(cx.listener(Self::paste))            .gap_2()            .p_2()            .bg(editor_bg_color)            .border_t_1()            .border_color(cx.theme().colors().border)            .child(                h_flex()                    .items_start()                    .justify_between()                    .child(self.context_strip.clone())                    .child(                        IconButton::new("toggle-height", expand_icon)                            .icon_size(IconSize::XSmall)                            .icon_color(Color::Muted)                            .tooltip({

Lines 1–500 of 1148.