diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index abc1f9412..1543a3bba 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -7,25 +7,18 @@ package sbt -import java.io.{ File, InputStream } +import java.io.File import java.nio.file.FileSystems -import sbt.BasicCommandStrings.ContinuousExecutePrefix import sbt.internal.LabeledFunctions._ -import sbt.internal.{ FileAttributes, LegacyWatched } +import sbt.internal.LegacyWatched import sbt.internal.io.{ EventMonitor, Source, WatchState } import sbt.internal.util.Types.const -import sbt.internal.util.complete.DefaultParsers._ -import sbt.internal.util.complete.Parser -import sbt.internal.util.{ AttributeKey, JLine, Util } -import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update } +import sbt.internal.util.AttributeKey import sbt.io._ -import sbt.util.{ Level, Logger } -import scala.annotation.tailrec import scala.concurrent.duration._ import scala.util.Properties -import scala.util.control.NonFatal @deprecated("Watched is no longer used to implement continuous execution", "1.3.0") trait Watched { @@ -58,283 +51,13 @@ trait Watched { object Watched { - /** - * This trait is used to communicate what the watch should do next at various points in time. It - * is heavily linked to a number of callbacks in [[WatchConfig]]. For example, when the event - * monitor detects a changed source we expect [[WatchConfig.onWatchEvent]] to return [[Trigger]]. - */ - sealed trait Action - - /** - * Provides a default Ordering for actions. Lower values correspond to higher priority actions. - * [[CancelWatch]] is higher priority than [[ContinueWatch]]. - */ - object Action { - implicit object ordering extends Ordering[Action] { - override def compare(left: Action, right: Action): Int = (left, right) match { - case (a: ContinueWatch, b: ContinueWatch) => ContinueWatch.ordering.compare(a, b) - case (_: ContinueWatch, _: CancelWatch) => 1 - case (a: CancelWatch, b: CancelWatch) => CancelWatch.ordering.compare(a, b) - case (_: CancelWatch, _: ContinueWatch) => -1 - } - } - } - - /** - * Action that indicates that the watch should stop. - */ - sealed trait CancelWatch extends Action - - /** - * Action that does not terminate the watch but might trigger a build. - */ - sealed trait ContinueWatch extends Action - - /** - * Provides a default Ordering for classes extending [[ContinueWatch]]. [[Trigger]] is higher - * priority than [[Ignore]]. - */ - object ContinueWatch { - - /** - * A default [[Ordering]] for [[ContinueWatch]]. [[Trigger]] is higher priority than [[Ignore]]. - */ - implicit object ordering extends Ordering[ContinueWatch] { - override def compare(left: ContinueWatch, right: ContinueWatch): Int = left match { - case Ignore => if (right == Ignore) 0 else 1 - case Trigger => if (right == Trigger) 0 else -1 - } - } - } - - /** - * Action that indicates that the watch should stop. - */ - case object CancelWatch extends CancelWatch { - - /** - * A default [[Ordering]] for [[ContinueWatch]]. The priority of each type of [[CancelWatch]] - * is reflected by the ordering of the case statements in the [[ordering.compare]] method, - * e.g. [[Custom]] is higher priority than [[HandleError]]. - */ - implicit object ordering extends Ordering[CancelWatch] { - override def compare(left: CancelWatch, right: CancelWatch): Int = left match { - // Note that a negative return value means the left CancelWatch is preferred to the right - // CancelWatch while the inverse is true for a positive return value. This logic could - // likely be simplified, but the pattern matching approach makes it very clear what happens - // for each type of Action. - case _: Custom => - right match { - case _: Custom => 0 - case _ => -1 - } - case _: HandleError => - right match { - case _: Custom => 1 - case _: HandleError => 0 - case _ => -1 - } - case _: Run => - right match { - case _: Run => 0 - case CancelWatch | Reload => -1 - case _ => 1 - } - case CancelWatch => - right match { - case CancelWatch => 0 - case Reload => -1 - case _ => 1 - } - case Reload => if (right == Reload) 0 else 1 - } - } - } - - /** - * Action that indicates that an error has occurred. The watch will be terminated when this action - * is produced. - */ - final class HandleError(val throwable: Throwable) extends CancelWatch { - override def equals(o: Any): Boolean = o match { - case that: HandleError => this.throwable == that.throwable - case _ => false - } - override def hashCode: Int = throwable.hashCode - override def toString: String = s"HandleError($throwable)" - } - - /** - * Action that indicates that the watch should continue as though nothing happened. This may be - * because, for example, no user input was yet available. - */ - case object Ignore extends ContinueWatch - - /** - * Action that indicates that the watch should pause while the build is reloaded. This is used to - * automatically reload the project when the build files (e.g. build.sbt) are changed. - */ - case object Reload extends CancelWatch - - /** - * Action that indicates that we should exit and run the provided command. - * @param commands the commands to run after we exit the watch - */ - final class Run(val commands: String*) extends CancelWatch { - override def toString: String = s"Run(${commands.mkString(", ")})" - } - // For now leave this private in case this isn't the best unapply type signature since it can't - // be evolved in a binary compatible way. - private object Run { - def unapply(r: Run): Option[List[Exec]] = Some(r.commands.toList.map(Exec(_, None))) - } - - /** - * Action that indicates that the watch process should re-run the command. - */ - case object Trigger extends ContinueWatch - - /** - * A user defined Action. It is not sealed so that the user can create custom instances. If any - * of the [[Watched.watch]] callbacks return [[Custom]], then watch will terminate. - */ - trait Custom extends CancelWatch - @deprecated("WatchSource is replaced by sbt.io.Glob", "1.3.0") type WatchSource = Source - private[sbt] type OnTermination = (Action, String, State) => State - private[sbt] type OnEnter = () => Unit def terminateWatch(key: Int): Boolean = Watched.isEnter(key) - /** - * A constant function that returns [[Trigger]]. - */ - final val trigger: (Int, Event[FileAttributes]) => Watched.Action = { - (_: Int, _: Event[FileAttributes]) => - Trigger - }.label("Watched.trigger") - - def ifChanged(action: Action): (Int, Event[FileAttributes]) => Watched.Action = - (_: Int, event: Event[FileAttributes]) => - event match { - case Update(prev, cur, _) if prev.value != cur.value => action - case _: Creation[_] | _: Deletion[_] => action - case _ => Ignore - } - - private[this] val options = - if (Util.isWindows) - "press 'enter' to return to the shell or the following keys followed by 'enter': 'r' to" + - " re-run the command, 'x' to exit sbt" - else "press 'r' to re-run the command, 'x' to exit sbt or 'enter' to return to the shell" private def waitMessage(project: String): String = - s"Waiting for source changes$project... (press enter to interrupt$options)" + s"Waiting for source changes$project... (press enter to interrupt)" - /** - * The minimum delay between build triggers for the same file. If the file is detected - * to have changed within this period from the last build trigger, the event will be discarded. - */ - final val defaultAntiEntropy: FiniteDuration = 500.milliseconds - - /** - * The duration in wall clock time for which a FileEventMonitor will retain anti-entropy - * events for files. This is an implementation detail of the FileEventMonitor. It should - * hopefully not need to be set by the users. It is needed because when a task takes a long time - * to run, it is possible that events will be detected for the file that triggered the build that - * occur within the anti-entropy period. We still allow it to be configured to limit the memory - * usage of the FileEventMonitor (but this is somewhat unlikely to be a problem). - */ - final val defaultAntiEntropyRetentionPeriod: FiniteDuration = 10.minutes - - /** - * The duration for which we delay triggering when a file is deleted. This is needed because - * many programs implement save as a file move of a temporary file onto the target file. - * Depending on how the move is implemented, this may be detected as a deletion immediately - * followed by a creation. If we trigger immediately on delete, we may, for example, try to - * compile before all of the source files are actually available. The longer this value is set, - * the less likely we are to spuriously trigger a build before all files are available, but - * the longer it will take to trigger a build when the file is actually deleted and not renamed. - */ - final val defaultDeletionQuarantinePeriod: FiniteDuration = 50.milliseconds - - /** - * Converts user input to an Action with the following rules: - * 1) on all platforms, new lines exit the watch - * 2) on posix platforms, 'r' or 'R' will trigger a build - * 3) on posix platforms, 's' or 'S' will exit the watch and run the shell command. This is to - * support the case where the user starts sbt in a continuous mode but wants to return to - * the shell without having to restart sbt. - */ - final val defaultInputParser: Parser[Action] = { - def posixOnly(legal: String, action: Action): Parser[Action] = - if (!Util.isWindows) chars(legal) ^^^ action - else Parser.invalid(Seq("Can't use jline for individual character entry on windows.")) - val rebuildParser: Parser[Action] = posixOnly(legal = "rR", Trigger) - val shellParser: Parser[Action] = posixOnly(legal = "sS", new Run("shell")) - val cancelParser: Parser[Action] = chars(legal = "\n\r") ^^^ CancelWatch - shellParser | rebuildParser | cancelParser - } - - /** - * A function that prints out the current iteration count and gives instructions for exiting - * or triggering the build. - */ - val defaultStartWatch: Int => Option[String] = - ((count: Int) => Some(s"$count. ${waitMessage("")}")).label("Watched.defaultStartWatch") - - /** - * Default no-op callback. - */ - val defaultOnEnter: () => Unit = () => {} - - private[sbt] val defaultCommandOnTermination: (Action, String, Int, State) => State = - onTerminationImpl(ContinuousExecutePrefix).label("Watched.defaultCommandOnTermination") - private[sbt] val defaultTaskOnTermination: (Action, String, Int, State) => State = - onTerminationImpl("watch", ContinuousExecutePrefix).label("Watched.defaultTaskOnTermination") - - /** - * Default handler to transform the state when the watch terminates. When the [[Watched.Action]] - * is [[Reload]], the handler will prepend the original command (prefixed by ~) to the - * [[State.remainingCommands]] and then invoke the [[StateOps.reload]] method. When the - * [[Watched.Action]] is [[HandleError]], the handler returns the result of [[StateOps.fail]]. - * When the [[Watched.Action]] is [[Watched.Run]], we add the commands specified by - * [[Watched.Run.commands]] to the stat's remaining commands. Otherwise the original state is - * returned. - */ - private def onTerminationImpl( - watchPrefixes: String* - ): (Action, String, Int, State) => State = { (action, command, count, state) => - val prefix = watchPrefixes.head - val rc = state.remainingCommands - .filterNot(c => watchPrefixes.exists(c.commandLine.trim.startsWith)) - action match { - case Run(commands) => state.copy(remainingCommands = commands ++ rc) - case Reload => - state.copy(remainingCommands = "reload".toExec :: s"$prefix $count $command".toExec :: rc) - case _: HandleError => state.copy(remainingCommands = rc).fail - case _ => state.copy(remainingCommands = rc) - } - } - - /** - * A constant function that always returns [[None]]. When - * `Keys.watchTriggeredMessage := Watched.defaultOnTriggerMessage`, then nothing is logged when - * a build is triggered. - */ - final val defaultOnTriggerMessage: (Int, Event[FileAttributes]) => Option[String] = - ((_: Int, _: Event[FileAttributes]) => None).label("Watched.defaultOnTriggerMessage") - - /** - * The minimum delay between file system polling when a [[PollingWatchService]] is used. - */ - final val defaultPollInterval: FiniteDuration = 500.milliseconds - - /** - * A constant function that returns an Option wrapped string that clears the screen when - * written to stdout. - */ - final val clearOnTrigger: Int => Option[String] = - ((_: Int) => Some(clearScreen)).label("Watched.clearOnTrigger") def clearScreen: String = "\u001b[2J\u001b[0;0H" @deprecated("WatchSource has been replaced by sbt.io.Glob", "1.3.0") @@ -361,87 +84,6 @@ object Watched { } - private type RunCommand = () => State - private type NextAction = () => Watched.Action - private[sbt] type Monitor = FileEventMonitor[FileAttributes] - - /** - * Runs a task and then blocks until the task is ready to run again or we no longer wish to - * block execution. - * - * @param task the aggregated task to run with each iteration - * @param onStart function to be invoked before we start polling for events - * @param nextAction function that returns the next state transition [[Watched.Action]]. - * @return the exit [[Watched.Action]] that can be used to potentially modify the build state and - * the count of the number of iterations that were run. If - */ - def watch(task: () => Unit, onStart: NextAction, nextAction: NextAction): Watched.Action = { - def safeNextAction(delegate: NextAction): Watched.Action = - try delegate() - catch { case NonFatal(t) => new HandleError(t) } - @tailrec def next(): Watched.Action = safeNextAction(nextAction) match { - // This should never return Ignore due to this condition. - case Ignore => next() - case action => action - } - @tailrec def impl(): Watched.Action = { - task() - safeNextAction(onStart) match { - case Ignore => - next() match { - case Trigger => impl() - case action => action - } - case Trigger => impl() - case a => a - } - } - try impl() - catch { case NonFatal(t) => new HandleError(t) } - } - - private[sbt] object NullLogger extends Logger { - override def trace(t: => Throwable): Unit = {} - override def success(message: => String): Unit = {} - override def log(level: Level.Value, message: => String): Unit = {} - } - - /** - * Traverse all of the events and find the one for which we give the highest - * weight. Within the [[Action]] hierarchy: - * [[Custom]] > [[HandleError]] > [[Run]] > [[CancelWatch]] > [[Reload]] > [[Trigger]] > [[Ignore]] - * the first event of each kind is returned so long as there are no higher priority events - * in the collection. For example, if there are multiple events that all return [[Trigger]], then - * the first one is returned. If, on the other hand, one of the events returns [[Reload]], - * then that event "wins" and the [[Reload]] action is returned with the [[Event[FileAttributes]]] - * that triggered it. - * - * @param events the ([[Action]], [[Event[FileAttributes]]]) pairs - * @return the ([[Action]], [[Event[FileAttributes]]]) pair with highest weight if the input events - * are non empty. - */ - @inline - private[sbt] def aggregate( - events: Seq[(Action, Event[FileAttributes])] - ): Option[(Action, Event[FileAttributes])] = - if (events.isEmpty) None else Some(events.minBy(_._1)) - - private implicit class StringToExec(val s: String) extends AnyVal { - def toExec: Exec = Exec(s, None) - } - - private[sbt] def withCharBufferedStdIn[R](f: InputStream => R): R = - if (!Util.isWindows) JLine.usingTerminal { terminal => - terminal.init() - val in = terminal.wrapInIfNeeded(System.in) - try { - f(in) - } finally { - terminal.reset() - } - } else - f(System.in) - private[sbt] val newWatchService: () => WatchService = (() => createWatchService()).label("Watched.newWatchService") def createWatchService(pollDelay: FiniteDuration): WatchService = { @@ -479,9 +121,9 @@ object Watched { ((ws: WatchState) => projectOnWatchMessage(projectId)(ws.count).get) .label("Watched.projectWatchingMessage") @deprecated("unused", "1.3.0") - def projectOnWatchMessage(project: String): Int => Option[String] = - ((count: Int) => Some(s"$count. ${waitMessage(s" in project $project")}")) - .label("Watched.projectOnWatchMessage") + def projectOnWatchMessage(project: String): Int => Option[String] = { (count: Int) => + Some(s"$count. ${waitMessage(s" in project $project")}") + }.label("Watched.projectOnWatchMessage") @deprecated("This method is not used and may be removed in a future version of sbt", "1.3.0") private[this] class AWatched extends Watched @@ -522,7 +164,8 @@ object Watched { @deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0") val defaultWatchingMessage: WatchState => String = - ((ws: WatchState) => defaultStartWatch(ws.count).get).label("Watched.defaultWatchingMessage") + ((ws: WatchState) => s"${ws.count}. ${waitMessage("")} ") + .label("Watched.projectWatchingMessage") @deprecated( "Use defaultOnTriggerMessage in conjunction with the watchTriggeredMessage key", "1.3.0" diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 516f4d919..0d151743e 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -333,21 +333,21 @@ object Defaults extends BuildCommon { insideCI :== sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") || System.getProperty("sbt.ci", "false") == "true", // watch related settings - pollInterval :== Watched.defaultPollInterval, - watchAntiEntropy :== Watched.defaultAntiEntropy, - watchAntiEntropyRetentionPeriod :== Watched.defaultAntiEntropyRetentionPeriod, + pollInterval :== Watch.defaultPollInterval, + watchAntiEntropy :== Watch.defaultAntiEntropy, + watchAntiEntropyRetentionPeriod :== Watch.defaultAntiEntropyRetentionPeriod, watchLogLevel :== Level.Info, - watchOnEnter :== Watched.defaultOnEnter, - watchOnMetaBuildEvent :== Watched.ifChanged(Watched.Reload), - watchOnInputEvent :== Watched.trigger, - watchOnTriggerEvent :== Watched.trigger, - watchDeletionQuarantinePeriod :== Watched.defaultDeletionQuarantinePeriod, + watchOnEnter :== Watch.defaultOnEnter, + watchOnMetaBuildEvent :== Watch.ifChanged(Watch.Reload), + watchOnInputEvent :== Watch.trigger, + watchOnTriggerEvent :== Watch.trigger, + watchDeletionQuarantinePeriod :== Watch.defaultDeletionQuarantinePeriod, watchService :== Watched.newWatchService, - watchStartMessage :== Watched.defaultStartWatch, + watchStartMessage :== Watch.defaultStartWatch, watchTasks := Continuous.continuousTask.evaluated, aggregate in watchTasks :== false, watchTrackMetaBuild :== true, - watchTriggeredMessage :== Watched.defaultOnTriggerMessage, + watchTriggeredMessage :== Watch.defaultOnTriggerMessage, ) ) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 0d78d65fc..e25f962cf 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -100,19 +100,19 @@ object Keys { val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration]("Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely.").withRank(BMinusSetting) val watchDeletionQuarantinePeriod = settingKey[FiniteDuration]("Period for which deletion events will be quarantined. This is to prevent spurious builds when a file is updated with a rename which manifests as a file deletion followed by a file creation. The higher this value is set, the longer the delay will be between a file deletion and a build trigger but the less likely it is for a spurious trigger.").withRank(DSetting) val watchLogLevel = settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.").withRank(DSetting) - val watchInputHandler = settingKey[InputStream => Watched.Action]("Function that is periodically invoked to determine if the continuous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands. This is only invoked if watchInputStream is set.").withRank(DSetting) + val watchInputHandler = settingKey[InputStream => Watch.Action]("Function that is periodically invoked to determine if the continuous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands. This is only invoked if watchInputStream is set.").withRank(DSetting) val watchInputStream = taskKey[InputStream]("The input stream to read for user input events. This will usually be System.in").withRank(DSetting) - val watchInputParser = settingKey[Parser[Watched.Action]]("A parser of user input that can be used to trigger or exit a continuous build").withRank(DSetting) + val watchInputParser = settingKey[Parser[Watch.Action]]("A parser of user input that can be used to trigger or exit a continuous build").withRank(DSetting) val watchOnEnter = settingKey[() => Unit]("Function to run prior to beginning a continuous build. This will run before the continuous task(s) is(are) first evaluated.").withRank(DSetting) val watchOnExit = settingKey[() => Unit]("Function to run upon exit of a continuous build. It can be used to cleanup resources used during the watch.").withRank(DSetting) - val watchOnInputEvent = settingKey[(Int, Event[FileAttributes]) => Watched.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive inputs. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) - val watchOnEvent = settingKey[Continuous.Arguments => Event[FileAttributes] => Watched.Action]("Determines how to handle a file event. The Seq[Glob] contains all of the transitive inputs for the task(s) being run by the continuous build.").withRank(DSetting) - val watchOnMetaBuildEvent = settingKey[(Int, Event[FileAttributes]) => Watched.Action]("Callback to invoke if an event is triggered in a continuous build by one of the meta build triggers.").withRank(DSetting) - val watchOnTermination = settingKey[(Watched.Action, String, Int, State) => State]("Transforms the state upon completion of a watch. The String argument is the command that was run during the watch. The Int parameter specifies how many times the command was run during the watch.").withRank(DSetting) + val watchOnInputEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive inputs. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) + val watchOnEvent = settingKey[Continuous.Arguments => Event[FileAttributes] => Watch.Action]("Determines how to handle a file event. The Seq[Glob] contains all of the transitive inputs for the task(s) being run by the continuous build.").withRank(DSetting) + val watchOnMetaBuildEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the meta build triggers.").withRank(DSetting) + val watchOnTermination = settingKey[(Watch.Action, String, Int, State) => State]("Transforms the state upon completion of a watch. The String argument is the command that was run during the watch. The Int parameter specifies how many times the command was run during the watch.").withRank(DSetting) val watchOnTrigger = settingKey[Continuous.Arguments => Event[FileAttributes] => Unit]("Callback to invoke when a continuous build triggers. The first parameter is the number of previous watch task invocations. The second parameter is the Event that triggered this build").withRank(DSetting) - val watchOnTriggerEvent = settingKey[(Int, Event[FileAttributes]) => Watched.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive triggers. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) - val watchOnIteration = settingKey[Int => Watched.Action]("Function that is invoked before waiting for file system events or user input events. This is only invoked if watchOnStart is not explicitly set.").withRank(DSetting) - val watchOnStart = settingKey[Continuous.Arguments => () => Watched.Action]("Function is invoked before waiting for file system or input events. The returned Action is used to either trigger the build, terminate the watch or wait for events.").withRank(DSetting) + val watchOnTriggerEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive triggers. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) + val watchOnIteration = settingKey[Int => Watch.Action]("Function that is invoked before waiting for file system events or user input events. This is only invoked if watchOnStart is not explicitly set.").withRank(DSetting) + val watchOnStart = settingKey[Continuous.Arguments => () => Watch.Action]("Function is invoked before waiting for file system or input events. The returned Action is used to either trigger the build, terminate the watch or wait for events.").withRank(DSetting) val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting).withRank(DSetting) val watchStartMessage = settingKey[Int => Option[String]]("The message to show when triggered execution waits for sources to change. The parameter is the current watch iteration count.").withRank(DSetting) // The watchTasks key should really be named watch, but that is already taken by the deprecated watch key. I'd be surprised if there are any plugins that use it so I think we should consider breaking binary compatibility to rename this task. diff --git a/main/src/main/scala/sbt/Watch.scala b/main/src/main/scala/sbt/Watch.scala new file mode 100644 index 000000000..730993d9f --- /dev/null +++ b/main/src/main/scala/sbt/Watch.scala @@ -0,0 +1,373 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +import java.io.InputStream + +import sbt.BasicCommandStrings.ContinuousExecutePrefix +import sbt.internal.FileAttributes +import sbt.internal.LabeledFunctions._ +import sbt.internal.util.{ JLine, Util } +import sbt.internal.util.complete.Parser +import sbt.internal.util.complete.Parser._ +import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update } +import sbt.util.{ Level, Logger } + +import scala.annotation.tailrec +import scala.concurrent.duration._ +import scala.util.control.NonFatal + +object Watch { + + /** + * This trait is used to control the state of [[Watch.apply]]. The [[Watch.Trigger]] action + * indicates that [[Watch.apply]] should re-run the input task. The [[Watch.CancelWatch]] + * actions indicate that [[Watch.apply]] should exit and return the [[Watch.CancelWatch]] + * instance that caused the function to exit. The [[Watch.Ignore]] action is used to indicate + * that the method should keep polling for new actions. + */ + sealed trait Action + + /** + * Provides a default `Ordering` for actions. Lower values correspond to higher priority actions. + * [[CancelWatch]] is higher priority than [[ContinueWatch]]. + */ + object Action { + implicit object ordering extends Ordering[Action] { + override def compare(left: Action, right: Action): Int = (left, right) match { + case (a: ContinueWatch, b: ContinueWatch) => ContinueWatch.ordering.compare(a, b) + case (_: ContinueWatch, _: CancelWatch) => 1 + case (a: CancelWatch, b: CancelWatch) => CancelWatch.ordering.compare(a, b) + case (_: CancelWatch, _: ContinueWatch) => -1 + } + } + } + + /** + * Action that indicates that the watch should stop. + */ + sealed trait CancelWatch extends Action + + /** + * Action that does not terminate the watch but might trigger a build. + */ + sealed trait ContinueWatch extends Action + + /** + * Provides a default `Ordering` for classes extending [[ContinueWatch]]. [[Trigger]] is higher + * priority than [[Ignore]]. + */ + object ContinueWatch { + + /** + * A default `Ordering` for [[ContinueWatch]]. [[Trigger]] is higher priority than [[Ignore]]. + */ + implicit object ordering extends Ordering[ContinueWatch] { + override def compare(left: ContinueWatch, right: ContinueWatch): Int = left match { + case Ignore => if (right == Ignore) 0 else 1 + case Trigger => if (right == Trigger) 0 else -1 + } + } + } + + /** + * Action that indicates that the watch should stop. + */ + case object CancelWatch extends CancelWatch { + + /** + * A default `Ordering` for [[ContinueWatch]]. The priority of each type of [[CancelWatch]] + * is reflected by the ordering of the case statements in the [[ordering.compare]] method, + * e.g. [[Custom]] is higher priority than [[HandleError]]. + */ + implicit object ordering extends Ordering[CancelWatch] { + override def compare(left: CancelWatch, right: CancelWatch): Int = left match { + // Note that a negative return value means the left CancelWatch is preferred to the right + // CancelWatch while the inverse is true for a positive return value. This logic could + // likely be simplified, but the pattern matching approach makes it very clear what happens + // for each type of Action. + case _: Custom => + right match { + case _: Custom => 0 + case _ => -1 + } + case _: HandleError => + right match { + case _: Custom => 1 + case _: HandleError => 0 + case _ => -1 + } + case _: Run => + right match { + case _: Run => 0 + case CancelWatch | Reload => -1 + case _ => 1 + } + case CancelWatch => + right match { + case CancelWatch => 0 + case Reload => -1 + case _ => 1 + } + case Reload => if (right == Reload) 0 else 1 + } + } + } + + /** + * Action that indicates that an error has occurred. The watch will be terminated when this action + * is produced. + */ + final class HandleError(val throwable: Throwable) extends CancelWatch { + override def equals(o: Any): Boolean = o match { + case that: HandleError => this.throwable == that.throwable + case _ => false + } + override def hashCode: Int = throwable.hashCode + override def toString: String = s"HandleError($throwable)" + } + + /** + * Action that indicates that the watch should continue as though nothing happened. This may be + * because, for example, no user input was yet available. + */ + case object Ignore extends ContinueWatch + + /** + * Action that indicates that the watch should pause while the build is reloaded. This is used to + * automatically reload the project when the build files (e.g. build.sbt) are changed. + */ + case object Reload extends CancelWatch + + /** + * Action that indicates that we should exit and run the provided command. + * @param commands the commands to run after we exit the watch + */ + final class Run(val commands: String*) extends CancelWatch + // For now leave this private in case this isn't the best unapply type signature since it can't + // be evolved in a binary compatible way. + private object Run { + def unapply(r: Run): Option[List[Exec]] = Some(r.commands.toList.map(Exec(_, None))) + } + + /** + * Action that indicates that the watch process should re-run the command. + */ + case object Trigger extends ContinueWatch + + /** + * A user defined Action. It is not sealed so that the user can create custom instances. If + * the onStart or nextAction function passed into [[Watch.apply]] returns [[Watch.Custom]], then + * the watch will terminate. + */ + trait Custom extends CancelWatch + + private type NextAction = () => Watch.Action + + /** + * Runs a task and then blocks until the task is ready to run again or we no longer wish to + * block execution. + * + * @param task the aggregated task to run with each iteration + * @param onStart function to be invoked before we start polling for events + * @param nextAction function that returns the next state transition [[Watch.Action]]. + * @return the exit [[Watch.Action]] that can be used to potentially modify the build state and + * the count of the number of iterations that were run. If + */ + def apply(task: () => Unit, onStart: NextAction, nextAction: NextAction): Watch.Action = { + def safeNextAction(delegate: NextAction): Watch.Action = + try delegate() + catch { case NonFatal(t) => new HandleError(t) } + @tailrec def next(): Watch.Action = safeNextAction(nextAction) match { + // This should never return Ignore due to this condition. + case Ignore => next() + case action => action + } + @tailrec def impl(): Watch.Action = { + task() + safeNextAction(onStart) match { + case Ignore => + next() match { + case Trigger => impl() + case action => action + } + case Trigger => impl() + case a => a + } + } + try impl() + catch { case NonFatal(t) => new HandleError(t) } + } + + private[sbt] object NullLogger extends Logger { + override def trace(t: => Throwable): Unit = {} + override def success(message: => String): Unit = {} + override def log(level: Level.Value, message: => String): Unit = {} + } + + /** + * Traverse all of the events and find the one for which we give the highest + * weight. Within the [[Action]] hierarchy: + * [[Custom]] > [[HandleError]] > [[CancelWatch]] > [[Reload]] > [[Trigger]] > [[Ignore]] + * the first event of each kind is returned so long as there are no higher priority events + * in the collection. For example, if there are multiple events that all return [[Trigger]], then + * the first one is returned. If, on the other hand, one of the events returns [[Reload]], + * then that event "wins" and the [[Reload]] action is returned with the [[Event[FileAttributes]]] that triggered it. + * + * @param events the ([[Action]], [[Event[FileAttributes]]]) pairs + * @return the ([[Action]], [[Event[FileAttributes]]]) pair with highest weight if the input events + * are non empty. + */ + @inline + private[sbt] def aggregate( + events: Seq[(Action, Event[FileAttributes])] + ): Option[(Action, Event[FileAttributes])] = + if (events.isEmpty) None else Some(events.minBy(_._1)) + + private implicit class StringToExec(val s: String) extends AnyVal { + def toExec: Exec = Exec(s, None) + } + + private[sbt] def withCharBufferedStdIn[R](f: InputStream => R): R = + if (!Util.isWindows) JLine.usingTerminal { terminal => + terminal.init() + val in = terminal.wrapInIfNeeded(System.in) + try { + f(in) + } finally { + terminal.reset() + } + } else + f(System.in) + + /** + * A constant function that returns [[Trigger]]. + */ + final val trigger: (Int, Event[FileAttributes]) => Watch.Action = { + (_: Int, _: Event[FileAttributes]) => + Trigger + }.label("Watched.trigger") + + def ifChanged(action: Action): (Int, Event[FileAttributes]) => Watch.Action = + (_: Int, event: Event[FileAttributes]) => + event match { + case Update(prev, cur, _) if prev.value != cur.value => action + case _: Creation[_] | _: Deletion[_] => action + case _ => Ignore + } + + /** + * The minimum delay between build triggers for the same file. If the file is detected + * to have changed within this period from the last build trigger, the event will be discarded. + */ + final val defaultAntiEntropy: FiniteDuration = 500.milliseconds + + /** + * The duration in wall clock time for which a FileEventMonitor will retain anti-entropy + * events for files. This is an implementation detail of the FileEventMonitor. It should + * hopefully not need to be set by the users. It is needed because when a task takes a long time + * to run, it is possible that events will be detected for the file that triggered the build that + * occur within the anti-entropy period. We still allow it to be configured to limit the memory + * usage of the FileEventMonitor (but this is somewhat unlikely to be a problem). + */ + final val defaultAntiEntropyRetentionPeriod: FiniteDuration = 10.minutes + + /** + * The duration for which we delay triggering when a file is deleted. This is needed because + * many programs implement save as a file move of a temporary file onto the target file. + * Depending on how the move is implemented, this may be detected as a deletion immediately + * followed by a creation. If we trigger immediately on delete, we may, for example, try to + * compile before all of the source files are actually available. The longer this value is set, + * the less likely we are to spuriously trigger a build before all files are available, but + * the longer it will take to trigger a build when the file is actually deleted and not renamed. + */ + final val defaultDeletionQuarantinePeriod: FiniteDuration = 50.milliseconds + + /** + * Converts user input to an Action with the following rules: + * 1) on all platforms, new lines exit the watch + * 2) on posix platforms, 'r' or 'R' will trigger a build + * 3) on posix platforms, 's' or 'S' will exit the watch and run the shell command. This is to + * support the case where the user starts sbt in a continuous mode but wants to return to + * the shell without having to restart sbt. + */ + final val defaultInputParser: Parser[Action] = { + def posixOnly(legal: String, action: Action): Parser[Action] = + if (!Util.isWindows) chars(legal) ^^^ action + else Parser.invalid(Seq("Can't use jline for individual character entry on windows.")) + val rebuildParser: Parser[Action] = posixOnly(legal = "rR", Trigger) + val shellParser: Parser[Action] = posixOnly(legal = "sS", new Run("shell")) + val cancelParser: Parser[Action] = chars(legal = "\n\r") ^^^ CancelWatch + shellParser | rebuildParser | cancelParser + } + + private[this] val reRun = + if (Util.isWindows) "" else ", 'r' to re-run the command or 's' to return to the shell" + private[sbt] def waitMessage(project: String): String = + s"Waiting for source changes$project... (press enter to interrupt$reRun)" + + /** + * A function that prints out the current iteration count and gives instructions for exiting + * or triggering the build. + */ + val defaultStartWatch: Int => Option[String] = + ((count: Int) => Some(s"$count. ${waitMessage("")}")).label("Watched.defaultStartWatch") + + /** + * Default no-op callback. + */ + val defaultOnEnter: () => Unit = () => {} + + private[sbt] val defaultCommandOnTermination: (Action, String, Int, State) => State = + onTerminationImpl(ContinuousExecutePrefix).label("Watched.defaultCommandOnTermination") + private[sbt] val defaultTaskOnTermination: (Action, String, Int, State) => State = + onTerminationImpl("watch", ContinuousExecutePrefix).label("Watched.defaultTaskOnTermination") + + /** + * Default handler to transform the state when the watch terminates. When the [[Watch.Action]] + * is [[Reload]], the handler will prepend the original command (prefixed by ~) to the + * [[State.remainingCommands]] and then invoke the [[StateOps.reload]] method. When the + * [[Watch.Action]] is [[HandleError]], the handler returns the result of [[StateOps.fail]]. + * When the [[Watch.Action]] is [[Watch.Run]], we add the commands specified by + * [[Watch.Run.commands]] to the stat's remaining commands. Otherwise the original state is + * returned. + */ + private def onTerminationImpl( + watchPrefixes: String* + ): (Action, String, Int, State) => State = { (action, command, count, state) => + val prefix = watchPrefixes.head + val rc = state.remainingCommands + .filterNot(c => watchPrefixes.exists(c.commandLine.trim.startsWith)) + action match { + case Run(commands) => state.copy(remainingCommands = commands ++ rc) + case Reload => + state.copy(remainingCommands = "reload".toExec :: s"$prefix $count $command".toExec :: rc) + case _: HandleError => state.copy(remainingCommands = rc).fail + case _ => state.copy(remainingCommands = rc) + } + } + + /** + * A constant function that always returns `None`. When + * `Keys.watchTriggeredMessage := Watched.defaultOnTriggerMessage`, then nothing is logged when + * a build is triggered. + */ + final val defaultOnTriggerMessage: (Int, Event[FileAttributes]) => Option[String] = + ((_: Int, _: Event[FileAttributes]) => None).label("Watched.defaultOnTriggerMessage") + + /** + * The minimum delay between file system polling when a `PollingWatchService` is used. + */ + final val defaultPollInterval: FiniteDuration = 500.milliseconds + + /** + * A constant function that returns an Option wrapped string that clears the screen when + * written to stdout. + */ + final val clearOnTrigger: Int => Option[String] = + ((_: Int) => Some(Watched.clearScreen)).label("Watched.clearOnTrigger") +} diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 5f601a74e..be1a15214 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -9,38 +9,28 @@ package sbt package internal import java.io.IOException +import java.net.Socket import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic._ -import scala.collection.mutable.ListBuffer -import scala.annotation.tailrec -import BasicKeys.{ - autoStartServer, - fullServerHandlers, - logLevel, - serverAuthentication, - serverConnectionType, - serverHost, - serverLogLevel, - serverPort -} -import java.net.Socket - -import sbt.Watched.NullLogger +import sbt.BasicKeys._ +import sbt.Watch.NullLogger +import sbt.internal.langserver.{ LogMessageParams, MessageType } +import sbt.internal.server._ +import sbt.internal.util.codec.JValueFormats +import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent } +import sbt.io.syntax._ +import sbt.io.{ Hash, IO } +import sbt.protocol.{ EventMessage, ExecStatusEvent } +import sbt.util.{ Level, LogExchange, Logger } import sjsonnew.JsonFormat import sjsonnew.shaded.scalajson.ast.unsafe._ +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer import scala.concurrent.Await import scala.concurrent.duration._ import scala.util.{ Failure, Success, Try } -import sbt.io.syntax._ -import sbt.io.{ Hash, IO } -import sbt.internal.server._ -import sbt.internal.langserver.{ LogMessageParams, MessageType } -import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent } -import sbt.internal.util.codec.JValueFormats -import sbt.protocol.{ EventMessage, ExecStatusEvent } -import sbt.util.{ Level, LogExchange, Logger } /** * The command exchange merges multiple command channels (e.g. network and console), diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index 277fa968b..ce6eaf86b 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -20,13 +20,13 @@ import sbt.BasicCommandStrings.{ import sbt.BasicCommands.otherCommandParser import sbt.Def._ import sbt.Scope.Global -import sbt.Watched.Monitor import sbt.internal.FileManagement.FileTreeRepositoryOps +import sbt.internal.LabeledFunctions._ import sbt.internal.io.WatchState import sbt.internal.util.Types.const import sbt.internal.util.complete.Parser._ import sbt.internal.util.complete.{ Parser, Parsers } -import sbt.internal.util.{ AttributeKey, AttributeMap } +import sbt.internal.util.{ AttributeKey, AttributeMap, Util } import sbt.io._ import sbt.util.{ Level, _ } @@ -92,17 +92,17 @@ object Continuous extends DeprecatedContinuous { } /** - * Create a function from InputStream => [[Watched.Action]] from a [[Parser]]. This is intended + * Create a function from InputStream => [[Watch.Action]] from a [[Parser]]. This is intended * to be used to set the watchInputHandler setting for a task. * @param parser the parser * @return the function */ - def defaultInputHandler(parser: Parser[Watched.Action]): InputStream => Watched.Action = { + def defaultInputHandler(parser: Parser[Watch.Action]): InputStream => Watch.Action = { val builder = new StringBuilder val any = matched(Parsers.any.*) val fullParser = any ~> parser ~ any - inputStream => - parse(inputStream, builder, fullParser) + ((inputStream: InputStream) => parse(inputStream, builder, fullParser)) + .label("Continuous.defaultInputHandler") } /** @@ -254,7 +254,7 @@ object Continuous extends DeprecatedContinuous { command: String, count: Int, isCommand: Boolean - ): State = Watched.withCharBufferedStdIn { in => + ): State = Watch.withCharBufferedStdIn { in => val duped = new DupedInputStream(in) setup(state.put(DupedSystemIn, duped), command) { (s, commands, valid, invalid) => implicit val extracted: Extracted = Project.extract(s) @@ -276,7 +276,7 @@ object Continuous extends DeprecatedContinuous { // or Watched.Reload. The task defined above will be run at least once. It will be run // additional times whenever the state transition callbacks return Watched.Trigger. try { - val terminationAction = Watched.watch(task, callbacks.onStart, callbacks.nextEvent) + val terminationAction = Watch(task, callbacks.onStart, callbacks.nextEvent) callbacks.onTermination(terminationAction, command, currentCount.get(), state) } finally callbacks.onExit() } else { @@ -315,11 +315,11 @@ object Continuous extends DeprecatedContinuous { } private class Callbacks( - val nextEvent: () => Watched.Action, + val nextEvent: () => Watch.Action, val onEnter: () => Unit, val onExit: () => Unit, - val onStart: () => Watched.Action, - val onTermination: (Watched.Action, String, Int, State) => State + val onStart: () => Watch.Action, + val onTermination: (Watch.Action, String, Int, State) => State ) /** @@ -329,19 +329,19 @@ object Continuous extends DeprecatedContinuous { * To monitor all of the inputs and triggers, it creates a [[FileEventMonitor]] for each task * and then aggregates each of the individual [[FileEventMonitor]] instances into an aggregated * instance. It aggregates all of the event callbacks into a single callback that delegates - * to each of the individual callbacks. For the callbacks that return a [[Watched.Action]], - * the aggregated callback will select the minimum [[Watched.Action]] returned where the ordering - * is such that the highest priority [[Watched.Action]] have the lowest values. Finally, to + * to each of the individual callbacks. For the callbacks that return a [[Watch.Action]], + * the aggregated callback will select the minimum [[Watch.Action]] returned where the ordering + * is such that the highest priority [[Watch.Action]] have the lowest values. Finally, to * handle user input, we read from the provided input stream and buffer the result. Each * task's input parser is then applied to the buffered result and, again, we return the mimimum - * [[Watched.Action]] returned by the parsers (when the parsers fail, they just return - * [[Watched.Ignore]], which is the lowest priority [[Watched.Action]]. + * [[Watch.Action]] returned by the parsers (when the parsers fail, they just return + * [[Watch.Ignore]], which is the lowest priority [[Watch.Action]]. * * @param configs the [[Config]] instances * @param rawLogger the default sbt logger instance * @param state the current state * @param extracted the [[Extracted]] instance for the current build - * @return the [[Callbacks]] to pass into [[Watched.watch]] + * @return the [[Callbacks]] to pass into [[Watch.apply]] */ private def aggregate( configs: Seq[Config], @@ -355,11 +355,11 @@ object Continuous extends DeprecatedContinuous { ): Callbacks = { val logger = setLevel(rawLogger, configs.map(_.watchSettings.logLevel).min, state) val onEnter = () => configs.foreach(_.watchSettings.onEnter()) - val onStart: () => Watched.Action = getOnStart(configs, logger, count) - val nextInputEvent: () => Watched.Action = parseInputEvents(configs, state, inputStream, logger) - val (nextFileEvent, cleanupFileMonitor): (() => Watched.Action, () => Unit) = + val onStart: () => Watch.Action = getOnStart(configs, logger, count) + val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger) + val (nextFileEvent, cleanupFileMonitor): (() => Watch.Action, () => Unit) = getFileEvents(configs, logger, state, count) - val nextEvent: () => Watched.Action = + val nextEvent: () => Watch.Action = combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger) val onExit = () => { cleanupFileMonitor() @@ -372,7 +372,7 @@ object Continuous extends DeprecatedContinuous { private def getOnTermination( configs: Seq[Config], isCommand: Boolean - ): (Watched.Action, String, Int, State) => State = { + ): (Watch.Action, String, Int, State) => State = { configs.flatMap(_.watchSettings.onTermination).distinct match { case Seq(head, tail @ _*) => tail.foldLeft(head) { @@ -381,7 +381,7 @@ object Continuous extends DeprecatedContinuous { configOnTermination(action, cmd, count, onTermination(action, cmd, count, state)) } case _ => - if (isCommand) Watched.defaultCommandOnTermination else Watched.defaultTaskOnTermination + if (isCommand) Watch.defaultCommandOnTermination else Watch.defaultTaskOnTermination } } @@ -389,7 +389,7 @@ object Continuous extends DeprecatedContinuous { configs: Seq[Config], logger: Logger, count: AtomicInteger - ): () => Watched.Action = { + ): () => Watch.Action = { val f = configs.map { params => val ws = params.watchSettings ws.onStart.map(_.apply(params.arguments(logger))).getOrElse { () => @@ -398,10 +398,10 @@ object Continuous extends DeprecatedContinuous { ws.startMessage match { case Some(Left(sm)) => logger.info(sm(params.watchState(count.get()))) case Some(Right(sm)) => sm(count.get()).foreach(logger.info(_)) - case None => Watched.defaultStartWatch(count.get()).foreach(logger.info(_)) + case None => Watch.defaultStartWatch(count.get()).foreach(logger.info(_)) } } - Watched.Ignore + Watch.Ignore } } } @@ -409,7 +409,7 @@ object Continuous extends DeprecatedContinuous { { val res = f.view.map(_()).min // Print the default watch message if there are multiple tasks - if (configs.size > 1) Watched.defaultStartWatch(count.get()).foreach(logger.info(_)) + if (configs.size > 1) Watch.defaultStartWatch(count.get()).foreach(logger.info(_)) res } } @@ -418,7 +418,7 @@ object Continuous extends DeprecatedContinuous { logger: Logger, state: State, count: AtomicInteger, - )(implicit extracted: Extracted): (() => Watched.Action, () => Unit) = { + )(implicit extracted: Extracted): (() => Watch.Action, () => Unit) = { val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild) val buildGlobs = if (trackMetaBuild) extracted.getOpt(Keys.fileInputs in Keys.settingsData).getOrElse(Nil) @@ -430,7 +430,7 @@ object Continuous extends DeprecatedContinuous { * motivation is to allow the user to specify this callback via setting so that, for example, * they can clear the screen when the build triggers. */ - val onTrigger: Event => Watched.Action = { + val onTrigger: Event => Watch.Action = { val f: Seq[Event => Unit] = configs.map { params => val ws = params.watchSettings ws.onTrigger @@ -449,38 +449,39 @@ object Continuous extends DeprecatedContinuous { } event: Event => f.view.foreach(_.apply(event)) - Watched.Trigger + Watch.Trigger } - val onEvent: Event => (Event, Watched.Action) = { + val defaultTrigger = if (Util.isWindows) Watch.ifChanged(Watch.Trigger) else Watch.trigger + val onEvent: Event => (Event, Watch.Action) = { val f = configs.map { params => val ws = params.watchSettings val oe = ws.onEvent .map(_.apply(params.arguments(logger))) .getOrElse { - val onInputEvent = ws.onInputEvent.getOrElse(Watched.trigger) - val onTriggerEvent = ws.onTriggerEvent.getOrElse(Watched.trigger) - val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watched.ifChanged(Watched.Reload)) + val onInputEvent = ws.onInputEvent.getOrElse(defaultTrigger) + val onTriggerEvent = ws.onTriggerEvent.getOrElse(defaultTrigger) + val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watch.ifChanged(Watch.Reload)) val inputFilter = params.inputs.toEntryFilter val triggerFilter = params.triggers.toEntryFilter event: Event => val c = count.get() - Seq[Watched.Action]( - if (inputFilter(event.entry)) onInputEvent(c, event) else Watched.Ignore, - if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watched.Ignore, - if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watched.Ignore + Seq[Watch.Action]( + if (inputFilter(event.entry)) onInputEvent(c, event) else Watch.Ignore, + if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watch.Ignore, + if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watch.Ignore ).min } event: Event => event -> (oe(event) match { - case Watched.Trigger => onTrigger(event) - case a => a + case Watch.Trigger => onTrigger(event) + case a => a }) } event: Event => f.view.map(_.apply(event)).minBy(_._2) } - val monitor: Monitor = new FileEventMonitor[FileAttributes] { + val monitor: FileEventMonitor[FileAttributes] = new FileEventMonitor[FileAttributes] { private def setup( monitor: FileEventMonitor[FileAttributes], globs: Seq[Glob] @@ -527,11 +528,11 @@ object Continuous extends DeprecatedContinuous { ) (() => { val actions = antiEntropyMonitor.poll(2.milliseconds).map(onEvent) - if (actions.exists(_._2 != Watched.Ignore)) { + if (actions.exists(_._2 != Watch.Ignore)) { val min = actions.minBy(_._2) logger.debug(s"Received file event actions: ${actions.mkString(", ")}. Returning: $min") min._2 - } else Watched.Ignore + } else Watch.Ignore }, () => monitor.close()) } @@ -539,16 +540,16 @@ object Continuous extends DeprecatedContinuous { * Each task has its own input parser that can be used to modify the watch based on the input * read from System.in as well as a custom task-specific input stream that can be used as * an alternative source of control. In this method, we create two functions for each task, - * one from `String => Seq[Watched.Action]` and another from `() => Seq[Watched.Action]`. + * one from `String => Seq[Watch.Action]` and another from `() => Seq[Watch.Action]`. * Each of these functions is invoked to determine the next state transformation for the watch. * The first function is a task specific copy of System.in. For each task we keep a mutable * buffer of the characters previously seen from System.in. Every time we receive new characters - * we update the buffer and then try to parse a Watched.Action for each task. Any trailing + * we update the buffer and then try to parse a Watch.Action for each task. Any trailing * characters are captured and can be used for the next trigger. Because each task has a local * copy of the buffer, we do not have to worry about one task breaking parsing of another. We * also provide an alternative per task InputStream that is read in a similar way except that * we don't need to copy the custom InputStream which allows the function to be - * `() => Seq[Watched.Action]` which avoids actually exposing the InputStream anywhere. + * `() => Seq[Watch.Action]` which avoids actually exposing the InputStream anywhere. */ private def parseInputEvents( configs: Seq[Config], @@ -557,15 +558,15 @@ object Continuous extends DeprecatedContinuous { logger: Logger )( implicit extracted: Extracted - ): () => Watched.Action = { + ): () => Watch.Action = { /* * This parses the buffer until all possible actions are extracted. By draining the input * to a state where it does not parse an action, we can wait until we receive new input * to attempt to parse again. */ - type ActionParser = String => Watched.Action + type ActionParser = String => Watch.Action // Transform the Config.watchSettings.inputParser instances to functions of type - // String => Watched.Action. The String that is provided will contain any characters that + // String => Watch.Action. The String that is provided will contain any characters that // have been read from stdin. If there are any characters available, then it calls the // parse method with the InputStream set to a ByteArrayInputStream that wraps the input // string. The parse method then appends those bytes to a mutable buffer and attempts to @@ -581,7 +582,7 @@ object Continuous extends DeprecatedContinuous { val systemInBuilder = new StringBuilder def inputStream(string: String): InputStream = new ByteArrayInputStream(string.getBytes) // This string is provided in the closure below by reading from System.in - val default: String => Watched.Action = + val default: String => Watch.Action = string => parse(inputStream(string), systemInBuilder, parser) val alternative = c.watchSettings.inputStream .map { inputStreamKey => @@ -590,7 +591,7 @@ object Continuous extends DeprecatedContinuous { () => handler(is) } - .getOrElse(() => Watched.Ignore) + .getOrElse(() => Watch.Ignore) (string: String) => (default(string) :: alternative() :: Nil).min } @@ -599,32 +600,32 @@ object Continuous extends DeprecatedContinuous { val stringBuilder = new StringBuilder while (inputStream.available > 0) stringBuilder += inputStream.read().toChar val newBytes = stringBuilder.toString - val parse: ActionParser => Watched.Action = parser => parser(newBytes) - val allEvents = inputHandlers.map(parse).filterNot(_ == Watched.Ignore) - if (allEvents.exists(_ != Watched.Ignore)) { + val parse: ActionParser => Watch.Action = parser => parser(newBytes) + val allEvents = inputHandlers.map(parse).filterNot(_ == Watch.Ignore) + if (allEvents.exists(_ != Watch.Ignore)) { val res = allEvents.min logger.debug(s"Received input events: ${allEvents mkString ","}. Taking $res") res - } else Watched.Ignore + } else Watch.Ignore } } private def combineInputAndFileEvents( - nextInputEvent: () => Watched.Action, - nextFileEvent: () => Watched.Action, + nextInputEvent: () => Watch.Action, + nextFileEvent: () => Watch.Action, logger: Logger - ): () => Watched.Action = () => { - val Seq(inputEvent: Watched.Action, fileEvent: Watched.Action) = + ): () => Watch.Action = () => { + val Seq(inputEvent: Watch.Action, fileEvent: Watch.Action) = Seq(nextInputEvent, nextFileEvent).par.map(_.apply()).toIndexedSeq - val min: Watched.Action = Seq[Watched.Action](inputEvent, fileEvent).min + val min: Watch.Action = Seq[Watch.Action](inputEvent, fileEvent).min lazy val inputMessage = s"Received input event: $inputEvent." + (if (inputEvent != min) s" Dropping in favor of file event: $min" else "") lazy val fileMessage = s"Received file event: $fileEvent." + (if (fileEvent != min) s" Dropping in favor of input event: $min" else "") - if (inputEvent != Watched.Ignore) logger.debug(inputMessage) - if (fileEvent != Watched.Ignore) logger.debug(fileMessage) + if (inputEvent != Watch.Ignore) logger.debug(inputMessage) + if (fileEvent != Watch.Ignore) logger.debug(fileMessage) min } @@ -632,8 +633,8 @@ object Continuous extends DeprecatedContinuous { private final def parse( is: InputStream, builder: StringBuilder, - parser: Parser[(Watched.Action, String)] - ): Watched.Action = { + parser: Parser[(Watch.Action, String)] + ): Watch.Action = { if (is.available > 0) builder += is.read().toChar Parser.parse(builder.toString, parser) match { case Right((action, rest)) => @@ -641,7 +642,7 @@ object Continuous extends DeprecatedContinuous { builder ++= rest action case _ if is.available > 0 => parse(is, builder, parser) - case _ => Watched.Ignore + case _ => Watch.Ignore } } @@ -672,14 +673,14 @@ object Continuous extends DeprecatedContinuous { } } - private type WatchOnEvent = (Int, Event) => Watched.Action + private type WatchOnEvent = (Int, Event) => Watch.Action /** * Contains all of the user defined settings that will be used to build a [[Callbacks]] - * instance that is used to produce the arguments to [[Watched.watch]]. The + * instance that is used to produce the arguments to [[Watch.apply]]. The * callback settings (e.g. onEvent or onInputEvent) come in two forms: those that return a * function from [[Arguments]] => F for some function type `F` and those that directly return a function, e.g. - * `(Int, Boolean) => Watched.Action`. The former are a low level interface that will usually + * `(Int, Boolean) => Watch.Action`. The former are a low level interface that will usually * be unspecified and automatically filled in by [[Continuous.aggregate]]. The latter are * intended to be user configurable and will be scoped to the input [[ScopedKey]]. To ensure * that the scoping makes sense, we first try and extract the setting from the [[ScopedKey]] @@ -703,25 +704,25 @@ object Continuous extends DeprecatedContinuous { implicit extracted: Extracted ) { val antiEntropy: FiniteDuration = - key.get(Keys.watchAntiEntropy).getOrElse(Watched.defaultAntiEntropy) + key.get(Keys.watchAntiEntropy).getOrElse(Watch.defaultAntiEntropy) val antiEntropyRetentionPeriod: FiniteDuration = key .get(Keys.watchAntiEntropyRetentionPeriod) - .getOrElse(Watched.defaultAntiEntropyRetentionPeriod) + .getOrElse(Watch.defaultAntiEntropyRetentionPeriod) val deletionQuarantinePeriod: FiniteDuration = - key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watched.defaultDeletionQuarantinePeriod) - val inputHandler: Option[InputStream => Watched.Action] = key.get(Keys.watchInputHandler) - val inputParser: Parser[Watched.Action] = - key.get(Keys.watchInputParser).getOrElse(Watched.defaultInputParser) + key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watch.defaultDeletionQuarantinePeriod) + val inputHandler: Option[InputStream => Watch.Action] = key.get(Keys.watchInputHandler) + val inputParser: Parser[Watch.Action] = + key.get(Keys.watchInputParser).getOrElse(Watch.defaultInputParser) val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info) val onEnter: () => Unit = key.get(Keys.watchOnEnter).getOrElse(() => {}) - val onEvent: Option[Arguments => Event => Watched.Action] = key.get(Keys.watchOnEvent) + val onEvent: Option[Arguments => Event => Watch.Action] = key.get(Keys.watchOnEvent) val onExit: () => Unit = key.get(Keys.watchOnExit).getOrElse(() => {}) val onInputEvent: Option[WatchOnEvent] = key.get(Keys.watchOnInputEvent) - val onIteration: Option[Int => Watched.Action] = key.get(Keys.watchOnIteration) + val onIteration: Option[Int => Watch.Action] = key.get(Keys.watchOnIteration) val onMetaBuildEvent: Option[WatchOnEvent] = key.get(Keys.watchOnMetaBuildEvent) - val onStart: Option[Arguments => () => Watched.Action] = key.get(Keys.watchOnStart) - val onTermination: Option[(Watched.Action, String, Int, State) => State] = + val onStart: Option[Arguments => () => Watch.Action] = key.get(Keys.watchOnStart) + val onTermination: Option[(Watch.Action, String, Int, State) => State] = key.get(Keys.watchOnTermination) val onTrigger: Option[Arguments => Event => Unit] = key.get(Keys.watchOnTrigger) val onTriggerEvent: Option[WatchOnEvent] = key.get(Keys.watchOnTriggerEvent) @@ -758,12 +759,12 @@ object Continuous extends DeprecatedContinuous { def arguments(logger: Logger): Arguments = new Arguments(logger, inputs, triggers) } private def getStartMessage(key: ScopedKey[_])(implicit e: Extracted): StartMessage = Some { - lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watched.defaultStartWatch) + lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watch.defaultStartWatch) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) } private def getTriggerMessage(key: ScopedKey[_])(implicit e: Extracted): TriggerMessage = Some { lazy val default = - key.get(Keys.watchTriggeredMessage).getOrElse(Watched.defaultOnTriggerMessage) + key.get(Keys.watchTriggeredMessage).getOrElse(Watch.defaultOnTriggerMessage) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) } diff --git a/main/src/main/scala/sbt/internal/GlobLister.scala b/main/src/main/scala/sbt/internal/GlobLister.scala index 90d0aaa3e..03483b312 100644 --- a/main/src/main/scala/sbt/internal/GlobLister.scala +++ b/main/src/main/scala/sbt/internal/GlobLister.scala @@ -20,47 +20,47 @@ import sbt.io.Glob private[sbt] sealed trait GlobLister extends Any { /** - * Get the sources described this [[GlobLister]]. + * Get the sources described this `GlobLister`. * * @param repository the [[FileTree.Repository]] to delegate file i/o. - * @return the files described by this [[GlobLister]]. + * @return the files described by this `GlobLister`. */ def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] /** - * Get the unique sources described this [[GlobLister]]. + * Get the unique sources described this `GlobLister`. * * @param repository the [[FileTree.Repository]] to delegate file i/o. - * @return the files described by this [[GlobLister]] with any duplicates removed. + * @return the files described by this `GlobLister` with any duplicates removed. */ def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] } /** - * Provides implicit definitions to provide a [[GlobLister]] given a Glob or + * Provides implicit definitions to provide a `GlobLister` given a Glob or * Traversable[Glob]. */ -object GlobLister extends GlobListers +private[sbt] object GlobLister extends GlobListers /** - * Provides implicit definitions to provide a [[GlobLister]] given a Glob or + * Provides implicit definitions to provide a `GlobLister` given a Glob or * Traversable[Glob]. */ private[sbt] trait GlobListers { import GlobListers._ /** - * Generate a [[GlobLister]] given a particular [[Glob]]s. + * Generate a GlobLister given a particular [[Glob]]s. * * @param source the input Glob */ implicit def fromGlob(source: Glob): GlobLister = new impl(source :: Nil) /** - * Generate a [[GlobLister]] given a collection of Globs. If the input collection type - * preserves uniqueness, e.g. `Set[Glob]`, then the output of [[GlobLister.all]] will be + * Generate a GlobLister given a collection of Globs. If the input collection type + * preserves uniqueness, e.g. `Set[Glob]`, then the output of `GlobLister.all` will be * the unique source list. Otherwise duplicates are possible in all and it is necessary to call - * [[GlobLister.unique]] to de-duplicate the files. + * `GlobLister.unique` to de-duplicate the files. * * @param sources the collection of sources * @tparam T the source collection type @@ -71,7 +71,7 @@ private[sbt] trait GlobListers { private[internal] object GlobListers { /** - * Implements [[GlobLister]] given a collection of Globs. If the input collection type + * Implements `GlobLister` given a collection of Globs. If the input collection type * preserves uniqueness, e.g. `Set[Glob]`, then the output will be the unique source list. * Otherwise duplicates are possible. * diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main/src/test/scala/sbt/WatchSpec.scala similarity index 90% rename from main-command/src/test/scala/sbt/WatchedSpec.scala rename to main/src/test/scala/sbt/WatchSpec.scala index 538321c9a..c5139a228 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main/src/test/scala/sbt/WatchSpec.scala @@ -12,8 +12,8 @@ import java.nio.file.{ Files, Path } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger } import org.scalatest.{ FlatSpec, Matchers } -import sbt.Watched._ -import sbt.WatchedSpec._ +import sbt.Watch.{ NullLogger, _ } +import sbt.WatchSpec._ import sbt.internal.FileAttributes import sbt.io.FileEventMonitor.Event import sbt.io._ @@ -23,18 +23,18 @@ import sbt.util.Logger import scala.collection.mutable import scala.concurrent.duration._ -class WatchedSpec extends FlatSpec with Matchers { - private type NextAction = () => Watched.Action - private def watch(task: Task, callbacks: (NextAction, NextAction)): Watched.Action = - Watched.watch(task, callbacks._1, callbacks._2) +class WatchSpec extends FlatSpec with Matchers { + private type NextAction = () => Watch.Action + private def watch(task: Task, callbacks: (NextAction, NextAction)): Watch.Action = + Watch(task, callbacks._1, callbacks._2) object TestDefaults { def callbacks( inputs: Seq[Glob], fileEventMonitor: Option[FileEventMonitor[FileAttributes]] = None, logger: Logger = NullLogger, - parseEvent: () => Action = () => Ignore, - onStartWatch: () => Action = () => CancelWatch: Action, - onWatchEvent: Event[FileAttributes] => Action = _ => Ignore, + parseEvent: () => Watch.Action = () => Ignore, + onStartWatch: () => Watch.Action = () => CancelWatch: Watch.Action, + onWatchEvent: Event[FileAttributes] => Watch.Action = _ => Ignore, triggeredMessage: Event[FileAttributes] => Option[String] = _ => None, watchingMessage: () => Option[String] = () => None ): (NextAction, NextAction) = { @@ -57,7 +57,7 @@ class WatchedSpec extends FlatSpec with Matchers { val onTrigger: Event[FileAttributes] => Unit = event => { triggeredMessage(event).foreach(logger.info(_)) } - val onStart: () => Watched.Action = () => { + val onStart: () => Watch.Action = () => { watchingMessage().foreach(logger.info(_)) onStartWatch() } @@ -66,7 +66,7 @@ class WatchedSpec extends FlatSpec with Matchers { val fileActions = monitor.poll(10.millis).map { e: Event[FileAttributes] => onWatchEvent(e) match { case Trigger => onTrigger(e); Trigger - case act => act + case action => action } } (inputAction +: fileActions).min @@ -86,7 +86,7 @@ class WatchedSpec extends FlatSpec with Matchers { } def getCount: Int = count.get() } - "Watched.watch" should "stop" in IO.withTemporaryDirectory { dir => + "Watch" should "stop" in IO.withTemporaryDirectory { dir => val task = new Task watch(task, TestDefaults.callbacks(inputs = Seq(dir.toRealPath ** AllPassFilter))) shouldBe CancelWatch } @@ -168,7 +168,7 @@ class WatchedSpec extends FlatSpec with Matchers { } } -object WatchedSpec { +object WatchSpec { implicit class FileOps(val f: File) { def toRealPath: File = f.toPath.toRealPath().toFile } diff --git a/sbt/src/sbt-test/watch/command-parser/build.sbt b/sbt/src/sbt-test/watch/command-parser/build.sbt index 1e26f0e48..dbd347c9b 100644 --- a/sbt/src/sbt-test/watch/command-parser/build.sbt +++ b/sbt/src/sbt-test/watch/command-parser/build.sbt @@ -10,4 +10,4 @@ checkStringValue := checkStringValueImpl.evaluated setStringValue / watchTriggers := baseDirectory.value * "string.txt" :: Nil -watchOnEvent := { _ => _ => Watched.CancelWatch } +watchOnEvent := { _ => _ => Watch.CancelWatch } diff --git a/sbt/src/sbt-test/watch/custom-config/project/Build.scala b/sbt/src/sbt-test/watch/custom-config/project/Build.scala index 2696d5c75..a9acf4e87 100644 --- a/sbt/src/sbt-test/watch/custom-config/project/Build.scala +++ b/sbt/src/sbt-test/watch/custom-config/project/Build.scala @@ -31,10 +31,10 @@ object Build { setStringValueImpl.evaluated }, checkStringValue := checkStringValueImpl.evaluated, - watchOnEvent := { _ => _ => Watched.CancelWatch } + watchOnEvent := { _ => _ => Watch.CancelWatch } ) lazy val bar = project.settings(fileInputs in setStringValue += baseDirectory.value * "foo.txt") lazy val root = (project in file(".")).aggregate(foo, bar).settings( - watchOnEvent := { _ => _ => Watched.CancelWatch } + watchOnEvent := { _ => _ => Watch.CancelWatch } ) -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala b/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala index eca66f9b5..d99c46b54 100644 --- a/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala +++ b/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala @@ -30,9 +30,9 @@ object Build { setStringValueImpl.evaluated }, checkStringValue := checkStringValueImpl.evaluated, - watchOnTriggerEvent := { (_, _) => Watched.CancelWatch }, - watchOnInputEvent := { (_, _) => Watched.CancelWatch }, - Compile / compile / watchOnStart := { _ => () => Watched.CancelWatch }, + watchOnTriggerEvent := { (_, _) => Watch.CancelWatch }, + watchOnInputEvent := { (_, _) => Watch.CancelWatch }, + Compile / compile / watchOnStart := { _ => () => Watch.CancelWatch }, checkTriggers := { val actual = (Compile / compile / transitiveTriggers).value.toSet val base = baseDirectory.value.getParentFile @@ -83,7 +83,7 @@ object Build { checkGlobs := checkGlobsImpl.value ) lazy val root = (project in file(".")).aggregate(foo, bar).settings( - watchOnEvent := { _ => _ => Watched.CancelWatch }, + watchOnEvent := { _ => _ => Watch.CancelWatch }, checkTriggers := { val actual = (Compile / compile / transitiveTriggers).value val expected: Seq[Glob] = baseDirectory.value * "baz.txt" :: Nil @@ -91,4 +91,4 @@ object Build { }, checkGlobs := checkGlobsImpl.value ) -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/watch/input-parser/build.sbt b/sbt/src/sbt-test/watch/input-parser/build.sbt index 5de1267dc..0e7f77deb 100644 --- a/sbt/src/sbt-test/watch/input-parser/build.sbt +++ b/sbt/src/sbt-test/watch/input-parser/build.sbt @@ -1,9 +1 @@ -import sbt.input.parser.Build - -watchInputStream := Build.inputStream - -watchStartMessage := { count => - Build.outputStream.write('\n'.toByte) - Build.outputStream.flush() - Some("default start message") -} \ No newline at end of file +val root = sbt.input.parser.Build.root diff --git a/sbt/src/sbt-test/watch/input-parser/project/Build.scala b/sbt/src/sbt-test/watch/input-parser/project/Build.scala index d430bdb76..2bb9dde58 100644 --- a/sbt/src/sbt-test/watch/input-parser/project/Build.scala +++ b/sbt/src/sbt-test/watch/input-parser/project/Build.scala @@ -5,15 +5,25 @@ import complete.Parser import complete.Parser._ import java.io.{ PipedInputStream, PipedOutputStream } +import Keys._ object Build { + val root = (project in file(".")).settings( + useSuperShell := false, + watchInputStream := inputStream, + watchStartMessage := { count => + Build.outputStream.write('\n'.toByte) + Build.outputStream.flush() + Some("default start message") + } + ) val outputStream = new PipedOutputStream() val inputStream = new PipedInputStream(outputStream) - val byeParser: Parser[Watched.Action] = "bye" ^^^ Watched.CancelWatch - val helloParser: Parser[Watched.Action] = "hello" ^^^ Watched.Ignore + val byeParser: Parser[Watch.Action] = "bye" ^^^ Watch.CancelWatch + val helloParser: Parser[Watch.Action] = "hello" ^^^ Watch.Ignore // Note that the order is byeParser | helloParser. In general, we want the higher priority // action to come first because otherwise we would potentially scan past it. - val helloOrByeParser: Parser[Watched.Action] = byeParser | helloParser + val helloOrByeParser: Parser[Watch.Action] = byeParser | helloParser val alternativeStartMessage: Int => Option[String] = { _ => outputStream.write("xybyexyblahxyhelloxy".getBytes) outputStream.flush() diff --git a/sbt/src/sbt-test/watch/input-parser/test b/sbt/src/sbt-test/watch/input-parser/test index 981496f0b..8169581ba 100644 --- a/sbt/src/sbt-test/watch/input-parser/test +++ b/sbt/src/sbt-test/watch/input-parser/test @@ -7,8 +7,8 @@ > set watchInputParser := sbt.input.parser.Build.helloOrByeParser # this should exit because we write "xybyexyblahxyhelloxy" to Build.outputStream. The -# helloOrByeParser will produce Watched.Ignore and Watched.CancelWatch but the -# Watched.CancelWatch event should win. +# helloOrByeParser will produce Watch.Ignore and Watch.CancelWatch but the +# Watch.CancelWatch event should win. > ~ compile > set watchStartMessage := sbt.input.parser.Build.otherAlternativeStartMessage diff --git a/sbt/src/sbt-test/watch/legacy-sources/build.sbt b/sbt/src/sbt-test/watch/legacy-sources/build.sbt index 5ee39a863..bd3099063 100644 --- a/sbt/src/sbt-test/watch/legacy-sources/build.sbt +++ b/sbt/src/sbt-test/watch/legacy-sources/build.sbt @@ -8,6 +8,6 @@ setStringValue := setStringValueImpl.evaluated checkStringValue := checkStringValueImpl.evaluated -watchOnTriggerEvent := { (_, _) => Watched.CancelWatch } -watchOnInputEvent := { (_, _) => Watched.CancelWatch } -watchOnMetaBuildEvent := { (_, _) => Watched.CancelWatch } +watchOnTriggerEvent := { (_, _) => Watch.CancelWatch } +watchOnInputEvent := { (_, _) => Watch.CancelWatch } +watchOnMetaBuildEvent := { (_, _) => Watch.CancelWatch } diff --git a/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt b/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt index e8b658ba1..36ddd06e3 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt +++ b/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt @@ -1,4 +1,4 @@ val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded") checkReloaded := { () } -watchOnIteration := { _ => Watched.CancelWatch } +watchOnIteration := { _ => Watch.CancelWatch } diff --git a/sbt/src/sbt-test/watch/on-start-watch/extra.sbt b/sbt/src/sbt-test/watch/on-start-watch/extra.sbt index 6af4f2331..d6ee90d91 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/extra.sbt +++ b/sbt/src/sbt-test/watch/on-start-watch/extra.sbt @@ -1 +1 @@ -watchOnStart := { _ => () => Watched.Reload } +watchOnStart := { _ => () => Watch.Reload } diff --git a/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala b/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala index db2258f5c..67d3bf940 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala +++ b/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala @@ -4,11 +4,7 @@ import scala.util.Try object Count { private var count = 0 def get: Int = count - def increment(): Unit = { - count += 1 - } - def reset(): Unit = { - count = 0 - } + def increment(): Unit = count += 1 + def reset(): Unit = count = 0 def reloadCount(file: File): Int = Try(IO.read(file).toInt).getOrElse(0) } diff --git a/sbt/src/sbt-test/watch/on-start-watch/test b/sbt/src/sbt-test/watch/on-start-watch/test index f550e66b6..a917df61d 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/test +++ b/sbt/src/sbt-test/watch/on-start-watch/test @@ -1,4 +1,4 @@ -# verify that reloading occurs if watchOnStart returns Watched.Reload +# verify that reloading occurs if watchOnStart returns Watch.Reload $ copy-file changes/extra.sbt extra.sbt > ~compile @@ -6,19 +6,19 @@ $ copy-file changes/extra.sbt extra.sbt # verify that the watch terminates when we reach the specified count > resetCount -> set watchOnIteration := { (count: Int) => if (count == 2) Watched.CancelWatch else Watched.Ignore } +> set watchOnIteration := { (count: Int) => if (count == 2) Watch.CancelWatch else Watch.Ignore } > ~compile > checkCount 2 # verify that the watch terminates and returns an error when we reach the specified count > resetCount -> set watchOnIteration := { (count: Int) => if (count == 2) new Watched.HandleError(new Exception("")) else Watched.Ignore } -# Returning Watched.HandleError causes the '~' command to fail +> set watchOnIteration := { (count: Int) => if (count == 2) new Watch.HandleError(new Exception("")) else Watch.Ignore } +# Returning Watch.HandleError causes the '~' command to fail -> ~compile > checkCount 2 # verify that a re-build is triggered when we reach the specified count > resetCount -> set watchOnIteration := { (count: Int) => if (count == 2) Watched.Trigger else if (count == 3) Watched.CancelWatch else Watched.Ignore } +> set watchOnIteration := { (count: Int) => if (count == 2) Watch.Trigger else if (count == 3) Watch.CancelWatch else Watch.Ignore } > ~compile > checkCount 3 diff --git a/sbt/src/sbt-test/watch/task/changes/Build.scala b/sbt/src/sbt-test/watch/task/changes/Build.scala index 1c5162d37..abb5f37b9 100644 --- a/sbt/src/sbt-test/watch/task/changes/Build.scala +++ b/sbt/src/sbt-test/watch/task/changes/Build.scala @@ -22,6 +22,6 @@ object Build { IO.touch(baseDirectory.value / "foo.txt", true) Some("watching") }, - watchOnStart := { _ => () => Watched.CancelWatch } + watchOnStart := { _ => () => Watch.CancelWatch } ) -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/watch/task/project/Build.scala b/sbt/src/sbt-test/watch/task/project/Build.scala index f0beda1c1..f1a6adbd9 100644 --- a/sbt/src/sbt-test/watch/task/project/Build.scala +++ b/sbt/src/sbt-test/watch/task/project/Build.scala @@ -25,9 +25,9 @@ object Build { Some("watching") }, watchOnTriggerEvent := { (f, e) => - if (reloadFile.value.exists) Watched.CancelWatch else { + if (reloadFile.value.exists) Watch.CancelWatch else { IO.touch(reloadFile.value, true) - Watched.Reload + Watch.Reload } } ) diff --git a/sbt/src/sbt-test/watch/task/test b/sbt/src/sbt-test/watch/task/test index b3e02de19..93022959f 100644 --- a/sbt/src/sbt-test/watch/task/test +++ b/sbt/src/sbt-test/watch/task/test @@ -1,7 +1,7 @@ # this tests that if the watch _task_ is able to reload the project -# the original version of the build will only return Watched.Reload for trigger events while the -# updated version will return Watched.CancelWatch. If this test exits, it more or less works. +# the original version of the build will only return Watch.Reload for trigger events while the +# updated version will return Watch.CancelWatch. If this test exits, it more or less works. $ copy-file changes/Build.scala project/Build.scala # setStringValue has foo.txt as a watch source so running that command should first trigger a