mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Allow saving prompts from the Prompt Manager (#12359)
Adds the following features to the prompt manager: - New prompt – Create a new prompt from the UI. It will only persist if it is saved. - Save prompt – Save a prompt by clicking the save button in the UI. A keybinding will be added for this in the future. - Reveal prompt - Show the selected prompt on the file system. Only available for saved prompts. New prompts that are saved will use the `{slugified_title}_{ver}_{id}.md` format which all imported prompts will move to in the near future. Also orders prompts in alphabetical order by default. Release Notes: - N/A
This commit is contained in:
parent
345361cd38
commit
a6dd2ca694
1
assets/icons/reveal.svg
Normal file
1
assets/icons/reveal.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-search"><circle cx="17" cy="17" r="3"/><path d="M10.7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4.1"/><path d="m21 21-1.5-1.5"/></svg>
|
After Width: | Height: | Size: 400 B |
1
assets/icons/save.svg
Normal file
1
assets/icons/save.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-save"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>
|
After Width: | Height: | Size: 412 B |
@ -1,12 +1,10 @@
|
|||||||
use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer};
|
use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer};
|
||||||
use crate::prompts::prompt_library::PromptLibrary;
|
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
|
||||||
use crate::prompts::prompt_manager::PromptManager;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ambient_context::*,
|
ambient_context::*,
|
||||||
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
|
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
|
||||||
codegen::{self, Codegen, CodegenKind},
|
codegen::{self, Codegen, CodegenKind},
|
||||||
omit_ranges::text_in_range_omitting_ranges,
|
omit_ranges::text_in_range_omitting_ranges,
|
||||||
prompts::prompt::generate_content_prompt,
|
|
||||||
search::*,
|
search::*,
|
||||||
slash_command::{
|
slash_command::{
|
||||||
current_file_command, file_command, prompt_command, SlashCommandCleanup,
|
current_file_command, file_command, prompt_command, SlashCommandCleanup,
|
||||||
@ -148,7 +146,7 @@ impl AssistantPanel {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let prompt_library = Arc::new(
|
let prompt_library = Arc::new(
|
||||||
PromptLibrary::load(fs.clone())
|
PromptLibrary::load_index(fs.clone())
|
||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
pub mod prompt;
|
mod prompt;
|
||||||
pub mod prompt_library;
|
mod prompt_library;
|
||||||
pub mod prompt_manager;
|
mod prompt_manager;
|
||||||
|
|
||||||
|
pub use prompt::*;
|
||||||
|
pub use prompt_library::*;
|
||||||
|
pub use prompt_manager::*;
|
||||||
|
@ -1,10 +1,34 @@
|
|||||||
|
use fs::Fs;
|
||||||
use language::BufferSnapshot;
|
use language::BufferSnapshot;
|
||||||
use std::{fmt::Write, ops::Range};
|
use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
|
||||||
use ui::SharedString;
|
use ui::SharedString;
|
||||||
|
use util::paths::PROMPTS_DIR;
|
||||||
|
|
||||||
use gray_matter::{engine::YAML, Matter};
|
use gray_matter::{engine::YAML, Matter};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::prompt_library::PromptId;
|
||||||
|
|
||||||
|
pub const PROMPT_DEFAULT_TITLE: &str = "Untitled Prompt";
|
||||||
|
|
||||||
|
fn standardize_value(value: String) -> String {
|
||||||
|
value.replace(['\n', '\r', '"', '\''], "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slugify(input: String) -> String {
|
||||||
|
let mut slug = String::new();
|
||||||
|
for c in input.chars() {
|
||||||
|
if c.is_alphanumeric() {
|
||||||
|
slug.push(c.to_ascii_lowercase());
|
||||||
|
} else if c.is_whitespace() {
|
||||||
|
slug.push('-');
|
||||||
|
} else {
|
||||||
|
slug.push('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
pub struct StaticPromptFrontmatter {
|
pub struct StaticPromptFrontmatter {
|
||||||
title: String,
|
title: String,
|
||||||
@ -19,15 +43,51 @@ pub struct StaticPromptFrontmatter {
|
|||||||
impl Default for StaticPromptFrontmatter {
|
impl Default for StaticPromptFrontmatter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: "Untitled Prompt".to_string(),
|
title: PROMPT_DEFAULT_TITLE.to_string(),
|
||||||
version: "1.0".to_string(),
|
version: "1.0".to_string(),
|
||||||
author: "No Author".to_string(),
|
author: "You <you@email.com>".to_string(),
|
||||||
languages: vec!["*".to_string()],
|
languages: vec![],
|
||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StaticPromptFrontmatter {
|
||||||
|
/// Returns the frontmatter as a markdown frontmatter string
|
||||||
|
pub fn frontmatter_string(&self) -> String {
|
||||||
|
let mut frontmatter = format!(
|
||||||
|
"---\ntitle: \"{}\"\nversion: \"{}\"\nauthor: \"{}\"\n",
|
||||||
|
standardize_value(self.title.clone()),
|
||||||
|
standardize_value(self.version.clone()),
|
||||||
|
standardize_value(self.author.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if !self.languages.is_empty() {
|
||||||
|
let languages = self
|
||||||
|
.languages
|
||||||
|
.iter()
|
||||||
|
.map(|l| standardize_value(l.clone()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
writeln!(frontmatter, "languages: [{}]", languages).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.dependencies.is_empty() {
|
||||||
|
let dependencies = self
|
||||||
|
.dependencies
|
||||||
|
.iter()
|
||||||
|
.map(|d| standardize_value(d.clone()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
writeln!(frontmatter, "dependencies: [{}]", dependencies).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
frontmatter.push_str("---\n");
|
||||||
|
|
||||||
|
frontmatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A static prompt that can be loaded into the prompt library
|
/// A static prompt that can be loaded into the prompt library
|
||||||
/// from Markdown with a frontmatter header
|
/// from Markdown with a frontmatter header
|
||||||
///
|
///
|
||||||
@ -62,16 +122,39 @@ impl Default for StaticPromptFrontmatter {
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
pub struct StaticPrompt {
|
pub struct StaticPrompt {
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
id: PromptId,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
metadata: StaticPromptFrontmatter,
|
metadata: StaticPromptFrontmatter,
|
||||||
content: String,
|
content: String,
|
||||||
file_name: Option<String>,
|
file_name: Option<SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StaticPrompt {
|
||||||
|
fn default() -> Self {
|
||||||
|
let metadata = StaticPromptFrontmatter::default();
|
||||||
|
|
||||||
|
let content = metadata.clone().frontmatter_string();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: PromptId::new(),
|
||||||
|
metadata,
|
||||||
|
content,
|
||||||
|
file_name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticPrompt {
|
impl StaticPrompt {
|
||||||
pub fn new(content: String, file_name: Option<String>) -> Self {
|
pub fn new(content: String, file_name: Option<String>) -> Self {
|
||||||
let matter = Matter::<YAML>::new();
|
let matter = Matter::<YAML>::new();
|
||||||
let result = matter.parse(&content);
|
let result = matter.parse(&content);
|
||||||
|
let file_name = if let Some(file_name) = file_name {
|
||||||
|
let shared_filename: SharedString = file_name.into();
|
||||||
|
Some(shared_filename)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let metadata = result
|
let metadata = result
|
||||||
.data
|
.data
|
||||||
@ -91,19 +174,48 @@ impl StaticPrompt {
|
|||||||
StaticPromptFrontmatter::default()
|
StaticPromptFrontmatter::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let id = if let Some(file_name) = &file_name {
|
||||||
|
PromptId::from_str(file_name).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
PromptId::new()
|
||||||
|
};
|
||||||
|
|
||||||
StaticPrompt {
|
StaticPrompt {
|
||||||
|
id,
|
||||||
content,
|
content,
|
||||||
file_name,
|
file_name,
|
||||||
metadata,
|
metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, id: PromptId, content: String) {
|
||||||
|
let mut updated_prompt =
|
||||||
|
StaticPrompt::new(content, self.file_name.clone().map(|s| s.to_string()));
|
||||||
|
updated_prompt.id = id;
|
||||||
|
*self = updated_prompt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticPrompt {
|
impl StaticPrompt {
|
||||||
|
/// Returns the prompt's id
|
||||||
|
pub fn id(&self) -> &PromptId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_name(&self) -> Option<&SharedString> {
|
||||||
|
self.file_name.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the file name of the prompt
|
/// Sets the file name of the prompt
|
||||||
pub fn _file_name(&mut self, file_name: String) -> &mut Self {
|
pub fn new_file_name(&self) -> String {
|
||||||
self.file_name = Some(file_name);
|
let in_name = format!(
|
||||||
self
|
"{}_{}_{}",
|
||||||
|
standardize_value(self.metadata.title.clone()),
|
||||||
|
standardize_value(self.metadata.version.clone()),
|
||||||
|
standardize_value(self.id.0.to_string())
|
||||||
|
);
|
||||||
|
let out_name = slugify(in_name);
|
||||||
|
out_name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the prompt's content
|
/// Returns the prompt's content
|
||||||
@ -126,6 +238,32 @@ impl StaticPrompt {
|
|||||||
let result = matter.parse(self.content.as_str());
|
let result = matter.parse(self.content.as_str());
|
||||||
result.content.clone()
|
result.content.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> Option<PathBuf> {
|
||||||
|
if let Some(file_name) = self.file_name() {
|
||||||
|
let path_str = format!("{}", file_name);
|
||||||
|
Some(PROMPTS_DIR.join(path_str))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||||
|
let file_name = self.file_name();
|
||||||
|
let new_file_name = self.new_file_name();
|
||||||
|
|
||||||
|
let out_name = if let Some(file_name) = file_name {
|
||||||
|
file_name.to_owned().to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}.md", new_file_name)
|
||||||
|
};
|
||||||
|
let path = PROMPTS_DIR.join(&out_name);
|
||||||
|
let json = self.content.clone();
|
||||||
|
|
||||||
|
fs.atomic_write(path, json).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_content_prompt(
|
pub fn generate_content_prompt(
|
||||||
|
@ -25,6 +25,16 @@ impl PromptId {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(Uuid::new_v4())
|
Self(Uuid::new_v4())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_str(id: &str) -> anyhow::Result<Self> {
|
||||||
|
Ok(Self(Uuid::parse_str(id)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PromptId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize)]
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
@ -56,13 +66,20 @@ impl PromptLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prompts(&self) -> Vec<(PromptId, StaticPrompt)> {
|
pub fn new_prompt(&self) -> StaticPrompt {
|
||||||
|
StaticPrompt::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_prompt(&self, prompt: StaticPrompt) {
|
||||||
|
let mut state = self.state.write();
|
||||||
|
let id = *prompt.id();
|
||||||
|
state.prompts.insert(id, prompt);
|
||||||
|
state.version += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prompts(&self) -> HashMap<PromptId, StaticPrompt> {
|
||||||
let state = self.state.read();
|
let state = self.state.read();
|
||||||
state
|
state.prompts.clone()
|
||||||
.prompts
|
|
||||||
.iter()
|
|
||||||
.map(|(id, prompt)| (*id, prompt.clone()))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
|
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
|
||||||
@ -81,36 +98,37 @@ impl PromptLibrary {
|
|||||||
prompts
|
prompts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompt_by_id(&self, id: PromptId) -> Option<StaticPrompt> {
|
||||||
|
let state = self.state.read();
|
||||||
|
state.prompts.get(&id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn first_prompt_id(&self) -> Option<PromptId> {
|
pub fn first_prompt_id(&self) -> Option<PromptId> {
|
||||||
let state = self.state.read();
|
let state = self.state.read();
|
||||||
state.prompts.keys().next().cloned()
|
state.prompts.keys().next().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prompt(&self, id: PromptId) -> Option<StaticPrompt> {
|
pub fn is_dirty(&self, id: &PromptId) -> bool {
|
||||||
let state = self.state.read();
|
let state = self.state.read();
|
||||||
state.prompts.get(&id).cloned()
|
state.dirty_prompts.contains(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save the current state of the prompt library to the
|
pub fn set_dirty(&self, id: PromptId, dirty: bool) {
|
||||||
/// file system as a JSON file
|
let mut state = self.state.write();
|
||||||
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
if dirty {
|
||||||
fs.create_dir(&PROMPTS_DIR).await?;
|
if !state.dirty_prompts.contains(&id) {
|
||||||
|
state.dirty_prompts.push(id);
|
||||||
let path = PROMPTS_DIR.join("index.json");
|
}
|
||||||
|
state.version += 1;
|
||||||
let json = {
|
} else {
|
||||||
let state = self.state.read();
|
state.dirty_prompts.retain(|&i| i != id);
|
||||||
serde_json::to_string(&*state)?
|
state.version += 1;
|
||||||
};
|
}
|
||||||
|
|
||||||
fs.atomic_write(path, json).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the state of the prompt library from the file system
|
/// Load the state of the prompt library from the file system
|
||||||
/// or create a new one if it doesn't exist
|
/// or create a new one if it doesn't exist
|
||||||
pub async fn load(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
pub async fn load_index(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
||||||
let path = PROMPTS_DIR.join("index.json");
|
let path = PROMPTS_DIR.join("index.json");
|
||||||
|
|
||||||
let state = if fs.is_file(&path).await {
|
let state = if fs.is_file(&path).await {
|
||||||
@ -132,9 +150,6 @@ impl PromptLibrary {
|
|||||||
/// Load all prompts from the file system
|
/// Load all prompts from the file system
|
||||||
/// adding them to the library if they don't already exist
|
/// adding them to the library if they don't already exist
|
||||||
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
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();
|
self.state.get_mut().prompts.clear();
|
||||||
|
|
||||||
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
|
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
|
||||||
@ -182,7 +197,48 @@ impl PromptLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write any changes back to the file system
|
// Write any changes back to the file system
|
||||||
self.save(fs.clone()).await?;
|
self.save_index(fs.clone()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the current state of the prompt library to the
|
||||||
|
/// file system as a JSON file
|
||||||
|
pub async fn save_index(&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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_prompt(
|
||||||
|
&self,
|
||||||
|
prompt_id: PromptId,
|
||||||
|
updated_content: Option<String>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let Some(updated_content) = updated_content {
|
||||||
|
let mut state = self.state.write();
|
||||||
|
if let Some(prompt) = state.prompts.get_mut(&prompt_id) {
|
||||||
|
prompt.update(prompt_id, updated_content);
|
||||||
|
state.version += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(prompt) = self.prompt_by_id(prompt_id) {
|
||||||
|
prompt.save(fs).await?;
|
||||||
|
self.set_dirty(prompt_id, false);
|
||||||
|
} else {
|
||||||
|
log::warn!("Failed to save prompt: {:?}", prompt_id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::Editor;
|
use editor::{Editor, EditorEvent};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{prelude::FluentBuilder, *};
|
use gpui::{prelude::FluentBuilder, *};
|
||||||
use language::{language_settings, Buffer, LanguageRegistry};
|
use language::{language_settings, Buffer, LanguageRegistry};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing};
|
use ui::{prelude::*, IconButtonShape, Indicator, ListItem, ListItemSpacing, Tooltip};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::ModalView;
|
use workspace::ModalView;
|
||||||
|
|
||||||
use super::prompt_library::{PromptId, PromptLibrary, SortOrder};
|
use crate::prompts::{PromptId, PromptLibrary, SortOrder, StaticPrompt, PROMPT_DEFAULT_TITLE};
|
||||||
use crate::prompts::prompt::StaticPrompt;
|
|
||||||
|
actions!(prompt_manager, [NewPrompt, SavePrompt]);
|
||||||
|
|
||||||
pub struct PromptManager {
|
pub struct PromptManager {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
@ -21,6 +22,8 @@ pub struct PromptManager {
|
|||||||
picker: View<Picker<PromptManagerDelegate>>,
|
picker: View<Picker<PromptManagerDelegate>>,
|
||||||
prompt_editors: HashMap<PromptId, View<Editor>>,
|
prompt_editors: HashMap<PromptId, View<Editor>>,
|
||||||
active_prompt_id: Option<PromptId>,
|
active_prompt_id: Option<PromptId>,
|
||||||
|
last_new_prompt_id: Option<PromptId>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PromptManager {
|
impl PromptManager {
|
||||||
@ -39,6 +42,7 @@ impl PromptManager {
|
|||||||
matching_prompt_ids: vec![],
|
matching_prompt_ids: vec![],
|
||||||
prompt_library: prompt_library.clone(),
|
prompt_library: prompt_library.clone(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
|
_subscriptions: vec![],
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@ -48,6 +52,11 @@ impl PromptManager {
|
|||||||
|
|
||||||
let focus_handle = picker.focus_handle(cx);
|
let focus_handle = picker.focus_handle(cx);
|
||||||
|
|
||||||
|
let subscriptions = vec![
|
||||||
|
// cx.on_focus_in(&focus_handle, Self::focus_in),
|
||||||
|
// cx.on_focus_out(&focus_handle, Self::focus_out),
|
||||||
|
];
|
||||||
|
|
||||||
let mut manager = Self {
|
let mut manager = Self {
|
||||||
focus_handle,
|
focus_handle,
|
||||||
prompt_library,
|
prompt_library,
|
||||||
@ -56,6 +65,8 @@ impl PromptManager {
|
|||||||
picker,
|
picker,
|
||||||
prompt_editors: HashMap::default(),
|
prompt_editors: HashMap::default(),
|
||||||
active_prompt_id: None,
|
active_prompt_id: None,
|
||||||
|
last_new_prompt_id: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
|
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
|
||||||
@ -63,11 +74,105 @@ impl PromptManager {
|
|||||||
manager
|
manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
||||||
|
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||||
|
dispatch_context.add("PromptManager");
|
||||||
|
|
||||||
|
let identifier = match self.active_editor() {
|
||||||
|
Some(active_editor) if active_editor.focus_handle(cx).is_focused(cx) => "editing",
|
||||||
|
_ => "not_editing",
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch_context.add(identifier);
|
||||||
|
dispatch_context
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_prompt(&mut self, _: &NewPrompt, cx: &mut ViewContext<Self>) {
|
||||||
|
// TODO: Why doesn't this prevent making a new prompt if you
|
||||||
|
// move the picker selection/maybe unfocus the editor?
|
||||||
|
|
||||||
|
// Prevent making a new prompt if the last new prompt is still empty
|
||||||
|
//
|
||||||
|
// Instead, we'll focus the last new prompt
|
||||||
|
if let Some(last_new_prompt_id) = self.last_new_prompt_id() {
|
||||||
|
if let Some(last_new_prompt) = self.prompt_library.prompt_by_id(last_new_prompt_id) {
|
||||||
|
let normalized_body = last_new_prompt
|
||||||
|
.body()
|
||||||
|
.trim()
|
||||||
|
.replace(['\r', '\n'], "")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if last_new_prompt.title() == PROMPT_DEFAULT_TITLE && normalized_body.is_empty() {
|
||||||
|
self.set_editor_for_prompt(last_new_prompt_id, cx);
|
||||||
|
self.focus_active_editor(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = self.prompt_library.new_prompt();
|
||||||
|
self.set_last_new_prompt_id(Some(prompt.id().to_owned()));
|
||||||
|
|
||||||
|
self.prompt_library.add_prompt(prompt.clone());
|
||||||
|
|
||||||
|
let id = *prompt.id();
|
||||||
|
self.picker.update(cx, |picker, _cx| {
|
||||||
|
let prompts = self
|
||||||
|
.prompt_library
|
||||||
|
.sorted_prompts(SortOrder::Alphabetical)
|
||||||
|
.clone()
|
||||||
|
.into_iter();
|
||||||
|
|
||||||
|
picker.delegate.prompt_library = self.prompt_library.clone();
|
||||||
|
picker.delegate.matching_prompts = prompts.clone().map(|(_, p)| Arc::new(p)).collect();
|
||||||
|
picker.delegate.matching_prompt_ids = prompts.map(|(id, _)| id).collect();
|
||||||
|
picker.delegate.selected_index = picker
|
||||||
|
.delegate
|
||||||
|
.matching_prompts
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.id() == &id)
|
||||||
|
.unwrap_or(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.active_prompt_id = Some(id);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_prompt(
|
||||||
|
&mut self,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
prompt_id: PromptId,
|
||||||
|
new_content: String,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let library = self.prompt_library.clone();
|
||||||
|
if library.prompt_by_id(prompt_id).is_some() {
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
library
|
||||||
|
.save_prompt(prompt_id, Some(new_content), fs)
|
||||||
|
.log_err()
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||||
self.active_prompt_id = prompt_id;
|
self.active_prompt_id = prompt_id;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_new_prompt_id(&self) -> Option<PromptId> {
|
||||||
|
self.last_new_prompt_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_last_new_prompt_id(&mut self, id: Option<PromptId>) {
|
||||||
|
self.last_new_prompt_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
|
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||||
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
|
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
|
||||||
@ -78,38 +183,9 @@ impl PromptManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
pub fn active_editor(&self) -> Option<&View<Editor>> {
|
||||||
cx.emit(DismissEvent);
|
self.active_prompt_id
|
||||||
}
|
.and_then(|active_prompt_id| self.prompt_editors.get(&active_prompt_id))
|
||||||
|
|
||||||
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(
|
fn set_editor_for_prompt(
|
||||||
@ -121,7 +197,7 @@ impl PromptManager {
|
|||||||
|
|
||||||
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
|
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
|
||||||
cx.new_view(|cx| {
|
cx.new_view(|cx| {
|
||||||
let text = if let Some(prompt) = prompt_library.prompt(prompt_id) {
|
let text = if let Some(prompt) = prompt_library.prompt_by_id(prompt_id) {
|
||||||
prompt.content().to_owned()
|
prompt.content().to_owned()
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
@ -147,17 +223,76 @@ impl PromptManager {
|
|||||||
editor
|
editor
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
editor_for_prompt.clone()
|
editor_for_prompt.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_1_3()
|
||||||
|
.overflow_hidden()
|
||||||
|
.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)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(move |cx| Tooltip::text("New Prompt", cx))
|
||||||
|
.on_click(|_, cx| {
|
||||||
|
cx.dispatch_action(NewPrompt.boxed_clone());
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.h(rems(38.25))
|
||||||
|
.flex_grow()
|
||||||
|
.justify_start()
|
||||||
|
.child(picker),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for PromptManager {
|
impl Render for PromptManager {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let active_prompt_id = self.active_prompt_id;
|
||||||
|
let active_prompt = if let Some(active_prompt_id) = active_prompt_id {
|
||||||
|
self.prompt_library.clone().prompt_by_id(active_prompt_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let active_editor = self.active_editor().map(|editor| editor.clone());
|
||||||
|
let updated_content = if let Some(editor) = active_editor {
|
||||||
|
Some(editor.read(cx).text(cx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let can_save = active_prompt_id.is_some() && updated_content.is_some();
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.key_context("PromptManager")
|
.id("prompt-manager")
|
||||||
|
.key_context(self.dispatch_context(cx))
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.on_action(cx.listener(Self::dismiss))
|
.on_action(cx.listener(Self::dismiss))
|
||||||
// .on_action(cx.listener(Self::save_active_prompt))
|
.on_action(cx.listener(Self::new_prompt))
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
@ -166,7 +301,7 @@ impl Render for PromptManager {
|
|||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(self.render_prompt_list(cx))
|
.child(self.render_prompt_list(cx))
|
||||||
.child(
|
.child(
|
||||||
div().w_3_5().h_full().child(
|
div().w_2_3().h_full().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("prompt-editor")
|
.id("prompt-editor")
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
@ -176,6 +311,7 @@ impl Render for PromptManager {
|
|||||||
.flex_none()
|
.flex_none()
|
||||||
.min_w_64()
|
.min_w_64()
|
||||||
.h_full()
|
.h_full()
|
||||||
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.bg(cx.theme().colors().background)
|
.bg(cx.theme().colors().background)
|
||||||
@ -185,16 +321,60 @@ impl Render for PromptManager {
|
|||||||
.h_7()
|
.h_7()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(div())
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap(Spacing::XXLarge.rems(cx))
|
||||||
|
.child(if can_save {
|
||||||
|
IconButton::new("save", IconName::Save)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(move |cx| Tooltip::text("Save Prompt", cx))
|
||||||
|
.on_click(cx.listener(move |this, _event, cx| {
|
||||||
|
if let Some(prompt_id) = active_prompt_id {
|
||||||
|
this.save_prompt(
|
||||||
|
fs.clone(),
|
||||||
|
prompt_id,
|
||||||
|
updated_content.clone().unwrap_or(
|
||||||
|
"TODO: make unreachable"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
IconButton::new("save", IconName::Save)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.disabled(true)
|
||||||
|
})
|
||||||
|
.when_some(active_prompt, |this, active_prompt| {
|
||||||
|
let path = active_prompt.path();
|
||||||
|
|
||||||
|
this.child(
|
||||||
|
IconButton::new("reveal", IconName::Reveal)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.disabled(path.is_none())
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::text("Reveal in Finder", cx)
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(move |_, _event, cx| {
|
||||||
|
if let Some(path) = path.clone() {
|
||||||
|
cx.reveal_path(&path);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("dismiss", IconName::Close)
|
IconButton::new("dismiss", IconName::Close)
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(move |cx| Tooltip::text("Close", cx))
|
||||||
.on_click(|_, cx| {
|
.on_click(|_, cx| {
|
||||||
cx.dispatch_action(menu::Cancel.boxed_clone());
|
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when_some(self.active_prompt_id, |this, active_prompt_id| {
|
.when_some(active_prompt_id, |this, active_prompt_id| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@ -210,6 +390,8 @@ impl Render for PromptManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for PromptManager {}
|
impl EventEmitter<DismissEvent> for PromptManager {}
|
||||||
|
impl EventEmitter<EditorEvent> for PromptManager {}
|
||||||
|
|
||||||
impl ModalView for PromptManager {}
|
impl ModalView for PromptManager {}
|
||||||
|
|
||||||
impl FocusableView for PromptManager {
|
impl FocusableView for PromptManager {
|
||||||
@ -224,6 +406,7 @@ pub struct PromptManagerDelegate {
|
|||||||
matching_prompt_ids: Vec<PromptId>,
|
matching_prompt_ids: Vec<PromptId>,
|
||||||
prompt_library: Arc<PromptLibrary>,
|
prompt_library: Arc<PromptLibrary>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for PromptManagerDelegate {
|
impl PickerDelegate for PromptManagerDelegate {
|
||||||
@ -313,15 +496,17 @@ impl PickerDelegate for PromptManagerDelegate {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
_cx: &mut ViewContext<Picker<Self>>,
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let matching_prompt = self.matching_prompts.get(ix)?;
|
let prompt = self.matching_prompts.get(ix)?;
|
||||||
let prompt = matching_prompt.clone();
|
|
||||||
|
let is_diry = self.prompt_library.is_dirty(prompt.id());
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.child(Label::new(prompt.title())),
|
.child(Label::new(prompt.title()))
|
||||||
|
.end_slot(div().when(is_diry, |this| this.child(Indicator::dot()))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||||
use crate::prompts::prompt_library::PromptLibrary;
|
use crate::prompts::PromptLibrary;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
|
@ -164,6 +164,8 @@ pub enum IconName {
|
|||||||
ReplaceNext,
|
ReplaceNext,
|
||||||
ReplyArrowRight,
|
ReplyArrowRight,
|
||||||
Return,
|
Return,
|
||||||
|
Reveal,
|
||||||
|
Save,
|
||||||
Screen,
|
Screen,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
Server,
|
Server,
|
||||||
@ -277,10 +279,12 @@ impl IconName {
|
|||||||
IconName::Quote => "icons/quote.svg",
|
IconName::Quote => "icons/quote.svg",
|
||||||
IconName::Regex => "icons/regex.svg",
|
IconName::Regex => "icons/regex.svg",
|
||||||
IconName::Replace => "icons/replace.svg",
|
IconName::Replace => "icons/replace.svg",
|
||||||
|
IconName::Reveal => "icons/reveal.svg",
|
||||||
IconName::ReplaceAll => "icons/replace_all.svg",
|
IconName::ReplaceAll => "icons/replace_all.svg",
|
||||||
IconName::ReplaceNext => "icons/replace_next.svg",
|
IconName::ReplaceNext => "icons/replace_next.svg",
|
||||||
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
|
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
|
||||||
IconName::Return => "icons/return.svg",
|
IconName::Return => "icons/return.svg",
|
||||||
|
IconName::Save => "icons/save.svg",
|
||||||
IconName::Screen => "icons/desktop.svg",
|
IconName::Screen => "icons/desktop.svg",
|
||||||
IconName::SelectAll => "icons/select_all.svg",
|
IconName::SelectAll => "icons/select_all.svg",
|
||||||
IconName::Server => "icons/server.svg",
|
IconName::Server => "icons/server.svg",
|
||||||
|
Loading…
Reference in New Issue
Block a user