feat: toggle diff explorer and show file change indicators

This commit is contained in:
2026-10-11 00:54:35 +02:00
parent 83c37f5a19
commit 2f595ca4ca
9 changed files with 454 additions and 38 deletions
+47 -5
View File
@@ -38,6 +38,8 @@ pub struct FileRow<'a> {
/// The actual project-relative OS path, never reconstructed from the label.
pub path: &'a Path,
pub label: &'a str,
/// Original virtual-catalog input index; absent for scanned entries and directories.
pub catalog_index: Option<usize>,
pub is_dir: bool,
pub depth: usize,
pub expanded: bool,
@@ -48,6 +50,7 @@ struct Node {
path: PathBuf,
label: String,
search_label: String,
catalog_index: Option<usize>,
is_dir: bool,
parent: Option<usize>,
children: Vec<usize>,
@@ -153,6 +156,7 @@ impl FilePicker {
path,
search_label: label.to_lowercase(),
label,
catalog_index: None,
is_dir: entry.file_type().unwrap().is_dir(),
parent: None,
children: Vec::new(),
@@ -163,25 +167,26 @@ impl FilePicker {
/// Builds the changed-path catalog without consulting the working tree.
pub fn from_paths(root: PathBuf, paths: impl IntoIterator<Item = PathBuf>) -> Self {
let mut catalog = HashSet::new();
for path in paths {
let mut catalog = HashMap::new();
for (index, path) in paths.into_iter().enumerate() {
for parent in path
.ancestors()
.skip(1)
.filter(|path| !path.as_os_str().is_empty())
{
catalog.insert((parent.to_owned(), true));
catalog.insert((parent.to_owned(), true), None);
}
catalog.insert((path, false));
catalog.entry((path, false)).or_insert(Some(index));
}
let nodes = catalog
.into_iter()
.map(|(path, is_dir)| {
.map(|((path, is_dir), catalog_index)| {
let label = label(&path);
Node {
path,
search_label: label.to_lowercase(),
label,
catalog_index,
is_dir,
parent: None,
children: Vec::new(),
@@ -310,6 +315,7 @@ impl FilePicker {
.tree
.map_or(node.label.as_str(), |tree| self.tree[tree].label.as_str()),
is_dir: node.is_dir,
catalog_index: node.catalog_index,
depth: row.depth,
expanded: row.expanded,
}
@@ -797,6 +803,30 @@ mod tests {
picker.reveal_path(Path::new("src/deep/deleted.rs"));
assert_eq!(picker.accept(), Some(PathBuf::from("src/deep/deleted.rs")));
assert_eq!(picker.entries().filter(|row| !row.is_dir).count(), 3);
let expected_index = |row: &FileRow<'_>| {
if row.is_dir {
None
} else {
Some(match row.path.to_str().unwrap() {
"x" => 0,
"x/y" => 1,
"src/deep/deleted.rs" => 2,
path => panic!("unexpected path {path}"),
})
}
};
for view in [FileView::List, FileView::Tree] {
picker.set_view(view);
for query in ["", "x", "deleted"] {
picker.query = query.into();
picker.refilter();
let rows: Vec<_> = picker.entries().collect();
assert!(!rows.is_empty());
for row in rows {
assert_eq!(row.catalog_index, expected_index(&row));
}
}
}
}
use std::sync::atomic::{AtomicU64, Ordering};
@@ -831,6 +861,18 @@ mod tests {
.collect()
}
#[test]
fn scanned_files_and_directories_have_no_catalog_index() {
let directory = Directory::new();
directory.put("nested/file.txt", "");
let mut picker = directory.scan();
for view in [FileView::List, FileView::Tree] {
picker.set_view(view);
picker.reveal_path(Path::new("nested/file.txt"));
assert!(picker.entries().all(|row| row.catalog_index.is_none()));
}
}
#[test]
fn nested_ignore_negation_hidden_files_and_vcs_metadata() {
let directory = Directory::new();