mod file_tree_state; use crate::file_tree_store::file_tree_state::FileTreeMapStore; use crate::{BuildTreeError, Entry, FileId, FileMetadata, Repository}; use ignore::gitignore::Gitignore; use std::sync::Arc; use warp_util::standardized_path::StandardizedPath; use warpui::ModelHandle; #[derive(Debug, Clone)] pub struct FileTreeEntry { state_map: FileTreeMapStore, root_path: Arc, } impl FileTreeEntry { pub fn ignored(&self, path: &StandardizedPath) -> bool { let Some(entry_state) = self.state_map.get(path) else { return false; }; match entry_state { FileTreeEntryState::File(file) => file.ignored, FileTreeEntryState::Directory(directory) => directory.ignored, } } pub fn get(&self, path: &StandardizedPath) -> Option<&FileTreeEntryState> { self.state_map.get(path) } pub fn contains(&self, path: &StandardizedPath) -> bool { self.state_map.contains(path) } pub fn root_directory(&self) -> &Arc { &self.root_path } pub fn rename_path(&mut self, path: &StandardizedPath, new_path: &StandardizedPath) -> bool { self.state_map.rename_path(path, new_path) } pub fn load_at_path( &mut self, path: &StandardizedPath, gitignores: &mut Vec, ) -> Result<(), BuildTreeError> { self.state_map.load_at_path(path, gitignores) } pub fn insert_entry_at_path(&mut self, path: Arc, entry: Entry) { self.state_map.insert_entry_at_path(path, entry); } pub fn child_paths( &self, path: &StandardizedPath, ) -> impl Iterator> { self.state_map.children(path) } pub fn get_mut(&mut self, path: &StandardizedPath) -> Option<&mut FileTreeEntryState> { self.state_map.get_mut(path) } pub fn remove(&mut self, path: &StandardizedPath) { self.state_map.remove(path); } pub fn new_for_directory(root_path: Arc) -> Self { Self { state_map: FileTreeMapStore::new_for_directory(root_path.clone()), root_path, } } /// Similar to find_or_insert_child but specifically for creating directory entries. /// This is used when we know the path should be a directory (e.g., when ensuring parent directories exist). pub fn find_or_insert_directory( &mut self, parent_path: &StandardizedPath, target_path: &StandardizedPath, ) -> Option<&mut FileTreeEntryState> { if self.state_map.contains_child(parent_path, target_path) { return self.state_map.get_mut(target_path); } // Child not found, create new directory entry let new_entry = FileTreeEntryState::Directory(FileTreeDirectoryEntryState { path: Arc::new(target_path.clone()), ignored: false, loaded: false, }); self.state_map .insert_child(Arc::new(parent_path.clone()), new_entry); self.state_map.get_mut(target_path) } pub fn find_parent_directory(&self, path: &StandardizedPath) -> Option> { self.state_map.parent_directory(path) } pub fn find_or_insert_child( &mut self, parent_path: &StandardizedPath, child_path: &std::path::Path, ) -> Option> { let std_child = StandardizedPath::try_from_local(child_path).ok()?; if self.state_map.contains_child(parent_path, &std_child) { return Some(Arc::new(std_child)); } let child_arc = Arc::new(std_child); let new_entry = if child_path.is_dir() { FileTreeEntryState::Directory(FileTreeDirectoryEntryState { path: child_arc.clone(), loaded: false, ignored: false, }) } else if child_path.is_file() { FileTreeEntryState::File(FileTreeFileMetadata { path: child_arc.clone(), file_id: FileId::new(), extension: child_arc.extension().map(|s| s.to_owned()), ignored: false, }) } else { return None; }; self.state_map .insert_child(Arc::new(parent_path.clone()), new_entry) } pub fn insert_child_state( &mut self, parent_path: &StandardizedPath, child_state: FileTreeEntryState, ) -> Option> { self.state_map .insert_child(Arc::new(parent_path.clone()), child_state) } /// Ensures all ancestor directories between root and `target_parent` /// exist in the tree, creating unloaded directory entries as needed. /// /// This is essential for handling filesystem events that reference files deep /// in directory hierarchies where intermediate directories might not exist in our /// in-memory tree yet. pub fn ensure_parent_directories_exist(&mut self, target_parent: &StandardizedPath) { let root_directory = self.root_directory(); // Validate that target_parent is indeed under root_entry if !target_parent.starts_with(root_directory) { return; } let Some(FileTreeEntryState::Directory(root_directory)) = self.get(root_directory).cloned() else { return; }; // Get all ancestors between target parent and root (exclusive of root, inclusive of target) let ancestors: Vec<_> = target_parent .ancestors() .take_while(|ancestor| *ancestor != *root_directory.path.as_ref()) .collect(); // Create directories from root to target parent using find_or_insert_directory let mut current_parent = root_directory; for ancestor in ancestors.iter().rev() { match self.find_or_insert_directory(¤t_parent.path, ancestor) { Some(FileTreeEntryState::Directory(dir)) => { current_parent = dir.clone(); } Some(FileTreeEntryState::File(_)) => { log::warn!("Found file where directory expected: {ancestor:?}"); return; } None => { log::warn!("Failed to create or find directory: {ancestor:?}"); return; } } } } /// Applies a [`RepoMetadataUpdate`] to this file tree entry. /// /// Removals are processed first, then subtree patches are applied. /// This is the core mutation path used by the remote client to apply /// incremental updates received from the server. pub fn apply_repo_metadata_update( &mut self, update: &crate::file_tree_update::RepoMetadataUpdate, ) { // 1. Process removals for path in &update.remove_entries { self.remove(path); } // 2. Process subtree patches for entry_update in &update.update_entries { self.apply_entry_update(entry_update); } } fn apply_entry_update(&mut self, update: &crate::file_tree_update::FileTreeEntryUpdate) { use crate::file_tree_update::RepoNodeMetadata; // Ensure parent directories exist up to parent_path_to_replace self.ensure_parent_directories_exist(&update.parent_path_to_replace); // `subtree_metadata` is in depth-first pre-order: each directory // appears before its children. A single pass is sufficient because // by the time we encounter a file, its parent directory has already // been inserted. `insert_child_state` also registers the child in // `parent_to_child_map`, so no separate wiring step is needed. for node in &update.subtree_metadata { match node { RepoNodeMetadata::Directory(dir) => { let state = FileTreeEntryState::Directory(FileTreeDirectoryEntryState { path: Arc::new(dir.path.clone()), ignored: dir.ignored, loaded: dir.loaded, }); if let Some(parent) = self.find_parent_directory(&dir.path) { self.insert_child_state(&parent, state); } else { log::warn!("Could not find parent directory for node during incremental update: {:?}", dir.path); } } RepoNodeMetadata::File(file) => { // If the file already exists, preserve its FileId and just // update metadata (mirrors the local apply path). if let Some(existing) = self.get_mut(&file.path) { existing.set_ignored(file.ignored); } else { let state = FileTreeEntryState::File(FileTreeFileMetadata { path: Arc::new(file.path.clone()), file_id: FileId::new(), extension: file.extension.clone(), ignored: file.ignored, }); if let Some(parent) = self.find_parent_directory(&file.path) { self.insert_child_state(&parent, state); } else { log::warn!("Could not find parent directory for node during incremental update: {:?}", file.path); } } } } } } } #[derive(Debug, Clone)] pub enum FileTreeEntryState { File(FileTreeFileMetadata), Directory(FileTreeDirectoryEntryState), } impl FileTreeEntryState { fn as_directory(&self) -> Option<&FileTreeDirectoryEntryState> { match self { FileTreeEntryState::File(_) => None, FileTreeEntryState::Directory(directory) => Some(directory), } } } #[derive(Debug, Clone)] pub struct FileTreeFileMetadata { /// Absolute path to the file. pub path: Arc, pub file_id: FileId, pub extension: Option, pub ignored: bool, } impl From for FileTreeFileMetadata { fn from(value: FileMetadata) -> Self { Self { path: Arc::new(value.path), file_id: value.file_id, extension: value.extension.clone(), ignored: value.ignored, } } } impl FileTreeEntryState { pub fn set_ignored(&mut self, ignored: bool) { match self { Self::File(file) => file.ignored = ignored, Self::Directory(directory) => directory.ignored = ignored, } } pub fn ignored(&self) -> bool { match self { FileTreeEntryState::File(f) => f.ignored, FileTreeEntryState::Directory(d) => d.ignored, } } pub fn path(&self) -> &StandardizedPath { match self { FileTreeEntryState::File(file) => &file.path, FileTreeEntryState::Directory(directory) => &directory.path, } } fn path_arc(&self) -> Arc { match self { FileTreeEntryState::File(file) => file.path.clone(), FileTreeEntryState::Directory(directory) => directory.path.clone(), } } pub fn loaded(&self) -> bool { match self { FileTreeEntryState::File(_) => true, FileTreeEntryState::Directory(d) => d.loaded, } } } #[derive(Debug, Clone)] pub struct FileTreeDirectoryEntryState { /// Absolute path to the directory. pub path: Arc, pub ignored: bool, pub loaded: bool, } impl From for FileTreeEntry { fn from(value: Entry) -> Self { let root_path = Arc::new(value.path().clone()); let state_map = FileTreeMapStore::from(value); FileTreeEntry { state_map, root_path, } } } /// Represents the state of a file tree for a specific repository. #[derive(Debug, Clone)] pub struct FileTreeState { /// The entry representing the file tree structure. pub entry: FileTreeEntry, /// Gitignore rules applicable to this repository. pub gitignores: Vec, /// Handle to the backing repository (None for lazily-loaded standalone paths). #[expect(unused)] repository: Option>, } impl FileTreeState { /// Creates a new FileTreeState. pub fn new( entry: Entry, gitignores: Vec, repository: Option>, ) -> Self { Self { entry: entry.into(), gitignores, repository, } } /// Creates a new FileTreeState for a lazily-loaded standalone path. pub fn new_lazy_loaded(entry: Entry) -> Self { Self { entry: entry.into(), gitignores: vec![], repository: None, } } /// Creates a new FileTreeState from a pre-built [`FileTreeEntry`]. /// /// Used by the remote model where the entry is constructed via /// `apply_repo_metadata_update` rather than from a local `Entry`. pub fn from_file_tree_entry(entry: FileTreeEntry) -> Self { Self { entry, gitignores: vec![], repository: None, } } } #[cfg(test)] #[path = "file_tree_store_tests.rs"] mod tests;