This commit is contained in:
Josh Thomas 2025-08-25 10:34:57 -05:00
parent fb768a86d5
commit 20163b50f8
5 changed files with 94 additions and 94 deletions

View file

@ -9,7 +9,10 @@ use std::{collections::HashMap, sync::Arc};
use salsa::Setter; use salsa::Setter;
use super::{ use super::{
db::{parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, TemplateLoaderOrder}, db::{
parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst,
TemplateLoaderOrder,
},
vfs::{FileKind, VfsSnapshot}, vfs::{FileKind, VfsSnapshot},
FileId, FileId,
}; };
@ -31,6 +34,7 @@ pub struct FileStore {
impl FileStore { impl FileStore {
/// Construct an empty store and DB. /// Construct an empty store and DB.
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
db: Database::default(), db: Database::default(),
@ -118,8 +122,7 @@ impl FileStore {
pub fn get_template_errors(&self, id: FileId) -> Arc<[String]> { pub fn get_template_errors(&self, id: FileId) -> Arc<[String]> {
self.files self.files
.get(&id) .get(&id)
.map(|sf| template_errors(&self.db, *sf)) .map_or_else(|| Arc::from(vec![]), |sf| template_errors(&self.db, *sf))
.unwrap_or_else(|| Arc::from(vec![]))
} }
} }
@ -139,7 +142,7 @@ mod tests {
fn test_filestore_template_ast_caching() { fn test_filestore_template_ast_caching() {
let mut store = FileStore::new(); let mut store = FileStore::new();
let vfs = Vfs::default(); let vfs = Vfs::default();
// Create a template file in VFS // Create a template file in VFS
let url = url::Url::parse("file:///test.html").unwrap(); let url = url::Url::parse("file:///test.html").unwrap();
let path = Utf8PathBuf::from("/test.html"); let path = Utf8PathBuf::from("/test.html");
@ -151,15 +154,15 @@ mod tests {
TextSource::Overlay(content.clone()), TextSource::Overlay(content.clone()),
); );
vfs.set_overlay(file_id, content.clone()).unwrap(); vfs.set_overlay(file_id, content.clone()).unwrap();
// Apply VFS snapshot to FileStore // Apply VFS snapshot to FileStore
let snapshot = vfs.snapshot(); let snapshot = vfs.snapshot();
store.apply_vfs_snapshot(&snapshot); store.apply_vfs_snapshot(&snapshot);
// Get template AST - should parse and cache // Get template AST - should parse and cache
let ast1 = store.get_template_ast(file_id); let ast1 = store.get_template_ast(file_id);
assert!(ast1.is_some()); assert!(ast1.is_some());
// Get again - should return cached // Get again - should return cached
let ast2 = store.get_template_ast(file_id); let ast2 = store.get_template_ast(file_id);
assert!(ast2.is_some()); assert!(ast2.is_some());
@ -170,7 +173,7 @@ mod tests {
fn test_filestore_template_errors() { fn test_filestore_template_errors() {
let mut store = FileStore::new(); let mut store = FileStore::new();
let vfs = Vfs::default(); let vfs = Vfs::default();
// Create a template with an unclosed tag // Create a template with an unclosed tag
let url = url::Url::parse("file:///error.html").unwrap(); let url = url::Url::parse("file:///error.html").unwrap();
let path = Utf8PathBuf::from("/error.html"); let path = Utf8PathBuf::from("/error.html");
@ -182,16 +185,16 @@ mod tests {
TextSource::Overlay(content.clone()), TextSource::Overlay(content.clone()),
); );
vfs.set_overlay(file_id, content).unwrap(); vfs.set_overlay(file_id, content).unwrap();
// Apply VFS snapshot // Apply VFS snapshot
let snapshot = vfs.snapshot(); let snapshot = vfs.snapshot();
store.apply_vfs_snapshot(&snapshot); store.apply_vfs_snapshot(&snapshot);
// Get errors - should contain parsing errors // Get errors - should contain parsing errors
let errors = store.get_template_errors(file_id); let errors = store.get_template_errors(file_id);
// The template has unclosed tags, so there should be errors // The template has unclosed tags, so there should be errors
// We don't assert on specific error count as the parser may evolve // We don't assert on specific error count as the parser may evolve
// Verify errors are cached // Verify errors are cached
let errors2 = store.get_template_errors(file_id); let errors2 = store.get_template_errors(file_id);
assert!(Arc::ptr_eq(&errors, &errors2)); assert!(Arc::ptr_eq(&errors, &errors2));
@ -201,7 +204,7 @@ mod tests {
fn test_filestore_invalidation_on_content_change() { fn test_filestore_invalidation_on_content_change() {
let mut store = FileStore::new(); let mut store = FileStore::new();
let vfs = Vfs::default(); let vfs = Vfs::default();
// Create initial template // Create initial template
let url = url::Url::parse("file:///change.html").unwrap(); let url = url::Url::parse("file:///change.html").unwrap();
let path = Utf8PathBuf::from("/change.html"); let path = Utf8PathBuf::from("/change.html");
@ -213,20 +216,20 @@ mod tests {
TextSource::Overlay(content1.clone()), TextSource::Overlay(content1.clone()),
); );
vfs.set_overlay(file_id, content1).unwrap(); vfs.set_overlay(file_id, content1).unwrap();
// Apply snapshot and get AST // Apply snapshot and get AST
let snapshot1 = vfs.snapshot(); let snapshot1 = vfs.snapshot();
store.apply_vfs_snapshot(&snapshot1); store.apply_vfs_snapshot(&snapshot1);
let ast1 = store.get_template_ast(file_id); let ast1 = store.get_template_ast(file_id);
// Change content // Change content
let content2: Arc<str> = Arc::from("{% for item in items %}{{ item }}{% endfor %}"); let content2: Arc<str> = Arc::from("{% for item in items %}{{ item }}{% endfor %}");
vfs.set_overlay(file_id, content2).unwrap(); vfs.set_overlay(file_id, content2).unwrap();
// Apply new snapshot // Apply new snapshot
let snapshot2 = vfs.snapshot(); let snapshot2 = vfs.snapshot();
store.apply_vfs_snapshot(&snapshot2); store.apply_vfs_snapshot(&snapshot2);
// Get AST again - should be different due to content change // Get AST again - should be different due to content change
let ast2 = store.get_template_ast(file_id); let ast2 = store.get_template_ast(file_id);
assert!(ast1.is_some() && ast2.is_some()); assert!(ast1.is_some() && ast2.is_some());

View file

@ -118,14 +118,17 @@ pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option<Arc<
} }
let text = file.text(db); let text = file.text(db);
// Call the pure parsing function from djls-templates // Call the pure parsing function from djls-templates
match djls_templates::parse_template(&text) { match djls_templates::parse_template(text) {
Ok((ast, errors)) => { Ok((ast, errors)) => {
// Convert errors to strings // Convert errors to strings
let error_strings = errors.into_iter().map(|e| e.to_string()).collect(); let error_strings = errors.into_iter().map(|e| e.to_string()).collect();
Some(Arc::new(TemplateAst { ast, errors: error_strings })) Some(Arc::new(TemplateAst {
}, ast,
errors: error_strings,
}))
}
Err(err) => { Err(err) => {
// Even on fatal errors, return an empty AST with the error // Even on fatal errors, return an empty AST with the error
Some(Arc::new(TemplateAst { Some(Arc::new(TemplateAst {
@ -144,9 +147,7 @@ pub fn parse_template(db: &dyn salsa::Database, file: SourceFile) -> Option<Arc<
/// Returns an empty vector for non-template files. /// Returns an empty vector for non-template files.
#[salsa::tracked] #[salsa::tracked]
pub fn template_errors(db: &dyn salsa::Database, file: SourceFile) -> Arc<[String]> { pub fn template_errors(db: &dyn salsa::Database, file: SourceFile) -> Arc<[String]> {
parse_template(db, file) parse_template(db, file).map_or_else(|| Arc::from(vec![]), |ast| Arc::from(ast.errors.clone()))
.map(|ast| Arc::from(ast.errors.clone()))
.unwrap_or_else(|| Arc::from(vec![]))
} }
#[cfg(test)] #[cfg(test)]
@ -157,19 +158,19 @@ mod tests {
#[test] #[test]
fn test_template_parsing_caches_result() { fn test_template_parsing_caches_result() {
let db = Database::default(); let db = Database::default();
// Create a template file // Create a template file
let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }}{% endif %}"); let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }}{% endif %}");
let file = SourceFile::new(&db, FileKindMini::Template, template_content.clone()); let file = SourceFile::new(&db, FileKindMini::Template, template_content.clone());
// First parse - should execute the parsing // First parse - should execute the parsing
let ast1 = parse_template(&db, file); let ast1 = parse_template(&db, file);
assert!(ast1.is_some()); assert!(ast1.is_some());
// Second parse - should return cached result (same Arc) // Second parse - should return cached result (same Arc)
let ast2 = parse_template(&db, file); let ast2 = parse_template(&db, file);
assert!(ast2.is_some()); assert!(ast2.is_some());
// Verify they're the same Arc (cached) // Verify they're the same Arc (cached)
assert!(Arc::ptr_eq(&ast1.unwrap(), &ast2.unwrap())); assert!(Arc::ptr_eq(&ast1.unwrap(), &ast2.unwrap()));
} }
@ -177,23 +178,24 @@ mod tests {
#[test] #[test]
fn test_template_parsing_invalidates_on_change() { fn test_template_parsing_invalidates_on_change() {
let mut db = Database::default(); let mut db = Database::default();
// Create a template file // Create a template file
let template_content1: Arc<str> = Arc::from("{% if user %}Hello{% endif %}"); let template_content1: Arc<str> = Arc::from("{% if user %}Hello{% endif %}");
let file = SourceFile::new(&db, FileKindMini::Template, template_content1); let file = SourceFile::new(&db, FileKindMini::Template, template_content1);
// First parse // First parse
let ast1 = parse_template(&db, file); let ast1 = parse_template(&db, file);
assert!(ast1.is_some()); assert!(ast1.is_some());
// Change the content // Change the content
let template_content2: Arc<str> = Arc::from("{% for item in items %}{{ item }}{% endfor %}"); let template_content2: Arc<str> =
Arc::from("{% for item in items %}{{ item }}{% endfor %}");
file.set_text(&mut db).to(template_content2); file.set_text(&mut db).to(template_content2);
// Parse again - should re-execute due to changed content // Parse again - should re-execute due to changed content
let ast2 = parse_template(&db, file); let ast2 = parse_template(&db, file);
assert!(ast2.is_some()); assert!(ast2.is_some());
// Verify they're different Arcs (re-parsed) // Verify they're different Arcs (re-parsed)
assert!(!Arc::ptr_eq(&ast1.unwrap(), &ast2.unwrap())); assert!(!Arc::ptr_eq(&ast1.unwrap(), &ast2.unwrap()));
} }
@ -201,15 +203,15 @@ mod tests {
#[test] #[test]
fn test_non_template_files_return_none() { fn test_non_template_files_return_none() {
let db = Database::default(); let db = Database::default();
// Create a Python file // Create a Python file
let python_content: Arc<str> = Arc::from("def hello():\n print('Hello')"); let python_content: Arc<str> = Arc::from("def hello():\n print('Hello')");
let file = SourceFile::new(&db, FileKindMini::Python, python_content); let file = SourceFile::new(&db, FileKindMini::Python, python_content);
// Should return None for non-template files // Should return None for non-template files
let ast = parse_template(&db, file); let ast = parse_template(&db, file);
assert!(ast.is_none()); assert!(ast.is_none());
// Errors should be empty for non-template files // Errors should be empty for non-template files
let errors = template_errors(&db, file); let errors = template_errors(&db, file);
assert!(errors.is_empty()); assert!(errors.is_empty());
@ -218,15 +220,15 @@ mod tests {
#[test] #[test]
fn test_template_errors_tracked_separately() { fn test_template_errors_tracked_separately() {
let db = Database::default(); let db = Database::default();
// Create a template with an error (unclosed tag) // Create a template with an error (unclosed tag)
let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }"); let template_content: Arc<str> = Arc::from("{% if user %}Hello {{ user.name }");
let file = SourceFile::new(&db, FileKindMini::Template, template_content); let file = SourceFile::new(&db, FileKindMini::Template, template_content);
// Get errors // Get errors
let errors1 = template_errors(&db, file); let errors1 = template_errors(&db, file);
let errors2 = template_errors(&db, file); let errors2 = template_errors(&db, file);
// Should be cached (same Arc) // Should be cached (same Arc)
assert!(Arc::ptr_eq(&errors1, &errors2)); assert!(Arc::ptr_eq(&errors1, &errors2));
} }

View file

@ -3,9 +3,11 @@ mod db;
mod vfs; mod vfs;
mod watcher; mod watcher;
// Re-export public API
pub use bridge::FileStore; pub use bridge::FileStore;
pub use db::{parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst, TemplateLoaderOrder}; pub use db::{
parse_template, template_errors, Database, FileKindMini, SourceFile, TemplateAst,
TemplateLoaderOrder,
};
pub use vfs::{FileKind, FileMeta, FileRecord, Revision, TextSource, Vfs, VfsSnapshot}; pub use vfs::{FileKind, FileMeta, FileRecord, Revision, TextSource, Vfs, VfsSnapshot};
pub use watcher::{VfsWatcher, WatchConfig, WatchEvent}; pub use watcher::{VfsWatcher, WatchConfig, WatchEvent};

View file

@ -8,6 +8,7 @@ use anyhow::{anyhow, Result};
use camino::Utf8PathBuf; use camino::Utf8PathBuf;
use dashmap::DashMap; use dashmap::DashMap;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -18,7 +19,10 @@ use std::{
}; };
use url::Url; use url::Url;
use super::{FileId, watcher::{VfsWatcher, WatchConfig, WatchEvent}}; use super::{
watcher::{VfsWatcher, WatchConfig, WatchEvent},
FileId,
};
/// Monotonic counter representing global VFS state. /// Monotonic counter representing global VFS state.
/// ///
@ -115,7 +119,7 @@ pub struct Vfs {
head: AtomicU64, head: AtomicU64,
/// Optional file system watcher for external change detection /// Optional file system watcher for external change detection
watcher: std::sync::Mutex<Option<VfsWatcher>>, watcher: std::sync::Mutex<Option<VfsWatcher>>,
/// Map from filesystem path to FileId for watcher events /// Map from filesystem path to [`FileId`] for watcher events
by_path: DashMap<Utf8PathBuf, FileId>, by_path: DashMap<Utf8PathBuf, FileId>,
} }
@ -155,10 +159,6 @@ impl Vfs {
/// (detected via hash comparison). /// (detected via hash comparison).
/// ///
/// Returns a tuple of (new global revision, whether content changed). /// Returns a tuple of (new global revision, whether content changed).
///
/// # Errors
///
/// Returns an error if the provided `FileId` does not exist in the VFS.
pub fn set_overlay(&self, id: FileId, new_text: Arc<str>) -> Result<(Revision, bool)> { pub fn set_overlay(&self, id: FileId, new_text: Arc<str>) -> Result<(Revision, bool)> {
let mut rec = self let mut rec = self
.files .files
@ -200,7 +200,10 @@ impl Vfs {
/// Returns an error if file watching is disabled in the config or fails to start. /// Returns an error if file watching is disabled in the config or fails to start.
pub fn enable_file_watching(&self, config: WatchConfig) -> Result<()> { pub fn enable_file_watching(&self, config: WatchConfig) -> Result<()> {
let watcher = VfsWatcher::new(config)?; let watcher = VfsWatcher::new(config)?;
*self.watcher.lock().unwrap() = Some(watcher); *self
.watcher
.lock()
.map_err(|e| anyhow!("Failed to lock watcher mutex: {}", e))? = Some(watcher);
Ok(()) Ok(())
} }
@ -211,36 +214,38 @@ impl Vfs {
pub fn process_file_events(&self) -> usize { pub fn process_file_events(&self) -> usize {
// Get events from the watcher // Get events from the watcher
let events = { let events = {
let guard = self.watcher.lock().unwrap(); let Ok(guard) = self.watcher.lock() else {
return 0; // Return 0 if mutex is poisoned
};
if let Some(watcher) = guard.as_ref() { if let Some(watcher) = guard.as_ref() {
watcher.try_recv_events() watcher.try_recv_events()
} else { } else {
return 0; return 0;
} }
}; };
let mut updated_count = 0; let mut updated_count = 0;
for event in events { for event in events {
match event { match event {
WatchEvent::Modified(path) | WatchEvent::Created(path) => { WatchEvent::Modified(path) | WatchEvent::Created(path) => {
if let Err(e) = self.load_from_disk(&path) { if let Err(e) = self.load_from_disk(&path) {
eprintln!("Failed to load file from disk: {}: {}", path, e); eprintln!("Failed to load file from disk: {path}: {e}");
} else { } else {
updated_count += 1; updated_count += 1;
} }
} }
WatchEvent::Deleted(path) => { WatchEvent::Deleted(path) => {
// For now, we don't remove deleted files from VFS // For now, we don't remove deleted files from VFS
// This maintains stable FileIds for consumers // This maintains stable `FileId`s for consumers
eprintln!("File deleted (keeping in VFS): {}", path); eprintln!("File deleted (keeping in VFS): {path}");
} }
WatchEvent::Renamed { from, to } => { WatchEvent::Renamed { from, to } => {
// Handle rename by updating the path mapping // Handle rename by updating the path mapping
if let Some(file_id) = self.by_path.remove(&from).map(|(_, id)| id) { if let Some(file_id) = self.by_path.remove(&from).map(|(_, id)| id) {
self.by_path.insert(to.clone(), file_id); self.by_path.insert(to.clone(), file_id);
if let Err(e) = self.load_from_disk(&to) { if let Err(e) = self.load_from_disk(&to) {
eprintln!("Failed to load renamed file: {}: {}", to, e); eprintln!("Failed to load renamed file: {to}: {e}");
} else { } else {
updated_count += 1; updated_count += 1;
} }
@ -256,17 +261,15 @@ impl Vfs {
/// This method reads the file from the filesystem and updates the VFS entry /// This method reads the file from the filesystem and updates the VFS entry
/// if the content has changed. It's used by the file watcher to sync external changes. /// if the content has changed. It's used by the file watcher to sync external changes.
fn load_from_disk(&self, path: &Utf8PathBuf) -> Result<()> { fn load_from_disk(&self, path: &Utf8PathBuf) -> Result<()> {
use std::fs;
// Check if we have this file tracked // Check if we have this file tracked
if let Some(file_id) = self.by_path.get(path).map(|entry| *entry.value()) { if let Some(file_id) = self.by_path.get(path).map(|entry| *entry.value()) {
// Read content from disk // Read content from disk
let content = fs::read_to_string(path.as_std_path()) let content = fs::read_to_string(path.as_std_path())
.map_err(|e| anyhow!("Failed to read file {}: {}", path, e))?; .map_err(|e| anyhow!("Failed to read file {}: {}", path, e))?;
let new_text = TextSource::Disk(Arc::from(content.as_str())); let new_text = TextSource::Disk(Arc::from(content.as_str()));
let new_hash = content_hash(&new_text); let new_hash = content_hash(&new_text);
// Update the file if content changed // Update the file if content changed
if let Some(mut record) = self.files.get_mut(&file_id) { if let Some(mut record) = self.files.get_mut(&file_id) {
if record.hash != new_hash { if record.hash != new_hash {
@ -281,7 +284,7 @@ impl Vfs {
/// Check if file watching is currently enabled. /// Check if file watching is currently enabled.
pub fn is_file_watching_enabled(&self) -> bool { pub fn is_file_watching_enabled(&self) -> bool {
self.watcher.lock().unwrap().is_some() self.watcher.lock().map(|g| g.is_some()).unwrap_or(false) // Return false if mutex is poisoned
} }
} }

View file

@ -27,10 +27,7 @@ pub enum WatchEvent {
/// A file was deleted /// A file was deleted
Deleted(Utf8PathBuf), Deleted(Utf8PathBuf),
/// A file was renamed from one path to another /// A file was renamed from one path to another
Renamed { Renamed { from: Utf8PathBuf, to: Utf8PathBuf },
from: Utf8PathBuf,
to: Utf8PathBuf,
},
} }
/// Configuration for the file watcher. /// Configuration for the file watcher.
@ -51,6 +48,7 @@ pub struct WatchConfig {
pub exclude_patterns: Vec<String>, pub exclude_patterns: Vec<String>,
} }
// TODO: Allow for user config instead of hardcoding defaults
impl Default for WatchConfig { impl Default for WatchConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -119,7 +117,7 @@ impl VfsWatcher {
// Spawn background thread for event processing // Spawn background thread for event processing
let config_clone = config.clone(); let config_clone = config.clone();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
Self::process_events(event_rx, watch_tx, config_clone); Self::process_events(&event_rx, &watch_tx, &config_clone);
}); });
Ok(Self { Ok(Self {
@ -134,23 +132,19 @@ impl VfsWatcher {
/// ///
/// This is a non-blocking operation that returns immediately. If no events /// This is a non-blocking operation that returns immediately. If no events
/// are available, it returns an empty vector. /// are available, it returns an empty vector.
#[must_use]
pub fn try_recv_events(&self) -> Vec<WatchEvent> { pub fn try_recv_events(&self) -> Vec<WatchEvent> {
match self.rx.try_recv() { self.rx.try_recv().unwrap_or_default()
Ok(events) => events,
Err(_) => Vec::new(),
}
} }
/// Background thread function for processing raw file system events. /// Background thread function for processing raw file system events.
/// ///
/// This function handles debouncing, filtering, and batching of events before /// This function handles debouncing, filtering, and batching of events before
/// sending them to the main thread for VFS synchronization. /// sending them to the main thread for VFS synchronization.
fn process_events( fn process_events(
event_rx: mpsc::Receiver<Event>, event_rx: &mpsc::Receiver<Event>,
watch_tx: mpsc::Sender<Vec<WatchEvent>>, watch_tx: &mpsc::Sender<Vec<WatchEvent>>,
config: WatchConfig, config: &WatchConfig,
) { ) {
let mut pending_events: HashMap<Utf8PathBuf, WatchEvent> = HashMap::new(); let mut pending_events: HashMap<Utf8PathBuf, WatchEvent> = HashMap::new();
let mut last_batch_time = Instant::now(); let mut last_batch_time = Instant::now();
@ -161,12 +155,11 @@ impl VfsWatcher {
match event_rx.recv_timeout(Duration::from_millis(50)) { match event_rx.recv_timeout(Duration::from_millis(50)) {
Ok(event) => { Ok(event) => {
// Process the raw notify event into our WatchEvent format // Process the raw notify event into our WatchEvent format
if let Some(watch_events) = Self::convert_notify_event(event, &config) { if let Some(watch_events) = Self::convert_notify_event(event, config) {
for watch_event in watch_events { for watch_event in watch_events {
if let Some(path) = Self::get_event_path(&watch_event) { let path = Self::get_event_path(&watch_event);
// Only keep the latest event for each path // Only keep the latest event for each path
pending_events.insert(path.clone(), watch_event); pending_events.insert(path.clone(), watch_event);
}
} }
} }
} }
@ -180,11 +173,9 @@ impl VfsWatcher {
} }
// Check if we should flush pending events // Check if we should flush pending events
if !pending_events.is_empty() if !pending_events.is_empty() && last_batch_time.elapsed() >= debounce_duration {
&& last_batch_time.elapsed() >= debounce_duration
{
let events: Vec<WatchEvent> = pending_events.values().cloned().collect(); let events: Vec<WatchEvent> = pending_events.values().cloned().collect();
if let Err(_) = watch_tx.send(events) { if watch_tx.send(events).is_err() {
// Main thread disconnected, exit // Main thread disconnected, exit
break; break;
} }
@ -194,7 +185,7 @@ impl VfsWatcher {
} }
} }
/// Convert a notify Event into our WatchEvent format. /// Convert a [`notify::Event`] into our [`WatchEvent`] format.
fn convert_notify_event(event: Event, config: &WatchConfig) -> Option<Vec<WatchEvent>> { fn convert_notify_event(event: Event, config: &WatchConfig) -> Option<Vec<WatchEvent>> {
let mut watch_events = Vec::new(); let mut watch_events = Vec::new();
@ -236,8 +227,7 @@ impl VfsWatcher {
// Check include patterns // Check include patterns
for pattern in &config.include_patterns { for pattern in &config.include_patterns {
if pattern.starts_with("*.") { if let Some(extension) = pattern.strip_prefix("*.") {
let extension = &pattern[2..];
if path_str.ends_with(extension) { if path_str.ends_with(extension) {
return true; return true;
} }
@ -249,13 +239,13 @@ impl VfsWatcher {
false false
} }
/// Extract the path from a WatchEvent. /// Extract the path from a [`WatchEvent`].
fn get_event_path(event: &WatchEvent) -> Option<&Utf8PathBuf> { fn get_event_path(event: &WatchEvent) -> &Utf8PathBuf {
match event { match event {
WatchEvent::Modified(path) => Some(path), WatchEvent::Modified(path) | WatchEvent::Created(path) | WatchEvent::Deleted(path) => {
WatchEvent::Created(path) => Some(path), path
WatchEvent::Deleted(path) => Some(path), }
WatchEvent::Renamed { to, .. } => Some(to), WatchEvent::Renamed { to, .. } => to,
} }
} }
} }
@ -270,7 +260,6 @@ impl Drop for VfsWatcher {
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_watch_config_default() { fn test_watch_config_default() {
let config = WatchConfig::default(); let config = WatchConfig::default();
@ -327,4 +316,5 @@ mod tests {
assert_ne!(created, deleted); assert_ne!(created, deleted);
assert_ne!(deleted, renamed); assert_ne!(deleted, renamed);
} }
} }