Prompt library updates (#11988)
Restructure prompts & the prompt library. - Prompts are now written in markdown - The prompt manager has a picker and editable prompts - Saving isn't wired up yet - This also removes the "Insert active prompt" button as this concept doesn't exist anymore, and will be replaced with slash commands. I didn't staff flag this, but if you do play around with it expect it to still be pretty rough. Release Notes: - N/A --------- Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com> Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
parent
a73a3ef243
commit
0a848f29e8
14 changed files with 964 additions and 679 deletions
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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<Self>) {
|
||||
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<Workspace>,
|
||||
) {
|
||||
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
// fn insert_active_prompt(
|
||||
// workspace: &mut Workspace,
|
||||
// _: &InsertActivePrompt,
|
||||
// cx: &mut ViewContext<Workspace>,
|
||||
// ) {
|
||||
// let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
// return;
|
||||
// };
|
||||
|
||||
if !panel.focus_handle(cx).contains_focused(cx) {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
}
|
||||
// if !panel.focus_handle(cx).contains_focused(cx) {
|
||||
// workspace.toggle_panel_focus::<AssistantPanel>(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<Self>) {
|
||||
let editor = self.editor.read(cx);
|
||||
|
|
|
@ -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<String, UserPrompt>,
|
||||
/// Prompts included in the default prompt
|
||||
default_prompts: Vec<String>,
|
||||
/// Prompts that have a pending update that hasn't been applied yet
|
||||
_updateable_prompts: Vec<String>,
|
||||
/// Prompts that have been changed since they were loaded
|
||||
/// and can be reverted to their original state
|
||||
_revertable_prompts: Vec<String>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
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<dyn Fs>) -> anyhow::Result<Self> {
|
||||
let prompt_library = PromptLibrary::new();
|
||||
prompt_library.load_prompts(fs)?;
|
||||
Ok(prompt_library)
|
||||
}
|
||||
|
||||
fn load_prompts(&self, fs: Arc<dyn Fs>) -> 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::<Vec<_>>();
|
||||
let mut state = self.state.write();
|
||||
state.prompts.extend(prompts_with_ids);
|
||||
state.version += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default_prompt(&self) -> Option<String> {
|
||||
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::<Vec<_>>()
|
||||
.join("\n\n---\n\n")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn prompts(&self) -> Vec<UserPrompt> {
|
||||
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<UserPrompt> {
|
||||
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<String> {
|
||||
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 <jane@kim.com>",
|
||||
/// "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<String>,
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
impl UserPrompt {
|
||||
async fn list(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<Self>> {
|
||||
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<PromptLibrary>,
|
||||
active_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(prompt_library: Arc<PromptLibrary>, 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<String>) {
|
||||
self.active_prompt = prompt_id;
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
let prompts = prompt_library
|
||||
.clone()
|
||||
.prompts_with_ids()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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<DismissEvent> for PromptManager {}
|
||||
impl ModalView for PromptManager {}
|
||||
|
||||
impl FocusableView for PromptManager {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
|
@ -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<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
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;
|
||||
|
|
278
crates/assistant/src/prompts/prompt.rs
Normal file
278
crates/assistant/src/prompts/prompt.rs
Normal file
|
@ -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<String>,
|
||||
#[serde(default)]
|
||||
dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
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<SharedString> {
|
||||
// self.languages
|
||||
// .clone()
|
||||
// .into_iter()
|
||||
// .map(|s| s.into())
|
||||
// .collect()
|
||||
// }
|
||||
|
||||
// pub fn dependencies(&self) -> Vec<SharedString> {
|
||||
// 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 <jane@kim.com
|
||||
/// languages: ["*"]
|
||||
/// dependencies: []
|
||||
/// ---
|
||||
///
|
||||
/// Foo and bar are terms used in programming to describe generic concepts.
|
||||
/// ```
|
||||
///
|
||||
/// ### Language-specific prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: UI with GPUI
|
||||
/// version: 1.0
|
||||
/// author: Nate Butler <iamnbutler@gmail.com>
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
pub fn new(content: String) -> Self {
|
||||
StaticPrompt {
|
||||
content,
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<SharedString> {
|
||||
self.metadata().map(|m| m.title())
|
||||
}
|
||||
|
||||
// pub fn version(&self) -> Option<SharedString> {
|
||||
// self.metadata().map(|m| m.version())
|
||||
// }
|
||||
|
||||
// pub fn author(&self) -> Option<SharedString> {
|
||||
// self.metadata().map(|m| m.author())
|
||||
// }
|
||||
|
||||
// pub fn languages(&self) -> Vec<SharedString> {
|
||||
// self.metadata().map(|m| m.languages()).unwrap_or_default()
|
||||
// }
|
||||
|
||||
// pub fn dependencies(&self) -> Vec<SharedString> {
|
||||
// self.metadata()
|
||||
// .map(|m| m.dependencies())
|
||||
// .unwrap_or_default()
|
||||
// }
|
||||
|
||||
// pub fn load(fs: Arc<Fs>, file_name: String) -> anyhow::Result<Self> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// pub fn save(&self, fs: Arc<Fs>) -> anyhow::Result<()> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// pub fn rename(&self, new_file_name: String, fs: Arc<Fs>) -> 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::<YAML>::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<StaticPromptFrontmatter> {
|
||||
self.parse().ok().map(|(front_matter, _)| front_matter)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
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)
|
||||
}
|
152
crates/assistant/src/prompts/prompt_library.rs
Normal file
152
crates/assistant/src/prompts/prompt_library.rs
Normal file
|
@ -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<PromptId>,
|
||||
/// All [Prompt]s loaded into the library
|
||||
prompts: HashMap<PromptId, StaticPrompt>,
|
||||
/// Prompts that have been changed but haven't been
|
||||
/// saved back to the file system
|
||||
dirty_prompts: Vec<PromptId>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
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<PromptId> {
|
||||
let state = self.state.read();
|
||||
state.prompts.keys().next().cloned()
|
||||
}
|
||||
|
||||
pub fn prompt(&self, id: PromptId) -> Option<StaticPrompt> {
|
||||
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<dyn Fs>) -> 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<dyn Fs>) -> anyhow::Result<Self> {
|
||||
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<dyn Fs>) -> 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(())
|
||||
}
|
||||
}
|
327
crates/assistant/src/prompts/prompt_manager.rs
Normal file
327
crates/assistant/src/prompts/prompt_manager.rs
Normal file
|
@ -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<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
#[allow(dead_code)]
|
||||
fs: Arc<dyn Fs>,
|
||||
picker: View<Picker<PromptManagerDelegate>>,
|
||||
prompt_editors: HashMap<PromptId, View<Editor>>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
|
||||
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<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> 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<Self>,
|
||||
) -> 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<Self>) -> 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<DismissEvent> 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<PromptManager>,
|
||||
matching_prompts: Vec<Arc<StaticPrompt>>,
|
||||
matching_prompt_ids: Vec<PromptId>,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl PickerDelegate for PromptManagerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"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<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn selected_index_changed(
|
||||
&self,
|
||||
ix: usize,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
|
||||
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<Picker<Self>>) -> 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::<Vec<_>>();
|
||||
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<Picker<Self>>) {
|
||||
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<Picker<Self>>) {
|
||||
self.prompt_manager
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
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())),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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::<Vec<_>>();
|
||||
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,
|
||||
|
|
|
@ -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<T>(
|
||||
self,
|
||||
option: Option<T>,
|
||||
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,
|
||||
|
|
|
@ -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<Uuid> 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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue