diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala index b5bc3a0e5..ee362ac13 100644 --- a/main/CommandSupport.scala +++ b/main/CommandSupport.scala @@ -26,6 +26,8 @@ object CommandSupport def processLine(s: String) = { val trimmed = s.trim; if(ignoreLine(trimmed)) None else Some(trimmed) } def ignoreLine(s: String) = s.isEmpty || s.startsWith("#") + /** The prefix used to identify a request to execute the remaining input on source changes.*/ + val ContinuousExecutePrefix = "~" val HelpCommand = "help" val ProjectCommand = "project" val ProjectsCommand = "projects" @@ -36,6 +38,9 @@ object CommandSupport /** The list of command names that may be used to terminate the program.*/ val TerminateActions: Seq[String] = Seq(Exit, Quit) + + def continuousBriefHelp = (ContinuousExecutePrefix + " ", "Executes the specified command whenever source files change.") + def helpBrief = (HelpCommand + " command*", "Displays this help message or prints detailed help on requested commands.") def helpDetailed = "If an argument is provided, this prints detailed help for that command.\nOtherwise, this prints a help summary." diff --git a/main/Main.scala b/main/Main.scala index 6aee461e5..5252551d1 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -144,6 +144,12 @@ object Commands } } + def continuous = Command { case s @ State(p: Project with Watched) => + Apply( Help(continuousBriefHelp) ) { + case in if in.line startsWith ContinuousExecutePrefix => Watched.executeContinuously(p, s, in) + } + } + def history = Command { case s @ State(p: HistoryEnabled) => Apply( historyHelp: _* ) { case in if in.line startsWith("!") => diff --git a/main/Watched.scala b/main/Watched.scala new file mode 100644 index 000000000..de871341a --- /dev/null +++ b/main/Watched.scala @@ -0,0 +1,43 @@ +/* sbt -- Simple Build Tool + * Copyright 2009, 2010 Mikko Peltonen, Stuart Roebuck, Mark Harrah + */ +package sbt + + import CommandSupport.FailureWall + +trait Watched +{ + /** A `PathFinder` that determines the files watched when an action is run with a preceeding ~ when this is the current + * project. This project does not need to include the watched paths for projects that this project depends on.*/ + def watchPaths: PathFinder = Path.emptyPathFinder + def terminateWatch(key: Int): Boolean = Watched.isEnter(key) +} + +object Watched +{ + val ContinuousCompilePollDelaySeconds = 1 + def isEnter(key: Int): Boolean = key == 10 || key == 13 + + def watched(p: Project, s: State): Seq[Watched] = MultiProject.topologicalSort(p, s).collect { case w: Watched => w } + def sourcePaths(p: Project, s: State): PathFinder = (Path.emptyPathFinder /: watched(p, s))(_ +++ _.watchPaths) + def executeContinuously(project: Project with Watched, s: State, in: Input): State = + { + def shouldTerminate: Boolean = (System.in.available > 0) && (project.terminateWatch(System.in.read()) || shouldTerminate) + val sourcesFinder = sourcePaths(project, s) + val watchState = s get ContinuousState getOrElse WatchState.empty + + if(watchState.count > 0) + System.out.println(watchState.count + ". Waiting for source changes... (press enter to interrupt)") + + val (triggered, newWatchState) = SourceModificationWatch.watch(sourcesFinder, ContinuousCompilePollDelaySeconds, watchState)(shouldTerminate) + + if(triggered) + (in.arguments :: FailureWall :: in.line :: s).put(ContinuousState, newWatchState) + else + { + while (System.in.available() > 0) System.in.read() + s.put(ContinuousState, WatchState.empty) + } + } + val ContinuousState = AttributeKey[WatchState]("watch state") +} \ No newline at end of file diff --git a/util/io/SourceModificationWatch.scala b/util/io/SourceModificationWatch.scala index 20dd4d0a3..7c9e183ee 100644 --- a/util/io/SourceModificationWatch.scala +++ b/util/io/SourceModificationWatch.scala @@ -3,35 +3,45 @@ */ package sbt + import annotation.tailrec + object SourceModificationWatch { - def watchUntil(sourcesFinder: PathFinder, pollDelaySec: Int)(terminationCondition: => Boolean)(onSourcesModified: => Unit) + @tailrec def watch(sourcesFinder: PathFinder, pollDelaySec: Int, state: WatchState)(terminationCondition: => Boolean): (Boolean, WatchState) = { + import state._ + def sourceFiles: Iterable[java.io.File] = sourcesFinder.getFiles - def loop(lastCallbackCallTime: Long, previousFileCount: Int, awaitingQuietPeriod:Boolean) + val (lastModifiedTime, fileCount) = + ( (0L, 0) /: sourceFiles) {(acc, file) => (math.max(acc._1, file.lastModified), acc._2 + 1)} + + val sourcesModified = + lastModifiedTime > lastCallbackCallTime || + previousFileCount != fileCount + + val (triggered, newCallbackCallTime) = + if (sourcesModified && !awaitingQuietPeriod) + (false, System.currentTimeMillis) + else if (!sourcesModified && awaitingQuietPeriod) + (true, lastCallbackCallTime) + else + (false, lastCallbackCallTime) + + val newState = new WatchState(newCallbackCallTime, fileCount, sourcesModified, if(triggered) count + 1 else count) + if(triggered) + (true, newState) + else { - val (lastModifiedTime, fileCount) = - ( (0L, 0) /: sourceFiles) {(acc, file) => (math.max(acc._1, file.lastModified), acc._2 + 1)} - - val sourcesModified = - lastModifiedTime > lastCallbackCallTime || - previousFileCount != fileCount - - val newCallbackCallTime = - if (sourcesModified && !awaitingQuietPeriod) - System.currentTimeMillis - else if (!sourcesModified && awaitingQuietPeriod) - { - onSourcesModified - lastCallbackCallTime - } - else - lastCallbackCallTime - Thread.sleep(pollDelaySec * 1000) - if(!terminationCondition) - loop(newCallbackCallTime, fileCount, sourcesModified) + if(terminationCondition) + (false, newState) + else + watch(sourcesFinder, pollDelaySec, newState)(terminationCondition) } - loop(0L, 0, false) } +} +final class WatchState(val lastCallbackCallTime: Long, val previousFileCount: Int, val awaitingQuietPeriod:Boolean, val count: Int) +object WatchState +{ + def empty = new WatchState(0L, 0, false, 0) } \ No newline at end of file