1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
use std::iter;
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant};
use codespan_reporting::term::termcolor::WriteColor;
use codespan_reporting::term::{self, termcolor};
use ecow::eco_format;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
use same_file::is_same_file;
use typst::diag::{bail, StrResult};
use typst::utils::format_duration;
use crate::args::{Input, Output, WatchCommand};
use crate::compile::{compile_once, CompileConfig};
use crate::timings::Timer;
use crate::world::{SystemWorld, WorldCreationError};
use crate::{print_error, terminal};
/// Execute a watching compilation command.
pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
let mut config = CompileConfig::watching(command)?;
let Output::Path(output) = &config.output else {
bail!("cannot write document to stdout in watch mode");
};
// Create a file system watcher.
let mut watcher = Watcher::new(output.clone())?;
// Create the world that serves sources, files, and fonts.
// Additionally, if any files do not exist, wait until they do.
let mut world = loop {
match SystemWorld::new(
&command.args.input,
&command.args.world,
&command.args.process,
) {
Ok(world) => break world,
Err(
ref err @ (WorldCreationError::InputNotFound(ref path)
| WorldCreationError::RootNotFound(ref path)),
) => {
watcher.update([path.clone()])?;
Status::Error.print(&config).unwrap();
print_error(&err.to_string()).unwrap();
watcher.wait()?;
}
Err(err) => return Err(err.into()),
}
};
// Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Watch all dependencies of the initial compilation.
watcher.update(world.dependencies())?;
// Recompile whenever something relevant happens.
loop {
// Wait until anything relevant happens.
watcher.wait()?;
// Reset all dependencies.
world.reset();
// Recompile.
timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Evict the cache.
comemo::evict(10);
// Adjust the file watching.
watcher.update(world.dependencies())?;
}
}
/// Watches file system activity.
struct Watcher {
/// The output file. We ignore any events for it.
output: PathBuf,
/// The underlying watcher.
watcher: RecommendedWatcher,
/// Notify event receiver.
rx: Receiver<notify::Result<Event>>,
/// Keeps track of which paths are watched via `watcher`. The boolean is
/// used during updating for mark-and-sweep garbage collection of paths we
/// should unwatch.
watched: HashMap<PathBuf, bool>,
/// A set of files that should be watched, but don't exist. We manually poll
/// for those.
missing: HashSet<PathBuf>,
}
impl Watcher {
/// How long to wait for a shortly following file system event when
/// watching.
const BATCH_TIMEOUT: Duration = Duration::from_millis(100);
/// The maximum time we spend batching events before quitting wait().
const STARVE_TIMEOUT: Duration = Duration::from_millis(500);
/// The interval in which we poll when falling back to poll watching
/// due to missing files.
const POLL_INTERVAL: Duration = Duration::from_millis(300);
/// Create a new, blank watcher.
fn new(output: PathBuf) -> StrResult<Self> {
// Setup file watching.
let (tx, rx) = std::sync::mpsc::channel();
// Set the poll interval to something more eager than the default. That
// default seems a bit excessive for our purposes at around 30s.
// Depending on feedback, some tuning might still be in order. Note that
// this only affects a tiny number of systems. Most do not use the
// [`notify::PollWatcher`].
let config = notify::Config::default().with_poll_interval(Self::POLL_INTERVAL);
let watcher = RecommendedWatcher::new(tx, config)
.map_err(|err| eco_format!("failed to setup file watching ({err})"))?;
Ok(Self {
output,
rx,
watcher,
watched: HashMap::new(),
missing: HashSet::new(),
})
}
/// Update the watching to watch exactly the listed files.
///
/// Files that are not yet watched will be watched. Files that are already
/// watched, but don't need to be watched anymore, will be unwatched.
fn update(&mut self, iter: impl IntoIterator<Item = PathBuf>) -> StrResult<()> {
// Mark all files as not "seen" so that we may unwatch them if they
// aren't in the dependency list.
for seen in self.watched.values_mut() {
*seen = false;
}
// Reset which files are missing.
self.missing.clear();
// Retrieve the dependencies of the last compilation and watch new paths
// that weren't watched yet.
for path in iter {
// We can't watch paths that don't exist with notify-rs. Instead, we
// add those to a `missing` set and fall back to manual poll
// watching.
if !path.exists() {
self.missing.insert(path);
continue;
}
// Watch the path if it's not already watched.
if !self.watched.contains_key(&path) {
self.watcher
.watch(&path, RecursiveMode::NonRecursive)
.map_err(|err| eco_format!("failed to watch {path:?} ({err})"))?;
}
// Mark the file as "seen" so that we don't unwatch it.
self.watched.insert(path, true);
}
// Unwatch old paths that don't need to be watched anymore.
self.watched.retain(|path, &mut seen| {
if !seen {
self.watcher.unwatch(path).ok();
}
seen
});
Ok(())
}
/// Wait until there is a change to a watched path.
fn wait(&mut self) -> StrResult<()> {
loop {
// Wait for an initial event. If there are missing files, we need to
// poll those regularly to check whether they are created, so we
// wait with a smaller timeout.
let first = self.rx.recv_timeout(if self.missing.is_empty() {
Duration::MAX
} else {
Self::POLL_INTERVAL
});
// Watch for file system events. If multiple events happen
// consecutively all within a certain duration, then they are
// bunched up without a recompile in-between. This helps against
// some editors' remove & move behavior. Events are also only
// watched until a certain point, to hinder a barrage of events from
// preventing recompilations.
let mut relevant = false;
let batch_start = Instant::now();
for event in first
.into_iter()
.chain(iter::from_fn(|| self.rx.recv_timeout(Self::BATCH_TIMEOUT).ok()))
.take_while(|_| batch_start.elapsed() <= Self::STARVE_TIMEOUT)
{
let event = event
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
if !is_relevant_event_kind(&event.kind) {
continue;
}
// Workaround for notify-rs' implicit unwatch on remove/rename
// (triggered by some editors when saving files) with the
// inotify backend. By keeping track of the potentially
// unwatched files, we can allow those we still depend on to be
// watched again later on.
if matches!(
event.kind,
notify::EventKind::Remove(notify::event::RemoveKind::File)
| notify::EventKind::Modify(notify::event::ModifyKind::Name(
notify::event::RenameMode::From
))
) {
for path in &event.paths {
// Remove affected path from the watched map to restart
// watching on it later again.
self.watcher.unwatch(path).ok();
self.watched.remove(path);
}
}
// Don't recompile because the output file changed.
// FIXME: This doesn't work properly for multifile image export.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
continue;
}
relevant = true;
}
// If we found a relevant event or if any of the missing files now
// exists, stop waiting.
if relevant || self.missing.iter().any(|path| path.exists()) {
return Ok(());
}
}
}
}
/// Whether a kind of watch event is relevant for compilation.
fn is_relevant_event_kind(kind: ¬ify::EventKind) -> bool {
match kind {
notify::EventKind::Any => true,
notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true,
notify::EventKind::Modify(kind) => match kind {
notify::event::ModifyKind::Any => true,
notify::event::ModifyKind::Data(_) => true,
notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
notify::event::ModifyKind::Other => false,
},
notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false,
}
}
/// The status in which the watcher can be.
pub enum Status {
Compiling,
Success(std::time::Duration),
PartialSuccess(std::time::Duration),
Error,
}
impl Status {
/// Clear the terminal and render the status message.
pub fn print(&self, config: &CompileConfig) -> io::Result<()> {
let timestamp = chrono::offset::Local::now().format("%H:%M:%S");
let color = self.color();
let mut out = terminal::out();
out.clear_screen()?;
out.set_color(&color)?;
write!(out, "watching")?;
out.reset()?;
match &config.input {
Input::Stdin => writeln!(out, " <stdin>"),
Input::Path(path) => writeln!(out, " {}", path.display()),
}?;
out.set_color(&color)?;
write!(out, "writing to")?;
out.reset()?;
writeln!(out, " {}", config.output)?;
#[cfg(feature = "http-server")]
if let Some(server) = &config.server {
out.set_color(&color)?;
write!(out, "serving at")?;
out.reset()?;
writeln!(out, " http://{}", server.addr())?;
}
writeln!(out)?;
writeln!(out, "[{timestamp}] {}", self.message())?;
writeln!(out)?;
out.flush()
}
fn message(&self) -> String {
match *self {
Self::Compiling => "compiling ...".into(),
Self::Success(duration) => {
format!("compiled successfully in {}", format_duration(duration))
}
Self::PartialSuccess(duration) => {
format!("compiled with warnings in {}", format_duration(duration))
}
Self::Error => "compiled with errors".into(),
}
}
fn color(&self) -> termcolor::ColorSpec {
let styles = term::Styles::default();
match self {
Self::Error => styles.header_error,
Self::PartialSuccess(_) => styles.header_warning,
_ => styles.header_note,
}
}
}