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
|
package sbt
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ File, InputStream }
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
|
|
||||||
import sbt.BasicCommandStrings.{
|
import sbt.BasicCommandStrings.{
|
||||||
|
|
@ -20,9 +20,9 @@ import sbt.BasicCommands.otherCommandParser
|
||||||
import sbt.internal.LegacyWatched
|
import sbt.internal.LegacyWatched
|
||||||
import sbt.internal.inc.Stamper
|
import sbt.internal.inc.Stamper
|
||||||
import sbt.internal.io.{ EventMonitor, Source, WatchState }
|
import sbt.internal.io.{ EventMonitor, Source, WatchState }
|
||||||
import sbt.internal.util.AttributeKey
|
|
||||||
import sbt.internal.util.Types.const
|
import sbt.internal.util.Types.const
|
||||||
import sbt.internal.util.complete.DefaultParsers
|
import sbt.internal.util.complete.DefaultParsers
|
||||||
|
import sbt.internal.util.{ AttributeKey, JLine }
|
||||||
import sbt.io.FileEventMonitor.Event
|
import sbt.io.FileEventMonitor.Event
|
||||||
import sbt.io._
|
import sbt.io._
|
||||||
import sbt.util.{ Level, Logger }
|
import sbt.util.{ Level, Logger }
|
||||||
|
|
@ -108,25 +108,44 @@ object Watched {
|
||||||
|
|
||||||
type WatchSource = Source
|
type WatchSource = Source
|
||||||
def terminateWatch(key: Int): Boolean = Watched.isEnter(key)
|
def terminateWatch(key: Int): Boolean = Watched.isEnter(key)
|
||||||
/*
|
|
||||||
* Without jline, checking for enter is nearly pointless because System.in.available will not
|
private def withCharBufferedStdIn[R](f: InputStream => R): R = JLine.usingTerminal { terminal =>
|
||||||
* return a non-zero value until the user presses enter.
|
val in = terminal.wrapInIfNeeded(System.in)
|
||||||
*/
|
try {
|
||||||
@tailrec
|
while (in.available > 0) in.read()
|
||||||
final def shouldTerminate: Boolean =
|
terminal.init()
|
||||||
(System.in.available > 0) && (terminateWatch(System.in.read()) || shouldTerminate)
|
f(in)
|
||||||
final val handleInput: () => Action = () => if (shouldTerminate) CancelWatch else Ignore
|
} finally {
|
||||||
val defaultStartWatch: Int => Option[String] = count =>
|
while (in.available > 0) in.read()
|
||||||
Some(s"$count. Waiting for source changes... (press enter to interrupt)")
|
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")
|
@deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0")
|
||||||
val defaultWatchingMessage: WatchState => String = ws => defaultStartWatch(ws.count).get
|
val defaultWatchingMessage: WatchState => String = ws => defaultStartWatch(ws.count).get
|
||||||
def projectWatchingMessage(projectId: String): WatchState => String =
|
def projectWatchingMessage(projectId: String): WatchState => String =
|
||||||
ws => projectOnWatchMessage(projectId)(ws.count).get
|
ws => projectOnWatchMessage(projectId)(ws.count).get
|
||||||
def projectOnWatchMessage(project: String): Int => Option[String] =
|
def projectOnWatchMessage(project: String): Int => Option[String] =
|
||||||
count =>
|
count => Some(s"$count. ${waitMessage(s" in project $project")}")
|
||||||
Some(
|
|
||||||
s"$count. Waiting for source changes in project $project... (press enter to interrupt)"
|
|
||||||
)
|
|
||||||
|
|
||||||
val defaultOnTriggerMessage: Int => Option[String] = _ => None
|
val defaultOnTriggerMessage: Int => Option[String] = _ => None
|
||||||
@deprecated(
|
@deprecated(
|
||||||
|
|
@ -182,6 +201,7 @@ object Watched {
|
||||||
val PollDelay: FiniteDuration = 500.milliseconds
|
val PollDelay: FiniteDuration = 500.milliseconds
|
||||||
val AntiEntropy: FiniteDuration = 40.milliseconds
|
val AntiEntropy: FiniteDuration = 40.milliseconds
|
||||||
def isEnter(key: Int): Boolean = key == 10 || key == 13
|
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)
|
def printIfDefined(msg: String): Unit = if (!msg.isEmpty) System.out.println(msg)
|
||||||
|
|
||||||
private type RunCommand = () => State
|
private type RunCommand = () => State
|
||||||
|
|
@ -231,7 +251,7 @@ object Watched {
|
||||||
state: State,
|
state: State,
|
||||||
command: String,
|
command: String,
|
||||||
setup: WatchSetup,
|
setup: WatchSetup,
|
||||||
): State = {
|
): State = withCharBufferedStdIn { in =>
|
||||||
val (s0, config, newState) = setup(state, command)
|
val (s0, config, newState) = setup(state, command)
|
||||||
val failureCommandName = "SbtContinuousWatchOnFail"
|
val failureCommandName = "SbtContinuousWatchOnFail"
|
||||||
val onFail = Command.command(failureCommandName)(identity)
|
val onFail = Command.command(failureCommandName)(identity)
|
||||||
|
|
@ -263,7 +283,7 @@ object Watched {
|
||||||
case (status, Right(t)) => if (status.getOrElse(true)) t() else status
|
case (status, Right(t)) => if (status.getOrElse(true)) t() else status
|
||||||
case _ => throw new IllegalStateException("Should be unreachable")
|
case _ => throw new IllegalStateException("Should be unreachable")
|
||||||
}
|
}
|
||||||
val terminationAction = watch(task, config)
|
val terminationAction = watch(in, task, config)
|
||||||
config.onWatchTerminated(terminationAction, command, state)
|
config.onWatchTerminated(terminationAction, command, state)
|
||||||
} else {
|
} else {
|
||||||
config.logger.error(
|
config.logger.error(
|
||||||
|
|
@ -274,6 +294,7 @@ object Watched {
|
||||||
}
|
}
|
||||||
|
|
||||||
private[sbt] def watch(
|
private[sbt] def watch(
|
||||||
|
in: InputStream,
|
||||||
task: () => Either[Exception, Boolean],
|
task: () => Either[Exception, Boolean],
|
||||||
config: WatchConfig
|
config: WatchConfig
|
||||||
): Action = {
|
): Action = {
|
||||||
|
|
@ -284,7 +305,7 @@ object Watched {
|
||||||
def impl(count: Int): Action = {
|
def impl(count: Int): Action = {
|
||||||
@tailrec
|
@tailrec
|
||||||
def nextAction(): Action = {
|
def nextAction(): Action = {
|
||||||
config.handleInput() match {
|
config.handleInput(in) match {
|
||||||
case action @ (CancelWatch | HandleError | Reload | _: Custom) => action
|
case action @ (CancelWatch | HandleError | Reload | _: Custom) => action
|
||||||
case Trigger => Trigger
|
case Trigger => Trigger
|
||||||
case _ =>
|
case _ =>
|
||||||
|
|
@ -348,12 +369,8 @@ object Watched {
|
||||||
HandleError
|
HandleError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try impl(count = 1)
|
||||||
impl(count = 1)
|
finally config.fileEventMonitor.close()
|
||||||
} finally {
|
|
||||||
config.fileEventMonitor.close()
|
|
||||||
while (System.in.available() > 0) System.in.read()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@deprecated("Replaced by Watched.command", "1.3.0")
|
@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.
|
* 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.
|
* @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.
|
* This is run before each watch iteration and if it returns true, the watch is terminated.
|
||||||
|
|
@ -496,7 +513,7 @@ object WatchConfig {
|
||||||
def default(
|
def default(
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
fileEventMonitor: FileEventMonitor[StampedFile],
|
fileEventMonitor: FileEventMonitor[StampedFile],
|
||||||
handleInput: () => Watched.Action,
|
handleInput: InputStream => Watched.Action,
|
||||||
preWatch: (Int, Boolean) => Watched.Action,
|
preWatch: (Int, Boolean) => Watched.Action,
|
||||||
onWatchEvent: Event[StampedFile] => Watched.Action,
|
onWatchEvent: Event[StampedFile] => Watched.Action,
|
||||||
onWatchTerminated: (Watched.Action, String, State) => State,
|
onWatchTerminated: (Watched.Action, String, State) => State,
|
||||||
|
|
@ -514,7 +531,7 @@ object WatchConfig {
|
||||||
new WatchConfig {
|
new WatchConfig {
|
||||||
override def logger: Logger = l
|
override def logger: Logger = l
|
||||||
override def fileEventMonitor: FileEventMonitor[StampedFile] = fem
|
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 =
|
override def preWatch(count: Int, lastResult: Boolean): Watched.Action =
|
||||||
pw(count, lastResult)
|
pw(count, lastResult)
|
||||||
override def onWatchEvent(event: Event[StampedFile]): Watched.Action = owe(event)
|
override def onWatchEvent(event: Event[StampedFile]): Watched.Action = owe(event)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
package sbt
|
package sbt
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ File, InputStream }
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
||||||
sources: Seq[WatchSource],
|
sources: Seq[WatchSource],
|
||||||
fileEventMonitor: Option[FileEventMonitor[StampedFile]] = None,
|
fileEventMonitor: Option[FileEventMonitor[StampedFile]] = None,
|
||||||
logger: Logger = NullLogger,
|
logger: Logger = NullLogger,
|
||||||
handleInput: () => Action = () => Ignore,
|
handleInput: InputStream => Action = _ => Ignore,
|
||||||
preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch,
|
preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch,
|
||||||
onWatchEvent: Event[StampedFile] => Action = _ => Ignore,
|
onWatchEvent: Event[StampedFile] => Action = _ => Ignore,
|
||||||
triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None,
|
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 =>
|
"Watched.watch" should "stop" in IO.withTemporaryDirectory { dir =>
|
||||||
val config = Defaults.config(sources = Seq(WatchSource(dir.toRealPath)))
|
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 =>
|
it should "trigger" in IO.withTemporaryDirectory { dir =>
|
||||||
val triggered = new AtomicBoolean(false)
|
val triggered = new AtomicBoolean(false)
|
||||||
|
|
@ -63,7 +67,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
||||||
new File(dir, "file").createNewFile; None
|
new File(dir, "file").createNewFile; None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Watched.watch(() => Right(true), config) shouldBe CancelWatch
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||||
assert(triggered.get())
|
assert(triggered.get())
|
||||||
}
|
}
|
||||||
it should "filter events" in IO.withTemporaryDirectory { dir =>
|
it should "filter events" in IO.withTemporaryDirectory { dir =>
|
||||||
|
|
@ -78,7 +82,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
||||||
triggeredMessage = (tp, _) => { queue += tp; None },
|
triggeredMessage = (tp, _) => { queue += tp; None },
|
||||||
watchingMessage = _ => { Files.createFile(bar); Thread.sleep(5); Files.createFile(foo); 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)
|
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(foo)
|
||||||
}
|
}
|
||||||
it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir =>
|
it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir =>
|
||||||
|
|
@ -102,7 +106,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Watched.watch(() => Right(true), config) shouldBe CancelWatch
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||||
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(bar, foo)
|
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(bar, foo)
|
||||||
}
|
}
|
||||||
it should "halt on error" in IO.withTemporaryDirectory { dir =>
|
it should "halt on error" in IO.withTemporaryDirectory { dir =>
|
||||||
|
|
@ -111,7 +115,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
||||||
sources = Seq(WatchSource(dir.toRealPath)),
|
sources = Seq(WatchSource(dir.toRealPath)),
|
||||||
preWatch = (_, lastStatus) => if (lastStatus) Ignore else { halted.set(true); HandleError }
|
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())
|
assert(halted.get())
|
||||||
}
|
}
|
||||||
it should "reload" in IO.withTemporaryDirectory { dir =>
|
it should "reload" in IO.withTemporaryDirectory { dir =>
|
||||||
|
|
@ -121,7 +125,7 @@ class WatchedSpec extends FlatSpec with Matchers {
|
||||||
onWatchEvent = _ => Reload,
|
onWatchEvent = _ => Reload,
|
||||||
watchingMessage = _ => { new File(dir, "file").createNewFile(); None }
|
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
|
package sbt
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ File, InputStream }
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import scala.concurrent.duration.{ FiniteDuration, Duration }
|
import scala.concurrent.duration.{ FiniteDuration, Duration }
|
||||||
import Def.ScopedKey
|
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 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 watchConfig = taskKey[WatchConfig]("The configuration for continuous execution.").withRank(BMinusSetting)
|
||||||
val watchLogger = taskKey[Logger]("A logger that reports watch events.").withRank(DSetting)
|
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 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 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)
|
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue