mirror of https://github.com/sbt/sbt.git
Rework watch options
In this commit, I both restore some sbt 1.2.8 behavior and enhance the
api for setting keyboard shortcuts in watch. I change the default start
message to just show the watch count, the tasks that are being monitored
and, on a new line, the instructions to terminate the watch or show more
options.
Here's what it looks like:
[info] 1. Monitoring source files for spark/compile...
[info] Press <enter> to interrupt or '?' for more options.
?
[info] Options:
[info] <enter> : interrupt (exits sbt in batch mode)
[info] <ctrl-d> : interrupt (exits sbt in batch mode)
[info] 'r' : re-run the command
[info] 's' : return to shell
[info] 'q' : quit sbt
[info] '?' : print options
I also made it so that the new options can be added (and old options
removed) with the watchInputOptions key. For example, to add an option
to reload the build with the key 'l', you could add
ThisBuild / watchInputOptions += Watch.InputOption('l', "reload", Watch.Reload)
to your global build.sbt.
After adding that to my global ~/sbt/1.0/global.sbt file, the output of
'?' became:
[info] Options:
[info] <ctrl-d> : interrupt (exits sbt in batch mode)
[info] <enter> : interrupt (exits sbt in batch mode)
[info] '?' : print options
[info] 'l' : reload
[info] 'q' : quit sbt
[info] 'r' : re-run the command
[info] 's' : return to shell
This commit is contained in:
parent
680659210f
commit
263f00f3b2
|
|
@ -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