3479 lines · 129.9 KB · Rust
RawDownload
use crate::{    language_model_selector::{LanguageModelSelector, language_model_selector},    ui::ModelSelectorTooltip,};use anyhow::Result;use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};use client::{proto, zed_urls};use collections::{BTreeSet, HashMap, HashSet, hash_map};use editor::{    Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,    MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,    actions::{MoveToEndOfLine, Newline, ShowCompletions},    display_map::{        BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,        RenderBlock, ToDisplayPoint,    },    scroll::ScrollOffset,};use editor::{FoldPlaceholder, display_map::CreaseId};use fs::Fs;use futures::FutureExt;use gpui::{    Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,    EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,    ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,    Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,    pulsating_between, size,};use language::{    BufferSnapshot, LspAdapterDelegate, ToOffset,    language_settings::{SoftWrap, all_language_settings},};use language_model::{    ConfigurationError, IconOrSvg, LanguageModelImage, LanguageModelRegistry, Role,};use multi_buffer::MultiBufferRow;use picker::{Picker, popover_menu::PickerPopoverMenu};use project::{Project, Worktree};use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};use rope::Point;use serde::{Deserialize, Serialize};use settings::{    LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,    update_settings_file,};use std::{    any::{Any, TypeId},    cmp,    ops::Range,    path::{Path, PathBuf},    rc::Rc,    sync::Arc,    time::Duration,};use text::SelectionGoal;use ui::{    ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,    TintColor, Tooltip, prelude::*,};use util::{ResultExt, maybe};use workspace::{    CollaboratorId,    searchable::{Direction, SearchToken, SearchableItemHandle},}; use workspace::{    Save, Toast, Workspace,    item::{self, FollowableItem, Item},    notifications::NotificationId,    pane,    searchable::{SearchEvent, SearchableItem},};use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector}; use crate::CycleFavoriteModels; use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};use assistant_text_thread::{    CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,    MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,    TextThreadId, ThoughtProcessOutputSection,}; actions!(    assistant,    [        /// Sends the current message to the assistant.        Assist,        /// Confirms and executes the entered slash command.        ConfirmCommand,        /// Copies code from the assistant's response to the clipboard.        CopyCode,        /// Cycles between user and assistant message roles.        CycleMessageRole,        /// Inserts the selected text into the active editor.        InsertIntoEditor,        /// Splits the conversation at the current cursor position.        Split,    ]); /// Inserts files that were dragged and dropped into the assistant conversation.#[derive(PartialEq, Clone, Action)]#[action(namespace = assistant, no_json, no_register)]pub enum InsertDraggedFiles {    ProjectPaths(Vec<ProjectPath>),    ExternalFiles(Vec<PathBuf>),} #[derive(Copy, Clone, Debug, PartialEq)]struct ScrollPosition {    offset_before_cursor: gpui::Point<ScrollOffset>,    cursor: Anchor,} type MessageHeader = MessageMetadata; #[derive(Clone)]enum AssistError {    PaymentRequired,    Message(SharedString),} pub enum ThoughtProcessStatus {    Pending,    Completed,} pub trait AgentPanelDelegate {    fn active_text_thread_editor(        &self,        workspace: &mut Workspace,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Option<Entity<TextThreadEditor>>;     fn open_local_text_thread(        &self,        workspace: &mut Workspace,        path: Arc<Path>,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Task<Result<()>>;     fn open_remote_text_thread(        &self,        workspace: &mut Workspace,        text_thread_id: TextThreadId,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Task<Result<Entity<TextThreadEditor>>>;     fn quote_selection(        &self,        workspace: &mut Workspace,        selection_ranges: Vec<Range<Anchor>>,        buffer: Entity<MultiBuffer>,        window: &mut Window,        cx: &mut Context<Workspace>,    );     fn quote_terminal_text(        &self,        workspace: &mut Workspace,        text: String,        window: &mut Window,        cx: &mut Context<Workspace>,    );} impl dyn AgentPanelDelegate {    /// Returns the global [`AssistantPanelDelegate`], if it exists.    pub fn try_global(cx: &App) -> Option<Arc<Self>> {        cx.try_global::<GlobalAssistantPanelDelegate>()            .map(|global| global.0.clone())    }     /// Sets the global [`AssistantPanelDelegate`].    pub fn set_global(delegate: Arc<Self>, cx: &mut App) {        cx.set_global(GlobalAssistantPanelDelegate(delegate));    }} struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>); impl Global for GlobalAssistantPanelDelegate {} pub struct TextThreadEditor {    text_thread: Entity<TextThread>,    fs: Arc<dyn Fs>,    slash_commands: Arc<SlashCommandWorkingSet>,    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,    editor: Entity<Editor>,    pending_thought_process: Option<(CreaseId, language::Anchor)>,    blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,    image_blocks: HashSet<CustomBlockId>,    scroll_position: Option<ScrollPosition>,    remote_id: Option<workspace::ViewId>,    pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,    invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,    _subscriptions: Vec<Subscription>,    last_error: Option<AssistError>,    pub(crate) slash_menu_handle:        PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,    // dragged_file_worktrees is used to keep references to worktrees that were added    // when the user drag/dropped an external file onto the context editor. Since    // the worktree is not part of the project panel, it would be dropped as soon as    // the file is opened. In order to keep the worktree alive for the duration of the    // context editor, we keep a reference here.    dragged_file_worktrees: Vec<Entity<Worktree>>,    language_model_selector: Entity<LanguageModelSelector>,    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,} const MAX_TAB_TITLE_LEN: usize = 16; impl TextThreadEditor {    pub fn init(cx: &mut App) {        workspace::FollowableViewRegistry::register::<TextThreadEditor>(cx);         cx.observe_new(            |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {                workspace                    .register_action(TextThreadEditor::quote_selection)                    .register_action(TextThreadEditor::insert_selection)                    .register_action(TextThreadEditor::copy_code)                    .register_action(TextThreadEditor::handle_insert_dragged_files);            },        )        .detach();    }     pub fn for_text_thread(        text_thread: Entity<TextThread>,        fs: Arc<dyn Fs>,        workspace: WeakEntity<Workspace>,        project: Entity<Project>,        lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let completion_provider = SlashCommandCompletionProvider::new(            text_thread.read(cx).slash_commands().clone(),            Some(cx.entity().downgrade()),            Some(workspace.clone()),        );         let editor = cx.new(|cx| {            let mut editor =                Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);            editor.disable_scrollbars_and_minimap(window, cx);            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);            editor.set_show_line_numbers(false, cx);            editor.set_show_git_diff_gutter(false, cx);            editor.set_show_code_actions(false, cx);            editor.set_show_runnables(false, cx);            editor.set_show_breakpoints(false, cx);            editor.set_show_wrap_guides(false, cx);            editor.set_show_indent_guides(false, cx);            editor.set_completion_provider(Some(Rc::new(completion_provider)));            editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);            editor.set_collaboration_hub(Box::new(project.clone()));             let show_edit_predictions = all_language_settings(None, cx)                .edit_predictions                .enabled_in_text_threads;             editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);             editor        });         let _subscriptions = vec![            cx.observe(&text_thread, |_, _, cx| cx.notify()),            cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),            cx.subscribe_in(&editor, window, Self::handle_editor_event),            cx.subscribe_in(&editor, window, Self::handle_editor_search_event),            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),        ];         let slash_command_sections = text_thread            .read(cx)            .slash_command_output_sections()            .to_vec();        let thought_process_sections = text_thread            .read(cx)            .thought_process_output_sections()            .to_vec();        let slash_commands = text_thread.read(cx).slash_commands().clone();        let focus_handle = editor.read(cx).focus_handle(cx);         let mut this = Self {            text_thread,            slash_commands,            editor,            lsp_adapter_delegate,            blocks: Default::default(),            image_blocks: Default::default(),            scroll_position: None,            remote_id: None,            pending_thought_process: None,            fs: fs.clone(),            workspace,            project,            pending_slash_command_creases: HashMap::default(),            invoked_slash_command_creases: HashMap::default(),            _subscriptions,            last_error: None,            slash_menu_handle: Default::default(),            dragged_file_worktrees: Vec::new(),            language_model_selector: cx.new(|cx| {                language_model_selector(                    |cx| LanguageModelRegistry::read_global(cx).default_model(),                    {                        let fs = fs.clone();                        move |model, cx| {                            update_settings_file(fs.clone(), cx, move |settings, _| {                                let provider = model.provider_id().0.to_string();                                let model_id = model.id().0.to_string();                                settings.agent.get_or_insert_default().set_model(                                    LanguageModelSelection {                                        provider: LanguageModelProviderSetting(provider),                                        model: model_id,                                        enable_thinking: model.supports_thinking(),                                        effort: model                                            .default_effort_level()                                            .map(|effort| effort.value.to_string()),                                    },                                )                            });                        }                    },                    {                        let fs = fs.clone();                        move |model, should_be_favorite, cx| {                            crate::favorite_models::toggle_in_settings(                                model,                                should_be_favorite,                                fs.clone(),                                cx,                            );                        }                    },                    true, // Use popover styles for picker                    focus_handle,                    window,                    cx,                )            }),            language_model_selector_menu_handle: PopoverMenuHandle::default(),        };        this.update_message_headers(cx);        this.update_image_blocks(cx);        this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);        this.insert_thought_process_output_sections(            thought_process_sections                .into_iter()                .map(|section| (section, ThoughtProcessStatus::Completed)),            window,            cx,        );        this    }     fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.editor.update(cx, |editor, cx| {            let show_edit_predictions = all_language_settings(None, cx)                .edit_predictions                .enabled_in_text_threads;             editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);        });    }     pub fn text_thread(&self) -> &Entity<TextThread> {        &self.text_thread    }     pub fn editor(&self) -> &Entity<Editor> {        &self.editor    }     pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {        let command_name = DefaultSlashCommand.name();        self.editor.update(cx, |editor, cx| {            editor.insert(&format!("/{command_name}\n\n"), window, cx)        });        let command = self.text_thread.update(cx, |text_thread, cx| {            text_thread.reparse(cx);            text_thread.parsed_slash_commands()[0].clone()        });        self.run_command(            command.source_range,            &command.name,            &command.arguments,            false,            self.workspace.clone(),            window,            cx,        );    }     fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {        if self.sending_disabled(cx) {            return;        }        telemetry::event!("Agent Message Sent", agent = "zed-text");        self.send_to_model(window, cx);    }     fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.last_error = None;        if let Some(user_message) = self            .text_thread            .update(cx, |text_thread, cx| text_thread.assist(cx))        {            let new_selection = {                let cursor = user_message                    .start                    .to_offset(self.text_thread.read(cx).buffer().read(cx));                MultiBufferOffset(cursor)..MultiBufferOffset(cursor)            };            self.editor.update(cx, |editor, cx| {                editor.change_selections(Default::default(), window, cx, |selections| {                    selections.select_ranges([new_selection])                });            });            // Avoid scrolling to the new cursor position so the assistant's output is stable.            cx.defer_in(window, |this, _, _| this.scroll_position = None);        }         cx.notify();    }     fn cancel(        &mut self,        _: &editor::actions::Cancel,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        self.last_error = None;         if self            .text_thread            .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))        {            return;        }         cx.propagate();    }     fn cycle_message_role(        &mut self,        _: &CycleMessageRole,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        let cursors = self.cursors(cx);        self.text_thread.update(cx, |text_thread, cx| {            let messages = text_thread                .messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)                .into_iter()                .map(|message| message.id)                .collect();            text_thread.cycle_message_roles(messages, cx)        });    }     fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {        let selections = self.editor.update(cx, |editor, cx| {            editor                .selections                .all::<MultiBufferOffset>(&editor.display_snapshot(cx))        });        selections            .into_iter()            .map(|selection| selection.head())            .collect()    }     pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {        if let Some(command) = self.slash_commands.command(name, cx) {            self.editor.update(cx, |editor, cx| {                editor.transact(window, cx, |editor, window, cx| {                    editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());                    let snapshot = editor.buffer().read(cx).snapshot(cx);                    let newest_cursor = editor                        .selections                        .newest::<Point>(&editor.display_snapshot(cx))                        .head();                    if newest_cursor.column > 0                        || snapshot                            .chars_at(newest_cursor)                            .next()                            .is_some_and(|ch| ch != '\n')                    {

Lines 1–500 of 3479.