ratatui_toolkit/diff_file_tree/constructors/
from_paths.rs1use 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 #[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 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 dir_map
54 .entry(String::new())
55 .or_default()
56 .push((path.to_string(), *status));
57 } else {
58 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 let mut nodes = Vec::new();
69
70 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 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 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 if !tree.nodes.is_empty() {
105 tree.state.select(vec![0]);
106 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
118fn build_directory_node(dir_name: &str, files: &[(String, FileStatus)]) -> TreeNode<DiffFileEntry> {
120 let entry = DiffFileEntry::directory(dir_name, dir_name);
121
122 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_files.push((path.clone(), *status));
134 } else {
135 let subdir = parts[0].to_string();
137 subdirs
138 .entry(subdir)
139 .or_default()
140 .push((path.clone(), *status));
141 }
142 }
143
144 let mut children = Vec::new();
146
147 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 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 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
181fn 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 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_files.push((path.clone(), *status));
201 } else {
202 let subdir = parts[0].to_string();
204 subdirs
205 .entry(subdir)
206 .or_default()
207 .push((path.clone(), *status));
208 }
209 }
210
211 let mut children = Vec::new();
213
214 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 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 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}