1548 lines · 62.7 KB · Rust
RawDownload
use crate::SlashCommandWorkingSet;use crate::{slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssistant};use anyhow::{anyhow, Result};use chrono::{DateTime, Utc};use collections::{HashMap, HashSet};use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};use futures::{    future::{self, BoxFuture, Shared},    FutureExt,};use fuzzy::StringMatchCandidate;use gpui::{    actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,    EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,    UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,};use heed::{    types::{SerdeBincode, SerdeJson, Str},    Database, RoTxn,};use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};use language_model::{    LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,};use parking_lot::RwLock;use picker::{Picker, PickerDelegate};use release_channel::ReleaseChannel;use rope::Rope;use serde::{Deserialize, Serialize};use settings::Settings;use std::{    cmp::Reverse,    future::Future,    path::PathBuf,    sync::{atomic::AtomicBool, Arc},    time::Duration,};use text::LineEnding;use theme::ThemeSettings;use ui::{    div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,    SharedString, Styled, Tooltip, ViewContext, VisualContext,};use util::{ResultExt, TryFutureExt};use uuid::Uuid;use workspace::Workspace;use zed_actions::InlineAssist; actions!(    prompt_library,    [        NewPrompt,        DeletePrompt,        DuplicatePrompt,        ToggleDefaultPrompt    ]); /// Init starts loading the PromptStore in the background and assigns/// a shared future to a global.pub fn init(cx: &mut AppContext) {    let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb");    let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())        .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))        .boxed()        .shared();    cx.set_global(GlobalPromptStore(prompt_store_future))} const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!(    "This prompt supports special functionality.\n",    "It's read-only, but you can remove it from your default prompt."); /// This function opens a new prompt library window if one doesn't exist already./// If one exists, it brings it to the foreground.////// Note that, when opening a new window, this waits for the PromptStore to be/// initialized. If it was initialized successfully, it returns a window handle/// to a prompt library.pub fn open_prompt_library(    language_registry: Arc<LanguageRegistry>,    cx: &mut AppContext,) -> Task<Result<WindowHandle<PromptLibrary>>> {    let existing_window = cx        .windows()        .into_iter()        .find_map(|window| window.downcast::<PromptLibrary>());    if let Some(existing_window) = existing_window {        existing_window            .update(cx, |_, cx| cx.activate_window())            .ok();        Task::ready(Ok(existing_window))    } else {        let store = PromptStore::global(cx);        cx.spawn(|cx| async move {            let store = store.await?;            cx.update(|cx| {                let app_id = ReleaseChannel::global(cx).app_id();                let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);                cx.open_window(                    WindowOptions {                        titlebar: Some(TitlebarOptions {                            title: Some("Prompt Library".into()),                            appears_transparent: cfg!(target_os = "macos"),                            traffic_light_position: Some(point(px(9.0), px(9.0))),                        }),                        app_id: Some(app_id.to_owned()),                        window_bounds: Some(WindowBounds::Windowed(bounds)),                        ..Default::default()                    },                    |cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),                )            })?        })    }} pub struct PromptLibrary {    store: Arc<PromptStore>,    language_registry: Arc<LanguageRegistry>,    prompt_editors: HashMap<PromptId, PromptEditor>,    active_prompt_id: Option<PromptId>,    picker: View<Picker<PromptPickerDelegate>>,    pending_load: Task<()>,    _subscriptions: Vec<Subscription>,} struct PromptEditor {    title_editor: View<Editor>,    body_editor: View<Editor>,    token_count: Option<usize>,    pending_token_count: Task<Option<()>>,    next_title_and_body_to_save: Option<(String, Rope)>,    pending_save: Option<Task<Option<()>>>,    _subscriptions: Vec<Subscription>,} struct PromptPickerDelegate {    store: Arc<PromptStore>,    selected_index: usize,    matches: Vec<PromptMetadata>,} enum PromptPickerEvent {    Selected { prompt_id: PromptId },    Confirmed { prompt_id: PromptId },    Deleted { prompt_id: PromptId },    ToggledDefault { prompt_id: PromptId },} impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {} impl PickerDelegate for PromptPickerDelegate {    type ListItem = ListItem;     fn match_count(&self) -> usize {        self.matches.len()    }     fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {        if self.store.prompt_count() == 0 {            "No prompts.".into()        } else {            "No prompts found matching your search.".into()        }    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {        self.selected_index = ix;        if let Some(prompt) = self.matches.get(self.selected_index) {            cx.emit(PromptPickerEvent::Selected {                prompt_id: prompt.id,            });        }    }     fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {        "Search...".into()    }     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {        let search = self.store.search(query);        let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);        cx.spawn(|this, mut cx| async move {            let (matches, selected_index) = cx                .background_executor()                .spawn(async move {                    let matches = search.await;                     let selected_index = prev_prompt_id                        .and_then(|prev_prompt_id| {                            matches.iter().position(|entry| entry.id == prev_prompt_id)                        })                        .unwrap_or(0);                    (matches, selected_index)                })                .await;             this.update(&mut cx, |this, cx| {                this.delegate.matches = matches;                this.delegate.set_selected_index(selected_index, cx);                cx.notify();            })            .ok();        })    }     fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {        if let Some(prompt) = self.matches.get(self.selected_index) {            cx.emit(PromptPickerEvent::Confirmed {                prompt_id: prompt.id,            });        }    }     fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}     fn render_match(        &self,        ix: usize,        selected: bool,        cx: &mut ViewContext<Picker<Self>>,    ) -> Option<Self::ListItem> {        let prompt = self.matches.get(ix)?;        let default = prompt.default;        let prompt_id = prompt.id;        let element = ListItem::new(ix)            .inset(true)            .spacing(ListItemSpacing::Sparse)            .toggle_state(selected)            .child(h_flex().h_5().line_height(relative(1.)).child(Label::new(                prompt.title.clone().unwrap_or("Untitled".into()),            )))            .end_slot::<IconButton>(default.then(|| {                IconButton::new("toggle-default-prompt", IconName::SparkleFilled)                    .toggle_state(true)                    .icon_color(Color::Accent)                    .shape(IconButtonShape::Square)                    .tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))                    .on_click(cx.listener(move |_, _, cx| {                        cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })                    }))            }))            .end_hover_slot(                h_flex()                    .gap_2()                    .child(if prompt_id.is_built_in() {                        div()                            .id("built-in-prompt")                            .child(Icon::new(IconName::FileLock).color(Color::Muted))                            .tooltip(move |cx| {                                Tooltip::with_meta(                                    "Built-in prompt",                                    None,                                    BUILT_IN_TOOLTIP_TEXT,                                    cx,                                )                            })                            .into_any()                    } else {                        IconButton::new("delete-prompt", IconName::Trash)                            .icon_color(Color::Muted)                            .shape(IconButtonShape::Square)                            .tooltip(move |cx| Tooltip::text("Delete Prompt", cx))                            .on_click(cx.listener(move |_, _, cx| {                                cx.emit(PromptPickerEvent::Deleted { prompt_id })                            }))                            .into_any_element()                    })                    .child(                        IconButton::new("toggle-default-prompt", IconName::Sparkle)                            .toggle_state(default)                            .selected_icon(IconName::SparkleFilled)                            .icon_color(if default { Color::Accent } else { Color::Muted })                            .shape(IconButtonShape::Square)                            .tooltip(move |cx| {                                Tooltip::text(                                    if default {                                        "Remove from Default Prompt"                                    } else {                                        "Add to Default Prompt"                                    },                                    cx,                                )                            })                            .on_click(cx.listener(move |_, _, cx| {                                cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })                            })),                    ),            );        Some(element)    }     fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {        h_flex()            .bg(cx.theme().colors().editor_background)            .rounded_md()            .overflow_hidden()            .flex_none()            .py_1()            .px_2()            .mx_1()            .child(editor.clone())    }} impl PromptLibrary {    fn new(        store: Arc<PromptStore>,        language_registry: Arc<LanguageRegistry>,        cx: &mut ViewContext<Self>,    ) -> Self {        let delegate = PromptPickerDelegate {            store: store.clone(),            selected_index: 0,            matches: Vec::new(),        };         let picker = cx.new_view(|cx| {            let picker = Picker::uniform_list(delegate, cx)                .modal(false)                .max_height(None);            picker.focus(cx);            picker        });        Self {            store: store.clone(),            language_registry,            prompt_editors: HashMap::default(),            active_prompt_id: None,            pending_load: Task::ready(()),            _subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],            picker,        }    }     fn handle_picker_event(        &mut self,        _: View<Picker<PromptPickerDelegate>>,        event: &PromptPickerEvent,        cx: &mut ViewContext<Self>,    ) {        match event {            PromptPickerEvent::Selected { prompt_id } => {                self.load_prompt(*prompt_id, false, cx);            }            PromptPickerEvent::Confirmed { prompt_id } => {                self.load_prompt(*prompt_id, true, cx);            }            PromptPickerEvent::ToggledDefault { prompt_id } => {                self.toggle_default_for_prompt(*prompt_id, cx);            }            PromptPickerEvent::Deleted { prompt_id } => {                self.delete_prompt(*prompt_id, cx);            }        }    }     pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {        // If we already have an untitled prompt, use that instead        // of creating a new one.        if let Some(metadata) = self.store.first() {            if metadata.title.is_none() {                self.load_prompt(metadata.id, true, cx);                return;            }        }         let prompt_id = PromptId::new();        let save = self.store.save(prompt_id, None, false, "".into());        self.picker.update(cx, |picker, cx| picker.refresh(cx));        cx.spawn(|this, mut cx| async move {            save.await?;            this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))        })        .detach_and_log_err(cx);    }     pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {        const SAVE_THROTTLE: Duration = Duration::from_millis(500);         if prompt_id.is_built_in() {            return;        }         let prompt_metadata = self.store.metadata(prompt_id).unwrap();        let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();        let title = prompt_editor.title_editor.read(cx).text(cx);        let body = prompt_editor.body_editor.update(cx, |editor, cx| {            editor                .buffer()                .read(cx)                .as_singleton()                .unwrap()                .read(cx)                .as_rope()                .clone()        });         let store = self.store.clone();        let executor = cx.background_executor().clone();         prompt_editor.next_title_and_body_to_save = Some((title, body));        if prompt_editor.pending_save.is_none() {            prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {                async move {                    loop {                        let title_and_body = this.update(&mut cx, |this, _| {                            this.prompt_editors                                .get_mut(&prompt_id)?                                .next_title_and_body_to_save                                .take()                        })?;                         if let Some((title, body)) = title_and_body {                            let title = if title.trim().is_empty() {                                None                            } else {                                Some(SharedString::from(title))                            };                            store                                .save(prompt_id, title, prompt_metadata.default, body)                                .await                                .log_err();                            this.update(&mut cx, |this, cx| {                                this.picker.update(cx, |picker, cx| picker.refresh(cx));                                cx.notify();                            })?;                             executor.timer(SAVE_THROTTLE).await;                        } else {                            break;                        }                    }                     this.update(&mut cx, |this, _cx| {                        if let Some(prompt_editor) = this.prompt_editors.get_mut(&prompt_id) {                            prompt_editor.pending_save = None;                        }                    })                }                .log_err()            }));        }    }     pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {        if let Some(active_prompt_id) = self.active_prompt_id {            self.delete_prompt(active_prompt_id, cx);        }    }     pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {        if let Some(active_prompt_id) = self.active_prompt_id {            self.duplicate_prompt(active_prompt_id, cx);        }    }     pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {        if let Some(active_prompt_id) = self.active_prompt_id {            self.toggle_default_for_prompt(active_prompt_id, cx);        }    }     pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {        if let Some(prompt_metadata) = self.store.metadata(prompt_id) {            self.store                .save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)                .detach_and_log_err(cx);            self.picker.update(cx, |picker, cx| picker.refresh(cx));            cx.notify();        }    }     pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {        if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {            if focus {                prompt_editor                    .body_editor                    .update(cx, |editor, cx| editor.focus(cx));            }            self.set_active_prompt(Some(prompt_id), cx);        } else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {            let language_registry = self.language_registry.clone();            let prompt = self.store.load(prompt_id);            self.pending_load = cx.spawn(|this, mut cx| async move {                let prompt = prompt.await;                let markdown = language_registry.language_for_name("Markdown").await;                this.update(&mut cx, |this, cx| match prompt {                    Ok(prompt) => {                        let title_editor = cx.new_view(|cx| {                            let mut editor = Editor::auto_width(cx);                            editor.set_placeholder_text("Untitled", cx);                            editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);                            if prompt_id.is_built_in() {

Lines 1–500 of 1548.