From d15fb00099b8f879b17a5cef106ac92ff559e34e Mon Sep 17 00:00:00 2001 From: krrish175-byte Date: Thu, 1 Jan 2026 13:29:09 +0530 Subject: [PATCH 1/4] feat: add --watch-clear-screen option to clear terminal on rebuild --- .../main/scala/scala/cli/commands/WatchUtil.scala | 10 ++++++++++ .../scala/scala/cli/commands/compile/Compile.scala | 4 ++++ .../scala/scala/cli/commands/package0/Package.scala | 4 ++++ .../scala/scala/cli/commands/publish/Publish.scala | 12 +++++++++--- .../scala/cli/commands/publish/PublishLocal.scala | 1 + .../main/scala/scala/cli/commands/repl/Repl.scala | 4 ++++ .../src/main/scala/scala/cli/commands/run/Run.scala | 4 ++++ .../cli/commands/shared/SharedWatchOptions.scala | 7 ++++++- .../main/scala/scala/cli/commands/test/Test.scala | 4 ++++ 9 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala b/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala index 4881c18b66..f587a8372a 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala @@ -22,6 +22,16 @@ object WatchUtil { s"$gray$message$reset" } + /** Clears the terminal screen using ANSI escape codes. This prints the escape sequence to clear + * the entire screen and moves the cursor to the top-left corner. + */ + def clearScreen(): Unit = { + // \u001b[2J clears the entire screen + // \u001b[H moves the cursor to the top-left corner (home position) + System.out.print("\u001b[2J\u001b[H") + System.out.flush() + } + def printWatchMessage(): Unit = System.err.println(waitMessage("Watching sources")) diff --git a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala index bb0203273f..7d1a673501 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala @@ -103,6 +103,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if (options.watch.watchMode) { + var isFirstRun = true val watcher = Build.watch( inputs, buildOptions, @@ -115,6 +116,9 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => + if (options.watch.watchClearScreen && !isFirstRun) + WatchUtil.clearScreen() + isFirstRun = false for (builds <- res.orReport(logger)) postBuild(builds, allowExit = false) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index 8adb90cd5c..2a65e07f8b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -89,6 +89,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { val withTestScope = options.shared.scope.test.getOrElse(false) if options.watch.watchMode then { var expectedModifyEpochSecondOpt = Option.empty[Long] + var isFirstRun = true val watcher = Build.watch( inputs, initialBuildOptions, @@ -101,6 +102,9 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => + if (options.watch.watchClearScreen && !isFirstRun) + WatchUtil.clearScreen() + isFirstRun = false res.orReport(logger).map(_.builds).foreach { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index 2799bbb40f..29662bf37d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -255,7 +255,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { publishLocal = false, forceSigningExternally = options.signingCli.forceSigningExternally.getOrElse(false), parallelUpload = options.parallelUpload, - options.watch.watch, + watch = options.watch.watch, + watchClearScreen = options.watch.watchClearScreen, isCi = options.publishParams.isCi, () => configDb, options.mainClass, @@ -279,6 +280,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { forceSigningExternally: Boolean, parallelUpload: Option[Boolean], watch: Boolean, + watchClearScreen: Boolean, isCi: Boolean, configDb: () => ConfigDb, mainClassOptions: MainClassOptions, @@ -288,6 +290,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { val actionableDiagnostics = configDb().get(Keys.actions).getOrElse(None) if watch then { + var isFirstRun = true val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, @@ -299,8 +302,11 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { partial = None, actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() - ) { - _.orReport(logger).foreach { builds => + ) { res => + if (watchClearScreen && !isFirstRun) + WatchUtil.clearScreen() + isFirstRun = false + res.orReport(logger).foreach { builds => maybePublish( builds = builds, workingDir = workingDir, diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala index 8355c2df8c..f50da72aef 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala @@ -81,6 +81,7 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] { forceSigningExternally = options.scalaSigning.forceSigningExternally.getOrElse(false), parallelUpload = Some(true), watch = options.watch.watch, + watchClearScreen = options.watch.watchClearScreen, isCi = options.publishParams.isCi, configDb = () => ConfigDb.empty, // shouldn't be used, no need of repo credentials here mainClassOptions = options.mainClass, diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index de45c9c607..9d1a3554ae 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -208,6 +208,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { } } else if (options.sharedRepl.watch.watchMode) { + var isFirstRun = true val watcher = Build.watch( inputs, initialBuildOptions, @@ -220,6 +221,9 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => + if (options.sharedRepl.watch.watchClearScreen && !isFirstRun) + WatchUtil.clearScreen() + isFirstRun = false for (builds <- res.orReport(logger)) postBuild(builds, allowExit = false) { successfulBuilds => diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index b74604aaf4..ffb9f67453 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -252,6 +252,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { */ val mainThreadOpt = AtomicReference(Option.empty[Thread]) + var isFirstRun = true val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, @@ -266,6 +267,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { if processOpt.get().exists(_._1.isAlive()) then WatchUtil.printWatchWhileRunningMessage() else WatchUtil.printWatchMessage() ) { res => + if (options.sharedRun.watch.watchClearScreen && !isFirstRun) + WatchUtil.clearScreen() + isFirstRun = false for ((process, onExitProcess) <- processOpt.get()) { onExitProcess.cancel(true) ProcUtil.interruptProcess(process, logger) diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala index df4e28ec7e..84901aad51 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala @@ -18,7 +18,12 @@ final case class SharedWatchOptions( @Tag(tags.should) @Tag(tags.inShortHelp) @Name("revolver") - restart: Boolean = false + restart: Boolean = false, + @Group(HelpGroup.Watch.toString) + @HelpMessage("Clear the screen each time the watch mode detects changes and re-compiles or re-runs") + @Tag(tags.implementation) + @Name("watch-cls") + watchClearScreen: Boolean = false ) { // format: on lazy val watchMode: Boolean = watch || restart diff --git a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala index c1d528a530..1898fbc8d3 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala @@ -146,6 +146,7 @@ object Test extends ScalaCommand[TestOptions] { } if (options.watch.watchMode) { + var isFirstRun = true val watcher = Build.watch( inputs, initialBuildOptions, @@ -158,6 +159,9 @@ object Test extends ScalaCommand[TestOptions] { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => + if (options.watch.watchClearScreen && !isFirstRun) + WatchUtil.clearScreen() + isFirstRun = false for (builds <- res.orReport(logger)) maybeTest(builds, allowExit = false) } From 0e3a1b2c19868316316e47cc0d8906e4c77b4581 Mon Sep 17 00:00:00 2001 From: krrish175-byte Date: Fri, 2 Jan 2026 21:02:50 +0530 Subject: [PATCH 2/4] Address review: use AtomicBoolean, add watchCls alias, add integration tests --- .../scala/scala/cli/commands/WatchUtil.scala | 3 -- .../scala/cli/commands/compile/Compile.scala | 6 +-- .../scala/cli/commands/package0/Package.scala | 6 +-- .../scala/cli/commands/publish/Publish.scala | 6 +-- .../scala/scala/cli/commands/repl/Repl.scala | 6 +-- .../scala/scala/cli/commands/run/Run.scala | 7 ++- .../commands/shared/SharedWatchOptions.scala | 5 +- .../scala/scala/cli/commands/test/Test.scala | 6 +-- .../integration/CompileTestDefinitions.scala | 37 +++++++++++++ .../integration/PackageTestDefinitions.scala | 35 +++++++++++++ .../PublishLocalTestDefinitions.scala | 52 +++++++++++++++++++ .../cli/integration/ReplTestDefinitions.scala | 34 ++++++++++++ .../RunWithWatchTestDefinitions.scala | 38 ++++++++++++++ .../cli/integration/TestTestDefinitions.scala | 47 +++++++++++++++++ website/docs/reference/cli-options.md | 6 +++ .../reference/scala-command/cli-options.md | 8 +++ .../scala-command/runner-specification.md | 30 +++++++++++ 17 files changed, 308 insertions(+), 24 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala b/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala index f587a8372a..69e5bb4677 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/WatchUtil.scala @@ -22,9 +22,6 @@ object WatchUtil { s"$gray$message$reset" } - /** Clears the terminal screen using ANSI escape codes. This prints the escape sequence to clear - * the entire screen and moves the cursor to the top-left corner. - */ def clearScreen(): Unit = { // \u001b[2J clears the entire screen // \u001b[H moves the cursor to the top-left corner (home position) diff --git a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala index 7d1a673501..7fc331f8d4 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala @@ -4,6 +4,7 @@ import caseapp.* import caseapp.core.help.HelpFormat import java.io.File +import java.util.concurrent.atomic.AtomicBoolean import scala.build.options.Scope import scala.build.{Build, BuildThreads, Builds, Logger} @@ -103,7 +104,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if (options.watch.watchMode) { - var isFirstRun = true + val isFirstRun = new AtomicBoolean(true) val watcher = Build.watch( inputs, buildOptions, @@ -116,9 +117,8 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => - if (options.watch.watchClearScreen && !isFirstRun) + if (options.watch.watchClearScreen && !isFirstRun.getAndSet(false)) WatchUtil.clearScreen() - isFirstRun = false for (builds <- res.orReport(logger)) postBuild(builds, allowExit = false) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index 2a65e07f8b..bb4fb9f845 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -16,6 +16,7 @@ import packager.windows.WindowsPackage import java.io.{ByteArrayOutputStream, OutputStream} import java.nio.file.attribute.FileTime +import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.{ZipEntry, ZipOutputStream} import scala.build.* @@ -89,7 +90,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { val withTestScope = options.shared.scope.test.getOrElse(false) if options.watch.watchMode then { var expectedModifyEpochSecondOpt = Option.empty[Long] - var isFirstRun = true + val isFirstRun = new AtomicBoolean(true) val watcher = Build.watch( inputs, initialBuildOptions, @@ -102,9 +103,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => - if (options.watch.watchClearScreen && !isFirstRun) + if (options.watch.watchClearScreen && !isFirstRun.getAndSet(false)) WatchUtil.clearScreen() - isFirstRun = false res.orReport(logger).map(_.builds).foreach { case b if b.forall(_.success) => val successfulBuilds = b.collect { case s: Build.Successful => s } diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index 29662bf37d..bbc9a8f7fc 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Paths import java.time.{Instant, LocalDateTime, ZoneOffset} import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean import scala.build.* import scala.build.EitherCps.{either, value} @@ -290,7 +291,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { val actionableDiagnostics = configDb().get(Keys.actions).getOrElse(None) if watch then { - var isFirstRun = true + val isFirstRun = new AtomicBoolean(true) val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, @@ -303,9 +304,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => - if (watchClearScreen && !isFirstRun) + if (watchClearScreen && !isFirstRun.getAndSet(false)) WatchUtil.clearScreen() - isFirstRun = false res.orReport(logger).foreach { builds => maybePublish( builds = builds, diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 9d1a3554ae..1c2c50a57c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -6,6 +6,7 @@ import coursier.error.ResolutionError import dependency.* import java.io.File +import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.ZipFile import scala.build.* @@ -208,7 +209,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { } } else if (options.sharedRepl.watch.watchMode) { - var isFirstRun = true + val isFirstRun = new AtomicBoolean(true) val watcher = Build.watch( inputs, initialBuildOptions, @@ -221,9 +222,8 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => - if (options.sharedRepl.watch.watchClearScreen && !isFirstRun) + if (options.sharedRepl.watch.watchClearScreen && !isFirstRun.getAndSet(false)) WatchUtil.clearScreen() - isFirstRun = false for (builds <- res.orReport(logger)) postBuild(builds, allowExit = false) { successfulBuilds => diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index ffb9f67453..bcfb4305bf 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -7,7 +7,7 @@ import caseapp.core.help.HelpFormat import java.io.File import java.util.Locale import java.util.concurrent.CompletableFuture -import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import scala.build.* import scala.build.EitherCps.{either, value} @@ -252,7 +252,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { */ val mainThreadOpt = AtomicReference(Option.empty[Thread]) - var isFirstRun = true + val isFirstRun = new AtomicBoolean(true) val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, @@ -267,9 +267,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { if processOpt.get().exists(_._1.isAlive()) then WatchUtil.printWatchWhileRunningMessage() else WatchUtil.printWatchMessage() ) { res => - if (options.sharedRun.watch.watchClearScreen && !isFirstRun) + if (options.sharedRun.watch.watchClearScreen && !isFirstRun.getAndSet(false)) WatchUtil.clearScreen() - isFirstRun = false for ((process, onExitProcess) <- processOpt.get()) { onExitProcess.cancel(true) ProcUtil.interruptProcess(process, logger) diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala index 84901aad51..11f43c9fed 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedWatchOptions.scala @@ -20,9 +20,10 @@ final case class SharedWatchOptions( @Name("revolver") restart: Boolean = false, @Group(HelpGroup.Watch.toString) - @HelpMessage("Clear the screen each time the watch mode detects changes and re-compiles or re-runs") + @HelpMessage("Clear the screen each time watch mode detects changes and re-compiles or re-runs") @Tag(tags.implementation) - @Name("watch-cls") + @Name("watchCls") + @Name("watchClear") watchClearScreen: Boolean = false ) { // format: on diff --git a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala index 1898fbc8d3..72bbcc0c55 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala @@ -4,6 +4,7 @@ import caseapp.* import caseapp.core.help.HelpFormat import java.nio.file.Path +import java.util.concurrent.atomic.AtomicBoolean import scala.build.* import scala.build.EitherCps.{either, value} @@ -146,7 +147,7 @@ object Test extends ScalaCommand[TestOptions] { } if (options.watch.watchMode) { - var isFirstRun = true + val isFirstRun = new AtomicBoolean(true) val watcher = Build.watch( inputs, initialBuildOptions, @@ -159,9 +160,8 @@ object Test extends ScalaCommand[TestOptions] { actionableDiagnostics = actionableDiagnostics, postAction = () => WatchUtil.printWatchMessage() ) { res => - if (options.watch.watchClearScreen && !isFirstRun) + if (options.watch.watchClearScreen && !isFirstRun.getAndSet(false)) WatchUtil.clearScreen() - isFirstRun = false for (builds <- res.orReport(logger)) maybeTest(builds, allowExit = false) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala index 311eef2601..ce111ab4ab 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala @@ -5,6 +5,7 @@ import com.eed3si9n.expecty.Expecty.expect import java.io.File import scala.cli.integration.util.BloopUtil +import scala.concurrent.duration.DurationInt import scala.util.Properties abstract class CompileTestDefinitions @@ -903,4 +904,40 @@ abstract class CompileTestDefinitions ) } } + + // TODO make this pass reliably on Mac CI + if (!Properties.isMac || !TestUtil.isCI) + test("compile --watch with --watch-clear-screen clears screen on recompile") { + val inputPath = os.rel / "example.scala" + + def code(hasError: Boolean) = + if (hasError) """object Example { val x: String = 1 }""" // compile error + else """object Example { val x: Int = 1 }""" // compiles fine + + TestInputs(inputPath -> code(hasError = false)).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "compile", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + var line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + os.write.over(root / inputPath, code(hasError = true)) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("error") && !line.contains("\u001b[2J")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.toLowerCase.contains("error")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + expect(line.toLowerCase.contains("error")) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index 4d5346af2a..bc4fcd676e 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -9,6 +9,7 @@ import java.util import java.util.zip.ZipFile import scala.cli.integration.TestUtil.removeAnsiColors +import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.* import scala.util.{Properties, Using} @@ -1507,4 +1508,38 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio expect(res.out.trim().contains(s"$moduleName.js")) } } + + // TODO make this pass reliably on Mac CI + if (!Properties.isMac || !TestUtil.isCI) + test("package --watch with --watch-clear-screen clears screen on repackage") { + val inputPath = os.rel / "example.scala" + + def code(message: String) = + s"""object Example extends App { println("$message") }""" + + TestInputs(inputPath -> code("Hello1")).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "--power", + "package", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + var line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + os.write.over(root / inputPath, code("Hello2")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources") && !line.contains("\u001b[2J")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + expect(line.contains("Watching sources") || line.contains("\u001b[2J")) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala index d26469eeb2..a573401d24 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala @@ -2,6 +2,9 @@ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect +import scala.concurrent.duration.DurationInt +import scala.util.Properties + abstract class PublishLocalTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { this: TestScalaVersion => protected def extraOptions: Seq[String] = @@ -384,4 +387,53 @@ abstract class PublishLocalTestDefinitions extends ScalaCliSuite with TestScalaV .call(cwd = root) } } + + // TODO make this pass reliably on Mac CI + if (!Properties.isMac || !TestUtil.isCI) + test("publish local --watch with --watch-clear-screen clears screen on republish") { + TestInputs( + os.rel / "project.scala" -> + s"""//> using publish.organization test.org + |//> using publish.name test-proj + |//> using publish.version 1.0.0 + | + |object Project { def value = 1 } + |""".stripMargin + ).fromRoot { root => + val ivy2Local = root / "ivy2-local" + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "--power", + "publish", + "local", + ".", + "--watch", + "--watch-clear-screen", + "--ivy2-home", + ivy2Local.toString, + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 180.seconds + ) { (proc, timeout, ec) => + var line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + os.write.over( + root / "project.scala", + s"""//> using publish.organization test.org + |//> using publish.name test-proj + |//> using publish.version 1.0.0 + | + |object Project { def value = 2 } + |""".stripMargin + ) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources") && !line.contains("\u001b[2J")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + expect(line.contains("Watching sources") || line.contains("\u001b[2J")) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala index 7460946d60..f61d87503b 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala @@ -3,6 +3,7 @@ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.TestUtil.removeAnsiColors +import scala.concurrent.duration.DurationInt import scala.util.Properties abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { @@ -287,4 +288,37 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr } } } + + // TODO make this pass reliably on Mac CI + if (!Properties.isMac || !TestUtil.isCI) + test("repl --watch with --watch-clear-screen clears screen on rerun") { + val inputPath = os.rel / "deps.scala" + + def code(value: Int) = s"""object Deps { val x = $value }""" + + TestInputs(inputPath -> code(1)).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "repl", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + "--repl-dry-run", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + var line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + os.write.over(root / inputPath, code(2)) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources") && !line.contains("\u001b[2J")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + expect(line.contains("Watching sources") || line.contains("\u001b[2J")) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala index 9975cf68bb..1672d713dd 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala @@ -428,4 +428,42 @@ trait RunWithWatchTestDefinitions { this: RunTestDefinitions => test("watch mode doesnt hang on Bloop when rebuilding repeatedly") { testRepeatedRerunsWithWatch() } + + // TODO make this pass reliably on Mac CI + if (!Properties.isMac || !TestUtil.isCI) + test("--watch with --watch-clear-screen clears screen on rerun") { + val expectedMessage1 = "Hello1" + val expectedMessage2 = "Hello2" + val inputPath = os.rel / "example.scala" + + def code(message: String) = s"""object Example extends App { println("$message") }""" + + TestInputs(inputPath -> code(expectedMessage1)).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "run", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + val output1 = TestUtil.readLine(proc.stdout, ec, timeout) + expect(output1 == expectedMessage1) + var line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + os.write.over(root / inputPath, code(expectedMessage2)) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains(expectedMessage2) && !line.contains("\u001b[2J")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains(expectedMessage2)) + line = TestUtil.readLine(proc.stdout, ec, timeout) + expect(line.contains(expectedMessage2)) + } + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala index 2e3bdd6f55..a13f301f28 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala @@ -5,6 +5,8 @@ import com.eed3si9n.expecty.Expecty.expect import scala.annotation.tailrec import scala.cli.integration.Constants.munitVersion import scala.cli.integration.TestUtil.StringOps +import scala.concurrent.duration.DurationInt +import scala.util.Properties abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { this: TestScalaVersion => @@ -1038,4 +1040,49 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr expect(err.countOccurrences(expectedWarning) == 1) } } + + // TODO make this pass reliably on Mac CI + if (!Properties.isMac || !TestUtil.isCI) + test("test --watch with --watch-clear-screen clears screen on rerun") { + val inputPath = os.rel / "MyTests.test.scala" + val expectedMessage1 = "test1" + val expectedMessage2 = "test2" + + def code(message: String) = + s"""//> using dep org.scalameta::munit::$munitVersion + | + |class MyTests extends munit.FunSuite { + | test("foo") { + | assert(2 + 2 == 4) + | println("$message") + | } + |} + |""".stripMargin + + TestInputs(inputPath -> code(expectedMessage1)).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "test", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 180.seconds + ) { (proc, timeout, ec) => + var line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains("Watching sources")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + os.write.over(root / inputPath, code(expectedMessage2)) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains(expectedMessage2) && !line.contains("\u001b[2J")) + line = TestUtil.readLine(proc.stdout, ec, timeout) + while (!line.contains(expectedMessage2)) + line = TestUtil.readLine(proc.stdout, ec, timeout) + expect(line.contains(expectedMessage2)) + } + } + } } diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index 0e51ecde60..5918e83a0e 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1953,6 +1953,12 @@ Aliases: `--revolver` Run the application in the background, automatically kill the process and restart if sources have been changed +### `--watch-clear-screen` + +Aliases: `--watch-clear`, `--watch-cls` + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + ## Internal options ### Add path options diff --git a/website/docs/reference/scala-command/cli-options.md b/website/docs/reference/scala-command/cli-options.md index fe34ea5834..4299c80742 100644 --- a/website/docs/reference/scala-command/cli-options.md +++ b/website/docs/reference/scala-command/cli-options.md @@ -1459,6 +1459,14 @@ Aliases: `--revolver` Run the application in the background, automatically kill the process and restart if sources have been changed +### `--watch-clear-screen` + +Aliases: `--watch-clear`, `--watch-cls` + +`IMPLEMENTATION specific` per Scala Runner specification + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + ## Internal options ### Bsp options diff --git a/website/docs/reference/scala-command/runner-specification.md b/website/docs/reference/scala-command/runner-specification.md index 32d84cd048..90a8523215 100644 --- a/website/docs/reference/scala-command/runner-specification.md +++ b/website/docs/reference/scala-command/runner-specification.md @@ -650,6 +650,12 @@ Aliases: `--toolkit` Exclude sources +**--watch-clear-screen** + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + +Aliases: `--watch-cls` ,`--watch-clear` + --- @@ -2055,6 +2061,12 @@ Add java properties. Note that options equal `-Dproperty=value` are assumed to b Aliases: `--java-prop` +**--watch-clear-screen** + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + +Aliases: `--watch-cls` ,`--watch-clear` + **--repl-dry-run** Don't actually run the REPL, just fetch it @@ -2694,6 +2706,12 @@ Add java properties. Note that options equal `-Dproperty=value` are assumed to b Aliases: `--java-prop` +**--watch-clear-screen** + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + +Aliases: `--watch-cls` ,`--watch-clear` + **--scratch-dir** Temporary / working directory where to write generated launchers @@ -3342,6 +3360,12 @@ Add java properties. Note that options equal `-Dproperty=value` are assumed to b Aliases: `--java-prop` +**--watch-clear-screen** + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + +Aliases: `--watch-cls` ,`--watch-clear` + **--scratch-dir** Temporary / working directory where to write generated launchers @@ -4632,6 +4656,12 @@ Add java properties. Note that options equal `-Dproperty=value` are assumed to b Aliases: `--java-prop` +**--watch-clear-screen** + +Clear the screen each time watch mode detects changes and re-compiles or re-runs + +Aliases: `--watch-cls` ,`--watch-clear` + --- From 7f689d00061565b9a535a6afba5ac9f222173337 Mon Sep 17 00:00:00 2001 From: krrish175-byte Date: Fri, 2 Jan 2026 21:37:01 +0530 Subject: [PATCH 3/4] Resolve merge conflict in PackageTestDefinitions imports --- .../scala/scala/cli/integration/PackageTestDefinitions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index bc4fcd676e..105befcf99 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -8,7 +8,7 @@ import java.nio.file.Files import java.util import java.util.zip.ZipFile -import scala.cli.integration.TestUtil.removeAnsiColors +import scala.cli.integration.TestUtil.* import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.* import scala.util.{Properties, Using} From 3ae42f431d3d63075014b2f5063a705fd8c4b630 Mon Sep 17 00:00:00 2001 From: krrish175-byte Date: Tue, 6 Jan 2026 15:42:21 +0530 Subject: [PATCH 4/4] Fix watch-clear-screen integration test and add unit/integration tests --- .../scala/cli/commands/compile/Compile.scala | 2 +- .../scala/cli/commands/publish/Publish.scala | 2 +- .../scala/scala/cli/commands/repl/Repl.scala | 2 +- .../scala/scala/cli/commands/run/Run.scala | 2 +- .../scala/scala/cli/commands/test/Test.scala | 2 +- .../test/scala/cli/tests/WatchUtilTests.scala | 55 +++++++++ .../RunWithWatchTestDefinitions.scala | 116 +++++++++++++++++- 7 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 modules/cli/src/test/scala/cli/tests/WatchUtilTests.scala diff --git a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala index 7fc331f8d4..0d4b89d3c3 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/compile/Compile.scala @@ -105,7 +105,7 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers { val shouldBuildTestScope = options.shared.scope.test.getOrElse(false) if (options.watch.watchMode) { val isFirstRun = new AtomicBoolean(true) - val watcher = Build.watch( + val watcher = Build.watch( inputs, buildOptions, compilerMaker, diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index bbc9a8f7fc..ef2f5fd5c2 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -292,7 +292,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { if watch then { val isFirstRun = new AtomicBoolean(true) - val watcher = Build.watch( + val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, compilerMaker = compilerMaker, diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 1c2c50a57c..ee5e1bdafa 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -210,7 +210,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { } else if (options.sharedRepl.watch.watchMode) { val isFirstRun = new AtomicBoolean(true) - val watcher = Build.watch( + val watcher = Build.watch( inputs, initialBuildOptions, compilerMaker, diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index bcfb4305bf..c74b46ae4b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -253,7 +253,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val mainThreadOpt = AtomicReference(Option.empty[Thread]) val isFirstRun = new AtomicBoolean(true) - val watcher = Build.watch( + val watcher = Build.watch( inputs = inputs, options = initialBuildOptions, compilerMaker = compilerMaker, diff --git a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala index 72bbcc0c55..1c201ec0b1 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/test/Test.scala @@ -148,7 +148,7 @@ object Test extends ScalaCommand[TestOptions] { if (options.watch.watchMode) { val isFirstRun = new AtomicBoolean(true) - val watcher = Build.watch( + val watcher = Build.watch( inputs, initialBuildOptions, compilerMaker, diff --git a/modules/cli/src/test/scala/cli/tests/WatchUtilTests.scala b/modules/cli/src/test/scala/cli/tests/WatchUtilTests.scala new file mode 100644 index 0000000000..9eb4783d40 --- /dev/null +++ b/modules/cli/src/test/scala/cli/tests/WatchUtilTests.scala @@ -0,0 +1,55 @@ +package cli.tests + +import com.eed3si9n.expecty.Expecty.expect +import munit.FunSuite +import scala.cli.commands.WatchUtil +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class WatchUtilTests extends FunSuite { + + test("clearScreen prints correct ANSI escape codes") { + val out = new ByteArrayOutputStream() + val ps = new PrintStream(out) + val oldOut = System.out + try { + System.setOut(ps) + WatchUtil.clearScreen() + ps.flush() + val output = out.toString() + expect(output == "\u001b[2J\u001b[H") + } + finally + System.setOut(oldOut) + } + + test("printWatchMessage prints to stderr") { + val err = new ByteArrayOutputStream() + val ps = new PrintStream(err) + val oldErr = System.err + try { + System.setErr(ps) + WatchUtil.printWatchMessage() + ps.flush() + val output = err.toString() + expect(output.contains("Watching sources")) + } + finally + System.setErr(oldErr) + } + + test("printWatchWhileRunningMessage prints to stderr") { + val err = new ByteArrayOutputStream() + val ps = new PrintStream(err) + val oldErr = System.err + try { + System.setErr(ps) + WatchUtil.printWatchWhileRunningMessage() + ps.flush() + val output = err.toString() + expect(output.contains("Watching sources")) + } + finally + System.setErr(oldErr) + } +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala index 1672d713dd..37f6e47cc1 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunWithWatchTestDefinitions.scala @@ -2,6 +2,7 @@ package scala.cli.integration import com.eed3si9n.expecty.Expecty.expect +import scala.annotation.tailrec import scala.cli.integration.TestUtil.ProcOps import scala.concurrent.duration.DurationInt import scala.util.{Properties, Try} @@ -451,19 +452,122 @@ trait RunWithWatchTestDefinitions { this: RunTestDefinitions => .spawn(cwd = root, mergeErrIntoOut = true), timeout = 120.seconds ) { (proc, timeout, ec) => - val output1 = TestUtil.readLine(proc.stdout, ec, timeout) + def readLine(): String = TestUtil.readLine(proc.stdout, ec, timeout) + @tailrec + def readNextStableLine(): String = { + val line = readLine() + if (line.contains("Compiling project") || line.contains("Compiled project")) + readNextStableLine() + else line + } + val output1 = readNextStableLine() expect(output1 == expectedMessage1) - var line = TestUtil.readLine(proc.stdout, ec, timeout) + var line = readLine() while (!line.contains("Watching sources")) - line = TestUtil.readLine(proc.stdout, ec, timeout) + line = readLine() + Thread.sleep(1000) os.write.over(root / inputPath, code(expectedMessage2)) - line = TestUtil.readLine(proc.stdout, ec, timeout) + line = readLine() while (!line.contains(expectedMessage2) && !line.contains("\u001b[2J")) - line = TestUtil.readLine(proc.stdout, ec, timeout) + line = readLine() while (!line.contains(expectedMessage2)) - line = TestUtil.readLine(proc.stdout, ec, timeout) + line = readLine() expect(line.contains(expectedMessage2)) } } } + + if (!Properties.isMac || !TestUtil.isCI) + test("compile --watch with --watch-clear-screen clears screen on rerun") { + val inputPath = os.rel / "example.scala" + def code = s"""object Example extends App { println("Hello") }""" + + TestInputs(inputPath -> code).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "compile", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + def readLine(): String = TestUtil.readLine(proc.stdout, ec, timeout) + @tailrec + def readNextStableLine(): String = { + val line = readLine() + if (line.contains("Compiling project") || line.contains("Compiled project")) + readNextStableLine() + else line + } + // First run + var line = readLine() + while (!line.contains("Watching sources")) + line = readLine() + + Thread.sleep(1000) + os.write.append(root / inputPath, "\n// comment") + + line = readLine() + // We expect the clear screen escape code to be present before the next "Compiling project" or "Watching sources" + while (!line.contains("\u001b[2J") && !line.contains("Watching sources")) + line = readLine() + + expect(line.contains("\u001b[2J")) + } + } + } + + if (!Properties.isMac || !TestUtil.isCI) + test("test --watch with --watch-clear-screen clears screen on rerun") { + val inputPath = os.rel / "example.test.scala" + def code(message: String) = + s"""//> using dep org.scalameta::munit::0.7.29 + |class MyTests extends munit.FunSuite { + | test("test") { println("$message"); assert(true) } + |} + |""".stripMargin + + TestInputs(inputPath -> code("Hello1")).fromRoot { root => + TestUtil.withProcessWatching( + proc = os.proc( + TestUtil.cli, + "test", + inputPath.toString(), + "--watch", + "--watch-clear-screen", + extraOptions + ) + .spawn(cwd = root, mergeErrIntoOut = true), + timeout = 120.seconds + ) { (proc, timeout, ec) => + def readLine(): String = TestUtil.readLine(proc.stdout, ec, timeout) + @tailrec + def readNextStableLine(): String = { + val line = readLine() + if (line.contains("Compiling project") || line.contains("Compiled project")) + readNextStableLine() + else line + } + + // Wait for first run to finish + var line = readLine() + while (!line.contains("Watching sources")) + line = readLine() + + Thread.sleep(1000) + os.write.over(root / inputPath, code("Hello2")) + + line = readLine() + // We expect the clear screen escape code to be present + while (!line.contains("\u001b[2J") && !line.contains("Hello2")) + line = readLine() + + expect(line.contains("\u001b[2J")) + } + } + } }