fast-fs 0.2.1

High-speed async file system traversal library with batteries-included file browser component
Documentation
// <FILE>crates/fast-fs/src/nav/cls_selection.rs</FILE> - <DESC>Multi-selection state</DESC>
// <VERS>VERSION: 0.2.0</VERS>
// <WCTX>Adding range selection support</WCTX>
// <CLOG>Added anchor-based range selection (Shift+Arrow)</CLOG>

//! Multi-selection state
//!
//! Selection maintains a set of selected paths, independent of the current
//! directory. This allows selections to persist across navigation.

use hashbrown::HashSet;
use std::path::{Path, PathBuf};

/// Multi-selection state using a HashSet of paths
///
/// Selection is independent of the current directory view. This means:
/// - Selections persist when navigating directories
/// - Selections may include paths not currently visible
/// - Filter changes don't affect the selection set
///
/// # Range Selection
///
/// Range selection uses an anchor point. When the user starts selecting
/// (e.g., Shift+Arrow), the anchor is set. Moving with Shift held extends
/// the selection from anchor to cursor. Moving without Shift clears the anchor.
///
/// # Examples
///
/// ```
/// use fast_fs::nav::Selection;
/// use std::path::Path;
///
/// let mut selection = Selection::new();
///
/// selection.select(Path::new("/home/user/file1.txt"));
/// selection.select(Path::new("/home/user/file2.txt"));
/// assert_eq!(selection.count(), 2);
///
/// selection.toggle(Path::new("/home/user/file1.txt"));
/// assert_eq!(selection.count(), 1);
/// assert!(!selection.is_selected(Path::new("/home/user/file1.txt")));
/// ```
#[derive(Debug, Clone, Default)]
pub struct Selection {
    selected: HashSet<PathBuf>,
    /// Anchor index for range selection (None = no active range)
    anchor: Option<usize>,
}

impl Selection {
    /// Create a new empty selection
    pub fn new() -> Self {
        Self::default()
    }

    /// Toggle selection of a path
    ///
    /// If the path is selected, deselect it. If not selected, select it.
    pub fn toggle(&mut self, path: &Path) {
        if self.selected.contains(path) {
            self.selected.remove(path);
        } else {
            self.selected.insert(path.to_path_buf());
        }
    }

    /// Add a path to the selection
    pub fn select(&mut self, path: &Path) {
        self.selected.insert(path.to_path_buf());
    }

    /// Remove a path from the selection
    pub fn deselect(&mut self, path: &Path) {
        self.selected.remove(path);
    }

    /// Clear all selections
    pub fn clear(&mut self) {
        self.selected.clear();
    }

    /// Check if a path is selected
    pub fn is_selected(&self, path: &Path) -> bool {
        self.selected.contains(path)
    }

    /// Get the number of selected items
    pub fn count(&self) -> usize {
        self.selected.len()
    }

    /// Check if the selection is empty
    pub fn is_empty(&self) -> bool {
        self.selected.is_empty()
    }

    /// Iterate over selected paths
    pub fn iter(&self) -> impl Iterator<Item = &Path> {
        self.selected.iter().map(|p| p.as_path())
    }

    /// Get all selected paths as a vector
    pub fn paths(&self) -> Vec<&Path> {
        self.iter().collect()
    }

    /// Remove paths that no longer exist or match a predicate
    pub fn retain<F>(&mut self, f: F)
    where
        F: FnMut(&PathBuf) -> bool,
    {
        self.selected.retain(f);
    }

    /// Select multiple paths at once
    pub fn select_all<I, P>(&mut self, paths: I)
    where
        I: IntoIterator<Item = P>,
        P: AsRef<Path>,
    {
        for path in paths {
            self.selected.insert(path.as_ref().to_path_buf());
        }
    }

    // --- Range Selection ---

    /// Get the current anchor index for range selection
    pub fn anchor(&self) -> Option<usize> {
        self.anchor
    }

    /// Set the anchor for range selection
    ///
    /// Call this when starting a range selection (e.g., first Shift+Arrow).
    pub fn set_anchor(&mut self, index: usize) {
        self.anchor = Some(index);
    }

    /// Clear the anchor (end range selection mode)
    ///
    /// Call this when the user moves without Shift held.
    pub fn clear_anchor(&mut self) {
        self.anchor = None;
    }

    /// Check if range selection is active
    pub fn has_anchor(&self) -> bool {
        self.anchor.is_some()
    }

    /// Select a range of items by index
    ///
    /// This selects all items from `from` to `to` (inclusive) using the provided
    /// path lookup function. Does not modify the anchor.
    ///
    /// # Arguments
    /// * `from` - Start index (inclusive)
    /// * `to` - End index (inclusive)
    /// * `get_path` - Function to get path at index
    pub fn select_range<F>(&mut self, from: usize, to: usize, get_path: F)
    where
        F: Fn(usize) -> Option<PathBuf>,
    {
        let (start, end) = if from <= to { (from, to) } else { (to, from) };
        for i in start..=end {
            if let Some(path) = get_path(i) {
                self.selected.insert(path);
            }
        }
    }

    /// Extend selection from anchor to new cursor position
    ///
    /// If no anchor is set, sets anchor to `from` first.
    /// Clears previous range selection and selects new range from anchor to `to`.
    ///
    /// # Arguments
    /// * `from` - Current cursor position (becomes anchor if none set)
    /// * `to` - New cursor position (end of range)
    /// * `get_path` - Function to get path at index
    /// * `total` - Total number of items (for bounds checking)
    pub fn extend_to<F>(&mut self, from: usize, to: usize, get_path: F, _total: usize)
    where
        F: Fn(usize) -> Option<PathBuf>,
    {
        // Set anchor if not already set
        let anchor = self.anchor.unwrap_or(from);
        if self.anchor.is_none() {
            self.anchor = Some(from);
        }

        // Select range from anchor to new position
        self.select_range(anchor, to, get_path);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_is_empty() {
        let selection = Selection::new();
        assert!(selection.is_empty());
        assert_eq!(selection.count(), 0);
    }

    #[test]
    fn test_select_and_deselect() {
        let mut selection = Selection::new();
        let path = Path::new("/test/file.txt");

        selection.select(path);
        assert!(selection.is_selected(path));
        assert_eq!(selection.count(), 1);

        selection.deselect(path);
        assert!(!selection.is_selected(path));
        assert!(selection.is_empty());
    }

    #[test]
    fn test_toggle() {
        let mut selection = Selection::new();
        let path = Path::new("/test/file.txt");

        selection.toggle(path);
        assert!(selection.is_selected(path));

        selection.toggle(path);
        assert!(!selection.is_selected(path));
    }

    #[test]
    fn test_clear() {
        let mut selection = Selection::new();
        selection.select(Path::new("/a"));
        selection.select(Path::new("/b"));
        selection.select(Path::new("/c"));

        assert_eq!(selection.count(), 3);
        selection.clear();
        assert!(selection.is_empty());
    }

    #[test]
    fn test_iter() {
        let mut selection = Selection::new();
        selection.select(Path::new("/a"));
        selection.select(Path::new("/b"));

        let paths: Vec<_> = selection.iter().collect();
        assert_eq!(paths.len(), 2);
    }

    #[test]
    fn test_select_all() {
        let mut selection = Selection::new();
        selection.select_all(["/a", "/b", "/c"]);
        assert_eq!(selection.count(), 3);
    }

    #[test]
    fn test_retain() {
        let mut selection = Selection::new();
        selection.select(Path::new("/keep/a"));
        selection.select(Path::new("/remove/b"));
        selection.select(Path::new("/keep/c"));

        selection.retain(|p| p.starts_with("/keep"));
        assert_eq!(selection.count(), 2);
        assert!(selection.is_selected(Path::new("/keep/a")));
        assert!(!selection.is_selected(Path::new("/remove/b")));
    }

    #[test]
    fn test_anchor_lifecycle() {
        let mut selection = Selection::new();
        assert!(!selection.has_anchor());
        assert_eq!(selection.anchor(), None);

        selection.set_anchor(5);
        assert!(selection.has_anchor());
        assert_eq!(selection.anchor(), Some(5));

        selection.clear_anchor();
        assert!(!selection.has_anchor());
        assert_eq!(selection.anchor(), None);
    }

    #[test]
    fn test_select_range() {
        let mut selection = Selection::new();
        let paths = vec![
            PathBuf::from("/a"),
            PathBuf::from("/b"),
            PathBuf::from("/c"),
            PathBuf::from("/d"),
            PathBuf::from("/e"),
        ];

        // Select range 1..3 (b, c, d)
        selection.select_range(1, 3, |i| paths.get(i).cloned());

        assert_eq!(selection.count(), 3);
        assert!(!selection.is_selected(Path::new("/a")));
        assert!(selection.is_selected(Path::new("/b")));
        assert!(selection.is_selected(Path::new("/c")));
        assert!(selection.is_selected(Path::new("/d")));
        assert!(!selection.is_selected(Path::new("/e")));
    }

    #[test]
    fn test_select_range_reversed() {
        let mut selection = Selection::new();
        let paths = vec![
            PathBuf::from("/a"),
            PathBuf::from("/b"),
            PathBuf::from("/c"),
        ];

        // Select range 2..0 (should still select a, b, c)
        selection.select_range(2, 0, |i| paths.get(i).cloned());

        assert_eq!(selection.count(), 3);
    }

    #[test]
    fn test_extend_to_sets_anchor() {
        let mut selection = Selection::new();
        let paths = vec![
            PathBuf::from("/a"),
            PathBuf::from("/b"),
            PathBuf::from("/c"),
        ];

        // Extend from cursor 0 to 2 (no anchor set)
        selection.extend_to(0, 2, |i| paths.get(i).cloned(), paths.len());

        // Anchor should be set to initial position
        assert_eq!(selection.anchor(), Some(0));
        assert_eq!(selection.count(), 3);
    }

    #[test]
    fn test_extend_to_uses_existing_anchor() {
        let mut selection = Selection::new();
        let paths = vec![
            PathBuf::from("/a"),
            PathBuf::from("/b"),
            PathBuf::from("/c"),
            PathBuf::from("/d"),
            PathBuf::from("/e"),
        ];

        // Set anchor at 1
        selection.set_anchor(1);

        // Extend from current (2) to new position (4)
        // Should select from anchor (1) to 4
        selection.extend_to(2, 4, |i| paths.get(i).cloned(), paths.len());

        assert_eq!(selection.anchor(), Some(1));
        assert_eq!(selection.count(), 4); // b, c, d, e
        assert!(!selection.is_selected(Path::new("/a")));
        assert!(selection.is_selected(Path::new("/b")));
        assert!(selection.is_selected(Path::new("/e")));
    }
}

// <FILE>crates/fast-fs/src/nav/cls_selection.rs</FILE>
// <VERS>END OF VERSION: 0.2.0</VERS>