use crate::diff::FileStatus; use crate::repository::WorkingFileChange; use ignore::WalkBuilder; use std::collections::{HashMap, HashSet}; use std::fs; use std::io; use std::path::{Path, PathBuf}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FilePresentation { Floating, Docked, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FileView { List, Tree, } impl FileView { fn index(self) -> usize { match self { Self::List => 0, Self::Tree => 1, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DockSide { Left, Right, Top, Bottom, } 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, pub status: Option, pub old_path: Option<&'a Path>, pub is_dir: bool, pub depth: usize, pub expanded: bool, } #[derive(Debug)] struct Node { path: PathBuf, label: String, search_label: String, catalog_index: Option, is_dir: bool, status: Option, old_path: Option, virtual_entry: bool, parent: Option, children: Vec, } #[derive(Debug)] struct TreeNode { node: usize, label: String, parent: Option, children: Vec, } #[derive(Debug)] struct VisibleRow { node: usize, tree: Option, depth: usize, expanded: bool, } #[derive(Debug)] pub struct FilePicker { pub root: PathBuf, pub query: String, pub selected: usize, pub presentation: FilePresentation, pub view: FileView, pub searching: bool, pub focused: bool, pub error: Option, nodes: Vec, tree: Vec, tree_for_node: Vec, tree_roots: Vec, rows: Vec, viewport_revision: u64, expanded: HashSet, filter_collapsed: HashSet, saved_selection: [Option; 2], filtered_selection: [Option; 2], tree_searching: bool, warning: Option, repository_changes: Vec, conflicted_paths: HashSet, has_virtual_entries: bool, } // Lowercased field/token. Score: match class, missed word boundaries, gaps, start, length. pub(crate) fn fuzzy_match(field: &str, token: &str) -> Option<[usize; 5]> { if field == token { return Some([0, 0, 0, 0, field.len()]); } if field.starts_with(token) { return Some([1, 0, 0, 0, field.len()]); } let contiguous = field .match_indices(token) .map(|(start, _)| { let boundary = field[..start] .chars() .next_back() .is_none_or(|ch| !ch.is_alphanumeric()); [2, usize::from(!boundary), 0, start, field.len()] }) .min(); if contiguous.is_some() { return contiguous; } // ponytail: greedy subsequences can miss tighter later alignments; use dynamic // programming if that ranking precision becomes necessary. let mut wanted = token.chars(); let mut next = wanted.next()?; let mut start = 0; let mut matched = 0; let mut boundaries = 0; let mut previous_is_word = false; for (position, ch) in field.chars().enumerate() { if ch == next { if matched == 0 { start = position; } matched += 1; boundaries += usize::from(previous_is_word); if let Some(ch) = wanted.next() { next = ch; } else { return Some([ 3, boundaries, position - start + 1 - matched, start, field.len(), ]); } } previous_is_word = ch.is_alphanumeric(); } None } fn label(path: &Path) -> String { path.to_string_lossy() .chars() .map(|ch| if ch.is_control() { '�' } else { ch }) .collect() } fn safe_repository_path(path: &Path) -> bool { !path.as_os_str().is_empty() && path.components().all(|component| { matches!(component, std::path::Component::Normal(name) if name != ".git" && name != ".jj") }) } fn missing_repository_path(root: &Path, path: &Path) -> bool { // A deleted leaf may be absent with all its parents, but never walk through symlinks. let mut current = root.to_owned(); for component in path.components() { current.push(component); match fs::symlink_metadata(¤t) { Ok(metadata) if metadata.is_dir() => {} Ok(_) => return false, Err(error) if error.kind() == io::ErrorKind::NotFound => return true, Err(_) => return false, } } false } impl FilePicker { pub fn scan(root: PathBuf, presentation: FilePresentation, view: FileView) -> io::Result { if !fs::metadata(&root)?.is_dir() { return Err(io::Error::new( io::ErrorKind::NotADirectory, "Project root is not a directory", )); } // Root failures are fatal; descendant failures leave a visibly incomplete catalog. fs::read_dir(&root)?; // ponytail: synchronous scan; move indexing off the input thread if large projects stall. let mut walk = WalkBuilder::new(&root); walk.require_git(false) .parents(false) .hidden(false) .git_global(false) .follow_links(false) .filter_entry(|entry| { entry.depth() == 0 || (entry.file_name() != ".git" && entry.file_name() != ".jj") }); let mut nodes = Vec::new(); let mut warning = None; for entry in walk.build() { let entry = match entry { Ok(entry) => entry, Err(error) => { if error.depth() == Some(0) { return Err(io::Error::other(error)); } warning.get_or_insert_with(|| format!("Incomplete scan: {error}")); continue; } }; if let Some(error) = entry.error() { warning.get_or_insert_with(|| format!("Incomplete scan: {error}")); } // Symlinks (including file symlinks) and the implicit project root are excluded. if entry.depth() == 0 || !entry .file_type() .is_some_and(|kind| kind.is_file() || kind.is_dir()) { continue; } let path = entry .path() .strip_prefix(&root) .expect("walk stays within root") .to_owned(); let label = label(&path); nodes.push(Node { path, search_label: label.to_lowercase(), label, catalog_index: None, status: None, old_path: None, virtual_entry: false, is_dir: entry.file_type().unwrap().is_dir(), parent: None, children: Vec::new(), }); } Ok(Self::from_nodes(root, nodes, presentation, view, warning)) } /// Builds a changed-path catalog, retaining each path's comparison index. pub fn from_paths(root: PathBuf, paths: impl IntoIterator) -> Self { let mut catalog = HashMap::new(); for (index, path) in paths { for parent in path .ancestors() .skip(1) .filter(|path| !path.as_os_str().is_empty()) { catalog.insert((parent.to_owned(), true), None); } catalog.entry((path, false)).or_insert(Some(index)); } let nodes = catalog .into_iter() .map(|((path, is_dir), catalog_index)| { let label = label(&path); Node { path, search_label: label.to_lowercase(), label, catalog_index, status: None, old_path: None, virtual_entry: false, 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, presentation: FilePresentation, view: FileView, warning: Option, ) -> 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(); for index in 0..nodes.len() { let parent = nodes[index] .path .parent() .and_then(|path| by_path.get(path)) .copied(); nodes[index].parent = parent; if let Some(parent) = parent { nodes[parent].children.push(index); } else { roots.push(index); } } let mut children: Vec<_> = nodes .iter_mut() .map(|node| std::mem::take(&mut node.children)) .collect(); let order = |left: &usize, right: &usize| { nodes[*right] .is_dir .cmp(&nodes[*left].is_dir) .then_with(|| { nodes[*left] .path .file_name() .cmp(&nodes[*right].path.file_name()) }) }; roots.sort_by(order); for children in &mut children { children.sort_by(order); } for (node, children) in nodes.iter_mut().zip(children) { node.children = children; } let mut picker = Self { root, query: String::new(), selected: 0, presentation, view, searching: false, focused: true, error: warning.clone(), tree_for_node: vec![0; nodes.len()], has_virtual_entries: nodes.iter().any(|node| node.virtual_entry), nodes, tree: Vec::new(), tree_roots: Vec::new(), rows: Vec::new(), viewport_revision: 0, expanded: HashSet::new(), filter_collapsed: HashSet::new(), saved_selection: [None, None], filtered_selection: [None, None], tree_searching: false, warning, repository_changes: Vec::new(), conflicted_paths: HashSet::new(), }; for root in roots { let tree = picker.compact(root, None); picker.tree_roots.push(tree); } picker.project(None, false); picker.remember_selection(false); picker } fn compact(&mut self, start: usize, parent: Option) -> usize { let tree = self.tree.len(); let mut terminal = start; let mut name = label(Path::new(self.nodes[start].path.file_name().unwrap())); self.tree_for_node[start] = tree; while self.nodes[terminal].children.len() == 1 { let child = self.nodes[terminal].children[0]; if !self.nodes[terminal].is_dir || !self.nodes[child].is_dir { break; } terminal = child; self.tree_for_node[terminal] = tree; name.push('/'); name.push_str(&label(Path::new( self.nodes[terminal].path.file_name().unwrap(), ))); } self.tree.push(TreeNode { node: terminal, label: name, parent, children: Vec::new(), }); for index in 0..self.nodes[terminal].children.len() { let child = self.compact(self.nodes[terminal].children[index], Some(tree)); self.tree[tree].children.push(child); } tree } pub(crate) fn viewport_revision(&self) -> u64 { self.viewport_revision } pub fn entries(&self) -> impl ExactSizeIterator> + '_ { self.rows.iter().map(|row| { let node = &self.nodes[row.node]; FileRow { path: &node.path, label: row .tree .map_or(node.label.as_str(), |tree| self.tree[tree].label.as_str()), is_dir: node.is_dir, catalog_index: node.catalog_index, status: node.status, old_path: node.old_path.as_deref(), depth: row.depth, expanded: row.expanded, } }) } /// Full catalog descendants, independent of filtering and folder expansion. pub(crate) fn descendant_catalog_indices<'a>( &'a self, path: &'a Path, ) -> impl Iterator + 'a { let start = self .nodes .partition_point(|node| node.path.as_path() <= path); self.nodes[start..] .iter() .take_while(move |node| node.path.starts_with(path)) .filter_map(|node| node.catalog_index) } fn selected_node(&self) -> Option { self.rows.get(self.selected).map(|row| row.node) } fn remember_selection(&mut self, explicit: bool) { let Some(node) = self.selected_node() else { return; }; self.saved_selection[self.view.index()] = Some(node); if !self.query.is_empty() { self.filtered_selection[self.view.index()] = Some(node); } if explicit && !self.nodes[node].is_dir { self.saved_selection = [Some(node), Some(node)]; if !self.query.is_empty() { self.filtered_selection = [Some(node), Some(node)]; } } } fn set_expanded(&mut self, tree: usize, expanded: bool) { let mut node = Some(self.tree[tree].node); while let Some(index) = node { if self.tree_for_node[index] != tree { break; } if expanded { self.expanded.insert(index); } else { self.expanded.remove(&index); } if !self.query.is_empty() { if expanded { self.filter_collapsed.remove(&index); } else { self.filter_collapsed.insert(index); } } node = self.nodes[index].parent; } } fn reveal(&mut self, node: usize) { let mut parent = self.tree[self.tree_for_node[node]].parent; while let Some(tree) = parent { self.set_expanded(tree, true); parent = self.tree[tree].parent; } } 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::{BackspaceKind, Input}; match input { Input::Ctrl('u') => self.clear_filter(), input if BackspaceKind::from_input(input).is_some() && (self.view == FileView::List || self.searching) => { if BackspaceKind::from_input(input) .expect("guard checked backspace input") .truncate(&mut self.query) { self.refilter(); } } 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; } let current = self.selected_node(); if !self.searching { self.remember_selection(false); } if self.view == FileView::Tree { self.tree_searching = self.searching; } self.view = view; let selected = if self.query.is_empty() { self.saved_selection[view.index()] } else { self.filtered_selection[view.index()].or(self.saved_selection[view.index()]) } .or(current); if view == FileView::Tree { self.searching = self.tree_searching; if !self.searching && self.query.is_empty() && let Some(node) = selected { self.reveal(node); } } else { self.searching = false; } self.project(selected, false); } pub fn toggle_view(&mut self) { self.set_view(match self.view { FileView::List => FileView::Tree, FileView::Tree => FileView::List, }); } pub(crate) fn start_search(&mut self) { let selected = self.selected_node(); self.searching = true; self.tree_searching = true; self.project(selected, false); self.clear_error(); } pub(crate) fn finish_search(&mut self) { let selected = self .selected_node() .or(self.filtered_selection[self.view.index()]) .or(self.saved_selection[self.view.index()]); self.searching = false; self.tree_searching = false; if self.query.is_empty() && let Some(node) = selected { self.reveal(node); } self.project(selected, false); self.remember_selection(false); self.clear_error(); } pub(crate) fn clear_filter(&mut self) { if self.query.is_empty() { return; } let selected = self .selected_node() .or(self.filtered_selection[self.view.index()]) .or(self.saved_selection[self.view.index()]); self.query.clear(); self.filter_collapsed.clear(); self.filtered_selection = [None, None]; if self.view == FileView::Tree && !self.searching && let Some(node) = selected { self.reveal(node); } self.project(selected, false); self.remember_selection(false); self.clear_error(); } fn project(&mut self, selected: Option, select_match: bool) { static NEXT_REVISION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); self.viewport_revision = NEXT_REVISION.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let query = self.query.to_lowercase(); let tokens: Vec<_> = query.split_whitespace().collect(); let matches = |node: &Node| { tokens .iter() .all(|token| fuzzy_match(&node.search_label, token).is_some()) }; self.rows.clear(); if self.view == FileView::List { self.rows .extend(self.nodes.iter().enumerate().filter_map(|(node, entry)| { (!entry.is_dir && matches(entry)).then_some(VisibleRow { node, tree: None, depth: 0, expanded: false, }) })); } else { let mut included = vec![false; self.tree.len()]; for (index, _) in self .nodes .iter() .enumerate() .filter(|(_, node)| matches(node)) { let mut tree = Some(self.tree_for_node[index]); while let Some(index) = tree { if included[index] { break; } included[index] = true; tree = self.tree[index].parent; } } let filtered = self.searching || !tokens.is_empty(); let mut pending: Vec<_> = self .tree_roots .iter() .rev() .map(|&tree| (tree, 0)) .collect(); while let Some((tree, depth)) = pending.pop() { if !included[tree] { continue; } let entry = &self.tree[tree]; let mut node = Some(entry.node); let mut collapsed = false; let mut saved_expansion = false; while let Some(index) = node { if self.tree_for_node[index] != tree { break; } collapsed |= self.filter_collapsed.contains(&index); saved_expansion |= self.expanded.contains(&index); node = self.nodes[index].parent; } let expanded = self.nodes[entry.node].is_dir && !collapsed && (saved_expansion || (filtered && entry.children.iter().any(|&child| included[child]))); self.rows.push(VisibleRow { node: entry.node, tree: Some(tree), depth, expanded, }); if expanded { pending.extend(entry.children.iter().rev().map(|&child| (child, depth + 1))); } } } // Query edits prefer direct matches; navigation keeps context directories. // Missing selections fall back to a visible ancestor, then the first row. let mut target = selected; self.selected = self .rows .iter() .position(|row| !select_match || matches(&self.nodes[row.node])) .unwrap_or(0); while let Some(node) = target { if let Some(index) = self.rows.iter().position(|row| { (!select_match || matches(&self.nodes[row.node])) && (row.node == node || (self.view == FileView::Tree && row.tree == Some(self.tree_for_node[node]))) }) { self.selected = index; break; } target = self.nodes[node].parent; } } pub(crate) fn refilter(&mut self) { let view = self.view.index(); let selected = self .selected_node() .or(self.filtered_selection[view]) .or(self.saved_selection[view]); self.filter_collapsed.clear(); if self.query.is_empty() { self.filtered_selection = [None, None]; if self.view == FileView::Tree && !self.searching && let Some(node) = selected { self.reveal(node); } } else if self.filtered_selection[view].is_none() { self.filtered_selection[view] = selected; } self.project(selected, !self.query.is_empty()); self.remember_selection(false); self.clear_error(); } pub(crate) fn navigate(&mut self, forward: bool) { let count = self.rows.len(); self.selected = if count == 0 { 0 } else if forward { (self.selected + 1) % count } else if self.selected == 0 { count - 1 } else { self.selected - 1 }; self.remember_selection(true); self.clear_error(); } pub(crate) fn right(&mut self) { let Some(row) = self.rows.get(self.selected) else { return; }; if self.searching { if self .rows .get(self.selected + 1) .is_some_and(|child| child.depth > row.depth) { self.selected += 1; } self.remember_selection(false); self.clear_error(); return; } if !self.nodes[row.node].is_dir { return; } if row.expanded { if self .rows .get(self.selected + 1) .is_some_and(|child| child.depth > row.depth) { self.selected += 1; } } else { let node = row.node; let tree = row.tree.expect("directory navigation is tree-only"); self.set_expanded(tree, true); self.project(Some(node), false); } self.remember_selection(true); self.clear_error(); } pub(crate) fn left(&mut self) { let Some(row) = self.rows.get(self.selected) else { return; }; if self.searching { if let Some(parent) = row.tree.and_then(|tree| self.tree[tree].parent) { let node = self.tree[parent].node; if let Some(index) = self.rows.iter().position(|row| row.node == node) { self.selected = index; } } self.remember_selection(false); self.clear_error(); return; } if row.expanded { let node = row.node; let tree = row.tree.expect("directory navigation is tree-only"); self.set_expanded(tree, false); self.project(Some(node), false); } else if let Some(parent) = row.tree.and_then(|tree| self.tree[tree].parent) { let node = self.tree[parent].node; if let Some(index) = self.rows.iter().position(|row| row.node == node) { self.selected = index; } } self.remember_selection(true); self.clear_error(); } /// Directories finish query editing and toggle; files retain the authoritative path. pub(crate) fn accept(&mut self) -> Option { self.clear_error(); let node = self.selected_node()?; if self.nodes[node].is_dir { if self.searching { self.finish_search(); } let tree = self.tree_for_node[node]; let expanded = !self.rows.get(self.selected).is_some_and(|row| row.expanded); self.set_expanded(tree, expanded); self.project(Some(node), false); self.remember_selection(true); None } else { self.remember_selection(true); Some(self.root.join(&self.nodes[node].path)) } } pub(crate) fn clear_error(&mut self) { self.error = self.warning.clone(); } pub(crate) fn refresh(&mut self) { match Self::scan(self.root.clone(), self.presentation, self.view) { Ok(mut replacement) => { replacement.set_repository_changes(self.repository_changes.clone()); self.replace_catalog(replacement); } Err(error) => self.error = Some(format!("Refresh failed: {error}")), } } pub(crate) fn set_repository_changes(&mut self, changes: Vec) -> bool { if !self.has_virtual_entries && changes == self.repository_changes { return false; } if self.nodes.iter().any(|node| { node.virtual_entry && !node.is_dir && fs::symlink_metadata(self.root.join(&node.path)) .is_ok_and(|metadata| metadata.is_file()) }) { // Reuse the scanner's ignore/symlink rules when a deleted file returns. match Self::scan(self.root.clone(), self.presentation, self.view) { Ok(mut replacement) => { replacement.set_repository_changes(changes); replacement.error = self.error.clone().or(replacement.error); self.replace_catalog(replacement); return true; } Err(error) => self.error = Some(format!("Refresh failed: {error}")), } } let changes: Vec<_> = changes .into_iter() .filter(|change| safe_repository_path(&change.path)) .map(|mut change| { change.old_path = change.old_path.filter(|path| safe_repository_path(path)); change }) .collect(); let by_path: HashMap<_, _> = changes .iter() .map(|change| (change.path.as_path(), change)) .collect(); let scanned: HashSet<_> = self .nodes .iter() .filter(|node| !node.virtual_entry) .map(|node| (node.path.as_path(), node.is_dir)) .collect(); let mut virtual_files = HashSet::new(); let mut virtual_dirs = HashSet::new(); for change in &changes { if change.status != FileStatus::Removed || scanned.contains(&(change.path.as_path(), false)) || !missing_repository_path(&self.root, &change.path) { continue; } virtual_files.insert(change.path.clone()); for parent in change.path.ancestors().skip(1) { if parent.as_os_str().is_empty() { break; } if !scanned.contains(&(parent, true)) && !virtual_dirs.contains(parent) { virtual_dirs.insert(parent.to_owned()); } } } let topology_changed = self.nodes.iter().filter(|node| node.virtual_entry).count() != virtual_files.len() + virtual_dirs.len() || self .nodes .iter() .filter(|node| node.virtual_entry) .any(|node| { !(if node.is_dir { &virtual_dirs } else { &virtual_files }) .contains(node.path.as_path()) }); let mut metadata_changed = false; for node in &mut self.nodes { let change = (!node.is_dir && node.catalog_index.is_none()) .then(|| by_path.get(node.path.as_path())) .flatten(); let status = change.map(|change| change.status); let old_path = change.and_then(|change| change.old_path.as_ref()); metadata_changed |= node.status != status || node.old_path.as_ref() != old_path; node.status = status; if node.old_path.as_ref() != old_path { node.old_path = old_path.cloned(); } } if topology_changed { let mut nodes: Vec<_> = self .nodes .iter() .filter(|node| !node.virtual_entry) .map(|node| Node { path: node.path.clone(), label: node.label.clone(), search_label: node.search_label.clone(), catalog_index: node.catalog_index, status: node.status, old_path: node.old_path.clone(), virtual_entry: false, is_dir: node.is_dir, parent: None, children: Vec::new(), }) .collect(); nodes.extend( virtual_files .into_iter() .map(|path| (path, false)) .chain(virtual_dirs.into_iter().map(|path| (path, true))) .map(|(path, is_dir)| { let label = label(&path); let change = (!is_dir).then(|| by_path.get(path.as_path())).flatten(); Node { status: change.map(|change| change.status), old_path: change.and_then(|change| change.old_path.clone()), path, search_label: label.to_lowercase(), label, catalog_index: None, virtual_entry: true, is_dir, parent: None, children: Vec::new(), } }), ); let mut replacement = Self::from_nodes( self.root.clone(), nodes, self.presentation, self.view, self.warning.clone(), ); replacement.error = self.error.clone(); self.replace_catalog(replacement); } self.repository_changes = changes; topology_changed || metadata_changed } /// Last authoritative conflict state, independent of ordinary change status. pub fn is_conflicted(&self, path: &Path) -> bool { self.conflicted_paths.contains(path) } pub(crate) fn set_conflicted(&mut self, path: &Path, conflicted: bool) { if conflicted { if !self.conflicted_paths.contains(path) { self.conflicted_paths.insert(path.to_owned()); } } else { self.conflicted_paths.remove(path); } } pub(crate) fn replace_paths(&mut self, paths: impl IntoIterator) { self.replace_catalog(Self::from_paths(self.root.clone(), paths)); } fn replace_catalog(&mut self, mut replacement: Self) { replacement.presentation = self.presentation; replacement.conflicted_paths = std::mem::take(&mut self.conflicted_paths); let by_path: HashMap<_, _> = replacement .nodes .iter() .enumerate() .map(|(index, node)| (node.path.clone(), index)) .collect(); let surviving = |node: usize| { let mut path = self.nodes[node].path.as_path(); loop { if let Some(&node) = by_path.get(path) { return Some(node); } path = path.parent()?; } }; replacement.expanded = self .expanded .iter() .filter_map(|&node| { by_path .get(&self.nodes[node].path) .copied() .filter(|&node| replacement.nodes[node].is_dir) }) .collect(); replacement.filter_collapsed = self .filter_collapsed .iter() .filter_map(|&node| { by_path .get(&self.nodes[node].path) .copied() .filter(|&node| replacement.nodes[node].is_dir) }) .collect(); replacement.saved_selection = self.saved_selection.map(|node| node.and_then(surviving)); replacement.filtered_selection = self.filtered_selection.map(|node| node.and_then(surviving)); replacement.query = self.query.clone(); replacement.focused = self.focused; replacement.view = self.view; replacement.searching = self.searching; replacement.tree_searching = self.tree_searching; let selected = self .selected_node() .or(self.filtered_selection[self.view.index()]) .or(self.saved_selection[self.view.index()]) .and_then(surviving); replacement.project(selected, false); replacement.remember_selection(false); *self = replacement; } } #[cfg(test)] #[path = "../tests/unit/files.rs"] mod tests;