feat: reuse the docked file tree for diff navigation
This commit is contained in:
+100
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user