diff --git a/Cargo.lock b/Cargo.lock index eafcdacb7a..938b19e658 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,7 @@ dependencies = [ "futures 0.3.28", "fuzzy", "gpui", + "gray_matter", "http 0.1.0", "indoc", "language", @@ -359,6 +360,7 @@ dependencies = [ "open_ai", "ordered-float 2.10.0", "parking_lot", + "picker", "project", "rand 0.8.5", "regex", @@ -2825,7 +2827,7 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.14.0", + "hashbrown 0.14.5", "log", "regalloc2", "smallvec", @@ -3145,7 +3147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -3637,11 +3639,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.31" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" dependencies = [ "serde", + "typeid", ] [[package]] @@ -4522,7 +4525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" dependencies = [ "fallible-iterator", - "indexmap 2.0.0", + "indexmap 2.2.6", "stable_deref_trait", ] @@ -4790,6 +4793,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "gray_matter" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554" +dependencies = [ + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust", +] + [[package]] name = "grid" version = "0.13.0" @@ -4856,9 +4871,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.8", "allocator-api2", @@ -4870,7 +4885,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.5", ] [[package]] @@ -5288,12 +5303,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.5", "serde", ] @@ -5523,9 +5538,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -5927,6 +5942,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linkify" version = "0.10.0" @@ -6030,9 +6051,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "serde", "value-bag", @@ -6392,7 +6413,7 @@ dependencies = [ "bitflags 2.4.2", "codespan-reporting", "hexf-parse", - "indexmap 2.0.0", + "indexmap 2.2.6", "log", "num-traits", "rustc-hash", @@ -6822,8 +6843,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "crc32fast", - "hashbrown 0.14.0", - "indexmap 2.0.0", + "hashbrown 0.14.5", + "indexmap 2.2.6", "memchr", ] @@ -7255,7 +7276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.0", + "indexmap 2.2.6", ] [[package]] @@ -8633,9 +8654,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safemem" @@ -8964,18 +8985,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", @@ -9004,11 +9025,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -9020,7 +9041,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26386958a1344003f2b2bcff51a23fbe70461a478ef29247c6c6ab2c1656f53e" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -9536,7 +9557,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.0", + "indexmap 2.2.6", "log", "memchr", "once_cell", @@ -10651,7 +10672,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -10664,7 +10685,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.15", ] @@ -10675,7 +10696,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -11099,6 +11120,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typeid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf" + [[package]] name = "typenum" version = "1.17.0" @@ -11335,9 +11362,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -11345,9 +11372,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.4.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394" +checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" dependencies = [ "erased-serde", "serde", @@ -11356,9 +11383,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.4.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d" +checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" dependencies = [ "sval", "sval_buffer", @@ -11609,7 +11636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" dependencies = [ "anyhow", - "indexmap 2.0.0", + "indexmap 2.2.6", "serde", "serde_derive", "serde_json", @@ -11625,7 +11652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ "bitflags 2.4.2", - "indexmap 2.0.0", + "indexmap 2.2.6", "semver", ] @@ -11652,7 +11679,7 @@ dependencies = [ "cfg-if", "encoding_rs", "gimli", - "indexmap 2.0.0", + "indexmap 2.2.6", "libc", "log", "object", @@ -11783,7 +11810,7 @@ dependencies = [ "cpp_demangle", "cranelift-entity", "gimli", - "indexmap 2.0.0", + "indexmap 2.2.6", "log", "object", "rustc-demangle", @@ -11834,7 +11861,7 @@ dependencies = [ "cc", "cfg-if", "encoding_rs", - "indexmap 2.0.0", + "indexmap 2.2.6", "libc", "log", "mach", @@ -11939,7 +11966,7 @@ checksum = "96326c9800fb6c099f50d1bd2126d636fc2f96950e1675acf358c0f52516cd38" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.0.0", + "indexmap 2.2.6", "wit-parser", ] @@ -12634,7 +12661,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.0.0", + "indexmap 2.2.6", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -12662,7 +12689,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", "bitflags 2.4.2", - "indexmap 2.0.0", + "indexmap 2.2.6", "log", "serde", "serde_derive", @@ -12681,7 +12708,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" dependencies = [ "anyhow", "id-arena", - "indexmap 2.0.0", + "indexmap 2.2.6", "log", "semver", "serde", @@ -12848,7 +12875,7 @@ version = "0.4.0" source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af" dependencies = [ "ahash 0.8.8", - "hashbrown 0.14.0", + "hashbrown 0.14.5", "log", "x11rb", "xim-ctext", @@ -12911,6 +12938,15 @@ dependencies = [ "toml 0.8.10", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index b6ea709e32..993c66e563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -376,7 +376,7 @@ unindent = "0.1.7" unicase = "2.6" unicode-segmentation = "1.10" url = "2.2" -uuid = { version = "1.1.2", features = ["v4", "v5"] } +uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] } wasmparser = "0.201" wasm-encoder = "0.201" wasmtime = { version = "19.0.0", default-features = false, features = [ diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 513ef765b8..e0e1e6f8a3 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -50,6 +50,8 @@ ui.workspace = true util.workspace = true uuid.workspace = true workspace.workspace = true +picker.workspace = true +gray_matter = "0.2.7" [dev-dependencies] ctor.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index f5272915f4..88ca5aad35 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -3,7 +3,6 @@ pub mod assistant_panel; pub mod assistant_settings; mod codegen; mod completion_provider; -mod prompt_library; mod prompts; mod saved_conversation; mod search; @@ -17,7 +16,7 @@ use client::{proto, Client}; use command_palette_hooks::CommandPaletteFilter; pub(crate) use completion_provider::*; use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal}; -pub(crate) use prompt_library::*; +pub(crate) use prompts::prompt_library::*; pub(crate) use saved_conversation::*; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index bcf1bbe731..cc87307745 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,17 +1,19 @@ +use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer}; +use crate::prompts::prompt_library::PromptLibrary; +use crate::prompts::prompt_manager::PromptManager; use crate::{ ambient_context::*, assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel}, codegen::{self, Codegen, CodegenKind}, - prompt_library::{PromptLibrary, PromptManager}, - prompts::generate_content_prompt, + prompts::prompt::generate_content_prompt, search::*, slash_command::{ SlashCommandCleanup, SlashCommandCompletionProvider, SlashCommandLine, SlashCommandRegistry, }, - ApplyEdit, Assist, CompletionProvider, CycleMessageRole, InlineAssist, InsertActivePrompt, - LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, - MessageStatus, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation, + ApplyEdit, Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel, + LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus, + QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage, + Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation, }; use anyhow::{anyhow, Result}; use client::telemetry::Telemetry; @@ -85,7 +87,7 @@ pub fn init(cx: &mut AppContext) { }) .register_action(AssistantPanel::inline_assist) .register_action(AssistantPanel::cancel_last_inline_assist) - .register_action(ConversationEditor::insert_active_prompt) + // .register_action(ConversationEditor::insert_active_prompt) .register_action(ConversationEditor::quote_selection); }, ) @@ -139,7 +141,7 @@ impl AssistantPanel { .unwrap_or_default(); let prompt_library = Arc::new( - PromptLibrary::init(fs.clone()) + PromptLibrary::load(fs.clone()) .await .log_err() .unwrap_or_default(), @@ -1035,20 +1037,20 @@ impl AssistantPanel { .ok(); } }) - .entry("Insert Active Prompt", None, { - let workspace = workspace.clone(); - move |cx| { - workspace - .update(cx, |workspace, cx| { - ConversationEditor::insert_active_prompt( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - } - }) + // .entry("Insert Active Prompt", None, { + // let workspace = workspace.clone(); + // move |cx| { + // workspace + // .update(cx, |workspace, cx| { + // ConversationEditor::insert_active_prompt( + // workspace, + // &Default::default(), + // cx, + // ) + // }) + // .ok(); + // } + // }) }) .into() }) @@ -1132,7 +1134,14 @@ impl AssistantPanel { fn show_prompt_manager(&mut self, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| PromptManager::new(self.prompt_library.clone(), cx)) + workspace.toggle_modal(cx, |cx| { + PromptManager::new( + self.prompt_library.clone(), + self.languages.clone(), + self.fs.clone(), + cx, + ) + }) }) } } @@ -3252,35 +3261,35 @@ impl ConversationEditor { } } - fn insert_active_prompt( - workspace: &mut Workspace, - _: &InsertActivePrompt, - cx: &mut ViewContext, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; + // fn insert_active_prompt( + // workspace: &mut Workspace, + // _: &InsertActivePrompt, + // cx: &mut ViewContext, + // ) { + // let Some(panel) = workspace.panel::(cx) else { + // return; + // }; - if !panel.focus_handle(cx).contains_focused(cx) { - workspace.toggle_panel_focus::(cx); - } + // if !panel.focus_handle(cx).contains_focused(cx) { + // workspace.toggle_panel_focus::(cx); + // } - if let Some(default_prompt) = panel.read(cx).prompt_library.clone().default_prompt() { - panel.update(cx, |panel, cx| { - if let Some(conversation) = panel - .active_conversation_editor() - .cloned() - .or_else(|| panel.new_conversation(cx)) - { - conversation.update(cx, |conversation, cx| { - conversation - .editor - .update(cx, |editor, cx| editor.insert(&default_prompt, cx)) - }); - }; - }); - }; - } + // if let Some(default_prompt) = panel.read(cx).prompt_library.clone().default_prompt() { + // panel.update(cx, |panel, cx| { + // if let Some(conversation) = panel + // .active_conversation_editor() + // .cloned() + // .or_else(|| panel.new_conversation(cx)) + // { + // conversation.update(cx, |conversation, cx| { + // conversation + // .editor + // .update(cx, |editor, cx| editor.insert(&default_prompt, cx)) + // }); + // }; + // }); + // }; + // } fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext) { let editor = self.editor.read(cx); diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs deleted file mode 100644 index 01bce68a5d..0000000000 --- a/crates/assistant/src/prompt_library.rs +++ /dev/null @@ -1,454 +0,0 @@ -use fs::Fs; -use futures::StreamExt; -use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render}; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use ui::{prelude::*, Checkbox, ModalHeader}; -use util::{paths::PROMPTS_DIR, ResultExt}; -use workspace::ModalView; - -pub struct PromptLibraryState { - /// The default prompt all assistant contexts will start with - _system_prompt: String, - /// All [UserPrompt]s loaded into the library - prompts: HashMap, - /// Prompts included in the default prompt - default_prompts: Vec, - /// Prompts that have a pending update that hasn't been applied yet - _updateable_prompts: Vec, - /// Prompts that have been changed since they were loaded - /// and can be reverted to their original state - _revertable_prompts: Vec, - version: usize, -} - -pub struct PromptLibrary { - state: RwLock, -} - -impl Default for PromptLibrary { - fn default() -> Self { - Self::new() - } -} - -impl PromptLibrary { - fn new() -> Self { - Self { - state: RwLock::new(PromptLibraryState { - _system_prompt: String::new(), - prompts: HashMap::new(), - default_prompts: Vec::new(), - _updateable_prompts: Vec::new(), - _revertable_prompts: Vec::new(), - version: 0, - }), - } - } - - pub async fn init(fs: Arc) -> anyhow::Result { - let prompt_library = PromptLibrary::new(); - prompt_library.load_prompts(fs)?; - Ok(prompt_library) - } - - fn load_prompts(&self, fs: Arc) -> anyhow::Result<()> { - let prompts = futures::executor::block_on(UserPrompt::list(fs))?; - let prompts_with_ids = prompts - .clone() - .into_iter() - .map(|prompt| { - let id = uuid::Uuid::new_v4().to_string(); - (id, prompt) - }) - .collect::>(); - let mut state = self.state.write(); - state.prompts.extend(prompts_with_ids); - state.version += 1; - - Ok(()) - } - - pub fn default_prompt(&self) -> Option { - let state = self.state.read(); - - if state.default_prompts.is_empty() { - None - } else { - Some(self.join_default_prompts()) - } - } - - pub fn add_prompt_to_default(&self, prompt_id: String) -> anyhow::Result<()> { - let mut state = self.state.write(); - - if !state.default_prompts.contains(&prompt_id) && state.prompts.contains_key(&prompt_id) { - state.default_prompts.push(prompt_id); - state.version += 1; - } - - Ok(()) - } - - pub fn remove_prompt_from_default(&self, prompt_id: String) -> anyhow::Result<()> { - let mut state = self.state.write(); - - state.default_prompts.retain(|id| id != &prompt_id); - state.version += 1; - Ok(()) - } - - fn join_default_prompts(&self) -> String { - let state = self.state.read(); - let active_prompt_ids = state.default_prompts.to_vec(); - - active_prompt_ids - .iter() - .filter_map(|id| state.prompts.get(id).map(|p| p.prompt.clone())) - .collect::>() - .join("\n\n---\n\n") - } - - #[allow(unused)] - pub fn prompts(&self) -> Vec { - let state = self.state.read(); - state.prompts.values().cloned().collect() - } - - pub fn prompts_with_ids(&self) -> Vec<(String, UserPrompt)> { - let state = self.state.read(); - state - .prompts - .iter() - .map(|(id, prompt)| (id.clone(), prompt.clone())) - .collect() - } - - pub fn _default_prompts(&self) -> Vec { - let state = self.state.read(); - state - .default_prompts - .iter() - .filter_map(|id| state.prompts.get(id).cloned()) - .collect() - } - - pub fn default_prompt_ids(&self) -> Vec { - let state = self.state.read(); - state.default_prompts.clone() - } -} - -/// A custom prompt that can be loaded into the prompt library -/// -/// Example: -/// -/// ```json -/// { -/// "title": "Foo", -/// "version": "1.0", -/// "author": "Jane Kim ", -/// "languages": ["*"], // or ["rust", "python", "javascript"] etc... -/// "prompt": "bar" -/// } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct UserPrompt { - version: String, - pub title: String, - author: String, - languages: Vec, - pub prompt: String, -} - -impl UserPrompt { - async fn list(fs: Arc) -> anyhow::Result> { - fs.create_dir(&PROMPTS_DIR).await?; - - let mut paths = fs.read_dir(&PROMPTS_DIR).await?; - let mut prompts = Vec::new(); - - while let Some(path_result) = paths.next().await { - let path = match path_result { - Ok(p) => p, - Err(e) => { - eprintln!("Error reading path: {:?}", e); - continue; - } - }; - - if path.extension() == Some(std::ffi::OsStr::new("json")) { - match fs.load(&path).await { - Ok(content) => { - let user_prompt: UserPrompt = - serde_json::from_str(&content).map_err(|e| { - anyhow::anyhow!("Failed to deserialize UserPrompt: {}", e) - })?; - - prompts.push(user_prompt); - } - Err(e) => eprintln!("Failed to load file {}: {}", path.display(), e), - } - } - } - - Ok(prompts) - } -} - -pub struct PromptManager { - focus_handle: FocusHandle, - prompt_library: Arc, - active_prompt: Option, -} - -impl PromptManager { - pub fn new(prompt_library: Arc, cx: &mut WindowContext) -> Self { - let focus_handle = cx.focus_handle(); - Self { - focus_handle, - prompt_library, - active_prompt: None, - } - } - - pub fn set_active_prompt(&mut self, prompt_id: Option) { - self.active_prompt = prompt_id; - } - - fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent); - } -} - -impl Render for PromptManager { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let prompt_library = self.prompt_library.clone(); - let prompts = prompt_library - .clone() - .prompts_with_ids() - .clone() - .into_iter() - .collect::>(); - - let active_prompt = self.active_prompt.as_ref().and_then(|id| { - prompt_library - .prompts_with_ids() - .iter() - .find(|(prompt_id, _)| prompt_id == id) - .map(|(_, prompt)| prompt.clone()) - }); - - v_flex() - .key_context("PromptManager") - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::dismiss)) - .elevation_3(cx) - .size_full() - .flex_none() - .w(rems(54.)) - .h(rems(40.)) - .overflow_hidden() - .child( - ModalHeader::new() - .headline("Prompt Library") - .show_dismiss_button(true), - ) - .child( - h_flex() - .flex_grow() - .overflow_hidden() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - div() - .id("prompt-preview") - .overflow_y_scroll() - .h_full() - .min_w_64() - .max_w_1_2() - .child( - v_flex() - .justify_start() - .py(Spacing::Medium.rems(cx)) - .px(Spacing::Large.rems(cx)) - .bg(cx.theme().colors().surface_background) - .when_else( - !prompts.is_empty(), - |with_items| { - with_items.children(prompts.into_iter().map( - |(id, prompt)| { - let prompt_library = prompt_library.clone(); - let prompt = prompt.clone(); - let prompt_id = id.clone(); - let shared_string_id: SharedString = - id.clone().into(); - - let default_prompt_ids = - prompt_library.clone().default_prompt_ids(); - let is_default = - default_prompt_ids.contains(&id); - // We'll use this for conditionally enabled prompts - // like those loaded only for certain languages - let is_conditional = false; - let selection = - match (is_default, is_conditional) { - (_, true) => Selection::Indeterminate, - (true, _) => Selection::Selected, - (false, _) => Selection::Unselected, - }; - - v_flex() - .id(ElementId::Name( - format!("prompt-{}", shared_string_id) - .into(), - )) - .p(Spacing::Small.rems(cx)) - - .on_click(cx.listener({ - let prompt_id = prompt_id.clone(); - move |this, _event, _cx| { - this.set_active_prompt(Some( - prompt_id.clone(), - )); - } - })) - .child( - h_flex() - .justify_between() - .child( - h_flex() - .gap(Spacing::Large.rems(cx)) - .child( - Checkbox::new( - shared_string_id, - selection, - ) - .on_click(move |_, _cx| { - if is_default { - prompt_library - .clone() - .remove_prompt_from_default( - prompt_id.clone(), - ) - .log_err(); - } else { - prompt_library - .clone() - .add_prompt_to_default( - prompt_id.clone(), - ) - .log_err(); - } - }), - ) - .child(Label::new( - prompt.title, - )), - ) - .child(div()), - ) - }, - )) - }, - |no_items| { - no_items.child( - Label::new("No prompts").color(Color::Placeholder), - ) - }, - ), - ), - ) - .child( - div() - .id("prompt-preview") - .overflow_y_scroll() - .border_l_1() - .border_color(cx.theme().colors().border) - .size_full() - .flex_none() - .child( - v_flex() - .justify_start() - .py(Spacing::Medium.rems(cx)) - .px(Spacing::Large.rems(cx)) - .gap(Spacing::Large.rems(cx)) - .when_else( - active_prompt.is_some(), - |with_prompt| { - let active_prompt = active_prompt.as_ref().unwrap(); - with_prompt - .child( - v_flex() - .gap_0p5() - .child( - Headline::new( - active_prompt.title.clone(), - ) - .size(HeadlineSize::XSmall), - ) - .child( - h_flex() - .child( - Label::new( - active_prompt - .author - .clone(), - ) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new( - if active_prompt - .languages - .is_empty() - || active_prompt - .languages[0] - == "*" - { - " · Global".to_string() - } else { - format!( - " · {}", - active_prompt - .languages - .join(", ") - ) - }, - ) - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ), - ) - .child( - div() - .w_full() - .max_w(rems(30.)) - .text_ui(cx) - .child(active_prompt.prompt.clone()), - ) - }, - |without_prompt| { - without_prompt.justify_center().items_center().child( - Label::new("Select a prompt to view details.") - .color(Color::Placeholder), - ) - }, - ), - ), - ), - ) - } -} - -impl EventEmitter for PromptManager {} -impl ModalView for PromptManager {} - -impl FocusableView for PromptManager { - fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 80dfc45c4f..9d0e2be23e 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,95 +1,3 @@ -use language::BufferSnapshot; -use std::{fmt::Write, ops::Range}; - -pub fn generate_content_prompt( - user_prompt: String, - language_name: Option<&str>, - buffer: BufferSnapshot, - range: Range, - project_name: Option, -) -> anyhow::Result { - let mut prompt = String::new(); - - let content_type = match language_name { - None | Some("Markdown" | "Plain Text") => { - writeln!(prompt, "You are an expert engineer.")?; - "Text" - } - Some(language_name) => { - writeln!(prompt, "You are an expert {language_name} engineer.")?; - writeln!( - prompt, - "Your answer MUST always and only be valid {}.", - language_name - )?; - "Code" - } - }; - - if let Some(project_name) = project_name { - writeln!( - prompt, - "You are currently working inside the '{project_name}' project in code editor Zed." - )?; - } - - // Include file content. - for chunk in buffer.text_for_range(0..range.start) { - prompt.push_str(chunk); - } - - if range.is_empty() { - prompt.push_str("<|START|>"); - } else { - prompt.push_str("<|START|"); - } - - for chunk in buffer.text_for_range(range.clone()) { - prompt.push_str(chunk); - } - - if !range.is_empty() { - prompt.push_str("|END|>"); - } - - for chunk in buffer.text_for_range(range.end..buffer.len()) { - prompt.push_str(chunk); - } - - prompt.push('\n'); - - if range.is_empty() { - writeln!( - prompt, - "Assume the cursor is located where the `<|START|>` span is." - ) - .unwrap(); - writeln!( - prompt, - "{content_type} can't be replaced, so assume your answer will be inserted at the cursor.", - ) - .unwrap(); - writeln!( - prompt, - "Generate {content_type} based on the users prompt: {user_prompt}", - ) - .unwrap(); - } else { - writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap(); - writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap(); - writeln!( - prompt, - "Double check that you only return code and not the '<|START|' and '|END|'> spans" - ) - .unwrap(); - } - - writeln!(prompt, "Never make remarks about the output.").unwrap(); - writeln!( - prompt, - "Do not return anything else, except the generated {content_type}." - ) - .unwrap(); - - Ok(prompt) -} +pub mod prompt; +pub mod prompt_library; +pub mod prompt_manager; diff --git a/crates/assistant/src/prompts/prompt.rs b/crates/assistant/src/prompts/prompt.rs new file mode 100644 index 0000000000..f3dfd8aa96 --- /dev/null +++ b/crates/assistant/src/prompts/prompt.rs @@ -0,0 +1,278 @@ +use language::BufferSnapshot; +use std::{fmt::Write, ops::Range}; +use ui::SharedString; + +use gray_matter::{engine::YAML, Matter}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct StaticPromptFrontmatter { + title: String, + version: String, + author: String, + #[serde(default)] + languages: Vec, + #[serde(default)] + dependencies: Vec, +} + +impl Default for StaticPromptFrontmatter { + fn default() -> Self { + Self { + title: "New Prompt".to_string(), + version: "1.0".to_string(), + author: "No Author".to_string(), + languages: vec!["*".to_string()], + dependencies: vec![], + } + } +} + +impl StaticPromptFrontmatter { + pub fn title(&self) -> SharedString { + self.title.clone().into() + } + + // pub fn version(&self) -> SharedString { + // self.version.clone().into() + // } + + // pub fn author(&self) -> SharedString { + // self.author.clone().into() + // } + + // pub fn languages(&self) -> Vec { + // self.languages + // .clone() + // .into_iter() + // .map(|s| s.into()) + // .collect() + // } + + // pub fn dependencies(&self) -> Vec { + // self.dependencies + // .clone() + // .into_iter() + // .map(|s| s.into()) + // .collect() + // } +} + +/// A statuc prompt that can be loaded into the prompt library +/// from Markdown with a frontmatter header +/// +/// Examples: +/// +/// ### Globally available prompt +/// +/// ```markdown +/// --- +/// title: Foo +/// version: 1.0 +/// author: Jane Kim +/// languages: ["rust"] +/// dependencies: ["gpui"] +/// --- +/// +/// When building a UI with GPUI, ensure you... +/// ``` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct StaticPrompt { + content: String, + file_name: Option, +} + +impl StaticPrompt { + pub fn new(content: String) -> Self { + StaticPrompt { + content, + file_name: None, + } + } + + pub fn title(&self) -> Option { + self.metadata().map(|m| m.title()) + } + + // pub fn version(&self) -> Option { + // self.metadata().map(|m| m.version()) + // } + + // pub fn author(&self) -> Option { + // self.metadata().map(|m| m.author()) + // } + + // pub fn languages(&self) -> Vec { + // self.metadata().map(|m| m.languages()).unwrap_or_default() + // } + + // pub fn dependencies(&self) -> Vec { + // self.metadata() + // .map(|m| m.dependencies()) + // .unwrap_or_default() + // } + + // pub fn load(fs: Arc, file_name: String) -> anyhow::Result { + // todo!() + // } + + // pub fn save(&self, fs: Arc) -> anyhow::Result<()> { + // todo!() + // } + + // pub fn rename(&self, new_file_name: String, fs: Arc) -> anyhow::Result<()> { + // todo!() + // } +} + +impl StaticPrompt { + // pub fn update(&mut self, contents: String) -> &mut Self { + // self.content = contents; + // self + // } + + /// Sets the file name of the prompt + pub fn file_name(&mut self, file_name: String) -> &mut Self { + self.file_name = Some(file_name); + self + } + + /// Sets the file name of the prompt based on the title + // pub fn file_name_from_title(&mut self) -> &mut Self { + // if let Some(title) = self.title() { + // let file_name = title.to_lowercase().replace(" ", "_"); + // if !file_name.is_empty() { + // self.file_name = Some(file_name); + // } + // } + // self + // } + + /// Returns the prompt's content + pub fn content(&self) -> &String { + &self.content + } + fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> { + let matter = Matter::::new(); + let result = matter.parse(self.content.as_str()); + match result.data { + Some(data) => { + let front_matter: StaticPromptFrontmatter = data.deserialize()?; + let body = result.content; + Ok((front_matter, body)) + } + None => Err(anyhow::anyhow!("Failed to parse frontmatter")), + } + } + + pub fn metadata(&self) -> Option { + self.parse().ok().map(|(front_matter, _)| front_matter) + } +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: BufferSnapshot, + range: Range, + project_name: Option, +) -> anyhow::Result { + let mut prompt = String::new(); + + let content_type = match language_name { + None | Some("Markdown" | "Plain Text") => { + writeln!(prompt, "You are an expert engineer.")?; + "Text" + } + Some(language_name) => { + writeln!(prompt, "You are an expert {language_name} engineer.")?; + writeln!( + prompt, + "Your answer MUST always and only be valid {}.", + language_name + )?; + "Code" + } + }; + + if let Some(project_name) = project_name { + writeln!( + prompt, + "You are currently working inside the '{project_name}' project in code editor Zed." + )?; + } + + // Include file content. + for chunk in buffer.text_for_range(0..range.start) { + prompt.push_str(chunk); + } + + if range.is_empty() { + prompt.push_str("<|START|>"); + } else { + prompt.push_str("<|START|"); + } + + for chunk in buffer.text_for_range(range.clone()) { + prompt.push_str(chunk); + } + + if !range.is_empty() { + prompt.push_str("|END|>"); + } + + for chunk in buffer.text_for_range(range.end..buffer.len()) { + prompt.push_str(chunk); + } + + prompt.push('\n'); + + if range.is_empty() { + writeln!( + prompt, + "Assume the cursor is located where the `<|START|>` span is." + ) + .unwrap(); + writeln!( + prompt, + "{content_type} can't be replaced, so assume your answer will be inserted at the cursor.", + ) + .unwrap(); + writeln!( + prompt, + "Generate {content_type} based on the users prompt: {user_prompt}", + ) + .unwrap(); + } else { + writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap(); + writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap(); + writeln!( + prompt, + "Double check that you only return code and not the '<|START|' and '|END|'> spans" + ) + .unwrap(); + } + + writeln!(prompt, "Never make remarks about the output.").unwrap(); + writeln!( + prompt, + "Do not return anything else, except the generated {content_type}." + ) + .unwrap(); + + Ok(prompt) +} diff --git a/crates/assistant/src/prompts/prompt_library.rs b/crates/assistant/src/prompts/prompt_library.rs new file mode 100644 index 0000000000..cdef98b452 --- /dev/null +++ b/crates/assistant/src/prompts/prompt_library.rs @@ -0,0 +1,152 @@ +use anyhow::Context; +use collections::HashMap; +use fs::Fs; + +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use smol::stream::StreamExt; +use std::sync::Arc; +use util::paths::PROMPTS_DIR; +use uuid::Uuid; + +use super::prompt::StaticPrompt; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct PromptId(pub Uuid); + +#[allow(unused)] +impl PromptId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct PromptLibraryState { + /// A set of prompts that all assistant contexts will start with + default_prompt: Vec, + /// All [Prompt]s loaded into the library + prompts: HashMap, + /// Prompts that have been changed but haven't been + /// saved back to the file system + dirty_prompts: Vec, + version: usize, +} + +pub struct PromptLibrary { + state: RwLock, +} + +impl Default for PromptLibrary { + fn default() -> Self { + Self::new() + } +} + +impl PromptLibrary { + fn new() -> Self { + Self { + state: RwLock::new(PromptLibraryState::default()), + } + } + + pub fn prompts(&self) -> Vec<(PromptId, StaticPrompt)> { + let state = self.state.read(); + state + .prompts + .iter() + .map(|(id, prompt)| (*id, prompt.clone())) + .collect() + } + + pub fn first_prompt_id(&self) -> Option { + let state = self.state.read(); + state.prompts.keys().next().cloned() + } + + pub fn prompt(&self, id: PromptId) -> Option { + let state = self.state.read(); + state.prompts.get(&id).cloned() + } + + /// Save the current state of the prompt library to the + /// file system as a JSON file + pub async fn save(&self, fs: Arc) -> anyhow::Result<()> { + fs.create_dir(&PROMPTS_DIR).await?; + + let path = PROMPTS_DIR.join("index.json"); + + let json = { + let state = self.state.read(); + serde_json::to_string(&*state)? + }; + + fs.atomic_write(path, json).await?; + + Ok(()) + } + + /// Load the state of the prompt library from the file system + /// or create a new one if it doesn't exist + pub async fn load(fs: Arc) -> anyhow::Result { + let path = PROMPTS_DIR.join("index.json"); + + let state = if fs.is_file(&path).await { + let json = fs.load(&path).await?; + serde_json::from_str(&json)? + } else { + PromptLibraryState::default() + }; + + let mut prompt_library = Self { + state: RwLock::new(state), + }; + + prompt_library.load_prompts(fs).await?; + + Ok(prompt_library) + } + + /// Load all prompts from the file system + /// adding them to the library if they don't already exist + pub async fn load_prompts(&mut self, fs: Arc) -> anyhow::Result<()> { + // let current_prompts = self.all_prompt_contents().clone(); + + // For now, we'll just clear the prompts and reload them all + self.state.get_mut().prompts.clear(); + + let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?; + + while let Some(prompt_path) = prompt_paths.next().await { + let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?; + + if !fs.is_file(&prompt_path).await + || prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md") + { + continue; + } + + let json = fs + .load(&prompt_path) + .await + .with_context(|| format!("Failed to load prompt {:?}", prompt_path))?; + let mut static_prompt = StaticPrompt::new(json); + + if let Some(file_name) = prompt_path.file_name() { + let file_name = file_name.to_string_lossy().into_owned(); + static_prompt.file_name(file_name); + } + + let state = self.state.get_mut(); + + let id = Uuid::new_v4(); + state.prompts.insert(PromptId(id), static_prompt); + state.version += 1; + } + + // Write any changes back to the file system + self.save(fs.clone()).await?; + + Ok(()) + } +} diff --git a/crates/assistant/src/prompts/prompt_manager.rs b/crates/assistant/src/prompts/prompt_manager.rs new file mode 100644 index 0000000000..b48f238535 --- /dev/null +++ b/crates/assistant/src/prompts/prompt_manager.rs @@ -0,0 +1,327 @@ +use collections::HashMap; +use editor::Editor; +use fs::Fs; +use gpui::{prelude::FluentBuilder, *}; +use language::{language_settings, Buffer, LanguageRegistry}; +use picker::{Picker, PickerDelegate}; +use std::sync::Arc; +use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing}; +use util::{ResultExt, TryFutureExt}; +use workspace::ModalView; + +use super::prompt_library::{PromptId, PromptLibrary}; +use crate::prompts::prompt::StaticPrompt; + +pub struct PromptManager { + focus_handle: FocusHandle, + prompt_library: Arc, + language_registry: Arc, + #[allow(dead_code)] + fs: Arc, + picker: View>, + prompt_editors: HashMap>, + active_prompt_id: Option, +} + +impl PromptManager { + pub fn new( + prompt_library: Arc, + language_registry: Arc, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let prompt_manager = cx.view().downgrade(); + let picker = cx.new_view(|cx| { + Picker::uniform_list( + PromptManagerDelegate { + prompt_manager, + matching_prompts: vec![], + matching_prompt_ids: vec![], + prompt_library: prompt_library.clone(), + selected_index: 0, + }, + cx, + ) + .max_height(rems(35.75)) + .modal(false) + }); + + let focus_handle = picker.focus_handle(cx); + + let mut manager = Self { + focus_handle, + prompt_library, + language_registry, + fs, + picker, + prompt_editors: HashMap::default(), + active_prompt_id: None, + }; + + manager.active_prompt_id = manager.prompt_library.first_prompt_id(); + + manager + } + + pub fn set_active_prompt(&mut self, prompt_id: Option, cx: &mut ViewContext) { + self.active_prompt_id = prompt_id; + cx.notify(); + } + + pub fn focus_active_editor(&self, cx: &mut ViewContext) { + if let Some(active_prompt_id) = self.active_prompt_id { + if let Some(editor) = self.prompt_editors.get(&active_prompt_id) { + let focus_handle = editor.focus_handle(cx); + + cx.focus(&focus_handle) + } + } + } + + fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent); + } + + fn render_prompt_list(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let picker = self.picker.clone(); + + v_flex() + .id("prompt-list") + .bg(cx.theme().colors().surface_background) + .h_full() + .w_2_5() + .child( + h_flex() + .bg(cx.theme().colors().background) + .p(Spacing::Small.rems(cx)) + .border_b_1() + .border_color(cx.theme().colors().border) + .h(rems(1.75)) + .w_full() + .flex_none() + .justify_between() + .child(Label::new("Prompt Library").size(LabelSize::Small)) + .child(IconButton::new("new-prompt", IconName::Plus).disabled(true)), + ) + .child( + v_flex() + .h(rems(38.25)) + .flex_grow() + .justify_start() + .child(picker), + ) + } + + fn set_editor_for_prompt( + &mut self, + prompt_id: PromptId, + cx: &mut ViewContext, + ) -> impl IntoElement { + let prompt_library = self.prompt_library.clone(); + + let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| { + cx.new_view(|cx| { + let text = if let Some(prompt) = prompt_library.prompt(prompt_id) { + prompt.content().to_owned() + } else { + "".to_string() + }; + + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::local(text, cx); + let markdown = self.language_registry.language_for_name("Markdown"); + cx.spawn(|buffer, mut cx| async move { + if let Some(markdown) = markdown.await.log_err() { + _ = buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx); + }); + } + }) + .detach(); + buffer.set_language_registry(self.language_registry.clone()); + buffer + }); + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor.set_show_gutter(false, cx); + editor + }) + }); + editor_for_prompt.clone() + } +} + +impl Render for PromptManager { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + h_flex() + .key_context("PromptManager") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::dismiss)) + // .on_action(cx.listener(Self::save_active_prompt)) + .elevation_3(cx) + .size_full() + .flex_none() + .w(rems(64.)) + .h(rems(40.)) + .overflow_hidden() + .child(self.render_prompt_list(cx)) + .child( + div().w_3_5().h_full().child( + v_flex() + .id("prompt-editor") + .border_l_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .size_full() + .flex_none() + .min_w_64() + .h_full() + .child( + h_flex() + .bg(cx.theme().colors().background) + .p(Spacing::Small.rems(cx)) + .border_b_1() + .border_color(cx.theme().colors().border) + .h_7() + .w_full() + .justify_between() + .child(div()) + .child( + IconButton::new("dismiss", IconName::Close) + .shape(IconButtonShape::Square) + .on_click(|_, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()); + }), + ), + ) + .when_some(self.active_prompt_id, |this, active_prompt_id| { + this.child( + h_flex() + .flex_1() + .w_full() + .py(Spacing::Large.rems(cx)) + .px(Spacing::XLarge.rems(cx)) + .child(self.set_editor_for_prompt(active_prompt_id, cx)), + ) + }), + ), + ) + } +} + +impl EventEmitter for PromptManager {} +impl ModalView for PromptManager {} + +impl FocusableView for PromptManager { + fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +pub struct PromptManagerDelegate { + prompt_manager: WeakView, + matching_prompts: Vec>, + matching_prompt_ids: Vec, + prompt_library: Arc, + selected_index: usize, +} + +impl PickerDelegate for PromptManagerDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Find a prompt…".into() + } + + fn match_count(&self) -> usize { + self.matching_prompt_ids.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn selected_index_changed( + &self, + ix: usize, + _cx: &mut ViewContext>, + ) -> Option> { + let prompt_id = self.matching_prompt_ids.get(ix).copied()?; + let prompt_manager = self.prompt_manager.upgrade()?; + + Some(Box::new(move |cx| { + prompt_manager.update(cx, |manager, cx| { + manager.set_active_prompt(Some(prompt_id), cx); + }) + })) + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + let prompt_library = self.prompt_library.clone(); + cx.spawn(|picker, mut cx| async move { + async { + let prompts = prompt_library.prompts(); + let matching_prompts = prompts + .into_iter() + .filter(|(_, prompt)| { + prompt + .content() + .to_lowercase() + .contains(&query.to_lowercase()) + }) + .collect::>(); + picker.update(&mut cx, |picker, cx| { + picker.delegate.matching_prompt_ids = + matching_prompts.iter().map(|(id, _)| *id).collect(); + picker.delegate.matching_prompts = matching_prompts + .into_iter() + .map(|(_, prompt)| Arc::new(prompt)) + .collect(); + cx.notify(); + })?; + anyhow::Ok(()) + } + .log_err() + .await; + }) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + let prompt_manager = self.prompt_manager.upgrade().unwrap(); + prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx)); + } + + fn should_dismiss(&self) -> bool { + false + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.prompt_manager + .update(cx, |_, cx| { + cx.emit(DismissEvent); + }) + .ok(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + let matching_prompt = self.matching_prompts.get(ix)?; + let prompt = matching_prompt.clone(); + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child(Label::new(prompt.title().unwrap_or_default().clone())), + ) + } +} diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index ee862d1d86..27c123f35c 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -1,5 +1,5 @@ use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation}; -use crate::PromptLibrary; +use crate::prompts::prompt_library::PromptLibrary; use anyhow::{anyhow, Context, Result}; use futures::channel::oneshot; use fuzzy::StringMatchCandidate; @@ -42,7 +42,12 @@ impl SlashCommand for PromptSlashCommand { .prompts() .into_iter() .enumerate() - .map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.title)) + .filter_map(|(ix, prompt)| { + prompt + .1 + .title() + .map(|title| StringMatchCandidate::new(ix, title.into())) + }) .collect::>(); let matches = fuzzy::match_strings( &candidates, @@ -75,9 +80,11 @@ impl SlashCommand for PromptSlashCommand { let prompt = library .prompts() .into_iter() - .find(|prompt| prompt.title == title) - .with_context(|| format!("no prompt found with title {:?}", title))?; - Ok(prompt.prompt) + .filter_map(|prompt| prompt.1.title().map(|title| (title, prompt))) + .find(|(t, _)| t == &title) + .with_context(|| format!("no prompt found with title {:?}", title))? + .1; + Ok(prompt.1.content().to_owned()) }); SlashCommandInvocation { output, diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 4bff3da740..ed957158c2 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -41,6 +41,25 @@ pub trait FluentBuilder { }) } + /// Conditionally unwrap and modify self with one closure if the given option is Some, or another if it is None. + fn when_some_else( + self, + option: Option, + then: impl FnOnce(Self, T) -> Self, + otherwise: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + otherwise(this) + } + }) + } + /// Conditionally modify self with one closure or another fn when_else( self, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index fea24410d1..370a082cb8 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -46,6 +46,7 @@ use std::{ }; use util::post_inc; use util::{measure, ResultExt}; +use uuid::Uuid; mod prompts; @@ -4514,6 +4515,8 @@ pub enum ElementId { Integer(usize), /// A string based ID. Name(SharedString), + /// A UUID. + Uuid(Uuid), /// An ID that's equated with a focus handle. FocusHandle(FocusId), /// A combination of a name and an integer. @@ -4528,6 +4531,7 @@ impl Display for ElementId { ElementId::Name(name) => write!(f, "{}", name)?, ElementId::FocusHandle(_) => write!(f, "FocusHandle")?, ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, + ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, } Ok(()) @@ -4594,6 +4598,12 @@ impl From<(&'static str, u64)> for ElementId { } } +impl From for ElementId { + fn from(value: Uuid) -> Self { + Self::Uuid(value) + } +} + impl From<(&'static str, u32)> for ElementId { fn from((name, id): (&'static str, u32)) -> Self { ElementId::NamedInteger(name.into(), id as usize) diff --git a/docs/src/assistant-panel.md b/docs/src/assistant-panel.md index 7e03aa4ea0..19a5cce9ab 100644 --- a/docs/src/assistant-panel.md +++ b/docs/src/assistant-panel.md @@ -134,9 +134,11 @@ You can use Ollama with the Zed assistant by making Ollama appear as an OpenAPI ``` 5. Restart Zed -## Prompt Manager +## Prompt Library -Zed has a prompt manager for enabling and disabling custom prompts. +**Warning: This feature is experimental and the format of prompts is _highly_ likely to change. Use at your own risk!** + +Zed has a prompt library that allows you to manage prompts. These are useful for: @@ -154,26 +156,16 @@ Checked prompts are included in your "default prompt", which can be inserted int Prompts have a simple format: -```json -{ - // ~/.config/zed/prompts/no-comments.json - "title": "No comments in code", - "version": "1.0", - "author": "Nate Butler ", - "languages": ["*"], - "prompt": "Do not add inline or doc comments to any returned code. Avoid removing existing comments unless they are no longer accurate due to changes in the code." -} +```md +--- +title: Foo +version: 1.0 +author: Jane Kim ) -> Self {\n Self { id.into() }\n }\n}\n\nimpl RenderOnce for MyComponent {\n fn render(self, cx: &mut WindowContext) -> impl IntoElement {\n div().id(self.id.clone()).child(text(\"Hello, world!\"))\n }\n}\n```" -} -``` - -In the future we'll allow creating and editing prompts directly in the prompt manager, reducing the need to do this by hand. +In the future we'll allow creating and editing prompts directly in the prompt manager.