mirror of https://github.com/sbt/sbt.git
Merge pull request #4859 from eatkins/watch-defaults
Rework watch options
This commit is contained in:
commit
8d20bd4c94
|
|
@ -28,7 +28,7 @@ import sbt.internal.util.complete.{ Parser, Parsers }
|
|||
import sbt.internal.util.{ AttributeKey, JLine, Util }
|
||||
import sbt.nio.FileStamper.LastModified
|
||||
import sbt.nio.Keys.{ fileInputs, _ }
|
||||
import sbt.nio.Watch.{ Creation, Deletion, Update }
|
||||
import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update }
|
||||
import sbt.nio.file.FileAttributes
|
||||
import sbt.nio.{ FileStamp, FileStamper, Watch }
|
||||
import sbt.util.{ Level, _ }
|
||||
|
|
@ -468,11 +468,13 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
val beforeCommand = () => configs.foreach(_.watchSettings.beforeCommand())
|
||||
val onStart: () => Watch.Action =
|
||||
getOnStart(project, commands, configs, rawLogger, count, extracted)
|
||||
val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger)
|
||||
val (message, parser, altParser) = getWatchInputOptions(configs, extracted)
|
||||
val nextInputEvent: () => Watch.Action =
|
||||
parseInputEvents(parser, altParser, state, inputStream, logger)
|
||||
val (nextFileEvent, cleanupFileMonitor): (() => Option[(Watch.Event, Watch.Action)], () => Unit) =
|
||||
getFileEvents(configs, rawLogger, state, count, commands, fileStampCache)
|
||||
val nextEvent: () => Watch.Action =
|
||||
combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger)
|
||||
combineInputAndFileEvents(nextInputEvent, nextFileEvent, message, logger, rawLogger)
|
||||
val onExit = () => cleanupFileMonitor()
|
||||
val onTermination = getOnTermination(configs, isCommand)
|
||||
new Callbacks(nextEvent, beforeCommand, onExit, onStart, onTermination)
|
||||
|
|
@ -494,6 +496,38 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
}
|
||||
}
|
||||
|
||||
private def getWatchInputOptions(
|
||||
configs: Seq[Config],
|
||||
extracted: Extracted
|
||||
): (String, Parser[Watch.Action], Option[(TaskKey[InputStream], InputStream => Watch.Action)]) = {
|
||||
configs match {
|
||||
case Seq(h) =>
|
||||
val settings = h.watchSettings
|
||||
val parser = settings.inputParser
|
||||
val alt = settings.inputStream.map { k =>
|
||||
k -> settings.inputHandler.getOrElse(defaultInputHandler(parser))
|
||||
}
|
||||
(settings.inputOptionsMessage, parser, alt)
|
||||
case _ =>
|
||||
val options =
|
||||
extracted.getOpt(watchInputOptions in ThisBuild).getOrElse(Watch.defaultInputOptions)
|
||||
val message = extracted
|
||||
.getOpt(watchInputOptionsMessage in ThisBuild)
|
||||
.getOrElse(Watch.defaultInputOptionsMessage(options))
|
||||
val parser = extracted
|
||||
.getOpt(watchInputParser in ThisBuild)
|
||||
.getOrElse(Watch.defaultInputParser(options))
|
||||
val alt = extracted
|
||||
.getOpt(watchInputStream in ThisBuild)
|
||||
.map { _ =>
|
||||
(watchInputStream in ThisBuild) -> extracted
|
||||
.getOpt(watchInputHandler in ThisBuild)
|
||||
.getOrElse(defaultInputHandler(parser))
|
||||
}
|
||||
(message, parser, alt)
|
||||
}
|
||||
}
|
||||
|
||||
private def getOnStart(
|
||||
project: ProjectRef,
|
||||
commands: Seq[String],
|
||||
|
|
@ -708,7 +742,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
}
|
||||
logger.debug(s"Received file event actions: $builder. Returning: $min")
|
||||
if (min._2 == Watch.Trigger) onTrigger(min._1)
|
||||
Some(min)
|
||||
if (min._2 == Watch.ShowOptions) None else Some(min)
|
||||
} else None
|
||||
}, () => monitor.close())
|
||||
}
|
||||
|
|
@ -729,7 +763,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
* `() => Seq[Watch.Action]` which avoids actually exposing the InputStream anywhere.
|
||||
*/
|
||||
private def parseInputEvents(
|
||||
configs: Seq[Config],
|
||||
parser: Parser[Watch.Action],
|
||||
alternative: Option[(TaskKey[InputStream], InputStream => Watch.Action)],
|
||||
state: State,
|
||||
inputStream: InputStream,
|
||||
logger: Logger
|
||||
|
|
@ -751,10 +786,9 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
// If the Config.watchSettings.inputStream is set, the same process is applied except that
|
||||
// instead of passing in the wrapped InputStream for the input string, we directly pass
|
||||
// in the inputStream provided by Config.watchSettings.inputStream.
|
||||
val inputHandlers: Seq[ActionParser] = configs.map { c =>
|
||||
val inputHandler: String => Watch.Action = {
|
||||
val any = Parsers.any.*
|
||||
val inputParser = c.watchSettings.inputParser
|
||||
val parser = any ~> inputParser ~ matched(any)
|
||||
val fullParser = any ~> parser ~ matched(any)
|
||||
// Each parser gets its own copy of System.in that it can modify while parsing.
|
||||
val systemInBuilder = new StringBuilder
|
||||
|
||||
|
|
@ -762,34 +796,33 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
|
||||
// This string is provided in the closure below by reading from System.in
|
||||
val default: String => Watch.Action =
|
||||
string => parse(inputStream(string), systemInBuilder, parser)
|
||||
val alternative = c.watchSettings.inputStream
|
||||
.map { inputStreamKey =>
|
||||
val is = extracted.runTask(inputStreamKey, state)._2
|
||||
val handler = c.watchSettings.inputHandler.getOrElse(defaultInputHandler(inputParser))
|
||||
() => handler(is)
|
||||
string => parse(inputStream(string), systemInBuilder, fullParser)
|
||||
val alt = alternative
|
||||
.map {
|
||||
case (key, handler) =>
|
||||
val is = extracted.runTask(key, state)._2
|
||||
() => handler(is)
|
||||
}
|
||||
.getOrElse(() => Watch.Ignore)
|
||||
(string: String) => (default(string) :: alternative() :: Nil).min
|
||||
string: String => (default(string) :: alt() :: Nil).min
|
||||
}
|
||||
() => {
|
||||
val stringBuilder = new StringBuilder
|
||||
while (inputStream.available > 0) stringBuilder += inputStream.read().toChar
|
||||
val newBytes = stringBuilder.toString
|
||||
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 Watch.Ignore
|
||||
val event = parse(inputHandler)
|
||||
if (event != Watch.Ignore) logger.debug(s"Received input event: $event.")
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
private def combineInputAndFileEvents(
|
||||
nextInputAction: () => Watch.Action,
|
||||
nextFileEvent: () => Option[(Watch.Event, Watch.Action)],
|
||||
logger: Logger
|
||||
options: String,
|
||||
logger: Logger,
|
||||
rawLogger: Logger
|
||||
): () => Watch.Action = () => {
|
||||
val (inputAction: Watch.Action, fileEvent: Option[(Watch.Event, Watch.Action)] @unchecked) =
|
||||
Seq(nextInputAction, nextFileEvent).map(_.apply()).toIndexedSeq match {
|
||||
|
|
@ -808,7 +841,13 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
(if (action != min) s" Dropping in favor of input event: $min" else "")
|
||||
}
|
||||
.foreach(logger.debug(_))
|
||||
min
|
||||
min match {
|
||||
case ShowOptions =>
|
||||
println("") // This is so the [info] tag appears at the head of a newline
|
||||
rawLogger.info(options)
|
||||
Watch.Ignore
|
||||
case m => m
|
||||
}
|
||||
}
|
||||
|
||||
@tailrec
|
||||
|
|
@ -896,8 +935,12 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
val deletionQuarantinePeriod: FiniteDuration =
|
||||
key.get(watchDeletionQuarantinePeriod).getOrElse(Watch.defaultDeletionQuarantinePeriod)
|
||||
val inputHandler: Option[InputStream => Watch.Action] = key.get(watchInputHandler)
|
||||
val inputOptions: Seq[Watch.InputOption] =
|
||||
key.get(watchInputOptions).getOrElse(Watch.defaultInputOptions)
|
||||
val inputOptionsMessage: String =
|
||||
key.get(watchInputOptionsMessage).getOrElse(Watch.defaultInputOptionsMessage(inputOptions))
|
||||
val inputParser: Parser[Watch.Action] =
|
||||
key.get(watchInputParser).getOrElse(Watch.defaultInputParser)
|
||||
key.get(watchInputParser).getOrElse(Watch.defaultInputParser(inputOptions))
|
||||
val logLevel: Level.Value = key.get(watchLogLevel).getOrElse(Level.Info)
|
||||
val beforeCommand: () => Unit = key.get(watchBeforeCommand).getOrElse(() => {})
|
||||
val onFileInputEvent: WatchOnEvent =
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ object Keys {
|
|||
private[sbt] 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 watchInputOptionsMessage = settingKey[String]("The help message for the watch input options")
|
||||
val watchInputOptions = settingKey[Seq[Watch.InputOption]]("The available input options")
|
||||
val watchInputStream = taskKey[InputStream](
|
||||
"The input stream to read for user input events. This will usually be System.in"
|
||||
).withRank(DSetting)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import sbt.nio.file.FileAttributes
|
|||
import sbt.util.{ Level, Logger }
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
|
|
@ -164,8 +165,9 @@ object Watch {
|
|||
*/
|
||||
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
|
||||
case ShowOptions => if (right == ShowOptions) 0 else -1
|
||||
case Ignore => if (right == Ignore) 0 else 1
|
||||
case Trigger => if (right == Trigger) 0 else if (right == ShowOptions) 1 else -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -236,11 +238,24 @@ object Watch {
|
|||
override def toString: String = s"HandleUnexpectedError($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. This trait can be used when we don't
|
||||
* want to take action but we do want to perform a side effect like printing options to the
|
||||
* terminal.
|
||||
*/
|
||||
sealed trait Ignore extends ContinueWatch
|
||||
|
||||
/**
|
||||
* Action that indicates that the available options should be printed.
|
||||
*/
|
||||
case object ShowOptions extends Ignore
|
||||
|
||||
/**
|
||||
* 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
|
||||
case object Ignore extends Ignore
|
||||
|
||||
/**
|
||||
* Action that indicates that the watch should pause while the build is reloaded. This is used to
|
||||
|
|
@ -258,7 +273,8 @@ object Watch {
|
|||
}
|
||||
// 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 {
|
||||
object Run {
|
||||
def apply(commands: String*): Run = new Watch.Run(commands: _*)
|
||||
def unapply(r: Run): Option[List[Exec]] = Some(r.commands.toList.map(Exec(_, None)))
|
||||
}
|
||||
|
||||
|
|
@ -274,6 +290,45 @@ object Watch {
|
|||
*/
|
||||
trait Custom extends CancelWatch
|
||||
|
||||
trait InputOption {
|
||||
private[sbt] def parser: Parser[Watch.Action]
|
||||
def input: String
|
||||
def display: String
|
||||
def description: String
|
||||
override def toString: String =
|
||||
s"InputOption(input = $input, display = $display, description = $description)"
|
||||
}
|
||||
object InputOption {
|
||||
private class impl(
|
||||
override val input: String,
|
||||
override val display: String,
|
||||
override val description: String,
|
||||
action: Action
|
||||
) extends InputOption {
|
||||
override private[sbt] def parser: Parser[Watch.Action] = input ^^^ action
|
||||
}
|
||||
def apply(key: Char, description: String, action: Action): InputOption =
|
||||
new impl(key.toString, s"'$key'", description, action)
|
||||
def apply(key: Char, display: String, description: String, action: Action): InputOption =
|
||||
new impl(key.toString, display, description, action)
|
||||
def apply(
|
||||
display: String,
|
||||
description: String,
|
||||
action: Action,
|
||||
chars: Char*
|
||||
): InputOption =
|
||||
new impl(chars.mkString("|"), display, description, action) {
|
||||
override private[sbt] def parser: Parser[Watch.Action] = chars match {
|
||||
case Seq(c) => c ^^^ action
|
||||
case Seq(h, rest @ _*) => rest.foldLeft(h: Parser[Char])(_ | _) ^^^ action
|
||||
}
|
||||
}
|
||||
def apply(input: String, description: String, action: Action): InputOption =
|
||||
new impl(input, s"'$input'", description, action)
|
||||
def apply(input: String, display: String, description: String, action: Action): InputOption =
|
||||
new impl(input, display, description, action)
|
||||
}
|
||||
|
||||
private type NextAction = () => Watch.Action
|
||||
|
||||
/**
|
||||
|
|
@ -385,27 +440,44 @@ object Watch {
|
|||
* 2) 'r' or 'R' will trigger a build
|
||||
* 3) new line characters cancel the watch and return to the shell
|
||||
*/
|
||||
final val defaultInputParser: Parser[Action] = {
|
||||
val exitParser: Parser[Action] = chars("xX") ^^^ new Run("exit")
|
||||
val rebuildParser: Parser[Action] = chars("rR") ^^^ Trigger
|
||||
val cancelParser: Parser[Action] = chars(legal = "\n\r") ^^^ new Run("iflast shell")
|
||||
exitParser | rebuildParser | cancelParser
|
||||
}
|
||||
final def defaultInputParser(options: Seq[Watch.InputOption]): Parser[Action] =
|
||||
distinctOptions(options) match {
|
||||
case Seq() => (('\n': Parser[Char]) | '\r' | 4.toChar) ^^^ Run("")
|
||||
case Seq(h, rest @ _*) => rest.foldLeft(h.parser)(_ | _.parser)
|
||||
}
|
||||
final val defaultInputOptions: Seq[Watch.InputOption] = Seq(
|
||||
Watch.InputOption("<enter>", "interrupt (exits sbt in batch mode)", Run(""), '\n', '\r'),
|
||||
Watch.InputOption(4.toChar, "<ctrl-d>", "interrupt (exits sbt in batch mode)", Run("")),
|
||||
Watch.InputOption('r', "re-run the command", Trigger),
|
||||
Watch.InputOption('s', "return to shell", Run("iflast shell")),
|
||||
Watch.InputOption('q', "quit sbt", Run("exit")),
|
||||
Watch.InputOption('?', "print options", ShowOptions)
|
||||
)
|
||||
|
||||
private[this] val options = {
|
||||
val enter = "<enter>"
|
||||
val opts = Seq(
|
||||
s"$enter: return to the shell",
|
||||
s"'r': repeat the current command",
|
||||
s"'x': exit sbt"
|
||||
)
|
||||
s"Options:\n${opts.mkString(" ", "\n ", "")}"
|
||||
def defaultInputOptionsMessage(options: Seq[InputOption]): String = {
|
||||
val opts = distinctOptions(options).sortBy(_.input)
|
||||
val alignmentLength = opts.map(_.display.length).max + 1
|
||||
val formatted =
|
||||
opts.map(o => s"${o.display}${" " * (alignmentLength - o.display.length)}: ${o.description}")
|
||||
s"Options:\n${formatted.mkString(" ", "\n ", "")}"
|
||||
}
|
||||
private def waitMessage(project: ProjectRef, commands: Seq[String]): String = {
|
||||
val plural = if (commands.size > 1) "s" else ""
|
||||
val cmds = commands.mkString("; ")
|
||||
s"Monitoring source files for updates...\n" +
|
||||
s"Project: ${project.project}\nCommand$plural: $cmds\n$options"
|
||||
private def distinctOptions(options: Seq[InputOption]): Seq[InputOption] = {
|
||||
val distinctOpts = mutable.Set.empty[String]
|
||||
val opts = new mutable.ArrayBuffer[InputOption]
|
||||
(options match {
|
||||
case Seq() => defaultInputOptions.headOption.toSeq
|
||||
case s => s
|
||||
}).reverse.foreach { o =>
|
||||
if (distinctOpts.add(o.input)) opts += o
|
||||
}
|
||||
opts.reverse
|
||||
}
|
||||
private def waitMessage(project: ProjectRef, commands: Seq[String]): Seq[String] = {
|
||||
val cmds = commands.map(project.project + "/" + _.trim).mkString("; ")
|
||||
Seq(
|
||||
s"Monitoring source files for $cmds...",
|
||||
s"Press <enter> to interrupt or '?' for more options."
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -414,13 +486,16 @@ object Watch {
|
|||
*/
|
||||
val defaultStartWatch: (Int, ProjectRef, Seq[String]) => Option[String] = {
|
||||
(count: Int, project: ProjectRef, commands: Seq[String]) =>
|
||||
Some(s"$count. ${waitMessage(project, commands)}")
|
||||
{
|
||||
val countStr = s"$count. "
|
||||
Some(s"$countStr${waitMessage(project, commands).mkString(s"\n${" " * countStr.length}")}")
|
||||
}
|
||||
}.label("Watched.defaultStartWatch")
|
||||
|
||||
/**
|
||||
* Default no-op callback.
|
||||
*/
|
||||
val defaultBeforeCommand: () => Unit = () => {}
|
||||
val defaultBeforeCommand: () => Unit = (() => {}).label("Watch.defaultBeforeCommand")
|
||||
|
||||
private[sbt] val defaultCommandOnTermination: (Action, String, Int, State) => State =
|
||||
onTerminationImpl(ContinuousExecutePrefix).label("Watched.defaultCommandOnTermination")
|
||||
|
|
@ -495,6 +570,7 @@ object Watch {
|
|||
watchOnFileInputEvent :== Watch.trigger,
|
||||
watchDeletionQuarantinePeriod :== Watch.defaultDeletionQuarantinePeriod,
|
||||
sbt.Keys.watchService :== Watched.newWatchService,
|
||||
watchInputOptions :== Watch.defaultInputOptions,
|
||||
watchStartMessage :== Watch.defaultStartWatch,
|
||||
watchTasks := Continuous.continuousTask.evaluated,
|
||||
sbt.Keys.aggregate in watchTasks :== false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue