mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Reparses files in the CLI incrementally and also uses the file modification timestamp to completely skip reparsing if possible.
200 lines
6.4 KiB
Rust
200 lines
6.4 KiB
Rust
use std::collections::HashSet;
|
|
use std::io::{self, IsTerminal, Write};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use codespan_reporting::term::{self, termcolor};
|
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
|
use same_file::is_same_file;
|
|
use termcolor::WriteColor;
|
|
use typst::diag::StrResult;
|
|
use typst::eval::eco_format;
|
|
|
|
use crate::args::CompileCommand;
|
|
use crate::color_stream;
|
|
use crate::compile::compile_once;
|
|
use crate::world::SystemWorld;
|
|
|
|
/// Execute a watching compilation command.
|
|
pub fn watch(mut command: CompileCommand) -> StrResult<()> {
|
|
// Create the world that serves sources, files, and fonts.
|
|
let mut world = SystemWorld::new(&command.common)?;
|
|
|
|
// Perform initial compilation.
|
|
compile_once(&mut world, &mut command, true)?;
|
|
|
|
// Setup file watching.
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default())
|
|
.map_err(|err| eco_format!("failed to setup file watching ({err})"))?;
|
|
|
|
// Watch all the files that are used by the input file and its dependencies.
|
|
watch_dependencies(&mut world, &mut watcher, HashSet::new())?;
|
|
|
|
// Handle events.
|
|
let timeout = std::time::Duration::from_millis(100);
|
|
let output = command.output();
|
|
loop {
|
|
let mut removed = HashSet::new();
|
|
let mut recompile = false;
|
|
for event in rx
|
|
.recv()
|
|
.into_iter()
|
|
.chain(std::iter::from_fn(|| rx.recv_timeout(timeout).ok()))
|
|
{
|
|
let event =
|
|
event.map_err(|err| eco_format!("failed to watch directory ({err})"))?;
|
|
|
|
// 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 removed 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)
|
|
) {
|
|
let path = &event.paths[0];
|
|
removed.insert(path.clone());
|
|
|
|
// Remove the watch in case it still exists.
|
|
watcher.unwatch(path).ok();
|
|
}
|
|
|
|
recompile |= is_event_relevant(&event, &output);
|
|
}
|
|
|
|
if recompile {
|
|
// Retrieve the dependencies of the last compilation.
|
|
let previous: HashSet<PathBuf> = world
|
|
.dependencies()
|
|
.filter(|path| !removed.contains(*path))
|
|
.map(ToOwned::to_owned)
|
|
.collect();
|
|
|
|
// Reset all dependencies.
|
|
world.reset();
|
|
|
|
// Recompile.
|
|
compile_once(&mut world, &mut command, true)?;
|
|
comemo::evict(10);
|
|
|
|
// Adjust the watching.
|
|
watch_dependencies(&mut world, &mut watcher, previous)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adjust the file watching. Watches all new dependencies and unwatches
|
|
/// all `previous` dependencies that are not relevant anymore.
|
|
#[tracing::instrument(skip_all)]
|
|
fn watch_dependencies(
|
|
world: &mut SystemWorld,
|
|
watcher: &mut dyn Watcher,
|
|
mut previous: HashSet<PathBuf>,
|
|
) -> StrResult<()> {
|
|
// Watch new paths that weren't watched yet.
|
|
for path in world.dependencies() {
|
|
let watched = previous.remove(path);
|
|
if path.exists() && !watched {
|
|
tracing::info!("Watching {}", path.display());
|
|
watcher
|
|
.watch(path, RecursiveMode::NonRecursive)
|
|
.map_err(|err| eco_format!("failed to watch {path:?} ({err})"))?;
|
|
}
|
|
}
|
|
|
|
// Unwatch old paths that don't need to be watched anymore.
|
|
for path in previous {
|
|
tracing::info!("Unwatching {}", path.display());
|
|
watcher.unwatch(&path).ok();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Whether a watch event is relevant for compilation.
|
|
fn is_event_relevant(event: ¬ify::Event, output: &Path) -> bool {
|
|
// Never recompile because the output file changed.
|
|
if event
|
|
.paths
|
|
.iter()
|
|
.all(|path| is_same_file(path, output).unwrap_or(false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
match &event.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, command: &CompileCommand) -> io::Result<()> {
|
|
let output = command.output();
|
|
let timestamp = chrono::offset::Local::now().format("%H:%M:%S");
|
|
let color = self.color();
|
|
|
|
let mut w = color_stream();
|
|
if std::io::stderr().is_terminal() {
|
|
// Clear the terminal.
|
|
let esc = 27 as char;
|
|
write!(w, "{esc}c{esc}[1;1H")?;
|
|
}
|
|
|
|
w.set_color(&color)?;
|
|
write!(w, "watching")?;
|
|
w.reset()?;
|
|
writeln!(w, " {}", command.common.input.display())?;
|
|
|
|
w.set_color(&color)?;
|
|
write!(w, "writing to")?;
|
|
w.reset()?;
|
|
writeln!(w, " {}", output.display())?;
|
|
|
|
writeln!(w)?;
|
|
writeln!(w, "[{timestamp}] {}", self.message())?;
|
|
writeln!(w)?;
|
|
|
|
w.flush()
|
|
}
|
|
|
|
fn message(&self) -> String {
|
|
match self {
|
|
Self::Compiling => "compiling ...".into(),
|
|
Self::Success(duration) => format!("compiled successfully in {duration:.2?}"),
|
|
Self::PartialSuccess(duration) => {
|
|
format!("compiled with warnings in {duration:.2?}")
|
|
}
|
|
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,
|
|
}
|
|
}
|
|
}
|