vfs/
loader.rs

1//! Dynamically compatible interface for file watching and reading.
2use std::fmt;
3
4use paths::{AbsPath, AbsPathBuf};
5
6/// A set of files on the file system.
7#[derive(Debug, Clone)]
8pub enum Entry {
9    /// The `Entry` is represented by a raw set of files.
10    Files(Vec<AbsPathBuf>),
11    /// The `Entry` is represented by `Directories`.
12    Directories(Directories),
13}
14
15/// Specifies a set of files on the file system.
16///
17/// A file is included if:
18///   * it has included extension
19///   * it is under an `include` path
20///   * it is not under `exclude` path
21///
22/// If many include/exclude paths match, the longest one wins.
23///
24/// If a path is in both `include` and `exclude`, the `exclude` one wins.
25#[derive(Debug, Clone, Default)]
26pub struct Directories {
27    pub extensions: Vec<String>,
28    pub include: Vec<AbsPathBuf>,
29    pub exclude: Vec<AbsPathBuf>,
30}
31
32/// [`Handle`]'s configuration.
33#[derive(Debug)]
34pub struct Config {
35    /// Version number to associate progress updates to the right config
36    /// version.
37    pub version: u32,
38    /// Set of initially loaded files.
39    pub load: Vec<Entry>,
40    /// Index of watched entries in `load`.
41    ///
42    /// If a path in a watched entry is modified,the [`Handle`] should notify it.
43    pub watch: Vec<usize>,
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
47pub enum LoadingProgress {
48    Started,
49    Progress(usize),
50    Finished,
51}
52
53/// Message about an action taken by a [`Handle`].
54pub enum Message {
55    /// Indicate a gradual progress.
56    ///
57    /// This is supposed to be the number of loaded files.
58    Progress {
59        /// The total files to be loaded.
60        n_total: usize,
61        /// The files that have been loaded successfully.
62        n_done: LoadingProgress,
63        /// The dir being loaded, `None` if its for a file.
64        dir: Option<AbsPathBuf>,
65        /// The [`Config`] version.
66        config_version: u32,
67    },
68    /// The handle loaded the following files' content for the first time.
69    Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
70    /// The handle loaded the following files' content.
71    Changed { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
72}
73
74/// Type that will receive [`Messages`](Message) from a [`Handle`].
75pub type Sender = crossbeam_channel::Sender<Message>;
76
77/// Interface for reading and watching files.
78pub trait Handle: fmt::Debug {
79    /// Spawn a new handle with the given `sender`.
80    fn spawn(sender: Sender) -> Self
81    where
82        Self: Sized;
83
84    /// Set this handle's configuration.
85    fn set_config(&mut self, config: Config);
86
87    /// The file's content at `path` has been modified, and should be reloaded.
88    fn invalidate(&mut self, path: AbsPathBuf);
89
90    /// Load the content of the given file, returning [`None`] if it does not
91    /// exists.
92    fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>>;
93}
94
95impl Entry {
96    /// Returns:
97    /// ```text
98    /// Entry::Directories(Directories {
99    ///     extensions: ["rs"],
100    ///     include: [base],
101    ///     exclude: [base/.git],
102    /// })
103    /// ```
104    pub fn rs_files_recursively(base: AbsPathBuf) -> Entry {
105        Entry::Directories(dirs(base, &[".git"]))
106    }
107
108    /// Returns:
109    /// ```text
110    /// Entry::Directories(Directories {
111    ///     extensions: ["rs"],
112    ///     include: [base],
113    ///     exclude: [base/.git, base/target],
114    /// })
115    /// ```
116    pub fn local_cargo_package(base: AbsPathBuf) -> Entry {
117        Entry::Directories(dirs(base, &[".git", "target"]))
118    }
119
120    /// Returns:
121    /// ```text
122    /// Entry::Directories(Directories {
123    ///     extensions: ["rs"],
124    ///     include: [base],
125    ///     exclude: [base/.git, /tests, /examples, /benches],
126    /// })
127    /// ```
128    pub fn cargo_package_dependency(base: AbsPathBuf) -> Entry {
129        Entry::Directories(dirs(base, &[".git", "/tests", "/examples", "/benches"]))
130    }
131
132    /// Returns `true` if `path` is included in `self`.
133    ///
134    /// See [`Directories::contains_file`].
135    pub fn contains_file(&self, path: &AbsPath) -> bool {
136        match self {
137            Entry::Files(files) => files.iter().any(|it| it == path),
138            Entry::Directories(dirs) => dirs.contains_file(path),
139        }
140    }
141
142    /// Returns `true` if `path` is included in `self`.
143    ///
144    /// - If `self` is `Entry::Files`, returns `false`
145    /// - Else, see [`Directories::contains_dir`].
146    pub fn contains_dir(&self, path: &AbsPath) -> bool {
147        match self {
148            Entry::Files(_) => false,
149            Entry::Directories(dirs) => dirs.contains_dir(path),
150        }
151    }
152}
153
154impl Directories {
155    /// Returns `true` if `path` is included in `self`.
156    pub fn contains_file(&self, path: &AbsPath) -> bool {
157        // First, check the file extension...
158        let ext = path.extension().unwrap_or_default();
159        if self.extensions.iter().all(|it| it.as_str() != ext) {
160            return false;
161        }
162
163        // Then, check for path inclusion...
164        self.includes_path(path)
165    }
166
167    /// Returns `true` if `path` is included in `self`.
168    ///
169    /// Since `path` is supposed to be a directory, this will not take extension
170    /// into account.
171    pub fn contains_dir(&self, path: &AbsPath) -> bool {
172        self.includes_path(path)
173    }
174
175    /// Returns `true` if `path` is included in `self`.
176    ///
177    /// It is included if
178    ///   - An element in `self.include` is a prefix of `path`.
179    ///   - This path is longer than any element in `self.exclude` that is a prefix
180    ///     of `path`. In case of equality, exclusion wins.
181    fn includes_path(&self, path: &AbsPath) -> bool {
182        let mut include: Option<&AbsPathBuf> = None;
183        for incl in &self.include {
184            if path.starts_with(incl) {
185                include = Some(match include {
186                    Some(prev) if prev.starts_with(incl) => prev,
187                    _ => incl,
188                });
189            }
190        }
191
192        let include = match include {
193            Some(it) => it,
194            None => return false,
195        };
196
197        !self.exclude.iter().any(|excl| path.starts_with(excl) && excl.starts_with(include))
198    }
199}
200
201/// Returns :
202/// ```text
203/// Directories {
204///     extensions: ["rs"],
205///     include: [base],
206///     exclude: [base/<exclude>],
207/// }
208/// ```
209fn dirs(base: AbsPathBuf, exclude: &[&str]) -> Directories {
210    let exclude = exclude.iter().map(|it| base.join(it)).collect::<Vec<_>>();
211    Directories { extensions: vec!["rs".to_owned()], include: vec![base], exclude }
212}
213
214impl fmt::Debug for Message {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        match self {
217            Message::Loaded { files } => {
218                f.debug_struct("Loaded").field("n_files", &files.len()).finish()
219            }
220            Message::Changed { files } => {
221                f.debug_struct("Changed").field("n_files", &files.len()).finish()
222            }
223            Message::Progress { n_total, n_done, dir, config_version } => f
224                .debug_struct("Progress")
225                .field("n_total", n_total)
226                .field("n_done", n_done)
227                .field("dir", dir)
228                .field("config_version", config_version)
229                .finish(),
230        }
231    }
232}
233
234#[test]
235fn handle_is_dyn_compatible() {
236    fn _assert(_: &dyn Handle) {}
237}