diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 670030768..fe88286e0 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -18,7 +18,8 @@ jobs: - name: Install TypeScript run: npm ci - name: Run test - run: sbt -J-Xmx4096M -J-Xss8M test + # Not running all tests because those outside of hkmc2 are obsolete (will be removed) + run: sbt -J-Xmx4096M -J-Xss8M hkmc2AllTests/test - name: Check no changes run: | git update-index -q --refresh diff --git a/README.md b/README.md index d236fe444..09271fba9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ # MLscript -What would TypeScript look like if it had been designed with type inference and soundness in mind? +This is the second iteration of the MLscript compiler, +nicknamed _hkmc2_ (Hong Kong MLscript Compiler v2). -We provide one possible answer in MLscript, an object-oriented and functional programming language with records, generic classes, mix-in traits, first-class unions and intersections, instance matching, and ML-style principal type inference. -These features can be used to implement expressive class hierarchies as well as extensible sums and products. - -MLscript supports union, intersection, and complement (or negation) connectives, making sure they form a Boolean algebra, and add enough structure to derive a sound and complete type inference algorithm. ## Getting Started @@ -13,25 +10,9 @@ MLscript supports union, intersection, and complement (or negation) connectives, #### Sub-Projects -- The ts2mls sub-project allows you to use TypeScript libraries in MLscript. It can generate libraries' declaration information in MLscript by parsing TypeScript AST, which can be used in MLscript type checking. - -#### Directories - -- The `shared/src/main/scala/mlscript` directory contains the sources of the MLscript compiler. - -- The `shared/src/test/scala/mlscript` directory contains the sources of the testing infrastructure. - -- The `shared/src/test/diff` directory contains the actual tests. - -- The `ts2mls/js/src/main/scala/ts2mls` directory contains the sources of the ts2mls module. - -- The `ts2mls/js/src/test/scala/ts2mls` directory contains the sources of the ts2mls declaration generation test code. +Most SBT subprojects are obsolete and will be removed in the future. -- The `ts2mls/jvm/src/test/scala/ts2mls` directory contains the sources of the ts2mls diff test code. - -- The `ts2mls/js/src/test/typescript` directory contains the TypeScript test code. - -- The `ts2mls/js/src/test/diff` directory contains the declarations generated by ts2mls. +Most of the important code of the new compiler is in the `hkmc2` folder. ### Prerequisites @@ -54,14 +35,30 @@ brew install mimalloc boost gmp ### Running the tests -Running the main MLscript tests only requires the Scala Build Tool installed. -In the terminal, run `sbt mlscriptJVM/test`. +Running the tests requires the Scala Build Tool (SBT) installed. + +We recommend running all tests in the SBT shell, +i.e., do not restart SBT every time, +but launch it in shell mode (with command `sbt`) +and then use one of the following commands. + +- `hkmc2AllTests/test` for running all hkmc2 tests. +- `hkmc2JVM/test` for running only the compilation tests, in `hkmc2/shared/src/test/mlscript-compile`. +- `hkmc2DiffTests/test` for running only the diff-tests, in `hkmc2/shared/src/test/mlscript`. +- `~hkmc2DiffTests/Test/run` for running the test watcher, + which updates test files as you save them and recompiles the Scala sources automatically on change. +- `test` for compiling all JVM and JS subprojects + and running every single test in the repository, + including obsolete ones. -Running the ts2mls MLscript tests requires NodeJS, and TypeScript in addition. -In the terminal, run `sbt ts2mlsTest/test`. +Another useful SBT incantation is `; hkmc2AllTests/test; ~hkmc2DiffTests/Test/run`. +This command runs all hkmc2 tests once and then starts the test watcher. +This is a useful command to use periodically while making changes to the compiler, +to check that you haven't broken anything. -You can also run all tests simultaneously. -In the terminal, run `sbt test`. +Note that when saved, the special file `ChangedTests.cmd` will trigger the test watcher to run +all tests that currently have unstaged changes in git. +This is useful when you have a working subset of tests that you want to run periodically. ### Running tests individually @@ -89,6 +86,8 @@ private val testsData = List( ) ``` + + + + diff --git a/build.sbt b/build.sbt index ee7ebe067..7434fb06f 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,7 @@ ThisBuild / scalacOptions ++= Seq( ) lazy val root = project.in(file(".")) - .aggregate(mlscriptJS, mlscriptJVM, ts2mlsTest, compilerJVM, hkmc2JS, hkmc2JVM, coreJS, coreJVM) + .aggregate(mlscriptJS, mlscriptJVM, ts2mlsTest, compilerJVM, hkmc2AllTests, coreJS, coreJVM) .settings( publish := {}, publishLocal := {}, @@ -49,8 +49,6 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript", "*.mls", NothingFilter), watchSources += WatchSource( baseDirectory.value.getParentFile()/"shared"/"src"/"test"/"mlscript", "*.cmd", NothingFilter), - - Test/run/fork := true, // so that CTRL+C actually terminates the watcher ) .jvmSettings( ) @@ -59,6 +57,22 @@ lazy val hkmc2 = crossProject(JSPlatform, JVMPlatform).in(file("hkmc2")) lazy val hkmc2JVM = hkmc2.jvm lazy val hkmc2JS = hkmc2.js +lazy val hkmc2DiffTests = project.in(file("hkmc2DiffTests")) + .dependsOn(hkmc2JVM) + .settings( + scalaVersion := scala3Version, + + libraryDependencies += "org.scalactic" %%% "scalactic" % "3.2.18", + libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.18" % "test", + + Test/run/fork := true, // so that CTRL+C actually terminates the watcher + ) + +lazy val hkmc2AllTests = project.in(file("hkmc2AllTests")) + .settings( + Test / test := ((hkmc2DiffTests / Test / test) dependsOn (hkmc2JVM / Test / test)).value + ) + lazy val core = crossProject(JSPlatform, JVMPlatform).in(file("core")) .settings( sourceDirectory := baseDirectory.value.getParentFile()/"shared", diff --git a/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala new file mode 100644 index 000000000..6404f4e72 --- /dev/null +++ b/hkmc2/jvm/src/test/scala/hkmc2/CompileTestRunner.scala @@ -0,0 +1,51 @@ +package hkmc2 + +import org.scalatest.{funsuite, ParallelTestExecution} +import org.scalatest.time._ +import org.scalatest.concurrent.{TimeLimitedTests, Signaler} +import os.up + +import mlscript.utils._, shorthands._ + + +class CompileTestRunner + extends funsuite.AnyFunSuite + with ParallelTestExecution + // with TimeLimitedTests // TODO +: + + private val inParallel = isInstanceOf[ParallelTestExecution] + + // val timeLimit = TimeLimit + + val pwd = os.pwd + val workingDir = pwd + + val dir = workingDir/"hkmc2"/"shared"/"src"/"test" + + val validExt = Set("mls") + + val allFiles = os.walk(dir) + .filter(_.toIO.isFile) + .filter(_.ext in validExt) + + protected lazy val compileTestFiles = allFiles.filter: file => + file.segments.contains("mlscript-compile") + + // TODO dedup path stuff with DiffTestRunner? + compileTestFiles.foreach: file => + + val basePath = file.segments.drop(dir.segmentCount).toList.init + val relativeName = basePath.map(_ + "/").mkString + file.baseName + + test(relativeName): + + println(s"Compiling: $relativeName") + + val preludePath = dir/"mlscript"/"decls"/"Prelude.mls" + + MLsCompiler(preludePath).compileModule(file) + +end CompileTestRunner + + diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala index 2015624b4..0189ea8bb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala @@ -78,7 +78,7 @@ object Printer: val docBody = if publicFields.isEmpty && privateFields.isEmpty then doc"" else doc" { #{ ${docPrivFlds}${docPubFlds} #} # }" val docCtorParams = if clsParams.isEmpty then doc"" else doc"(${ctorParams.mkString(", ")})" doc"class ${sym.nme}${docCtorParams}${docBody}" - + def mkDocument(arg: Arg)(using Raise, Scope): Document = val doc = mkDocument(arg.value) if arg.spread diff --git a/hkmc2/jvm/src/test/scala/hkmc2/ReplHost.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/ReplHost.scala similarity index 100% rename from hkmc2/jvm/src/test/scala/hkmc2/ReplHost.scala rename to hkmc2/shared/src/main/scala/hkmc2/utils/ReplHost.scala diff --git a/hkmc2/shared/src/test/scala/hkmc2/ReportFormatter.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala similarity index 100% rename from hkmc2/shared/src/test/scala/hkmc2/ReportFormatter.scala rename to hkmc2/shared/src/main/scala/hkmc2/utils/ReportFormatter.scala diff --git a/hkmc2/shared/src/test/mlscript/AllTests.cmd b/hkmc2/shared/src/test/mlscript/ChangedTests.cmd similarity index 100% rename from hkmc2/shared/src/test/mlscript/AllTests.cmd rename to hkmc2/shared/src/test/mlscript/ChangedTests.cmd diff --git a/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls b/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls index fa9279704..41dc6c99a 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Pwd.mls @@ -5,7 +5,7 @@ :sjs let folderName1 = process.env.PWD.split("/").pop() in let folderName2 = process.cwd().split("/").pop() -in folderName2 === folderName1 || folderName2 === "jvm" +in folderName2 === folderName1 || folderName2 === "shared" //│ JS (unsanitized): //│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6; //│ tmp = this.process.env.PWD.split("/") ?? null; @@ -16,7 +16,7 @@ in folderName2 === folderName1 || folderName2 === "jvm" //│ tmp4 = tmp3.pop() ?? null; //│ this.folderName2 = tmp4; //│ tmp5 = this.folderName2 === this.folderName1; -//│ tmp6 = this.folderName2 === "jvm"; +//│ tmp6 = this.folderName2 === "shared"; //│ tmp5 || tmp6 //│ = true diff --git a/hkmc2/jvm/src/test/scala/hkmc2/BbmlDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala similarity index 100% rename from hkmc2/jvm/src/test/scala/hkmc2/BbmlDiffMaker.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/BbmlDiffMaker.scala diff --git a/hkmc2/jvm/src/test/scala/hkmc2/DiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala similarity index 94% rename from hkmc2/jvm/src/test/scala/hkmc2/DiffMaker.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala index 941403513..6b65d8d1d 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/DiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffMaker.scala @@ -120,12 +120,13 @@ abstract class DiffMaker: val tests = Command("tests"): case "" => - new DiffTestRunner( - // * I don't understand why when I use `new` here - // * the test framework seems to reinstantiate `State` for every test - // new DiffTestRunner.State - DiffTestRunner.State - ){}.execute() + // * Note that making `DiffTestRunnerBase` extend `ParallelTestExecution`, + // * as we used to do, is quite dangerous, because of the way ScalaTest works (which is pretty dumb): + // * it would try to re-instantiate the test classes haphazardly without passing it any arguments, + // * which either crashes (as it would here) or recomputes the state every time + // * (as would be the case if we created an anonymous subclass here), + // * even when the tests, when run with `execute()`, are not run in parallel (also for dumb reasons). + DiffTestRunnerBase(new DiffTestRunner.StateWithGit).execute() val fileName = file.last @@ -198,7 +199,8 @@ abstract class DiffMaker: failures += globalStartLineNum unexpected("warning", blockLineNum, d.mkExtraInfo) case Diagnostic.Kind.Internal => - failures += globalStartLineNum + if !tolerateErrors then + failures += globalStartLineNum // unexpected("internal error", blockLineNum) throw d report(blockLineNum, d :: Nil, showRelativeLineNums.isSet) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/DiffTestRunner.scala b/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala similarity index 67% rename from hkmc2/jvm/src/test/scala/hkmc2/DiffTestRunner.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala index 5d770db33..98ca7894e 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/DiffTestRunner.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/DiffTestRunner.scala @@ -8,34 +8,28 @@ import os.up import mlscript.utils._, shorthands._ - -class MainDiffMaker(val rootPath: Str, val file: os.Path, val preludeFile: os.Path, val predefFile: os.Path, val relativeName: Str) - extends LlirDiffMaker - - - -class AllTests extends org.scalatest.Suites( - new CompileTestRunner(DiffTestRunner.State){}, - new DiffTestRunner(DiffTestRunner.State){}, -) - +// * Note: we used to use: +// * class AllTests extends org.scalatest.Suites( +// * new CompileTestRunner(DiffTestRunner.State){}, +// * new DiffTestRunner(DiffTestRunner.State){}, +// * ) +// * but this (very surprisinbgly) disables parallel execution each individual suite. +// * So now we just split tests into separate SBT projects. object DiffTestRunner: class State: - println(s"INITIALIZING DiffTestRunner.State") + val pwd = os.pwd - val TimeLimit = - if sys.env.get("CI").isDefined then Span(60, Seconds) - else Span(30, Seconds) + // println(s"INITIALIZING DiffTestRunner.State in ${pwd}") - val pwd = os.pwd - val workingDir = if pwd.last == "jvm" - then pwd/up/up // For some reason, when run from ~hkmc2JVM/Test/run in sbt, the pwd is ".../hkmc2/jvm" + val workingDir = if pwd.last == "hkmc2DiffTests" + then pwd/up // For some reason, when run from ~hkmc2JVM/Test/run in sbt, the pwd is ".../hkmc2/jvm" else pwd // val dir = workingDir/"hkmc2"/"shared"/"src"/"test"/"mlscript" + val dir = workingDir/"hkmc2"/"shared"/"src"/"test" val validExt = Set("mls") @@ -44,7 +38,19 @@ object DiffTestRunner: .filter(_.toIO.isFile) .filter(_.ext in validExt) - // Aggregate unstaged modified files to only run the tests on them, if there are any + def filter(file: os.RelPath): Bool = true + + val TimeLimit = + if sys.env.get("CI").isDefined then Span(60, Seconds) + else Span(30, Seconds) + + end State + + class StateWithGit extends State: + + println(s"Running git in ${dir}...") + + // * Aggregate unstaged modified files to only run the tests on them, if there are any val modified: Set[os.RelPath] = try os.proc("git", "status", "--porcelain", dir).call().out.lines().iterator.flatMap { gitStr => println(" [git] " + gitStr) @@ -59,16 +65,26 @@ object DiffTestRunner: System.err.println("/!\\ git command failed with: " + err) Set.empty - end State + if modified.isEmpty then + println("No test file with unstaged changes detected; no test will run.") + + override def filter(file: os.RelPath): Bool = + // println(s"Filtering: $file ${modified(file)}") + modified(file) + + end StateWithGit lazy val State = new State end DiffTestRunner -abstract class DiffTestRunner(state: DiffTestRunner.State) - extends funsuite.AnyFunSuite +class DiffTestRunner + extends DiffTestRunnerBase(DiffTestRunner.State) with ParallelTestExecution + +class DiffTestRunnerBase(state: DiffTestRunner.State) + extends funsuite.AnyFunSuite with TimeLimitedTests : import state.* @@ -93,6 +109,7 @@ abstract class DiffTestRunner(state: DiffTestRunner.State) ( !file.segments.contains("staging") // Exclude staging test files && !file.segments.contains("mlscript-compile") + && filter(file.relativeTo(state.workingDir)) ) diffTestFiles.foreach: file => @@ -105,7 +122,7 @@ abstract class DiffTestRunner(state: DiffTestRunner.State) val preludePath = dir/"mlscript"/"decls"/"Prelude.mls" val predefPath = dir/"mlscript-compile"/"Predef.mls" - val dm = new MainDiffMaker(state.workingDir.toString, file, preludePath, predefPath, relativeName) + val dm = new MainDiffMaker(workingDir.toString, file, preludePath, predefPath, relativeName) dm.run() @@ -113,38 +130,6 @@ abstract class DiffTestRunner(state: DiffTestRunner.State) fail(s"Unexpected test outcome(s) at: " + dm.failures.distinct.map("\n\t"+relativeName+"."+file.ext+":"+_).mkString(", ")) -end DiffTestRunner - - -abstract class CompileTestRunner(state: DiffTestRunner.State) - extends funsuite.AnyFunSuite - with ParallelTestExecution - with TimeLimitedTests -: - import state.* - - private val inParallel = isInstanceOf[ParallelTestExecution] - - val timeLimit = TimeLimit - - protected lazy val compileTestFiles = allFiles.filter: file => - file.segments.contains("mlscript-compile") - - compileTestFiles.foreach: file => - - // TODO dedup with DiffTestRunner? - - val basePath = file.segments.drop(dir.segmentCount).toList.init - val relativeName = basePath.map(_ + "/").mkString + file.baseName - - test(relativeName): - - println(s"Compiling: $relativeName") - - val preludePath = dir/"mlscript"/"decls"/"Prelude.mls" - - MLsCompiler(preludePath).compileModule(file) - -end CompileTestRunner +end DiffTestRunnerBase diff --git a/hkmc2/jvm/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala similarity index 100% rename from hkmc2/jvm/src/test/scala/hkmc2/JSBackendDiffMaker.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala diff --git a/hkmc2/jvm/src/test/scala/hkmc2/LlirDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/LlirDiffMaker.scala similarity index 100% rename from hkmc2/jvm/src/test/scala/hkmc2/LlirDiffMaker.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/LlirDiffMaker.scala diff --git a/hkmc2/jvm/src/test/scala/hkmc2/MLsDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala similarity index 100% rename from hkmc2/jvm/src/test/scala/hkmc2/MLsDiffMaker.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/MLsDiffMaker.scala diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala new file mode 100644 index 000000000..cd598ffbb --- /dev/null +++ b/hkmc2DiffTests/src/test/scala/hkmc2/MainDiffMaker.scala @@ -0,0 +1,14 @@ +package hkmc2 + +import org.scalatest.{funsuite, ParallelTestExecution} +import org.scalatest.time._ +import org.scalatest.concurrent.{TimeLimitedTests, Signaler} +import os.up + +import mlscript.utils._, shorthands._ + + +class MainDiffMaker(val rootPath: Str, val file: os.Path, val preludeFile: os.Path, val predefFile: os.Path, val relativeName: Str) + extends LlirDiffMaker + + diff --git a/hkmc2/jvm/src/test/scala/hkmc2/Watcher.scala b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala similarity index 81% rename from hkmc2/jvm/src/test/scala/hkmc2/Watcher.scala rename to hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala index 56532c9c4..1b26650bb 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/Watcher.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/Watcher.scala @@ -1,6 +1,8 @@ package hkmc2 import scala.collection.mutable +import scala.jdk.CollectionConverters.* + import mlscript.utils.*, shorthands.* import better.files.* @@ -11,13 +13,15 @@ import java.time.LocalDateTime import java.time.temporal._ // Note: when SBT's `fork` is set to `false`, the path should be `File("hkmc2/")` instead... -object MainWatcher extends Watcher(File("../")): +// * Only the first path can contain tests. The other paths are only watched for source changes. +object MainWatcher extends Watcher(File("../hkmc2/shared/src") :: File("./src") :: Nil): def main(args: Array[String]): Unit = run -class Watcher(dir: File): - val dirPath = os.Path(dir.pathAsString) +class Watcher(dirs: Ls[File]): + val dirPaths = dirs.map(d => os.Path(d.pathAsString)) - println((fansi.Color.Blue("Watching directory ") ++ fansi.Color.DarkGray(dir.toString)).toString) + dirs.foreach: dir => + println((fansi.Color.Blue("Watching directory ") ++ fansi.Color.DarkGray(dir.toString)).toString) val fileHashes = mutable.Map.empty[File, FileHash] val completionTime = mutable.Map.empty[File, LocalDateTime] @@ -25,7 +29,7 @@ class Watcher(dir: File): val watcher: DirectoryWatcher = DirectoryWatcher.builder() .logger(org.slf4j.helpers.NOPLogger.NOP_LOGGER) - .path(dir.toJava.toPath) + .paths(dirs.map(_.toJava.toPath).asJava) .fileHashing(false) // so that simple save events trigger processing eve if there's no file change .listener(new io.methvin.watcher.DirectoryChangeListener { def onEvent(event: io.methvin.watcher.DirectoryChangeEvent): Unit = try @@ -83,16 +87,16 @@ class Watcher(dir: File): else if isMls || file.toString.endsWith(".cmd") then Thread.sleep(100) val path = os.Path(file.pathAsString) - val basePath = path.segments.drop(dirPath.segmentCount).toList.init + val basePath = path.segments.drop(dirPaths.head.segmentCount).toList.init val relativeName = basePath.map(_ + "/").mkString + path.baseName - val preludePath = os.pwd/os.up/"shared"/"src"/"test"/"mlscript"/"decls"/"Prelude.mls" - val predefPath = os.pwd/os.up/"shared"/"src"/"test"/"mlscript-compile"/"Predef.mls" + val preludePath = os.pwd/os.up/"hkmc2"/"shared"/"src"/"test"/"mlscript"/"decls"/"Prelude.mls" + val predefPath = os.pwd/os.up/"hkmc2"/"shared"/"src"/"test"/"mlscript-compile"/"Predef.mls" val isModuleFile = path.segments.contains("mlscript-compile") if isModuleFile then MLsCompiler(preludePath).compileModule(path) else - val dm = new MainDiffMaker((dirPath/os.up).toString, path, preludePath, predefPath, relativeName): + val dm = new MainDiffMaker((dirPaths.head/os.up).toString, path, preludePath, predefPath, relativeName): override def unhandled(blockLineNum: Int, exc: Throwable): Unit = exc.printStackTrace() super.unhandled(blockLineNum, exc) @@ -101,7 +105,7 @@ class Watcher(dir: File): def show(file: File) = fansi.Color.Yellow: - file.toString.stripPrefix(dir.toString) + file.toString.stripPrefix(dirs.head.toString) def pre = fansi.Color.Blue(">> ").toString