diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index a566c76a63..972d955540 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -56,6 +56,43 @@ pub struct CompilerArgs { pub parser_args: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompilationOutcome { + Clean, + Warnings, +} + +fn has_output(output: &str) -> bool { + helpers::contains_ascii_characters(output) +} + +fn has_config_warnings(build_state: &BuildCommandState) -> bool { + build_state.packages.iter().any(|(_, package)| { + package.is_local_dep + && (!package.config.get_unsupported_fields().is_empty() + || !package.config.get_unknown_fields().is_empty()) + }) +} + +pub fn format_finished_compilation_message( + compilation_kind: Option<&str>, + outcome: CompilationOutcome, + duration: Duration, +) -> String { + let compilation_kind = compilation_kind + .map(|kind| format!("{kind} ")) + .unwrap_or_default(); + let (status, warning_suffix) = match outcome { + CompilationOutcome::Clean => (CHECKMARK, ""), + CompilationOutcome::Warnings => (WARNING, " with warnings"), + }; + + format!( + "{LINE_CLEAR}{status}Finished {compilation_kind}compilation{warning_suffix} in {:.2}s", + duration.as_secs_f64() + ) +} + pub fn get_compiler_args(rescript_file_path: &Path) -> Result { let filename = &helpers::get_abs_path(rescript_file_path); let current_package = helpers::get_abs_path( @@ -246,7 +283,7 @@ pub fn incremental_build( only_incremental: bool, create_sourcedirs: bool, plain_output: bool, -) -> Result<(), IncrementalBuildError> { +) -> Result { let build_folder = build_state.root_folder.to_string_lossy().to_string(); let _lock = get_lock_or_exit(LockKind::Build, &build_folder); @@ -264,7 +301,7 @@ pub fn incremental_build( ProgressStyle::with_template(&format!( "{} {}Parsing... {{spinner}} {{pos}}/{{len}} {{msg}}", format_step(current_step, total_steps), - CODE + PARSE )) .unwrap(), ); @@ -314,13 +351,14 @@ pub fn incremental_build( "{}{} {}Parsed {} source files in {:.2}s", LINE_CLEAR, format_step(current_step, total_steps), - CODE, + PARSE, num_dirty_modules, default_timing.unwrap_or(timing_parse_total).as_secs_f64() ); } } - if helpers::contains_ascii_characters(&parse_warnings) { + let has_parse_warnings = has_output(&parse_warnings); + if has_parse_warnings { eprintln!("{}", &parse_warnings); } @@ -389,13 +427,13 @@ pub fn incremental_build( ); } } - if helpers::contains_ascii_characters(&compile_warnings) { + if has_output(&compile_warnings) { eprintln!("{}", &compile_warnings); } if initial_build { log_config_warnings(build_state); } - if helpers::contains_ascii_characters(&compile_errors) { + if has_output(&compile_errors) { eprintln!("{}", &compile_errors); } @@ -406,6 +444,14 @@ pub fn incremental_build( plain_output, }) } else { + let has_compile_warnings = has_output(&compile_warnings); + let has_config_warning_output = initial_build && has_config_warnings(build_state); + let outcome = if has_parse_warnings || has_compile_warnings || has_config_warning_output { + CompilationOutcome::Warnings + } else { + CompilationOutcome::Clean + }; + if show_progress { if plain_output { println!("Compiled {num_compiled_modules} modules") @@ -421,7 +467,7 @@ pub fn incremental_build( } } - if helpers::contains_ascii_characters(&compile_warnings) { + if has_compile_warnings { eprintln!("{}", &compile_warnings); } if initial_build { @@ -432,7 +478,7 @@ pub fn incremental_build( write_compiler_info(build_state); let _lock = drop_lock(LockKind::Build, &build_folder); - Ok(()) + Ok(outcome) } } @@ -519,14 +565,16 @@ pub fn build( create_sourcedirs, plain_output, ) { - Ok(_) => { + Ok(result) => { if !plain_output && show_progress { let timing_total_elapsed = timing_total.elapsed(); println!( - "\n{}{}Finished Compilation in {:.2}s", - LINE_CLEAR, - SPARKLES, - default_timing.unwrap_or(timing_total_elapsed).as_secs_f64() + "\n{}", + format_finished_compilation_message( + None, + result, + default_timing.unwrap_or(timing_total_elapsed), + ) ); } clean::cleanup_after_build(&build_state); @@ -540,3 +588,31 @@ pub fn build( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn formats_successful_completion_message() { + assert_eq!( + format_finished_compilation_message(None, CompilationOutcome::Clean, Duration::from_millis(1500),), + format!("{LINE_CLEAR}{}Finished compilation in 1.50s", CHECKMARK) + ); + } + + #[test] + fn formats_warning_completion_message() { + assert_eq!( + format_finished_compilation_message( + Some("incremental"), + CompilationOutcome::Warnings, + Duration::from_millis(1500), + ), + format!( + "{LINE_CLEAR}{}Finished incremental compilation with warnings in 1.50s", + WARNING + ) + ); + } +} diff --git a/rewatch/src/cli.rs b/rewatch/src/cli.rs index 07c3cdfb9b..2a7fc40100 100644 --- a/rewatch/src/cli.rs +++ b/rewatch/src/cli.rs @@ -383,6 +383,16 @@ mod tests { } } + #[test] + fn watch_clear_screen_flag_is_parsed() { + let cli = parse(&["rescript", "watch", "--clear-screen"]).expect("expected watch command"); + + match cli.command { + Command::Watch(watch_args) => assert!(watch_args.clear_screen), + other => panic!("expected watch command, got {other:?}"), + } + } + #[test] fn clean_prod_flag_is_parsed() { let cli = parse(&["rescript", "clean", "--prod"]).expect("expected clean command"); @@ -428,6 +438,10 @@ pub struct WatchArgs { #[command(flatten)] pub warn_error: WarnErrorArg, + /// Clear terminal screen before each rebuild in interactive watch mode. + #[arg(long, default_value_t = false)] + pub clear_screen: bool, + /// Skip dev-dependencies and dev sources (type: "dev") #[arg(long, default_value_t = false)] pub prod: bool, @@ -440,6 +454,7 @@ impl From for WatchArgs { filter: build_args.filter, after_build: build_args.after_build, warn_error: build_args.warn_error, + clear_screen: false, prod: build_args.prod, } } diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index 600dd34ab6..46fa469629 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -18,13 +18,13 @@ pub mod deserialize; pub mod emojis { use console::Emoji; - pub static COMMAND: Emoji<'_, '_> = Emoji("๐Ÿƒ ", ""); - pub static SWEEP: Emoji<'_, '_> = Emoji("๐Ÿงน ", ""); - pub static CODE: Emoji<'_, '_> = Emoji("๐Ÿงฑ ", ""); - pub static SWORDS: Emoji<'_, '_> = Emoji("๐Ÿคบ ", ""); - pub static CHECKMARK: Emoji<'_, '_> = Emoji("โœ… ", ""); - pub static CROSS: Emoji<'_, '_> = Emoji("โŒ ", ""); - pub static SPARKLES: Emoji<'_, '_> = Emoji("โœจ ", ""); + pub static COMMAND: Emoji<'_, '_> = Emoji("๐Ÿƒ ", "[run] "); + pub static SWEEP: Emoji<'_, '_> = Emoji("๐Ÿงน ", "[clean] "); + pub static PARSE: Emoji<'_, '_> = Emoji("๐Ÿงฑ ", "[parse] "); + pub static SWORDS: Emoji<'_, '_> = Emoji("๐Ÿคบ ", "[build] "); + pub static CHECKMARK: Emoji<'_, '_> = Emoji("โœ… ", "[ok] "); + pub static WARNING: Emoji<'_, '_> = Emoji("โš ๏ธ ", "[warn] "); + pub static CROSS: Emoji<'_, '_> = Emoji("โŒ ", "[error] "); pub static LINE_CLEAR: &str = "\x1b[2K\r"; } diff --git a/rewatch/src/main.rs b/rewatch/src/main.rs index 8fe9c8d78c..8ab4eaa68c 100644 --- a/rewatch/src/main.rs +++ b/rewatch/src/main.rs @@ -79,6 +79,7 @@ fn main() -> Result<()> { true, // create_sourcedirs is now always enabled plain_output, (*watch_args.warn_error).clone(), + watch_args.clear_screen, watch_args.prod, ) { Err(e) => { diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 2c9ae73ecd..0cfa30a758 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -5,11 +5,11 @@ use crate::cmd; use crate::config; use crate::helpers; use crate::helpers::StrippedVerbatimPath; -use crate::helpers::emojis::*; use crate::lock::LockKind; use crate::queue::FifoQueue; use crate::queue::*; use anyhow::{Context, Result}; +use console::Term; use futures_timer::Delay; use notify::event::ModifyKind; use notify::{Config, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; @@ -178,6 +178,31 @@ fn carry_forward_compile_warnings(previous: &BuildCommandState, next: &mut Build } } +fn should_clear_screen( + clear_screen: bool, + show_progress: bool, + plain_output: bool, + initial_build: bool, +) -> bool { + clear_screen && show_progress && !plain_output && !initial_build +} + +fn clear_terminal_screen() { + let _ = Term::stdout().clear_screen(); +} + +fn print_rebuild_header(compile_type: CompileType) { + match compile_type { + CompileType::Incremental => println!("Change detected. Rebuilding..."), + CompileType::Full => println!("Change detected. Full rebuild..."), + CompileType::None => (), + } +} + +fn print_build_failed_footer() { + println!("\nBuild failed. Watching for changes..."); +} + struct AsyncWatchArgs<'a> { watcher: &'a mut RecommendedWatcher, current_watch_paths: Vec<(PathBuf, RecursiveMode)>, @@ -189,6 +214,7 @@ struct AsyncWatchArgs<'a> { after_build: Option, create_sourcedirs: bool, plain_output: bool, + clear_screen: bool, prod: bool, } @@ -204,6 +230,7 @@ async fn async_watch( after_build, create_sourcedirs, plain_output, + clear_screen, prod, }: AsyncWatchArgs<'_>, ) -> Result<()> { @@ -378,8 +405,13 @@ async fn async_watch( match needs_compile_type { CompileType::Incremental => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + clear_terminal_screen(); + print_rebuild_header(CompileType::Incremental); + } + let timing_total = Instant::now(); - if build::incremental_build( + let result = build::incremental_build( &mut build_state, None, initial_build, @@ -387,32 +419,46 @@ async fn async_watch( !initial_build, create_sourcedirs, plain_output, - ) - .is_ok() - { - if let Some(a) = after_build.clone() { - cmd::run(a) + ); + + match result { + Ok(result) => { + if let Some(a) = after_build.clone() { + cmd::run(a) + } + let timing_total_elapsed = timing_total.elapsed(); + if show_progress { + let compilation_type = if initial_build { "initial" } else { "incremental" }; + if plain_output { + println!("Finished {compilation_type} compilation") + } else { + println!( + "\n{}\n", + build::format_finished_compilation_message( + Some(compilation_type), + result, + timing_total_elapsed, + ) + ); + } + } } - let timing_total_elapsed = timing_total.elapsed(); - if show_progress { - let compilation_type = if initial_build { "initial" } else { "incremental" }; - if plain_output { - println!("Finished {compilation_type} compilation") - } else { - println!( - "\n{}{}Finished {} compilation in {:.2}s\n", - LINE_CLEAR, - SPARKLES, - compilation_type, - timing_total_elapsed.as_secs_f64() - ); + Err(_) => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + print_build_failed_footer(); } } } + needs_compile_type = CompileType::None; initial_build = false; } CompileType::Full => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + clear_terminal_screen(); + print_rebuild_header(CompileType::Full); + } + let timing_total = Instant::now(); let mut next_build_state = build::initialize_build( None, @@ -436,7 +482,7 @@ async fn async_watch( current_watch_paths = compute_watch_paths(&build_state, path); register_watches(watcher, ¤t_watch_paths); - let _ = build::incremental_build( + let result = build::incremental_build( &mut build_state, None, initial_build, @@ -445,25 +491,36 @@ async fn async_watch( create_sourcedirs, plain_output, ); - if let Some(a) = after_build.clone() { - cmd::run(a) - } - - build::write_build_ninja(&build_state); + match result { + Ok(result) => { + if let Some(a) = after_build.clone() { + cmd::run(a) + } - let timing_total_elapsed = timing_total.elapsed(); - if show_progress { - if plain_output { - println!("Finished compilation") - } else { - println!( - "\n{}{}Finished compilation in {:.2}s\n", - LINE_CLEAR, - SPARKLES, - timing_total_elapsed.as_secs_f64() - ); + let timing_total_elapsed = timing_total.elapsed(); + if show_progress { + if plain_output { + println!("Finished compilation") + } else { + println!( + "\n{}\n", + build::format_finished_compilation_message( + None, + result, + timing_total_elapsed, + ) + ); + } + } + } + Err(_) => { + if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) { + print_build_failed_footer(); + } } } + + build::write_build_ninja(&build_state); needs_compile_type = CompileType::None; initial_build = false; } @@ -485,6 +542,7 @@ pub fn start( create_sourcedirs: bool, plain_output: bool, warn_error: Option, + clear_screen: bool, prod: bool, ) -> Result<()> { futures::executor::block_on(async { @@ -524,6 +582,7 @@ pub fn start( after_build, create_sourcedirs, plain_output, + clear_screen, prod, }) .await @@ -655,6 +714,15 @@ mod tests { } } + #[test] + fn clears_screen_only_for_non_initial_interactive_rebuilds() { + assert!(should_clear_screen(true, true, false, false)); + assert!(!should_clear_screen(true, true, false, true)); + assert!(!should_clear_screen(true, true, true, false)); + assert!(!should_clear_screen(true, false, false, false)); + assert!(!should_clear_screen(false, true, false, false)); + } + #[test] fn carries_forward_implementation_warnings_for_matching_module_paths() { let previous = test_build_state(