#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug)]
pub enum SnippetError {
#[error("Failed to read snippets: {0}")]
ReadError(#[from] std::io::Error),
#[error("Failed to parse snippets: {0}")]
ParseError(#[from] toml::de::Error),
#[error("Failed to serialize snippets: {0}")]
SerializeError(#[from] toml::ser::Error),
#[error("Snippet not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snippet {
pub id: String,
pub title: String,
pub code: String,
pub language: String,
pub description: String,
pub tags: Vec<String>,
pub created_at: i64,
pub updated_at: i64,
pub usage_count: u32,
pub favorite: bool,
}
impl Snippet {
pub fn new(title: &str, code: &str, language: &str) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
id: Uuid::new_v4().to_string(),
title: title.to_string(),
code: code.to_string(),
language: language.to_string(),
description: String::new(),
tags: Vec::new(),
created_at: now,
updated_at: now,
usage_count: 0,
favorite: false,
}
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = description.to_string();
self
}
pub fn with_tags(mut self, tags: Vec<&str>) -> Self {
self.tags = tags.into_iter().map(|s| s.to_string()).collect();
self
}
pub fn add_tag(&mut self, tag: &str) {
if !self.tags.contains(&tag.to_string()) {
self.tags.push(tag.to_string());
self.updated_at = chrono::Utc::now().timestamp();
}
}
pub fn remove_tag(&mut self, tag: &str) {
self.tags.retain(|t| t != tag);
self.updated_at = chrono::Utc::now().timestamp();
}
pub fn increment_usage(&mut self) {
self.usage_count += 1;
self.updated_at = chrono::Utc::now().timestamp();
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SnippetCollection {
pub snippets: Vec<Snippet>,
}
impl SnippetCollection {
pub fn load() -> Result<Self, SnippetError> {
let path = Self::snippets_path()?;
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(&path)?;
let collection: Self = toml::from_str(&content)?;
Ok(collection)
}
pub fn save(&self) -> Result<(), SnippetError> {
let path = Self::snippets_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content)?;
Ok(())
}
fn snippets_path() -> Result<PathBuf, SnippetError> {
let home = dirs::home_dir()
.ok_or(SnippetError::InvalidInput("No home directory".into()))?;
Ok(home.join(".i-self").join("snippets.toml"))
}
pub fn add(&mut self, snippet: Snippet) {
self.snippets.push(snippet);
}
pub fn get(&self, id: &str) -> Option<&Snippet> {
self.snippets.iter().find(|s| s.id == id)
}
pub fn get_mut(&mut self, id: &str) -> Option<&mut Snippet> {
self.snippets.iter_mut().find(|s| s.id == id)
}
pub fn remove(&mut self, id: &str) -> Result<Snippet, SnippetError> {
let pos = self.snippets.iter().position(|s| s.id == id)
.ok_or(SnippetError::NotFound(id.to_string()))?;
Ok(self.snippets.remove(pos))
}
pub fn search(&self, query: &str) -> Vec<&Snippet> {
let query_lower = query.to_lowercase();
self.snippets.iter()
.filter(|s| {
s.title.to_lowercase().contains(&query_lower) ||
s.code.to_lowercase().contains(&query_lower) ||
s.description.to_lowercase().contains(&query_lower) ||
s.tags.iter().any(|t| t.to_lowercase().contains(&query_lower)) ||
s.language.to_lowercase().contains(&query_lower)
})
.collect()
}
pub fn search_by_language(&self, language: &str) -> Vec<&Snippet> {
let lang_lower = language.to_lowercase();
self.snippets.iter()
.filter(|s| s.language.to_lowercase() == lang_lower)
.collect()
}
pub fn search_by_tag(&self, tag: &str) -> Vec<&Snippet> {
let tag_lower = tag.to_lowercase();
self.snippets.iter()
.filter(|s| s.tags.iter().any(|t| t.to_lowercase() == tag_lower))
.collect()
}
pub fn favorites(&self) -> Vec<&Snippet> {
self.snippets.iter().filter(|s| s.favorite).collect()
}
pub fn recent(&self, count: usize) -> Vec<Snippet> {
let mut snippets = self.snippets.clone();
snippets.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
snippets.into_iter().take(count).collect()
}
pub fn most_used(&self, count: usize) -> Vec<Snippet> {
let mut snippets = self.snippets.clone();
snippets.sort_by(|a, b| b.usage_count.cmp(&a.usage_count));
snippets.into_iter().take(count).collect()
}
pub fn languages(&self) -> Vec<String> {
let mut langs: Vec<String> = self.snippets.iter()
.map(|s| s.language.clone())
.collect();
langs.sort();
langs.dedup();
langs
}
pub fn all_tags(&self) -> Vec<String> {
let mut tags: Vec<String> = self.snippets.iter()
.flat_map(|s| s.tags.clone())
.collect();
tags.sort();
tags.dedup();
tags
}
}
pub struct SnippetManager {
collection: SnippetCollection,
}
impl SnippetManager {
pub fn new() -> Result<Self, SnippetError> {
let collection = SnippetCollection::load()?;
Ok(Self { collection })
}
pub fn add(&mut self, title: &str, code: &str, language: &str, description: Option<&str>, tags: Vec<&str>) -> Result<Snippet, SnippetError> {
let mut snippet = Snippet::new(title, code, language);
if let Some(desc) = description {
snippet = snippet.with_description(desc);
}
if !tags.is_empty() {
snippet = snippet.with_tags(tags);
}
let snippet_clone = snippet.clone();
self.collection.add(snippet);
self.collection.save()?;
Ok(snippet_clone)
}
pub fn get(&self, id: &str) -> Option<&Snippet> {
self.collection.get(id)
}
pub fn remove(&mut self, id: &str) -> Result<Snippet, SnippetError> {
let snippet = self.collection.remove(id)?;
self.collection.save()?;
Ok(snippet)
}
pub fn update(&mut self, id: &str, title: Option<&str>, code: Option<&str>, description: Option<&str>) -> Result<Snippet, SnippetError> {
let snippet = self.collection.get_mut(id)
.ok_or_else(|| SnippetError::NotFound(id.to_string()))?;
if let Some(t) = title {
snippet.title = t.to_string();
}
if let Some(c) = code {
snippet.code = c.to_string();
}
if let Some(d) = description {
snippet.description = d.to_string();
}
snippet.updated_at = chrono::Utc::now().timestamp();
let snippet_clone = snippet.clone();
self.collection.save()?;
Ok(snippet_clone)
}
pub fn add_tag(&mut self, id: &str, tag: &str) -> Result<(), SnippetError> {
let snippet = self.collection.get_mut(id)
.ok_or_else(|| SnippetError::NotFound(id.to_string()))?;
snippet.add_tag(tag);
self.collection.save()?;
Ok(())
}
pub fn remove_tag(&mut self, id: &str, tag: &str) -> Result<(), SnippetError> {
let snippet = self.collection.get_mut(id)
.ok_or_else(|| SnippetError::NotFound(id.to_string()))?;
snippet.remove_tag(tag);
self.collection.save()?;
Ok(())
}
pub fn toggle_favorite(&mut self, id: &str) -> Result<bool, SnippetError> {
let snippet = self.collection.get_mut(id)
.ok_or_else(|| SnippetError::NotFound(id.to_string()))?;
snippet.favorite = !snippet.favorite;
snippet.updated_at = chrono::Utc::now().timestamp();
let is_favorite = snippet.favorite;
self.collection.save()?;
Ok(is_favorite)
}
pub fn increment_usage(&mut self, id: &str) -> Result<(), SnippetError> {
let snippet = self.collection.get_mut(id)
.ok_or_else(|| SnippetError::NotFound(id.to_string()))?;
snippet.increment_usage();
self.collection.save()?;
Ok(())
}
pub fn search(&self, query: &str) -> Vec<Snippet> {
self.collection.search(query).into_iter().cloned().collect()
}
pub fn search_by_language(&self, language: &str) -> Vec<Snippet> {
self.collection.search_by_language(language).into_iter().cloned().collect()
}
pub fn search_by_tag(&self, tag: &str) -> Vec<Snippet> {
self.collection.search_by_tag(tag).into_iter().cloned().collect()
}
pub fn list_all(&self) -> Vec<Snippet> {
self.collection.snippets.clone()
}
pub fn favorites(&self) -> Vec<Snippet> {
self.collection.favorites().into_iter().cloned().collect()
}
pub fn recent(&self, count: usize) -> Vec<Snippet> {
self.collection.recent(count)
}
pub fn most_used(&self, count: usize) -> Vec<Snippet> {
self.collection.most_used(count)
}
pub fn languages(&self) -> Vec<String> {
self.collection.languages()
}
pub fn all_tags(&self) -> Vec<String> {
self.collection.all_tags()
}
pub fn stats(&self) -> SnippetStats {
SnippetStats {
total: self.collection.snippets.len(),
languages: self.collection.languages().len(),
tags: self.collection.all_tags().len(),
favorites: self.collection.favorites().len(),
}
}
}
impl Default for SnippetManager {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
collection: SnippetCollection::default(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnippetStats {
pub total: usize,
pub languages: usize,
pub tags: usize,
pub favorites: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snippet_creation() {
let snippet = Snippet::new("Test", "fn main() {}", "rust");
assert_eq!(snippet.title, "Test");
assert_eq!(snippet.code, "fn main() {}");
assert_eq!(snippet.language, "rust");
assert_eq!(snippet.usage_count, 0);
assert!(!snippet.favorite);
}
#[test]
fn test_snippet_with_tags() {
let snippet = Snippet::new("Test", "code", "rust")
.with_tags(vec!["async", "tokio"]);
assert_eq!(snippet.tags.len(), 2);
assert!(snippet.tags.contains(&"async".to_string()));
}
#[test]
fn test_snippet_add_remove_tag() {
let mut snippet = Snippet::new("Test", "code", "rust");
snippet.add_tag("test");
assert!(snippet.tags.contains(&"test".to_string()));
snippet.remove_tag("test");
assert!(!snippet.tags.contains(&"test".to_string()));
}
#[test]
fn test_snippet_increment_usage() {
let mut snippet = Snippet::new("Test", "code", "rust");
assert_eq!(snippet.usage_count, 0);
snippet.increment_usage();
assert_eq!(snippet.usage_count, 1);
snippet.increment_usage();
assert_eq!(snippet.usage_count, 2);
}
#[test]
fn test_collection_search() {
let mut collection = SnippetCollection::default();
collection.add(Snippet::new("Rust HTTP", "reqwest", "rust").with_tags(vec!["http"]));
collection.add(Snippet::new("Python HTTP", "requests", "python").with_tags(vec!["http"]));
collection.add(Snippet::new("Go HTTP", "net/http", "go"));
let results = collection.search("http");
assert_eq!(results.len(), 3);
let rust_results = collection.search_by_language("rust");
assert_eq!(rust_results.len(), 1);
let tag_results = collection.search_by_tag("http");
assert_eq!(tag_results.len(), 2);
}
#[test]
fn test_collection_favorites() {
let mut collection = SnippetCollection::default();
let mut s1 = Snippet::new("Test1", "code1", "rust");
s1.favorite = true;
collection.add(s1);
collection.add(Snippet::new("Test2", "code2", "python"));
let favs = collection.favorites();
assert_eq!(favs.len(), 1);
}
#[test]
fn test_snippet_serialization() {
let snippet = Snippet::new("Test", "fn main() {}", "rust")
.with_description("A test snippet")
.with_tags(vec!["test"]);
let serialized = toml::to_string(&snippet).unwrap();
let deserialized: Snippet = toml::from_str(&serialized).unwrap();
assert_eq!(snippet.title, deserialized.title);
assert_eq!(snippet.code, deserialized.code);
assert_eq!(snippet.language, deserialized.language);
}
}