generated from nhcarrigan/template
feat: add snippet library for prompt templates
- Add Rust backend with persistent storage for snippets - Include 8 default snippets across categories (Code Review, Debugging, Testing, etc.) - Create SnippetLibraryPanel component with category filtering - Support create/edit/delete operations for custom snippets - Default snippets can be edited but not deleted - Add snippet button to InputBar for quick access Closes #22
This commit is contained in:
@@ -4,6 +4,7 @@ mod commands;
|
||||
mod config;
|
||||
mod notifications;
|
||||
mod sessions;
|
||||
mod snippets;
|
||||
mod stats;
|
||||
mod temp_manager;
|
||||
mod tray;
|
||||
@@ -18,6 +19,7 @@ use commands::load_saved_achievements;
|
||||
use commands::*;
|
||||
use notifications::*;
|
||||
use sessions::*;
|
||||
use snippets::*;
|
||||
use tauri::Manager;
|
||||
use temp_manager::create_shared_temp_manager;
|
||||
use tray::{setup_tray, should_minimize_to_tray};
|
||||
@@ -111,6 +113,11 @@ pub fn run() {
|
||||
delete_session,
|
||||
search_sessions,
|
||||
clear_all_sessions,
|
||||
list_snippets,
|
||||
save_snippet,
|
||||
delete_snippet,
|
||||
get_snippet_categories,
|
||||
reset_default_snippets,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
const SNIPPETS_STORE_KEY: &str = "snippets";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Snippet {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub content: String,
|
||||
pub category: String,
|
||||
pub is_default: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
fn get_default_snippets() -> Vec<Snippet> {
|
||||
let now = Utc::now();
|
||||
vec![
|
||||
Snippet {
|
||||
id: "default-explain-code".to_string(),
|
||||
name: "Explain this code".to_string(),
|
||||
content: "Please explain what this code does, step by step:".to_string(),
|
||||
category: "Code Review".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-fix-error".to_string(),
|
||||
name: "Fix this error".to_string(),
|
||||
content: "I'm getting the following error. Can you help me fix it?".to_string(),
|
||||
category: "Debugging".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-write-tests".to_string(),
|
||||
name: "Write tests".to_string(),
|
||||
content: "Please write unit tests for this code with good coverage:".to_string(),
|
||||
category: "Testing".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-refactor".to_string(),
|
||||
name: "Refactor for clarity".to_string(),
|
||||
content: "Please refactor this code to improve readability and maintainability:".to_string(),
|
||||
category: "Code Review".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-optimize".to_string(),
|
||||
name: "Optimize performance".to_string(),
|
||||
content: "Please analyze this code for performance issues and suggest optimizations:".to_string(),
|
||||
category: "Performance".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-review-pr".to_string(),
|
||||
name: "Review PR".to_string(),
|
||||
content: "Please review this pull request and provide feedback on code quality, potential issues, and suggestions for improvement.".to_string(),
|
||||
category: "Code Review".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-add-comments".to_string(),
|
||||
name: "Add documentation".to_string(),
|
||||
content: "Please add clear documentation comments to this code explaining what it does:".to_string(),
|
||||
category: "Documentation".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
Snippet {
|
||||
id: "default-security-review".to_string(),
|
||||
name: "Security review".to_string(),
|
||||
content: "Please review this code for security vulnerabilities and suggest fixes:".to_string(),
|
||||
category: "Security".to_string(),
|
||||
is_default: true,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn load_all_snippets(app: &AppHandle) -> Result<Vec<Snippet>, String> {
|
||||
let store = app
|
||||
.store("hikari-snippets.json")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match store.get(SNIPPETS_STORE_KEY) {
|
||||
Some(value) => {
|
||||
let mut snippets: Vec<Snippet> =
|
||||
serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
|
||||
|
||||
// Ensure default snippets exist (in case new ones were added in an update)
|
||||
let defaults = get_default_snippets();
|
||||
for default in defaults {
|
||||
if !snippets.iter().any(|s| s.id == default.id) {
|
||||
snippets.push(default);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(snippets)
|
||||
}
|
||||
None => Ok(get_default_snippets()),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_all_snippets(app: &AppHandle, snippets: &[Snippet]) -> Result<(), String> {
|
||||
let store = app
|
||||
.store("hikari-snippets.json")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let value = serde_json::to_value(snippets).map_err(|e| e.to_string())?;
|
||||
store.set(SNIPPETS_STORE_KEY, value);
|
||||
store.save().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_snippets(app: AppHandle) -> Result<Vec<Snippet>, String> {
|
||||
let mut snippets = load_all_snippets(&app)?;
|
||||
|
||||
// Sort by category, then by name
|
||||
snippets.sort_by(|a, b| {
|
||||
let cat_cmp = a.category.cmp(&b.category);
|
||||
if cat_cmp == std::cmp::Ordering::Equal {
|
||||
a.name.cmp(&b.name)
|
||||
} else {
|
||||
cat_cmp
|
||||
}
|
||||
});
|
||||
|
||||
Ok(snippets)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_snippet(app: AppHandle, snippet: Snippet) -> Result<(), String> {
|
||||
let mut snippets = load_all_snippets(&app)?;
|
||||
|
||||
// Update existing or add new
|
||||
if let Some(existing) = snippets.iter_mut().find(|s| s.id == snippet.id) {
|
||||
// Don't allow editing default snippets' is_default flag
|
||||
let mut updated = snippet;
|
||||
updated.is_default = existing.is_default;
|
||||
*existing = updated;
|
||||
} else {
|
||||
snippets.push(snippet);
|
||||
}
|
||||
|
||||
save_all_snippets(&app, &snippets)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_snippet(app: AppHandle, snippet_id: String) -> Result<(), String> {
|
||||
let mut snippets = load_all_snippets(&app)?;
|
||||
|
||||
// Don't allow deleting default snippets
|
||||
if snippets
|
||||
.iter()
|
||||
.any(|s| s.id == snippet_id && s.is_default)
|
||||
{
|
||||
return Err("Cannot delete default snippets".to_string());
|
||||
}
|
||||
|
||||
snippets.retain(|s| s.id != snippet_id);
|
||||
save_all_snippets(&app, &snippets)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_snippet_categories(app: AppHandle) -> Result<Vec<String>, String> {
|
||||
let snippets = load_all_snippets(&app)?;
|
||||
let mut categories: Vec<String> = snippets.iter().map(|s| s.category.clone()).collect();
|
||||
categories.sort();
|
||||
categories.dedup();
|
||||
Ok(categories)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reset_default_snippets(app: AppHandle) -> Result<(), String> {
|
||||
let mut snippets = load_all_snippets(&app)?;
|
||||
|
||||
// Remove all default snippets
|
||||
snippets.retain(|s| !s.is_default);
|
||||
|
||||
// Add fresh default snippets
|
||||
snippets.extend(get_default_snippets());
|
||||
|
||||
save_all_snippets(&app, &snippets)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_snippets_exist() {
|
||||
let defaults = get_default_snippets();
|
||||
assert!(!defaults.is_empty());
|
||||
assert!(defaults.iter().all(|s| s.is_default));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_snippets_have_required_fields() {
|
||||
let defaults = get_default_snippets();
|
||||
for snippet in defaults {
|
||||
assert!(!snippet.id.is_empty());
|
||||
assert!(!snippet.name.is_empty());
|
||||
assert!(!snippet.content.is_empty());
|
||||
assert!(!snippet.category.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user