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}