generated from nhcarrigan/template
b3d79a82ef
### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #71 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
440 lines
15 KiB
Rust
440 lines
15 KiB
Rust
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::*;
|
|
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<String> = 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);
|
|
}
|
|
}
|