From 6680119cb8b84a07967db42fa4a2aef2e8573858 Mon Sep 17 00:00:00 2001 From: "heesk0223@gmail.com" Date: Wed, 17 Jun 2026 12:58:43 +0900 Subject: [PATCH] feat(core): add symbol note search support --- crates/frilvault-core/src/cache/note_cache.rs | 15 +++- .../frilvault-core/src/cache/vault_context.rs | 33 ++++++-- crates/frilvault-core/src/note/mod.rs | 8 +- .../{repository.rs => note_repository.rs} | 0 .../src/note/{service.rs => note_service.rs} | 84 +++++++++++-------- .../frilvault-core/src/parser/note_parser.rs | 6 ++ .../src/storage/yaml_repository.rs | 5 ++ crates/frilvault-core/src/tests/helper.rs | 2 +- crates/frilvault-core/src/tests/mod.rs | 3 + .../src/tests/note_service_test.rs | 70 ++++++++++++++++ .../src/tests/vault_context_test.rs | 33 ++++++++ .../workspace/service/workspace_service.rs | 12 +++ 12 files changed, 219 insertions(+), 52 deletions(-) rename crates/frilvault-core/src/note/{repository.rs => note_repository.rs} (100%) rename crates/frilvault-core/src/note/{service.rs => note_service.rs} (70%) create mode 100644 crates/frilvault-core/src/tests/vault_context_test.rs diff --git a/crates/frilvault-core/src/cache/note_cache.rs b/crates/frilvault-core/src/cache/note_cache.rs index 5cd483b..b5a338a 100644 --- a/crates/frilvault-core/src/cache/note_cache.rs +++ b/crates/frilvault-core/src/cache/note_cache.rs @@ -1,5 +1,10 @@ +//! In-memory cache for note files. +//! +//! Used by long-running runtimes such as +//! VSCode extensions to reduce filesystem access. + use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::NoteFile; @@ -9,7 +14,7 @@ pub struct NoteCache { } impl NoteCache { - pub fn get(&self, path: &PathBuf) -> Option<&NoteFile> { + pub fn get(&self, path: &Path) -> Option<&NoteFile> { self.files.get(path) } @@ -17,11 +22,15 @@ impl NoteCache { self.files.insert(path, note_file); } - pub fn invalidate(&mut self, path: &PathBuf) { + pub fn invalidate(&mut self, path: &Path) { self.files.remove(path); } pub fn clear(&mut self) { self.files.clear(); } + + pub fn contains(&self, source_file: &Path) -> bool { + self.files.contains_key(source_file) + } } diff --git a/crates/frilvault-core/src/cache/vault_context.rs b/crates/frilvault-core/src/cache/vault_context.rs index ebfa7fa..8c6463d 100644 --- a/crates/frilvault-core/src/cache/vault_context.rs +++ b/crates/frilvault-core/src/cache/vault_context.rs @@ -1,9 +1,21 @@ +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 cache: NoteCache, + pub note_cache: NoteCache, } impl VaultContext { @@ -14,15 +26,13 @@ impl VaultContext { Self { note_repository, workspace_index_repository, - cache: NoteCache::default(), + note_cache: NoteCache::default(), } } - pub fn load_notes(&mut self, source_file: &std::path::Path) -> FrilVaultResult { - let key = source_file.to_path_buf(); - + pub fn load_notes(&mut self, source_file: &Path) -> FrilVaultResult { // 1. CACHE HIT - if let Some(cached) = self.cache.get(&key) { + if let Some(cached) = self.note_cache.get(source_file) { return Ok(cached.clone()); } @@ -30,12 +40,17 @@ impl VaultContext { let note_file = self.note_repository.load_by_source_file(source_file)?; // 3. CACHE STORE - self.cache.insert(key, note_file.clone()); + self.note_cache + .insert(source_file.to_path_buf(), note_file.clone()); Ok(note_file) } - pub fn invalidate_notes(&mut self, source_file: &std::path::Path) { - self.cache.invalidate(&source_file.to_path_buf()); + 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/note/mod.rs b/crates/frilvault-core/src/note/mod.rs index 01003c6..4b13ac8 100644 --- a/crates/frilvault-core/src/note/mod.rs +++ b/crates/frilvault-core/src/note/mod.rs @@ -1,9 +1,9 @@ mod dto; mod entity; -mod repository; -mod service; +mod note_repository; +mod note_service; pub use dto::*; pub use entity::*; -pub use repository::*; -pub use service::*; +pub use note_repository::*; +pub use note_service::*; diff --git a/crates/frilvault-core/src/note/repository.rs b/crates/frilvault-core/src/note/note_repository.rs similarity index 100% rename from crates/frilvault-core/src/note/repository.rs rename to crates/frilvault-core/src/note/note_repository.rs diff --git a/crates/frilvault-core/src/note/service.rs b/crates/frilvault-core/src/note/note_service.rs similarity index 70% rename from crates/frilvault-core/src/note/service.rs rename to crates/frilvault-core/src/note/note_service.rs index 1cf0bfb..d5e0239 100644 --- a/crates/frilvault-core/src/note/service.rs +++ b/crates/frilvault-core/src/note/note_service.rs @@ -1,3 +1,12 @@ +//! Application services for note operations. +//! +//! This module contains high-level note workflows +//! such as CRUD operations and searching. +//! +//! Services should access storage through +//! VaultContext rather than directly interacting +//! with repositories. + use std::path::Path; use chrono::Utc; @@ -9,6 +18,10 @@ use crate::{ note_view::NoteView, }; +/// Application service responsible for note operations. +/// +/// Coordinates repositories, caching, +/// and future runtime behaviors. pub struct NoteService { pub vault_context: VaultContext, } @@ -31,9 +44,9 @@ impl NoteService { self.vault_context .note_repository - .replace_notes(source_file, notes)?; + .replace_notes(source_file.as_ref(), notes)?; - self.vault_context.invalidate_notes(source_file); + self.vault_context.invalidate_notes(source_file.as_ref()); Ok(()) } @@ -115,54 +128,55 @@ impl NoteService { Ok(()) } - pub fn search_notes(&mut self, keyword: &str) -> FrilVaultResult> { + fn all_note_views(&self) -> FrilVaultResult> { let records = self.vault_context.note_repository.list_all_note_files()?; - let keyword = keyword.to_lowercase(); - let mut results = Vec::new(); for record in records { for note in record.note_file.notes { - let content_match = note.content.to_lowercase().contains(&keyword); - - let symbol_match = match ¬e.anchor { - NoteAnchor::Symbol(anchor) => anchor.name.to_lowercase().contains(&keyword), - _ => false, - }; - - if content_match || symbol_match { - results.push(NoteView { - source_file: record.source_file.clone(), - note, - }); - } + results.push(NoteView { + source_file: record.source_file.clone(), + note, + }); } } Ok(results) } - pub fn search_by_symbol(&mut self, symbol: &str) -> FrilVaultResult> { - let symbol = symbol.to_lowercase(); + pub fn search_notes(&mut self, keyword: &str) -> FrilVaultResult> { + let keyword = keyword.to_lowercase(); - let records = self.vault_context.note_repository.list_all_note_files()?; + Ok(self + .all_note_views()? + .into_iter() + .filter(|view| { + let content_match = view.note.content.to_lowercase().contains(&keyword); - let mut results = Vec::new(); + let symbol_match = match &view.note.anchor { + NoteAnchor::Symbol(anchor) => anchor.name.to_lowercase().contains(&keyword), + _ => false, + }; - for record in records { - for note in record.note_file.notes { - if let NoteAnchor::Symbol(anchor) = ¬e.anchor - && anchor.name.to_lowercase().contains(&symbol) - { - results.push(NoteView { - source_file: record.source_file.clone(), - note, - }); - } - } - } + content_match || symbol_match + }) + .collect()) + } - Ok(results) + pub fn search_by_symbol(&mut self, symbol: &str) -> FrilVaultResult> { + let symbol = symbol.to_lowercase(); + + Ok(self + .all_note_views()? + .into_iter() + .filter(|view| { + matches!( + &view.note.anchor, + NoteAnchor::Symbol(anchor) + if anchor.name.to_lowercase().contains(&symbol) + ) + }) + .collect()) } } diff --git a/crates/frilvault-core/src/parser/note_parser.rs b/crates/frilvault-core/src/parser/note_parser.rs index 51f58fa..f37d4a3 100644 --- a/crates/frilvault-core/src/parser/note_parser.rs +++ b/crates/frilvault-core/src/parser/note_parser.rs @@ -2,7 +2,13 @@ use crate::FrilVaultResult; use crate::note::NoteFile; pub trait NoteParser { + /// Converts a NoteFile into a serialized representation. + /// + /// Implementations may choose YAML, JSON, or any other format. fn serialize(&self, note_file: &NoteFile) -> FrilVaultResult; + /// Converts serialized content back into a NoteFile. + /// + /// Returns an error if the content is invalid or unsupported. fn deserialize(&self, content: &str) -> FrilVaultResult; } diff --git a/crates/frilvault-core/src/storage/yaml_repository.rs b/crates/frilvault-core/src/storage/yaml_repository.rs index 2cf98aa..55f99dc 100644 --- a/crates/frilvault-core/src/storage/yaml_repository.rs +++ b/crates/frilvault-core/src/storage/yaml_repository.rs @@ -1,3 +1,8 @@ +//! YAML-backed note repository. +//! +//! Notes are persisted as YAML files +//! inside the `.vault/notes` directory. + use crate::note::{Note, NoteFile}; use crate::parser::{NoteParser, YamlParser}; use crate::workspace::PathResolver; diff --git a/crates/frilvault-core/src/tests/helper.rs b/crates/frilvault-core/src/tests/helper.rs index e1b63fc..c7bed46 100644 --- a/crates/frilvault-core/src/tests/helper.rs +++ b/crates/frilvault-core/src/tests/helper.rs @@ -16,7 +16,7 @@ pub fn create_test_note_service(workspace_root: &Path) -> NoteService { NoteService::new(vault_context) } -fn create_test_vault_context(workspace_root: &Path) -> VaultContext { +pub fn create_test_vault_context(workspace_root: &Path) -> VaultContext { let resolver = PathResolver::new(workspace_root); let workspace_repository = WorkspaceRepository::new(resolver.clone()); workspace_repository.create_if_missing().unwrap(); diff --git a/crates/frilvault-core/src/tests/mod.rs b/crates/frilvault-core/src/tests/mod.rs index 33d1c0b..81718b8 100644 --- a/crates/frilvault-core/src/tests/mod.rs +++ b/crates/frilvault-core/src/tests/mod.rs @@ -23,3 +23,6 @@ mod workspace_index_test; #[cfg(test)] mod workspace_index_repository_test; + +#[cfg(test)] +mod vault_context_test; diff --git a/crates/frilvault-core/src/tests/note_service_test.rs b/crates/frilvault-core/src/tests/note_service_test.rs index 43e11bf..2f543c8 100644 --- a/crates/frilvault-core/src/tests/note_service_test.rs +++ b/crates/frilvault-core/src/tests/note_service_test.rs @@ -306,3 +306,73 @@ fn search_finds_symbol_anchor() { fs::remove_dir_all(workspace_root).unwrap(); } + +#[test] +fn search_by_symbol_returns_matching_notes() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let mut service = create_test_note_service(&workspace_root); + + service + .add_note(AddNoteInput { + source_file: "src/main.rs".into(), + + anchor: NoteAnchor::Symbol(SymbolAnchor { + name: "main".to_string(), + + kind: SymbolKind::Function, + + signature: None, + + line_hint: None, + }), + + content: "main symbol note".to_string(), + }) + .unwrap(); + + let results = service.search_by_symbol("main").unwrap(); + + assert_eq!(results.len(), 1); + + assert_eq!(results[0].note.content, "main symbol note"); + + fs::remove_dir_all(workspace_root).unwrap(); +} + +#[test] +fn search_by_symbol_returns_empty_when_not_found() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let mut service = create_test_note_service(&workspace_root); + + service + .add_note(AddNoteInput { + source_file: "src/main.rs".into(), + + anchor: NoteAnchor::Symbol(SymbolAnchor { + name: "main".to_string(), + + kind: SymbolKind::Function, + + signature: None, + + line_hint: None, + }), + + content: "main symbol note".to_string(), + }) + .unwrap(); + + let results = service.search_by_symbol("parser").unwrap(); + + assert!(results.is_empty()); + + fs::remove_dir_all(workspace_root).unwrap(); +} diff --git a/crates/frilvault-core/src/tests/vault_context_test.rs b/crates/frilvault-core/src/tests/vault_context_test.rs new file mode 100644 index 0000000..70098ea --- /dev/null +++ b/crates/frilvault-core/src/tests/vault_context_test.rs @@ -0,0 +1,33 @@ +use std::{fs, path::Path}; + +use crate::{AddNoteInput, LineAnchor, Note, NoteAnchor, tests::helper::create_test_vault_context}; + +#[test] +fn load_notes_populates_cache() { + let workspace_root = + std::env::temp_dir().join(format!("frilvault-test-{}", uuid::Uuid::new_v4())); + + fs::create_dir_all(&workspace_root).unwrap(); + + let mut vault_context = create_test_vault_context(&workspace_root); + + vault_context + .note_repository + .append_note( + Path::new("src/main.rs"), + &Note::new(AddNoteInput { + source_file: "src/main.rs".into(), + anchor: NoteAnchor::Line(LineAnchor { line: 1, column: 1 }), + content: "test".to_string(), + }), + ) + .unwrap(); + + assert!(!vault_context.contains_cached_notes(Path::new("src/main.rs"))); + + vault_context.load_notes(Path::new("src/main.rs")).unwrap(); + + assert!(vault_context.contains_cached_notes(Path::new("src/main.rs"))); + + fs::remove_dir_all(workspace_root).unwrap(); +} diff --git a/crates/frilvault-core/src/workspace/service/workspace_service.rs b/crates/frilvault-core/src/workspace/service/workspace_service.rs index 32a184c..4827544 100644 --- a/crates/frilvault-core/src/workspace/service/workspace_service.rs +++ b/crates/frilvault-core/src/workspace/service/workspace_service.rs @@ -1,3 +1,8 @@ +//! Workspace-level application services. +//! +//! This module provides statistics, +//! health checks, and repair workflows. + use std::path::Path; use crate::{ @@ -5,6 +10,13 @@ use crate::{ WorkspaceIndexRepository, WorkspaceStats, }; +/// Application service responsible for +/// workspace-level operations. +/// +/// Examples: +/// - statistics +/// - health checks +/// - repair suggestions pub struct WorkspaceService { pub vault_context: VaultContext, pub index_repository: WorkspaceIndexRepository,