1 //! Object safe interface for file watching and reading.
4 use paths::{AbsPath, AbsPathBuf};
6 /// A set of files on the file system.
7 #[derive(Debug, Clone)]
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),
15 /// Specifies a set of files on the file system.
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
22 /// If many include/exclude paths match, the longest one wins.
24 /// If a path is in both `include` and `exclude`, the `exclude` one wins.
25 #[derive(Debug, Clone, Default)]
26 pub struct Directories {
27 pub extensions: Vec<String>,
28 pub include: Vec<AbsPathBuf>,
29 pub exclude: Vec<AbsPathBuf>,
32 /// [`Handle`]'s configuration.
35 /// Version number to associate progress updates to the right config
38 /// Set of initially loaded files.
40 /// Index of watched entries in `load`.
42 /// If a path in a watched entry is modified,the [`Handle`] should notify it.
43 pub watch: Vec<usize>,
46 /// Message about an action taken by a [`Handle`].
48 /// Indicate a gradual progress.
50 /// This is supposed to be the number of loaded files.
51 Progress { n_total: usize, n_done: usize, config_version: u32 },
52 /// The handle loaded the following files' content.
53 Loaded { files: Vec<(AbsPathBuf, Option<Vec<u8>>)> },
56 /// Type that will receive [`Messages`](Message) from a [`Handle`].
57 pub type Sender = Box<dyn Fn(Message) + Send>;
59 /// Interface for reading and watching files.
60 pub trait Handle: fmt::Debug {
61 /// Spawn a new handle with the given `sender`.
62 fn spawn(sender: Sender) -> Self
66 /// Set this handle's configuration.
67 fn set_config(&mut self, config: Config);
69 /// The file's content at `path` has been modified, and should be reloaded.
70 fn invalidate(&mut self, path: AbsPathBuf);
72 /// Load the content of the given file, returning [`None`] if it does not
74 fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>>;
80 /// Entry::Directories(Directories {
81 /// extensions: ["rs"],
83 /// exclude: [base/.git],
86 pub fn rs_files_recursively(base: AbsPathBuf) -> Entry {
87 Entry::Directories(dirs(base, &[".git"]))
92 /// Entry::Directories(Directories {
93 /// extensions: ["rs"],
95 /// exclude: [base/.git, base/target],
98 pub fn local_cargo_package(base: AbsPathBuf) -> Entry {
99 Entry::Directories(dirs(base, &[".git", "target"]))
104 /// Entry::Directories(Directories {
105 /// extensions: ["rs"],
107 /// exclude: [base/.git, /tests, /examples, /benches],
110 pub fn cargo_package_dependency(base: AbsPathBuf) -> Entry {
111 Entry::Directories(dirs(base, &[".git", "/tests", "/examples", "/benches"]))
114 /// Returns `true` if `path` is included in `self`.
116 /// See [`Directories::contains_file`].
117 pub fn contains_file(&self, path: &AbsPath) -> bool {
119 Entry::Files(files) => files.iter().any(|it| it == path),
120 Entry::Directories(dirs) => dirs.contains_file(path),
124 /// Returns `true` if `path` is included in `self`.
126 /// - If `self` is `Entry::Files`, returns `false`
127 /// - Else, see [`Directories::contains_dir`].
128 pub fn contains_dir(&self, path: &AbsPath) -> bool {
130 Entry::Files(_) => false,
131 Entry::Directories(dirs) => dirs.contains_dir(path),
137 /// Returns `true` if `path` is included in `self`.
138 pub fn contains_file(&self, path: &AbsPath) -> bool {
139 // First, check the file extension...
140 let ext = path.extension().unwrap_or_default();
141 if self.extensions.iter().all(|it| it.as_str() != ext) {
145 // Then, check for path inclusion...
146 self.includes_path(path)
149 /// Returns `true` if `path` is included in `self`.
151 /// Since `path` is supposed to be a directory, this will not take extension
153 pub fn contains_dir(&self, path: &AbsPath) -> bool {
154 self.includes_path(path)
157 /// Returns `true` if `path` is included in `self`.
159 /// It is included if
160 /// - An element in `self.include` is a prefix of `path`.
161 /// - This path is longer than any element in `self.exclude` that is a prefix
162 /// of `path`. In case of equality, exclusion wins.
163 fn includes_path(&self, path: &AbsPath) -> bool {
164 let mut include: Option<&AbsPathBuf> = None;
165 for incl in &self.include {
166 if path.starts_with(incl) {
167 include = Some(match include {
168 Some(prev) if prev.starts_with(incl) => prev,
174 let include = match include {
176 None => return false,
179 !self.exclude.iter().any(|excl| path.starts_with(excl) && excl.starts_with(include))
186 /// extensions: ["rs"],
188 /// exclude: [base/<exclude>],
191 fn dirs(base: AbsPathBuf, exclude: &[&str]) -> Directories {
192 let exclude = exclude.iter().map(|it| base.join(it)).collect::<Vec<_>>();
193 Directories { extensions: vec!["rs".to_string()], include: vec![base], exclude }
196 impl fmt::Debug for Message {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 Message::Loaded { files } => {
200 f.debug_struct("Loaded").field("n_files", &files.len()).finish()
202 Message::Progress { n_total, n_done, config_version } => f
203 .debug_struct("Progress")
204 .field("n_total", n_total)
205 .field("n_done", n_done)
206 .field("config_version", config_version)
213 fn handle_is_object_safe() {
214 fn _assert(_: &dyn Handle) {}