feat: reuse the docked file tree for diff navigation

This commit is contained in:
2026-10-11 00:12:50 +02:00
parent 81a2bf9a02
commit 83c37f5a19
9 changed files with 531 additions and 117 deletions
+100 -1
View File
@@ -43,6 +43,7 @@ pub struct FileRow<'a> {
pub expanded: bool,
}
#[derive(Debug)]
struct Node {
path: PathBuf,
label: String,
@@ -52,6 +53,7 @@ struct Node {
children: Vec<usize>,
}
#[derive(Debug)]
struct TreeNode {
node: usize,
label: String,
@@ -59,6 +61,7 @@ struct TreeNode {
children: Vec<usize>,
}
#[derive(Debug)]
struct VisibleRow {
node: usize,
tree: Option<usize>,
@@ -66,6 +69,7 @@ struct VisibleRow {
expanded: bool,
}
#[derive(Debug)]
pub struct FilePicker {
pub root: PathBuf,
pub query: String,
@@ -154,10 +158,51 @@ impl FilePicker {
children: Vec::new(),
});
}
Ok(Self::from_nodes(root, nodes, presentation, view, warning))
}
/// 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 {
for parent in path
.ancestors()
.skip(1)
.filter(|path| !path.as_os_str().is_empty())
{
catalog.insert((parent.to_owned(), true));
}
catalog.insert((path, false));
}
let nodes = catalog
.into_iter()
.map(|(path, is_dir)| {
let label = label(&path);
Node {
path,
search_label: label.to_lowercase(),
label,
is_dir,
parent: None,
children: Vec::new(),
}
})
.collect();
Self::from_nodes(root, nodes, FilePresentation::Docked, FileView::Tree, None)
}
fn from_nodes(
root: PathBuf,
mut nodes: Vec<Node>,
presentation: FilePresentation,
view: FileView,
warning: Option<String>,
) -> Self {
nodes.sort_by(|left, right| left.path.cmp(&right.path));
let by_path: HashMap<_, _> = nodes
.iter()
.enumerate()
.filter(|(_, node)| node.is_dir)
.map(|(index, node)| (node.path.clone(), index))
.collect();
let mut roots = Vec::new();
@@ -223,7 +268,7 @@ impl FilePicker {
}
picker.project(None, false);
picker.remember_selection(false);
Ok(picker)
picker
}
fn compact(&mut self, start: usize, parent: Option<usize>) -> usize {
@@ -321,6 +366,37 @@ impl FilePicker {
}
}
pub(crate) fn reveal_path(&mut self, path: &Path) {
if let Some(node) = self
.nodes
.iter()
.position(|node| node.path == path && !node.is_dir)
{
self.reveal(node);
self.project(Some(node), false);
self.remember_selection(true);
}
}
pub(crate) fn handle_filter_input(&mut self, input: &crate::editor::Input) -> bool {
use crate::editor::Input;
match input {
Input::Ctrl('u') => self.clear_filter(),
Input::Backspace if self.view == FileView::List || self.searching => self.backspace(),
Input::Char('i') if self.view == FileView::Tree && !self.searching => {
self.start_search()
}
Input::Char(ch)
if !ch.is_control() && (self.view == FileView::List || self.searching) =>
{
self.query.push(*ch);
self.refilter();
}
_ => return false,
}
true
}
pub fn set_view(&mut self, view: FileView) {
if self.view == view {
return;
@@ -699,6 +775,29 @@ impl FilePicker {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_catalog_compacts_and_retains_file_directory_collisions() {
let mut picker = FilePicker::from_paths(
PathBuf::new(),
["x", "x/y", "src/deep/deleted.rs", "src/deep/deleted.rs"].map(PathBuf::from),
);
assert_eq!(
picker
.entries()
.map(|row| row.label.to_owned())
.collect::<Vec<_>>(),
["src/deep", "x", "x"]
);
picker.reveal_path(Path::new("x"));
assert!(!picker.entries().nth(picker.selected).unwrap().is_dir);
assert_eq!(picker.accept(), Some(PathBuf::from("x")));
picker.reveal_path(Path::new("x/y"));
assert_eq!(picker.accept(), Some(PathBuf::from("x/y")));
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);
}
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_ID: AtomicU64 = AtomicU64::new(0);