diff --git a/crates/frilvault-core/src/cache/vault_context.rs b/crates/frilvault-core/src/cache/vault_context.rs deleted file mode 100644 index 8c6463d..0000000 --- a/crates/frilvault-core/src/cache/vault_context.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::path::Path; - -use crate::{FrilVaultResult, NoteCache, NoteFile, WorkspaceIndexRepository, YamlNoteRepository}; - -/// Runtime container for FrilVault. -/// -/// VaultContext owns shared runtime resources: -/// -/// - repositories -/// - caches -/// - indexes -/// -/// Services should use VaultContext instead of -/// accessing repositories directly. -pub struct VaultContext { - pub note_repository: YamlNoteRepository, - pub workspace_index_repository: WorkspaceIndexRepository, - pub note_cache: NoteCache, -} - -impl VaultContext { - pub fn new( - note_repository: YamlNoteRepository, - workspace_index_repository: WorkspaceIndexRepository, - ) -> Self { - Self { - note_repository, - workspace_index_repository, - note_cache: NoteCache::default(), - } - } - - pub fn load_notes(&mut self, source_file: &Path) -> FrilVaultResult { - // 1. CACHE HIT - if let Some(cached) = self.note_cache.get(source_file) { - return Ok(cached.clone()); - } - - // 2. REPOSITORY LOAD - let note_file = self.note_repository.load_by_source_file(source_file)?; - - // 3. CACHE STORE - self.note_cache - .insert(source_file.to_path_buf(), note_file.clone()); - - Ok(note_file) - } - - pub fn invalidate_notes(&mut self, source_file: &Path) { - self.note_cache.invalidate(source_file); - } - - pub fn contains_cached_notes(&self, source_file: &Path) -> bool { - self.note_cache.contains(source_file) - } -} diff --git a/crates/frilvault-core/src/lib.rs b/crates/frilvault-core/src/lib.rs index de156d3..ac0fe92 100644 --- a/crates/frilvault-core/src/lib.rs +++ b/crates/frilvault-core/src/lib.rs @@ -1,15 +1,15 @@ -pub mod cache; pub mod constants; pub mod error; pub mod note; pub mod parser; +pub mod runtime; pub mod storage; pub mod workspace; -pub use cache::*; pub use error::*; pub use note::*; pub use parser::*; +pub use runtime::*; pub use storage::*; pub use workspace::*; diff --git a/crates/frilvault-core/src/cache/cache_stats.rs b/crates/frilvault-core/src/runtime/cache_stats.rs similarity index 100% rename from crates/frilvault-core/src/cache/cache_stats.rs rename to crates/frilvault-core/src/runtime/cache_stats.rs diff --git a/crates/frilvault-core/src/cache/mod.rs b/crates/frilvault-core/src/runtime/mod.rs similarity index 100% rename from crates/frilvault-core/src/cache/mod.rs rename to crates/frilvault-core/src/runtime/mod.rs diff --git a/crates/frilvault-core/src/cache/note_cache.rs b/crates/frilvault-core/src/runtime/note_cache.rs similarity index 95% rename from crates/frilvault-core/src/cache/note_cache.rs rename to crates/frilvault-core/src/runtime/note_cache.rs index b5a338a..3d1ab93 100644 --- a/crates/frilvault-core/src/cache/note_cache.rs +++ b/crates/frilvault-core/src/runtime/note_cache.rs @@ -8,7 +8,7 @@ use std::path::{Path, PathBuf}; use crate::NoteFile; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct NoteCache { files: HashMap, } diff --git a/crates/frilvault-core/src/runtime/vault_context.rs b/crates/frilvault-core/src/runtime/vault_context.rs new file mode 100644 index 0000000..6a18bb9 --- /dev/null +++ b/crates/frilvault-core/src/runtime/vault_context.rs @@ -0,0 +1,113 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + FrilVaultResult, NoteCache, NoteFile, NoteFileRecord, WorkspaceIndex, WorkspaceIndexRepository, + YamlNoteRepository, +}; + +/// Runtime container for FrilVault. +/// +/// VaultContext owns shared runtime resources: +/// +/// - repositories +/// - caches +/// - indexes +/// +/// Services should use VaultContext instead of +/// accessing repositories directly. +#[derive(Clone)] +pub struct VaultContext { + pub note_repository: YamlNoteRepository, + pub workspace_index_repository: WorkspaceIndexRepository, + pub note_cache: NoteCache, +} + +impl VaultContext { + pub fn new( + note_repository: YamlNoteRepository, + workspace_index_repository: WorkspaceIndexRepository, + ) -> Self { + Self { + note_repository, + workspace_index_repository, + note_cache: NoteCache::default(), + } + } + + pub fn load_notes(&mut self, source_file: &Path) -> FrilVaultResult { + // 1. CACHE HIT + if let Some(cached) = self.note_cache.get(source_file) { + return Ok(cached.clone()); + } + + // 2. REPOSITORY LOAD + let note_file = self.note_repository.load_by_source_file(source_file)?; + + // 3. CACHE STORE + self.note_cache + .insert(source_file.to_path_buf(), note_file.clone()); + + Ok(note_file) + } + + pub fn invalidate_notes(&mut self, source_file: &Path) { + self.note_cache.invalidate(source_file); + } + + pub fn rebuild_index(&self) -> FrilVaultResult { + self.workspace_index_repository.rebuild() + } + + pub fn contains_cached_notes(&self, source_file: &Path) -> bool { + self.note_cache.contains(source_file) + } + + pub fn list_all_note_files(&self) -> FrilVaultResult> { + self.note_repository.list_all_note_files() + } + + pub fn scan_workspace_files(&self) -> FrilVaultResult> { + let mut files = Vec::new(); + + self.collect_workspace_files(self.workspace_index_repository.workspace_root(), &mut files)?; + + Ok(files) + } + + pub fn resolve_note_path(&self, source_file: &str) -> PathBuf { + self.note_repository.resolve_note_path(source_file) + } + + fn collect_workspace_files( + &self, + directory: &Path, + files: &mut Vec, + ) -> FrilVaultResult<()> { + for entry in std::fs::read_dir(directory)? { + let entry = entry?; + let file_type = entry.file_type()?; + let path = entry.path(); + + if file_type.is_symlink() { + continue; + } + + if file_type.is_dir() { + if path.file_name().and_then(|n| n.to_str()) == Some(".vault") { + continue; + } + + self.collect_workspace_files(&path, files)?; + continue; + } + + let relative = path + .strip_prefix(self.workspace_index_repository.workspace_root()) + .map_err(|_| crate::FrilVaultError::SourcePathOutsideWorkspace)?; + + files.push(relative.to_string_lossy().to_string()); + } + + Ok(()) + } +} diff --git a/crates/frilvault-core/src/tests/helper.rs b/crates/frilvault-core/src/tests/helper.rs index c7bed46..cd0db96 100644 --- a/crates/frilvault-core/src/tests/helper.rs +++ b/crates/frilvault-core/src/tests/helper.rs @@ -35,3 +35,8 @@ pub fn create_test_workspace_service(workspace_root: &Path) -> WorkspaceService WorkspaceService::new(vault_context, repository) } + +pub fn create_test_index_repository(workspace_root: &Path) -> WorkspaceIndexRepository { + let resolver = PathResolver::new(workspace_root); + WorkspaceIndexRepository::new(resolver) +} diff --git a/crates/frilvault-core/src/tests/mod.rs b/crates/frilvault-core/src/tests/mod.rs index 81718b8..d2042ec 100644 --- a/crates/frilvault-core/src/tests/mod.rs +++ b/crates/frilvault-core/src/tests/mod.rs @@ -26,3 +26,6 @@ mod workspace_index_repository_test; #[cfg(test)] mod vault_context_test; + +#[cfg(test)] +mod repair_engin_test; diff --git a/crates/frilvault-core/src/tests/repair_engin_test.rs b/crates/frilvault-core/src/tests/repair_engin_test.rs new file mode 100644 index 0000000..e804c37 --- /dev/null +++ b/crates/frilvault-core/src/tests/repair_engin_test.rs @@ -0,0 +1,102 @@ +use std::{fs, path::Path}; +use uuid::Uuid; + +use crate::{ + AddNoteInput, FileMove, LineAnchor, NoteAnchor, NoteService, PathResolver, VaultContext, + WorkspaceIndexRepository, YamlNoteRepository, repair_engine::RepairEngine, +}; + +#[test] +fn repair_engine_moves_note_files() { + let workspace_root = std::env::temp_dir().join(format!("frilvault-test-{}", Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let resolver = PathResolver::new(&workspace_root); + + let note_repository = YamlNoteRepository::new(resolver.clone()); + + let index_repository = WorkspaceIndexRepository::new(resolver.clone()); + + let vault_context = VaultContext::new(note_repository, index_repository); + + let mut service = NoteService::new(vault_context.clone()); + + // 1. create note for original file + service + .add_note(AddNoteInput { + source_file: "src/main.rs".into(), + anchor: NoteAnchor::Line(LineAnchor { line: 1, column: 1 }), + content: "test note".to_string(), + }) + .unwrap(); + + // 2. simulate repair move + let moves = vec![FileMove { + from: "src/main.rs".to_string(), + to: "src/main_renamed.rs".to_string(), + confidence: 1.0, + }]; + + let mut engine = RepairEngine { vault_context }; + + let repaired = engine.apply_moves(moves).unwrap(); + + assert_eq!(repaired, 1); + + let old_path = resolver.note_path_for_source_file("src/main.rs"); + + let new_path = resolver.note_path_for_source_file("src/main_renamed.rs"); + + assert!(!old_path.exists()); + assert!(new_path.exists()); + + fs::remove_dir_all(workspace_root).unwrap(); +} + +#[test] +fn repair_engine_invalidates_cache_correctly() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let resolver = PathResolver::new(&workspace_root); + + let note_repository = YamlNoteRepository::new(resolver.clone()); + + let index_repository = WorkspaceIndexRepository::new(resolver.clone()); + + let mut vault_context = VaultContext::new(note_repository, index_repository); + + // preload cache + let _ = vault_context.load_notes("src/main.rs".as_ref()); + + assert!(vault_context.note_cache.contains(Path::new("src/main.rs"))); + + let mut engine = RepairEngine { vault_context }; + + let moves = vec![FileMove { + from: "src/main.rs".to_string(), + to: "src/main_renamed.rs".to_string(), + confidence: 1.0, + }]; + + let _ = engine.apply_moves(moves).unwrap(); + + assert!( + !engine + .vault_context + .note_cache + .contains(Path::new("src/main.rs")) + ); + + assert!( + engine + .vault_context + .note_cache + .contains(Path::new("src/main_renamed.rs")) + ); + + fs::remove_dir_all(workspace_root).unwrap(); +} diff --git a/crates/frilvault-core/src/tests/workspace_index_repository_test.rs b/crates/frilvault-core/src/tests/workspace_index_repository_test.rs index 3bbcefb..f4ce857 100644 --- a/crates/frilvault-core/src/tests/workspace_index_repository_test.rs +++ b/crates/frilvault-core/src/tests/workspace_index_repository_test.rs @@ -2,8 +2,9 @@ use std::fs; use super::helper::{create_test_note_service, create_test_workspace_service}; use crate::{ - AddNoteInput, LineAnchor, NoteAnchor, PathResolver, SymbolAnchor, SymbolKind, WorkspaceIndex, - WorkspaceIndexRepository, + AddNoteInput, IndexDiff, IndexedFile, LineAnchor, NoteAnchor, PathResolver, SymbolAnchor, + SymbolKind, WorkspaceIndex, WorkspaceIndexRepository, + tests::helper::create_test_index_repository, }; #[test] @@ -329,3 +330,92 @@ fn apply_repairs_moves_note_file() { fs::remove_dir_all(workspace_root).unwrap(); } + +#[test] +fn detects_renamed_file_by_name_similarity() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let old = WorkspaceIndex { + version: 1, + files: vec![IndexedFile { + source_file: "src/parser.rs".to_string(), + note_count: 1, + exists: true, + }], + }; + + let new = WorkspaceIndex { + version: 1, + files: vec![IndexedFile { + source_file: "src/core/parser.rs".to_string(), + note_count: 1, + exists: true, + }], + }; + + let repo = create_test_index_repository(&workspace_root); + + let moves = repo.detect_moves(&old, &new); + + assert_eq!(moves.len(), 1); + assert!(moves[0].confidence > 0.5); + + fs::remove_dir_all(workspace_root).unwrap(); +} + +#[test] +fn detects_removed_and_added_files() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let old = WorkspaceIndex { + version: 1, + files: vec![IndexedFile { + source_file: "a.rs".to_string(), + note_count: 1, + exists: true, + }], + }; + + let new = WorkspaceIndex { + version: 1, + files: vec![IndexedFile { + source_file: "b.rs".to_string(), + note_count: 1, + exists: true, + }], + }; + + let repo = create_test_index_repository(&workspace_root); + + let moves = repo.detect_moves(&old, &new); + + assert!(moves.is_empty()); + + fs::remove_dir_all(workspace_root).unwrap(); +} + +#[test] +fn detects_strong_rename_by_name_and_path() { + let old = "src/parser.rs"; + let new = "src/parser.rs"; + + let score = IndexDiff::similarity_score(old, new); + + assert!(score >= 1.0); +} + +#[test] +fn rejects_unrelated_files() { + let old = "src/parser.rs"; + let new = "src/ui/button.rs"; + + let score = IndexDiff::similarity_score(old, new); + + assert!(score < 1.0); +} diff --git a/crates/frilvault-core/src/workspace/diff.rs b/crates/frilvault-core/src/workspace/diff.rs new file mode 100644 index 0000000..acb8889 --- /dev/null +++ b/crates/frilvault-core/src/workspace/diff.rs @@ -0,0 +1,72 @@ +use std::path::Path; + +use crate::{FileMove, IndexedFile, WorkspaceIndex}; + +pub struct IndexDiff; + +impl IndexDiff { + pub fn diff(old: &WorkspaceIndex, new: &WorkspaceIndex) -> Vec { + let mut moves = Vec::new(); + + for old_file in &old.files { + let removed = !new + .files + .iter() + .any(|f| f.source_file == old_file.source_file); + + if removed { + let mut best: Option<(&IndexedFile, f32)> = None; + + for new_file in &new.files { + let score = + Self::similarity_score(&old_file.source_file, &new_file.source_file); + + if score >= 0.7 { + if let Some((_, best_score)) = &best { + if score > *best_score { + best = Some((new_file, score)); + } + } else { + best = Some((new_file, score)); + } + } + } + + if let Some((candidate, score)) = best { + moves.push(FileMove { + from: old_file.source_file.clone(), + to: candidate.source_file.clone(), + confidence: score, + }); + } + } + } + + moves + } + + fn filename_score(a: &str, b: &str) -> f32 { + let a_name = Path::new(a).file_name().and_then(|n| n.to_str()); + + let b_name = Path::new(b).file_name().and_then(|n| n.to_str()); + + match (a_name, b_name) { + (Some(a), Some(b)) if a == b => 0.8, + _ => 0.0, + } + } + + fn directory_score(a: &str, b: &str) -> f32 { + let a_parent = Path::new(a).parent(); + let b_parent = Path::new(b).parent(); + + match (a_parent, b_parent) { + (Some(a), Some(b)) if a == b => 0.2, + _ => 0.0, + } + } + + pub fn similarity_score(a: &str, b: &str) -> f32 { + Self::filename_score(a, b) + Self::directory_score(a, b) + } +} diff --git a/crates/frilvault-core/src/workspace/entity/file_move.rs b/crates/frilvault-core/src/workspace/entity/file_move.rs new file mode 100644 index 0000000..0225f1b --- /dev/null +++ b/crates/frilvault-core/src/workspace/entity/file_move.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FileMove { + pub from: String, + pub to: String, + + /// infer rename candidates using filename similarity + pub confidence: f32, +} diff --git a/crates/frilvault-core/src/workspace/entity/mod.rs b/crates/frilvault-core/src/workspace/entity/mod.rs index 6b90236..eec2e94 100644 --- a/crates/frilvault-core/src/workspace/entity/mod.rs +++ b/crates/frilvault-core/src/workspace/entity/mod.rs @@ -1,3 +1,4 @@ +pub mod file_move; pub mod indexed_file; pub mod repair_suggestion; pub mod workspace_health; @@ -5,6 +6,7 @@ pub mod workspace_index; pub mod workspace_metadata; pub mod workspace_stats; +pub use file_move::*; pub use indexed_file::*; pub use repair_suggestion::*; pub use workspace_health::*; diff --git a/crates/frilvault-core/src/workspace/entity/repair_suggestion.rs b/crates/frilvault-core/src/workspace/entity/repair_suggestion.rs index 539683d..bf3ab85 100644 --- a/crates/frilvault-core/src/workspace/entity/repair_suggestion.rs +++ b/crates/frilvault-core/src/workspace/entity/repair_suggestion.rs @@ -1,3 +1,5 @@ +use crate::FileMove; + #[derive(Debug, Clone)] pub struct RepairSuggestion { pub missing_file: String, @@ -8,4 +10,21 @@ impl RepairSuggestion { pub fn best_candidate(&self) -> Option<&String> { self.candidates.first() } + + pub fn from_moves(moves: Vec) -> Vec { + let mut suggestions = Vec::new(); + + for mv in moves { + if mv.confidence < 0.5 { + continue; + } + + suggestions.push(RepairSuggestion { + missing_file: mv.from, + candidates: vec![mv.to], + }); + } + + suggestions + } } diff --git a/crates/frilvault-core/src/workspace/mod.rs b/crates/frilvault-core/src/workspace/mod.rs index 32a2f9c..1b334f4 100644 --- a/crates/frilvault-core/src/workspace/mod.rs +++ b/crates/frilvault-core/src/workspace/mod.rs @@ -1,9 +1,16 @@ +pub mod diff; pub mod entity; pub mod path; +pub mod repair_engine; pub mod repository; pub mod service; +pub mod snapshot; +pub mod watcher; +pub use diff::*; pub use entity::*; pub use path::*; pub use repository::*; pub use service::*; +pub use snapshot::*; +pub use watcher::*; diff --git a/crates/frilvault-core/src/workspace/repair_engine.rs b/crates/frilvault-core/src/workspace/repair_engine.rs new file mode 100644 index 0000000..f8808c5 --- /dev/null +++ b/crates/frilvault-core/src/workspace/repair_engine.rs @@ -0,0 +1,38 @@ +use crate::{FileMove, FrilVaultResult, VaultContext}; + +pub struct RepairEngine { + pub vault_context: VaultContext, +} + +impl RepairEngine { + pub fn apply_moves(&mut self, moves: Vec) -> FrilVaultResult { + let mut repaired = 0; + + for mv in moves { + if mv.confidence < 1.0 { + continue; + } + + let old_path = self.vault_context.resolve_note_path(&mv.from); + + let new_path = self.vault_context.resolve_note_path(&mv.to); + + if let Some(parent) = new_path.parent() { + std::fs::create_dir_all(parent)?; + } + + if old_path.exists() { + std::fs::rename(&old_path, &new_path)?; + } + + self.vault_context.invalidate_notes(mv.from.as_ref()); + self.vault_context.invalidate_notes(mv.to.as_ref()); + + let _ = self.vault_context.load_notes(mv.to.as_ref()); + + repaired += 1; + } + + Ok(repaired) + } +} diff --git a/crates/frilvault-core/src/workspace/repository/workspace_index_repository.rs b/crates/frilvault-core/src/workspace/repository/workspace_index_repository.rs index 433edfd..a3f76cf 100644 --- a/crates/frilvault-core/src/workspace/repository/workspace_index_repository.rs +++ b/crates/frilvault-core/src/workspace/repository/workspace_index_repository.rs @@ -1,6 +1,9 @@ use std::{fs, path::Path}; -use crate::{FrilVaultResult, IndexedFile, PathResolver, WorkspaceIndex, YamlNoteRepository}; +use crate::{ + FileMove, FrilVaultResult, IndexDiff, IndexedFile, PathResolver, RepairSuggestion, + WorkspaceIndex, YamlNoteRepository, +}; #[derive(Debug, Clone)] pub struct WorkspaceIndexRepository { @@ -86,4 +89,22 @@ impl WorkspaceIndexRepository { Ok(index) } + + pub fn detect_moves( + &self, + old_index: &WorkspaceIndex, + new_index: &WorkspaceIndex, + ) -> Vec { + IndexDiff::diff(old_index, new_index) + } + + pub fn repair_suggestions( + &self, + old_index: &WorkspaceIndex, + new_index: &WorkspaceIndex, + ) -> Vec { + let moves = self.detect_moves(old_index, new_index); + + RepairSuggestion::from_moves(moves) + } } diff --git a/crates/frilvault-core/src/workspace/service/workspace_service.rs b/crates/frilvault-core/src/workspace/service/workspace_service.rs index 4827544..33e5ca5 100644 --- a/crates/frilvault-core/src/workspace/service/workspace_service.rs +++ b/crates/frilvault-core/src/workspace/service/workspace_service.rs @@ -3,8 +3,6 @@ //! This module provides statistics, //! health checks, and repair workflows. -use std::path::Path; - use crate::{ FrilVaultResult, NoteAnchor, RepairSuggestion, VaultContext, WorkspaceHealth, WorkspaceIndexRepository, WorkspaceStats, @@ -31,9 +29,9 @@ impl WorkspaceService { } pub fn stats(&mut self) -> FrilVaultResult { - let index = self.index_repository.rebuild()?; + let index = self.vault_context.rebuild_index()?; - let records = self.vault_context.note_repository.list_all_note_files()?; + let records = self.vault_context.list_all_note_files()?; let mut stats = WorkspaceStats { file_count: index.files.len(), @@ -76,50 +74,9 @@ impl WorkspaceService { Ok(health) } - fn scan_workspace_files(&self) -> FrilVaultResult> { - let mut files = Vec::new(); - - self.collect_workspace_files(self.index_repository.workspace_root(), &mut files)?; - - Ok(files) - } - - fn collect_workspace_files( - &self, - directory: &Path, - files: &mut Vec, - ) -> FrilVaultResult<()> { - for entry in std::fs::read_dir(directory)? { - let entry = entry?; - let file_type = entry.file_type()?; - let path = entry.path(); - - if file_type.is_symlink() { - continue; - } - - if file_type.is_dir() { - if path.file_name().and_then(|n| n.to_str()) == Some(".vault") { - continue; - } - - self.collect_workspace_files(&path, files)?; - continue; - } - - let relative = path - .strip_prefix(self.index_repository.workspace_root()) - .map_err(|_| crate::FrilVaultError::SourcePathOutsideWorkspace)?; - - files.push(relative.to_string_lossy().to_string()); - } - - Ok(()) - } - pub fn repair_suggestions(&mut self) -> FrilVaultResult> { let health = self.health_check()?; - let workspace_files = self.scan_workspace_files()?; + let workspace_files = self.vault_context.scan_workspace_files()?; let mut suggestions = Vec::new(); @@ -171,15 +128,9 @@ impl WorkspaceService { } fn move_note_file(&self, source_file: &str, target_file: &str) -> FrilVaultResult<()> { - let source_note = self - .vault_context - .note_repository - .resolve_note_path(source_file); - - let target_note = self - .vault_context - .note_repository - .resolve_note_path(target_file); + let source_note = self.vault_context.resolve_note_path(source_file); + + let target_note = self.vault_context.resolve_note_path(target_file); if let Some(parent) = target_note.parent() { std::fs::create_dir_all(parent)?; diff --git a/crates/frilvault-core/src/workspace/snapshot/mod.rs b/crates/frilvault-core/src/workspace/snapshot/mod.rs new file mode 100644 index 0000000..6bc4ce0 --- /dev/null +++ b/crates/frilvault-core/src/workspace/snapshot/mod.rs @@ -0,0 +1,5 @@ +pub mod snapshot_manager; +pub mod workspace_snapshot; + +pub use snapshot_manager::*; +pub use workspace_snapshot::*; diff --git a/crates/frilvault-core/src/workspace/snapshot/snapshot_manager.rs b/crates/frilvault-core/src/workspace/snapshot/snapshot_manager.rs new file mode 100644 index 0000000..4515907 --- /dev/null +++ b/crates/frilvault-core/src/workspace/snapshot/snapshot_manager.rs @@ -0,0 +1,22 @@ +use crate::{WorkspaceIndex, WorkspaceSnapshot}; + +#[derive(Default)] +pub struct SnapshotManager { + previous: Option, +} + +impl SnapshotManager { + pub fn new() -> Self { + Self { previous: None } + } + + pub fn update(&mut self, index: WorkspaceIndex) { + let snapshot = WorkspaceSnapshot { index }; + + self.previous = Some(snapshot); + } + + pub fn previous(&self) -> Option<&WorkspaceIndex> { + self.previous.as_ref().map(|s| &s.index) + } +} diff --git a/crates/frilvault-core/src/workspace/snapshot/workspace_snapshot.rs b/crates/frilvault-core/src/workspace/snapshot/workspace_snapshot.rs new file mode 100644 index 0000000..466a5b7 --- /dev/null +++ b/crates/frilvault-core/src/workspace/snapshot/workspace_snapshot.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +use crate::WorkspaceIndex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkspaceSnapshot { + pub index: WorkspaceIndex, +} diff --git a/crates/frilvault-core/src/workspace/watcher.rs b/crates/frilvault-core/src/workspace/watcher.rs new file mode 100644 index 0000000..3aceaf8 --- /dev/null +++ b/crates/frilvault-core/src/workspace/watcher.rs @@ -0,0 +1,31 @@ +use crate::{ + FrilVaultResult, + workspace::{ + repair_engine::RepairEngine, + repository::workspace_index_repository::WorkspaceIndexRepository, + snapshot::snapshot_manager::SnapshotManager, + }, +}; + +pub struct WorkspaceWatcher { + index_repository: WorkspaceIndexRepository, + snapshot: SnapshotManager, + repair_engine: RepairEngine, +} + +impl WorkspaceWatcher { + pub fn on_change(&mut self) -> FrilVaultResult<()> { + let old = self.snapshot.previous().cloned(); + let new = self.index_repository.rebuild()?; + + if let Some(old) = old { + let moves = self.index_repository.detect_moves(&old, &new); + + self.repair_engine.apply_moves(moves)?; + } + + self.snapshot.update(new); + + Ok(()) + } +}