mirror of https://github.com/sbt/sbt.git
Add support to rebuild a '~' task by pressing 'r'
Sometimes a user may want to rerun their task even if the source files haven't changed. Presently this is a little annoying because you have to hit enter to stop the build and then up arrow or <ctrl+r> plus enter to rebuild. It's more convenient to just be able to press the 'r' key to re-run the task. To implement this, I had to make the watch task set up a jline terminal so that System.in would be character buffered instead of line buffered. Furthermore, I took advantage of the NonBlockingInputStream implementation provided by jline to wrap System.in. This was necessary because even with the jline terminal, System.in.available doesn't return > 0 until a newline character is entered. Instead, the NonBlockingInputStream does provide a peek api with a timeout that will return the next unread key off of System.in if there is one available. This can be use to proxy available in the WrappedNonBlockingInputStream. To ensure maximum user flexibility, I also update the watchHandleInput Key to take an InputStream and return an Action. This setting will now receive the wrapped System.in, which will allow the user to create their own keybindings for watch actions without needing to use jline themselves. Future work might make it more straightforward to go back to a line buffered input if that is what the user desires.
This commit is contained in:
parent
25e97f99f5
commit
dc4f705500
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.io.{ File, InputStream }
|
||||
import java.nio.file.FileSystems
|
||||
|
||||
import sbt.BasicCommandStrings.{
|
||||
|
|
@ -20,9 +20,9 @@ import sbt.BasicCommands.otherCommandParser
|
|||
import sbt.internal.LegacyWatched
|
||||
import sbt.internal.inc.Stamper
|
||||
import sbt.internal.io.{ EventMonitor, Source, WatchState }
|
||||
import sbt.internal.util.AttributeKey
|
||||
import sbt.internal.util.Types.const
|
||||
import sbt.internal.util.complete.DefaultParsers
|
||||
import sbt.internal.util.{ AttributeKey, JLine }
|
||||
import sbt.io.FileEventMonitor.Event
|
||||
import sbt.io._
|
||||
import sbt.util.{ Level, Logger }
|
||||
|
|
@ -108,25 +108,44 @@ object Watched {
|
|||
|
||||
type WatchSource = Source
|
||||
def terminateWatch(key: Int): Boolean = Watched.isEnter(key)
|
||||
/*
|
||||
* Without jline, checking for enter is nearly pointless because System.in.available will not
|
||||
* return a non-zero value until the user presses enter.
|
||||
*/
|
||||
@tailrec
|
||||
final def shouldTerminate: Boolean =
|
||||
(System.in.available > 0) && (terminateWatch(System.in.read()) || shouldTerminate)
|
||||
final val handleInput: () => Action = () => if (shouldTerminate) CancelWatch else Ignore
|
||||
val defaultStartWatch: Int => Option[String] = count =>
|
||||
Some(s"$count. Waiting for source changes... (press enter to interrupt)")
|
||||
|
||||
private def withCharBufferedStdIn[R](f: InputStream => R): R = JLine.usingTerminal { terminal =>
|
||||
val in = terminal.wrapInIfNeeded(System.in)
|
||||
try {
|
||||
while (in.available > 0) in.read()
|
||||
terminal.init()
|
||||
f(in)
|
||||
} finally {
|
||||
while (in.available > 0) in.read()
|
||||
terminal.reset()
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] final val handleInput: InputStream => Action = in => {
|
||||
@tailrec
|
||||
def scanInput(): Action = {
|
||||
if (in.available > 0) {
|
||||
in.read() match {
|
||||
case key if isEnter(key) => CancelWatch
|
||||
case key if isR(key) => Trigger
|
||||
case key if key >= 0 => scanInput()
|
||||
case _ => Ignore
|
||||
}
|
||||
} else {
|
||||
Ignore
|
||||
}
|
||||
}
|
||||
scanInput()
|
||||
}
|
||||
private def waitMessage(project: String): String =
|
||||
s"Waiting for source changes$project... (press enter to interrupt or 'r' to re-run the command)"
|
||||
val defaultStartWatch: Int => Option[String] = count => Some(s"$count. ${waitMessage("")}")
|
||||
@deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0")
|
||||
val defaultWatchingMessage: WatchState => String = ws => defaultStartWatch(ws.count).get
|
||||
def projectWatchingMessage(projectId: String): WatchState => String =
|
||||
ws => projectOnWatchMessage(projectId)(ws.count).get
|
||||
def projectOnWatchMessage(project: String): Int => Option[String] =
|
||||
count =>
|
||||
Some(
|
||||
s"$count. Waiting for source changes in project $project... (press enter to interrupt)"
|
||||
)
|
||||
count => Some(s"$count. ${waitMessage(s" in project $project")}")
|
||||
|
||||
val defaultOnTriggerMessage: Int => Option[String] = _ => None
|
||||
@deprecated(
|
||||
|
|
@ -182,6 +201,7 @@ object Watched {
|
|||
val PollDelay: FiniteDuration = 500.milliseconds
|
||||
val AntiEntropy: FiniteDuration = 40.milliseconds
|
||||
def isEnter(key: Int): Boolean = key == 10 || key == 13
|
||||
def isR(key: Int): Boolean = key == 82 || key == 114
|
||||
def printIfDefined(msg: String): Unit = if (!msg.isEmpty) System.out.println(msg)
|
||||
|
||||
private type RunCommand = () => State
|
||||
|
|
@ -231,7 +251,7 @@ object Watched {
|
|||
state: State,
|
||||
command: String,
|
||||
setup: WatchSetup,
|
||||
): State = {
|
||||
): State = withCharBufferedStdIn { in =>
|
||||
val (s0, config, newState) = setup(state, command)
|
||||
val failureCommandName = "SbtContinuousWatchOnFail"
|
||||
val onFail = Command.command(failureCommandName)(identity)
|
||||
|
|
@ -263,7 +283,7 @@ object Watched {
|
|||
case (status, Right(t)) => if (status.getOrElse(true)) t() else status
|
||||
case _ => throw new IllegalStateException("Should be unreachable")
|
||||
}
|
||||
val terminationAction = watch(task, config)
|
||||
val terminationAction = watch(in, task, config)
|
||||
config.onWatchTerminated(terminationAction, command, state)
|
||||
} else {
|
||||
config.logger.error(
|
||||
|
|
@ -274,6 +294,7 @@ object Watched {
|
|||
}
|
||||
|
||||
private[sbt] def watch(
|
||||
in: InputStream,
|
||||
task: () => Either[Exception, Boolean],
|
||||
config: WatchConfig
|
||||
): Action = {
|
||||
|
|
@ -284,7 +305,7 @@ object Watched {
|
|||
def impl(count: Int): Action = {
|
||||
@tailrec
|
||||
def nextAction(): Action = {
|
||||
config.handleInput() match {
|
||||
config.handleInput(in) match {
|
||||
case action @ (CancelWatch | HandleError | Reload | _: Custom) => action
|
||||
case Trigger => Trigger
|
||||
case _ =>
|
||||
|
|
@ -348,12 +369,8 @@ object Watched {
|
|||
HandleError
|
||||
}
|
||||
}
|
||||
try {
|
||||
impl(count = 1)
|
||||
} finally {
|
||||
config.fileEventMonitor.close()
|
||||
while (System.in.available() > 0) System.in.read()
|
||||
}
|
||||
try impl(count = 1)
|
||||
finally config.fileEventMonitor.close()
|
||||
}
|
||||
|
||||
@deprecated("Replaced by Watched.command", "1.3.0")
|
||||
|
|
@ -423,7 +440,7 @@ trait WatchConfig {
|
|||
* trigger. Usually this will read from System.in to react to user input.
|
||||
* @return an [[Watched.Action Action]] that will determine the next step in the watch.
|
||||
*/
|
||||
def handleInput(): Watched.Action
|
||||
def handleInput(inputStream: InputStream): Watched.Action
|
||||
|
||||
/**
|
||||
* This is run before each watch iteration and if it returns true, the watch is terminated.
|
||||
|
|
@ -496,7 +513,7 @@ object WatchConfig {
|
|||
def default(
|
||||
logger: Logger,
|
||||
fileEventMonitor: FileEventMonitor[StampedFile],
|
||||
handleInput: () => Watched.Action,
|
||||
handleInput: InputStream => Watched.Action,
|
||||
preWatch: (Int, Boolean) => Watched.Action,
|
||||
onWatchEvent: Event[StampedFile] => Watched.Action,
|
||||
onWatchTerminated: (Watched.Action, String, State) => State,
|
||||
|
|
@ -514,7 +531,7 @@ object WatchConfig {
|
|||
new WatchConfig {
|
||||
override def logger: Logger = l
|
||||
override def fileEventMonitor: FileEventMonitor[StampedFile] = fem
|
||||
override def handleInput(): Watched.Action = hi()
|
||||
override def handleInput(inputStream: InputStream): Watched.Action = hi(inputStream)
|
||||
override def preWatch(count: Int, lastResult: Boolean): Watched.Action =
|
||||
pw(count, lastResult)
|
||||
override def onWatchEvent(event: Event[StampedFile]): Watched.Action = owe(event)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.io.{ File, InputStream }
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
sources: Seq[WatchSource],
|
||||
fileEventMonitor: Option[FileEventMonitor[StampedFile]] = None,
|
||||
logger: Logger = NullLogger,
|
||||
handleInput: () => Action = () => Ignore,
|
||||
handleInput: InputStream => Action = _ => Ignore,
|
||||
preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch,
|
||||
onWatchEvent: Event[StampedFile] => Action = _ => Ignore,
|
||||
triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None,
|
||||
|
|
@ -49,9 +49,13 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
)
|
||||
}
|
||||
}
|
||||
object NullInputStream extends InputStream {
|
||||
override def available(): Int = 0
|
||||
override def read(): Int = -1
|
||||
}
|
||||
"Watched.watch" should "stop" in IO.withTemporaryDirectory { dir =>
|
||||
val config = Defaults.config(sources = Seq(WatchSource(dir.toRealPath)))
|
||||
Watched.watch(() => Right(true), config) shouldBe CancelWatch
|
||||
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||
}
|
||||
it should "trigger" in IO.withTemporaryDirectory { dir =>
|
||||
val triggered = new AtomicBoolean(false)
|
||||
|
|
@ -63,7 +67,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
new File(dir, "file").createNewFile; None
|
||||
}
|
||||
)
|
||||
Watched.watch(() => Right(true), config) shouldBe CancelWatch
|
||||
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||
assert(triggered.get())
|
||||
}
|
||||
it should "filter events" in IO.withTemporaryDirectory { dir =>
|
||||
|
|
@ -78,7 +82,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
triggeredMessage = (tp, _) => { queue += tp; None },
|
||||
watchingMessage = _ => { Files.createFile(bar); Thread.sleep(5); Files.createFile(foo); None }
|
||||
)
|
||||
Watched.watch(() => Right(true), config) shouldBe CancelWatch
|
||||
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(foo)
|
||||
}
|
||||
it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir =>
|
||||
|
|
@ -102,7 +106,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
None
|
||||
}
|
||||
)
|
||||
Watched.watch(() => Right(true), config) shouldBe CancelWatch
|
||||
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(bar, foo)
|
||||
}
|
||||
it should "halt on error" in IO.withTemporaryDirectory { dir =>
|
||||
|
|
@ -111,7 +115,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
sources = Seq(WatchSource(dir.toRealPath)),
|
||||
preWatch = (_, lastStatus) => if (lastStatus) Ignore else { halted.set(true); HandleError }
|
||||
)
|
||||
Watched.watch(() => Right(false), config) shouldBe HandleError
|
||||
Watched.watch(NullInputStream, () => Right(false), config) shouldBe HandleError
|
||||
assert(halted.get())
|
||||
}
|
||||
it should "reload" in IO.withTemporaryDirectory { dir =>
|
||||
|
|
@ -121,7 +125,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
|||
onWatchEvent = _ => Reload,
|
||||
watchingMessage = _ => { new File(dir, "file").createNewFile(); None }
|
||||
)
|
||||
Watched.watch(() => Right(true), config) shouldBe Reload
|
||||
Watched.watch(NullInputStream, () => Right(true), config) shouldBe Reload
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.io.{ File, InputStream }
|
||||
import java.net.URL
|
||||
import scala.concurrent.duration.{ FiniteDuration, Duration }
|
||||
import Def.ScopedKey
|
||||
|
|
@ -152,7 +152,7 @@ object Keys {
|
|||
val watchAntiEntropy = settingKey[FiniteDuration]("Duration for which the watch EventMonitor will ignore events for a file after that file has triggered a build.").withRank(BMinusSetting)
|
||||
val watchConfig = taskKey[WatchConfig]("The configuration for continuous execution.").withRank(BMinusSetting)
|
||||
val watchLogger = taskKey[Logger]("A logger that reports watch events.").withRank(DSetting)
|
||||
val watchHandleInput = settingKey[() => Watched.Action]("Function that is periodically invoked to determine if the continous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands.").withRank(BMinusSetting)
|
||||
val watchHandleInput = settingKey[InputStream => Watched.Action]("Function that is periodically invoked to determine if the continous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands.").withRank(BMinusSetting)
|
||||
val watchOnEvent = taskKey[Event[StampedFile] => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting)
|
||||
val watchOnTermination = taskKey[(Watched.Action, String, State) => State]("Transforms the input state after the continuous build completes.").withRank(BMinusSetting)
|
||||
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting)
|
||||
|
|
|
|||
Loading…
Reference in New Issue