ratatui_toolkit/diff_file_tree/constructors/
from_paths.rs

1//! Constructor for building a tree from a list of file paths and statuses.
2
3use std::collections::HashMap;
4
5use crate::diff_file_tree::DiffFileEntry;
6use crate::diff_file_tree::DiffFileTree;
7use crate::diff_file_tree::FileStatus;
8use crate::primitives::tree_view::TreeNode;
9
10impl DiffFileTree {
11    /// Creates a `DiffFileTree` from a list of (path, status) pairs.
12    ///
13    /// Paths are parsed to build a hierarchical directory structure.
14    /// Intermediate directories are created automatically.
15    ///
16    /// # Arguments
17    ///
18    /// * `paths` - A slice of (path, status) pairs
19    ///
20    /// # Returns
21    ///
22    /// A new `DiffFileTree` with the file hierarchy.
23    ///
24    /// # Example
25    ///
26    /// ```rust
27    /// use ratatui_toolkit::diff_file_tree::{DiffFileTree, FileStatus};
28    ///
29    /// let files = vec![
30    ///     ("src/lib.rs", FileStatus::Modified),
31    ///     ("src/utils/helper.rs", FileStatus::Added),
32    /// ];
33    ///
34    /// let tree = DiffFileTree::from_paths(&files);
35    /// ```
36    #[must_use]
37    pub fn from_paths<S: AsRef<str>>(paths: &[(S, FileStatus)]) -> Self {
38        let mut tree = Self::new();
39
40        if paths.is_empty() {
41            return tree;
42        }
43
44        // Build a temporary structure to organize files by directory
45        let mut dir_map: HashMap<String, Vec<(String, FileStatus)>> = HashMap::new();
46
47        for (path, status) in paths {
48            let path = path.as_ref();
49            let parts: Vec<&str> = path.split('/').collect();
50
51            if parts.len() == 1 {
52                // Root-level file
53                dir_map
54                    .entry(String::new())
55                    .or_default()
56                    .push((path.to_string(), *status));
57            } else {
58                // File in a subdirectory - add to root level directory
59                let root_dir = parts[0].to_string();
60                dir_map
61                    .entry(root_dir)
62                    .or_default()
63                    .push((path.to_string(), *status));
64            }
65        }
66
67        // Build tree nodes
68        let mut nodes = Vec::new();
69
70        // First add root-level files
71        if let Some(root_files) = dir_map.get("") {
72            for (path, status) in root_files {
73                let name = path.split('/').last().unwrap_or(path);
74                let entry = DiffFileEntry::file(name, path, *status);
75                nodes.push(TreeNode::new(entry));
76            }
77        }
78
79        // Then add directories with their contents
80        let mut dir_names: Vec<_> = dir_map.keys().filter(|k| !k.is_empty()).collect();
81        dir_names.sort();
82
83        for dir_name in dir_names {
84            if let Some(files) = dir_map.get(dir_name) {
85                let dir_node = build_directory_node(dir_name, files);
86                nodes.push(dir_node);
87            }
88        }
89
90        // Sort: directories first, then alphabetically
91        nodes.sort_by(|a, b| {
92            let a_is_dir = a.data.is_dir;
93            let b_is_dir = b.data.is_dir;
94            match (a_is_dir, b_is_dir) {
95                (true, false) => std::cmp::Ordering::Less,
96                (false, true) => std::cmp::Ordering::Greater,
97                _ => a.data.name.to_lowercase().cmp(&b.data.name.to_lowercase()),
98            }
99        });
100
101        tree.nodes = nodes;
102
103        // Select first item if available
104        if !tree.nodes.is_empty() {
105            tree.state.select(vec![0]);
106            // Expand root-level directories by default
107            for i in 0..tree.nodes.len() {
108                if tree.nodes[i].expandable {
109                    tree.state.expand(vec![i]);
110                }
111            }
112        }
113
114        tree
115    }
116}
117
118/// Builds a directory node with all its children from a flat list of paths.
119fn build_directory_node(dir_name: &str, files: &[(String, FileStatus)]) -> TreeNode<DiffFileEntry> {
120    let entry = DiffFileEntry::directory(dir_name, dir_name);
121
122    // Group files by their next path component
123    let mut subdirs: HashMap<String, Vec<(String, FileStatus)>> = HashMap::new();
124    let mut direct_files: Vec<(String, FileStatus)> = Vec::new();
125
126    for (path, status) in files {
127        let relative = path.strip_prefix(dir_name).unwrap_or(path);
128        let relative = relative.strip_prefix('/').unwrap_or(relative);
129        let parts: Vec<&str> = relative.split('/').collect();
130
131        if parts.len() == 1 {
132            // Direct child file
133            direct_files.push((path.clone(), *status));
134        } else {
135            // Nested in subdirectory
136            let subdir = parts[0].to_string();
137            subdirs
138                .entry(subdir)
139                .or_default()
140                .push((path.clone(), *status));
141        }
142    }
143
144    // Build children
145    let mut children = Vec::new();
146
147    // Add subdirectories
148    let mut subdir_names: Vec<_> = subdirs.keys().collect();
149    subdir_names.sort();
150
151    for subdir_name in subdir_names {
152        if let Some(subdir_files) = subdirs.get(subdir_name) {
153            let subdir_path = format!("{}/{}", dir_name, subdir_name);
154            let subdir_node = build_subdirectory_node(&subdir_path, subdir_name, subdir_files);
155            children.push(subdir_node);
156        }
157    }
158
159    // Add direct files
160    direct_files.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
161    for (path, status) in direct_files {
162        let name = path.split('/').last().unwrap_or(&path);
163        let entry = DiffFileEntry::file(name, &path, status);
164        children.push(TreeNode::new(entry));
165    }
166
167    // Sort: directories first, then alphabetically
168    children.sort_by(|a, b| {
169        let a_is_dir = a.data.is_dir;
170        let b_is_dir = b.data.is_dir;
171        match (a_is_dir, b_is_dir) {
172            (true, false) => std::cmp::Ordering::Less,
173            (false, true) => std::cmp::Ordering::Greater,
174            _ => a.data.name.to_lowercase().cmp(&b.data.name.to_lowercase()),
175        }
176    });
177
178    TreeNode::with_children(entry, children)
179}
180
181/// Builds a subdirectory node recursively.
182fn build_subdirectory_node(
183    full_path: &str,
184    name: &str,
185    files: &[(String, FileStatus)],
186) -> TreeNode<DiffFileEntry> {
187    let entry = DiffFileEntry::directory(name, full_path);
188
189    // Group files by their next path component relative to this directory
190    let mut subdirs: HashMap<String, Vec<(String, FileStatus)>> = HashMap::new();
191    let mut direct_files: Vec<(String, FileStatus)> = Vec::new();
192
193    for (path, status) in files {
194        let relative = path.strip_prefix(full_path).unwrap_or(path);
195        let relative = relative.strip_prefix('/').unwrap_or(relative);
196        let parts: Vec<&str> = relative.split('/').collect();
197
198        if parts.len() == 1 {
199            // Direct child file
200            direct_files.push((path.clone(), *status));
201        } else {
202            // Nested in subdirectory
203            let subdir = parts[0].to_string();
204            subdirs
205                .entry(subdir)
206                .or_default()
207                .push((path.clone(), *status));
208        }
209    }
210
211    // Build children
212    let mut children = Vec::new();
213
214    // Add subdirectories
215    let mut subdir_names: Vec<_> = subdirs.keys().collect();
216    subdir_names.sort();
217
218    for subdir_name in subdir_names {
219        if let Some(subdir_files) = subdirs.get(subdir_name) {
220            let subdir_full_path = format!("{}/{}", full_path, subdir_name);
221            let subdir_node = build_subdirectory_node(&subdir_full_path, subdir_name, subdir_files);
222            children.push(subdir_node);
223        }
224    }
225
226    // Add direct files
227    direct_files.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
228    for (path, status) in direct_files {
229        let name = path.split('/').last().unwrap_or(&path);
230        let entry = DiffFileEntry::file(name, &path, status);
231        children.push(TreeNode::new(entry));
232    }
233
234    // Sort: directories first, then alphabetically
235    children.sort_by(|a, b| {
236        let a_is_dir = a.data.is_dir;
237        let b_is_dir = b.data.is_dir;
238        match (a_is_dir, b_is_dir) {
239            (true, false) => std::cmp::Ordering::Less,
240            (false, true) => std::cmp::Ordering::Greater,
241            _ => a.data.name.to_lowercase().cmp(&b.data.name.to_lowercase()),
242        }
243    });
244
245    TreeNode::with_children(entry, children)
246}