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. Load more
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() {