Detailed changes
@@ -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>
@@ -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>
@@ -1,12 +1,10 @@
use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer};
-use crate::prompts::prompt_library::PromptLibrary;
-use crate::prompts::prompt_manager::PromptManager;
+use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
use crate::{
ambient_context::*,
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
codegen::{self, Codegen, CodegenKind},
omit_ranges::text_in_range_omitting_ranges,
- prompts::prompt::generate_content_prompt,
search::*,
slash_command::{
current_file_command, file_command, prompt_command, SlashCommandCleanup,
@@ -148,7 +146,7 @@ impl AssistantPanel {
.unwrap_or_default();
let prompt_library = Arc::new(
- PromptLibrary::load(fs.clone())
+ PromptLibrary::load_index(fs.clone())
.await
.log_err()
.unwrap_or_default(),
@@ -1,3 +1,7 @@
-pub mod prompt;
-pub mod prompt_library;
-pub mod prompt_manager;
+mod prompt;
+mod prompt_library;
+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 std::{fmt::Write, ops::Range};
+use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
use ui::SharedString;
+use util::paths::PROMPTS_DIR;
use gray_matter::{engine::YAML, Matter};
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)]
pub struct StaticPromptFrontmatter {
title: String,
@@ -19,15 +43,51 @@ pub struct StaticPromptFrontmatter {
impl Default for StaticPromptFrontmatter {
fn default() -> Self {
Self {
- title: "Untitled Prompt".to_string(),
+ title: PROMPT_DEFAULT_TITLE.to_string(),
version: "1.0".to_string(),
- author: "No Author".to_string(),
- languages: vec!["*".to_string()],
+ author: "You <you@email.com>".to_string(),
+ languages: 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
/// from Markdown with a frontmatter header
///
@@ -62,16 +122,39 @@ impl Default for StaticPromptFrontmatter {
/// ```
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPrompt {
+ #[serde(skip_deserializing)]
+ id: PromptId,
#[serde(skip)]
metadata: StaticPromptFrontmatter,
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 {
pub fn new(content: String, file_name: Option<String>) -> Self {
let matter = Matter::<YAML>::new();
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
.data
@@ -91,19 +174,48 @@ impl StaticPrompt {
StaticPromptFrontmatter::default()
});
+ let id = if let Some(file_name) = &file_name {
+ PromptId::from_str(file_name).unwrap_or_default()
+ } else {
+ PromptId::new()
+ };
+
StaticPrompt {
+ id,
content,
file_name,
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 {
+ /// 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
- pub fn _file_name(&mut self, file_name: String) -> &mut Self {
- self.file_name = Some(file_name);
- self
+ pub fn new_file_name(&self) -> String {
+ let in_name = format!(
+ "{}_{}_{}",
+ 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
@@ -126,6 +238,32 @@ impl StaticPrompt {
let result = matter.parse(self.content.as_str());
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(
@@ -25,6 +25,16 @@ impl PromptId {
pub fn new() -> Self {
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)]
@@ -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();
- state
- .prompts
- .iter()
- .map(|(id, prompt)| (*id, prompt.clone()))
- .collect()
+ state.prompts.clone()
}
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
@@ -81,36 +98,37 @@ impl PromptLibrary {
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> {
let state = self.state.read();
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();
- state.prompts.get(&id).cloned()
+ state.dirty_prompts.contains(&id)
}
- /// 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(())
+ pub fn set_dirty(&self, id: PromptId, dirty: bool) {
+ let mut state = self.state.write();
+ if dirty {
+ if !state.dirty_prompts.contains(&id) {
+ state.dirty_prompts.push(id);
+ }
+ state.version += 1;
+ } else {
+ state.dirty_prompts.retain(|&i| i != id);
+ state.version += 1;
+ }
}
/// 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> {
+ pub async fn load_index(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let path = PROMPTS_DIR.join("index.json");
let state = if fs.is_file(&path).await {
@@ -132,9 +150,6 @@ impl PromptLibrary {
/// 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?;
@@ -182,7 +197,48 @@ impl PromptLibrary {
}
// 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(())
}
@@ -1,16 +1,17 @@
use collections::HashMap;
-use editor::Editor;
+use editor::{Editor, EditorEvent};
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 ui::{prelude::*, IconButtonShape, Indicator, ListItem, ListItemSpacing, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::ModalView;
-use super::prompt_library::{PromptId, PromptLibrary, SortOrder};
-use crate::prompts::prompt::StaticPrompt;
+use crate::prompts::{PromptId, PromptLibrary, SortOrder, StaticPrompt, PROMPT_DEFAULT_TITLE};
+
+actions!(prompt_manager, [NewPrompt, SavePrompt]);
pub struct PromptManager {
focus_handle: FocusHandle,
@@ -21,6 +22,8 @@ pub struct PromptManager {
picker: View<Picker<PromptManagerDelegate>>,
prompt_editors: HashMap<PromptId, View<Editor>>,
active_prompt_id: Option<PromptId>,
+ last_new_prompt_id: Option<PromptId>,
+ _subscriptions: Vec<Subscription>,
}
impl PromptManager {
@@ -39,6 +42,7 @@ impl PromptManager {
matching_prompt_ids: vec![],
prompt_library: prompt_library.clone(),
selected_index: 0,
+ _subscriptions: vec![],
},
cx,
)
@@ -48,6 +52,11 @@ impl PromptManager {
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 {
focus_handle,
prompt_library,
@@ -56,6 +65,8 @@ impl PromptManager {
picker,
prompt_editors: HashMap::default(),
active_prompt_id: None,
+ last_new_prompt_id: None,
+ _subscriptions: subscriptions,
};
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
@@ -63,11 +74,105 @@ impl PromptManager {
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>) {
self.active_prompt_id = prompt_id;
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>) {
if let Some(active_prompt_id) = self.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>) {
- 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),
- )
+ pub fn active_editor(&self) -> Option<&View<Editor>> {
+ self.active_prompt_id
+ .and_then(|active_prompt_id| self.prompt_editors.get(&active_prompt_id))
}
fn set_editor_for_prompt(
@@ -121,7 +197,7 @@ impl PromptManager {
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) {
+ let text = if let Some(prompt) = prompt_library.prompt_by_id(prompt_id) {
prompt.content().to_owned()
} else {
"".to_string()
@@ -147,17 +223,76 @@ impl PromptManager {
editor
})
});
+
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 {
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()
- .key_context("PromptManager")
+ .id("prompt-manager")
+ .key_context(self.dispatch_context(cx))
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
- // .on_action(cx.listener(Self::save_active_prompt))
+ .on_action(cx.listener(Self::new_prompt))
.elevation_3(cx)
.size_full()
.flex_none()
@@ -166,7 +301,7 @@ impl Render for PromptManager {
.overflow_hidden()
.child(self.render_prompt_list(cx))
.child(
- div().w_3_5().h_full().child(
+ div().w_2_3().h_full().child(
v_flex()
.id("prompt-editor")
.border_l_1()
@@ -176,6 +311,7 @@ impl Render for PromptManager {
.flex_none()
.min_w_64()
.h_full()
+ .overflow_hidden()
.child(
h_flex()
.bg(cx.theme().colors().background)
@@ -185,16 +321,60 @@ impl Render for PromptManager {
.h_7()
.w_full()
.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(
IconButton::new("dismiss", IconName::Close)
.shape(IconButtonShape::Square)
+ .tooltip(move |cx| Tooltip::text("Close", cx))
.on_click(|_, cx| {
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(
h_flex()
.flex_1()
@@ -210,6 +390,8 @@ impl Render for PromptManager {
}
impl EventEmitter<DismissEvent> for PromptManager {}
+impl EventEmitter<EditorEvent> for PromptManager {}
+
impl ModalView for PromptManager {}
impl FocusableView for PromptManager {
@@ -224,6 +406,7 @@ pub struct PromptManagerDelegate {
matching_prompt_ids: Vec<PromptId>,
prompt_library: Arc<PromptLibrary>,
selected_index: usize,
+ _subscriptions: Vec<Subscription>,
}
impl PickerDelegate for PromptManagerDelegate {
@@ -313,15 +496,17 @@ impl PickerDelegate for PromptManagerDelegate {
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
- let matching_prompt = self.matching_prompts.get(ix)?;
- let prompt = matching_prompt.clone();
+ let prompt = self.matching_prompts.get(ix)?;
+
+ let is_diry = self.prompt_library.is_dirty(prompt.id());
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.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 crate::prompts::prompt_library::PromptLibrary;
+use crate::prompts::PromptLibrary;
use anyhow::{anyhow, Context, Result};
use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
@@ -164,6 +164,8 @@ pub enum IconName {
ReplaceNext,
ReplyArrowRight,
Return,
+ Reveal,
+ Save,
Screen,
SelectAll,
Server,
@@ -277,10 +279,12 @@ impl IconName {
IconName::Quote => "icons/quote.svg",
IconName::Regex => "icons/regex.svg",
IconName::Replace => "icons/replace.svg",
+ IconName::Reveal => "icons/reveal.svg",
IconName::ReplaceAll => "icons/replace_all.svg",
IconName::ReplaceNext => "icons/replace_next.svg",
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
IconName::Return => "icons/return.svg",
+ IconName::Save => "icons/save.svg",
IconName::Screen => "icons/desktop.svg",
IconName::SelectAll => "icons/select_all.svg",
IconName::Server => "icons/server.svg",