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, pub updated_at: DateTime, } fn get_default_snippets() -> Vec { 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, 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 = 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, 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, String> { let snippets = load_all_snippets(&app)?; let mut categories: Vec = 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::*; use std::collections::HashSet; fn create_test_snippet(id: &str, name: &str, category: &str, is_default: bool) -> Snippet { Snippet { id: id.to_string(), name: name.to_string(), content: "Test content".to_string(), category: category.to_string(), is_default, created_at: Utc::now(), updated_at: Utc::now(), } } #[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()); } } #[test] fn test_default_snippets_count() { let defaults = get_default_snippets(); // Should have 8 default snippets assert_eq!(defaults.len(), 8); } #[test] fn test_default_snippets_have_unique_ids() { let defaults = get_default_snippets(); let ids: HashSet<&String> = defaults.iter().map(|s| &s.id).collect(); assert_eq!(ids.len(), defaults.len()); } #[test] fn test_default_snippets_ids_start_with_default() { let defaults = get_default_snippets(); assert!(defaults.iter().all(|s| s.id.starts_with("default-"))); } #[test] fn test_snippet_serialization() { let snippet = create_test_snippet("test-1", "Test Snippet", "Testing", false); let json = serde_json::to_string(&snippet).expect("Failed to serialize"); let parsed: Snippet = serde_json::from_str(&json).expect("Failed to deserialize"); assert_eq!(parsed.id, snippet.id); assert_eq!(parsed.name, snippet.name); assert_eq!(parsed.content, snippet.content); assert_eq!(parsed.category, snippet.category); assert_eq!(parsed.is_default, snippet.is_default); } #[test] fn test_snippet_clone() { let original = create_test_snippet("clone-test", "Clone Test", "Category", true); let cloned = original.clone(); assert_eq!(original.id, cloned.id); assert_eq!(original.name, cloned.name); assert_eq!(original.is_default, cloned.is_default); } #[test] #[allow(clippy::useless_vec)] fn test_snippet_sorting_by_category_then_name() { let mut snippets = vec![ create_test_snippet("s1", "Zebra", "B-Category", false), create_test_snippet("s2", "Apple", "A-Category", false), create_test_snippet("s3", "Banana", "B-Category", false), create_test_snippet("s4", "Alpha", "A-Category", false), ]; // Sort by category, then by name (mimics list_snippets behavior) 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 } }); // A-Category should come first assert_eq!(snippets[0].category, "A-Category"); assert_eq!(snippets[1].category, "A-Category"); assert_eq!(snippets[2].category, "B-Category"); assert_eq!(snippets[3].category, "B-Category"); // Within categories, alphabetically by name assert_eq!(snippets[0].name, "Alpha"); assert_eq!(snippets[1].name, "Apple"); assert_eq!(snippets[2].name, "Banana"); assert_eq!(snippets[3].name, "Zebra"); } #[test] fn test_known_default_snippets() { let defaults = get_default_snippets(); let ids: Vec<&str> = defaults.iter().map(|s| s.id.as_str()).collect(); assert!(ids.contains(&"default-explain-code")); assert!(ids.contains(&"default-fix-error")); assert!(ids.contains(&"default-write-tests")); assert!(ids.contains(&"default-refactor")); assert!(ids.contains(&"default-optimize")); assert!(ids.contains(&"default-review-pr")); assert!(ids.contains(&"default-add-comments")); assert!(ids.contains(&"default-security-review")); } #[test] fn test_default_snippet_categories() { let defaults = get_default_snippets(); let categories: HashSet<&String> = defaults.iter().map(|s| &s.category).collect(); assert!(categories.contains(&"Code Review".to_string())); assert!(categories.contains(&"Debugging".to_string())); assert!(categories.contains(&"Testing".to_string())); assert!(categories.contains(&"Performance".to_string())); assert!(categories.contains(&"Documentation".to_string())); assert!(categories.contains(&"Security".to_string())); } #[test] fn test_snippet_content_not_empty() { let defaults = get_default_snippets(); for snippet in defaults { assert!( snippet.content.len() > 10, "Content should be meaningful: {}", snippet.name ); } } #[test] fn test_snippet_timestamps() { let snippet = create_test_snippet("time-test", "Time Test", "Cat", false); assert!(snippet.created_at <= snippet.updated_at); } #[test] fn test_default_snippets_have_same_timestamps() { let defaults = get_default_snippets(); // All defaults are created at the same instant let first_created = defaults[0].created_at; let first_updated = defaults[0].updated_at; for snippet in &defaults { assert_eq!(snippet.created_at, first_created); assert_eq!(snippet.updated_at, first_updated); } } #[test] fn test_snippet_retain_non_default() { let mut snippets = vec![ create_test_snippet("default-1", "Default 1", "Cat", true), create_test_snippet("custom-1", "Custom 1", "Cat", false), create_test_snippet("default-2", "Default 2", "Cat", true), create_test_snippet("custom-2", "Custom 2", "Cat", false), ]; // Mimics reset_default_snippets behavior (retain non-defaults) snippets.retain(|s| !s.is_default); assert_eq!(snippets.len(), 2); assert!(snippets.iter().all(|s| !s.is_default)); } #[test] #[allow(clippy::useless_vec)] fn test_snippet_find_by_id() { let snippets = vec![ create_test_snippet("snippet-1", "First", "Cat", false), create_test_snippet("snippet-2", "Second", "Cat", false), create_test_snippet("snippet-3", "Third", "Cat", false), ]; let found = snippets.iter().find(|s| s.id == "snippet-2"); assert!(found.is_some()); assert_eq!(found.unwrap().name, "Second"); let not_found = snippets.iter().find(|s| s.id == "snippet-999"); assert!(not_found.is_none()); } #[test] #[allow(clippy::useless_vec)] fn test_extract_categories_sorted_and_deduped() { let snippets = vec![ create_test_snippet("s1", "S1", "Zebra", false), create_test_snippet("s2", "S2", "Alpha", false), create_test_snippet("s3", "S3", "Beta", false), create_test_snippet("s4", "S4", "Alpha", false), // Duplicate ]; let mut categories: Vec = snippets.iter().map(|s| s.category.clone()).collect(); categories.sort(); categories.dedup(); assert_eq!(categories.len(), 3); assert_eq!(categories[0], "Alpha"); assert_eq!(categories[1], "Beta"); assert_eq!(categories[2], "Zebra"); } #[test] fn test_snippet_category_code_review_count() { let defaults = get_default_snippets(); let code_review_count = defaults .iter() .filter(|s| s.category == "Code Review") .count(); // There should be multiple code review snippets assert!(code_review_count >= 2); } }