This commit is contained in:
andrzej.jozwik@gmail.com 2014-09-30 23:58:05 +02:00 committed by Eugene Yokota
parent 97a96d5bf8
commit a67c5fd187
3 changed files with 122 additions and 92 deletions

View File

@ -16,17 +16,17 @@ import sbt.internals.parser.SbtRefactorings
* Represents (potentially) transient settings added into a build via commands/user.
*
* @param currentBuild
* The current sbt build with which we scope new settings
* The current sbt build with which we scope new settings
* @param currentProject
* The current project with which we scope new settings.
* The current project with which we scope new settings.
* @param original
* The original list of settings for this build.
* The original list of settings for this build.
* @param append
* Settings which have been defined and appended that may ALSO be saved to disk.
* Settings which have been defined and appended that may ALSO be saved to disk.
* @param rawAppend
* Settings which have been defined and appended which CANNOT be saved to disk
* Settings which have been defined and appended which CANNOT be saved to disk
* @param currentEval
* A compiler we can use to compile new setting strings.
* A compiler we can use to compile new setting strings.
*/
final case class SessionSettings(currentBuild: URI, currentProject: Map[URI, String], original: Seq[Setting[_]], append: SessionMap, rawAppend: Seq[Setting[_]], currentEval: () => Eval) {
assert(currentProject contains currentBuild, "Current build (" + currentBuild + ") not associated with a current project.")
@ -71,16 +71,18 @@ final case class SessionSettings(currentBuild: URI, currentProject: Map[URI, Str
def clearExtraSettings: SessionSettings = copy(append = Map.empty, rawAppend = Nil)
private[this] def merge(map: SessionMap): Seq[Setting[_]] = map.values.toSeq.flatten[SessionSetting].map(_._1)
private[this] def modify(map: SessionMap, onSeq: Endo[Seq[SessionSetting]]): SessionMap =
{
val cur = current
map.updated(cur, onSeq(map.getOrElse(cur, Nil)))
}
private[this] def modify(map: SessionMap, onSeq: Endo[Seq[SessionSetting]]): SessionMap = {
val cur = current
map.updated(cur, onSeq(map.getOrElse(cur, Nil)))
}
}
object SessionSettings {
/** A session setting is simply a tuple of a Setting[_] and the strings which define it. */
type SessionSetting = (Setting[_], List[String])
type SessionSetting = (Setting[_], Seq[String])
type SessionMap = Map[ProjectRef, Seq[SessionSetting]]
type SbtConfigFile = (File, Seq[String])
/**
* This will re-evaluate all Setting[_]'s on this session against the current build state and
@ -96,6 +98,7 @@ object SessionSettings {
*/
def clearSettings(s: State): State =
withSettings(s)(session => reapply(session.copy(append = session.append - session.current), s))
/** This will clear ALL transient session settings in a given build state, returning the new build state. */
def clearAllSettings(s: State): State =
withSettings(s)(session => reapply(session.clearExtraSettings, s))
@ -107,16 +110,15 @@ object SessionSettings {
* @param f A function which takes the current SessionSettings and returns the new build state.
* @return The new build state
*/
def withSettings(s: State)(f: SessionSettings => State): State =
{
val extracted = Project extract s
import extracted._
if (session.append.isEmpty) {
s.log.info("No session settings defined.")
s
} else
f(session)
}
def withSettings(s: State)(f: SessionSettings => State): State = {
val extracted = Project extract s
import extracted._
if (session.append.isEmpty) {
s.log.info("No session settings defined.")
s
} else
f(session)
}
/** Adds `s` to a strings when needed. Maybe one day we'll care about non-english languages. */
def pluralize(size: Int, of: String) = size.toString + (if (size == 1) of else (of + "s"))
@ -127,25 +129,27 @@ object SessionSettings {
if (newSession.append.isEmpty && !oldSettings.isEmpty)
oldState.log.warn("Discarding " + pluralize(oldSettings.size, " session setting") + ". Use 'session save' to persist session settings.")
}
def removeRanges[T](in: Seq[T], ranges: Seq[(Int, Int)]): Seq[T] =
{
val asSet = (Set.empty[Int] /: ranges) { case (s, (hi, lo)) => s ++ (hi to lo) }
in.zipWithIndex.flatMap { case (t, index) => if (asSet(index + 1)) Nil else t :: Nil }
}
def removeRanges[T](in: Seq[T], ranges: Seq[(Int, Int)]): Seq[T] = {
val asSet = (Set.empty[Int] /: ranges) { case (s, (hi, lo)) => s ++ (hi to lo) }
in.zipWithIndex.flatMap { case (t, index) => if (asSet(index + 1)) Nil else t :: Nil }
}
def removeSettings(s: State, ranges: Seq[(Int, Int)]): State =
withSettings(s) { session =>
val current = session.current
val newAppend = session.append.updated(current, removeRanges(session.append.getOrElse(current, Nil), ranges))
reapply(session.copy(append = newAppend), s)
}
/** Saves *all* session settings to disk for all projects. */
def saveAllSettings(s: State): State = saveSomeSettings(s)(_ => true)
/** Saves the session settings to disk for the current project. */
def saveSettings(s: State): State =
{
val current = Project.session(s).current
saveSomeSettings(s)(_ == current)
}
def saveSettings(s: State): State = {
val current = Project.session(s).current
saveSomeSettings(s)(_ == current)
}
/**
* Saves session settings to disk if they match the filter.
@ -164,47 +168,47 @@ object SessionSettings {
val newSession = session.copy(append = newAppend.toMap, original = newOriginal.flatten.toSeq)
reapply(newSession.copy(original = newSession.mergeSettings, append = Map.empty), s)
}
def writeSettings(pref: ProjectRef, settings: List[SessionSetting], original: Seq[Setting[_]], structure: BuildStructure): (Seq[SessionSetting], Seq[Setting[_]]) =
{
val project = Project.getProject(pref, structure).getOrElse(sys.error("Invalid project reference " + pref))
val writeTo: File = BuildPaths.configurationSources(project.base).headOption.getOrElse(new File(project.base, "build.sbt"))
writeTo.createNewFile()
val path = writeTo.getAbsolutePath
val (inFile, other, _) = ((List[Setting[_]](), List[Setting[_]](), Set.empty[ScopedKey[_]]) /: original.reverse) {
case ((in, oth, keys), s) =>
s.pos match {
case RangePosition(`path`, _) if !keys.contains(s.key) => (s :: in, oth, keys + s.key)
case _ => (in, s :: oth, keys)
}
}
def writeSettings(pref: ProjectRef, settings: List[SessionSetting], original: Seq[Setting[_]], structure: BuildStructure): (Seq[SessionSetting], Seq[Setting[_]]) = {
val project = Project.getProject(pref, structure).getOrElse(sys.error("Invalid project reference " + pref))
val writeTo: File = BuildPaths.configurationSources(project.base).headOption.getOrElse(new File(project.base, "build.sbt"))
writeTo.createNewFile()
val (_, oldShifted, replace, statements) = ((0, List[Setting[_]](), List[SessionSetting](), List[List[String]]()) /: inFile) {
case ((offs, olds, repl, statements), s) =>
val RangePosition(_, r @ LineRange(start, end)) = s.pos
settings find (_._1.key == s.key) match {
case Some(ss @ (ns, newLines)) if !ns.init.dependencies.contains(ns.key) =>
val shifted = ns withPos RangePosition(path, LineRange(start - offs, start - offs + newLines.size))
(offs + end - start - newLines.size, shifted :: olds, ss :: repl, newLines +: statements)
case _ =>
val shifted = s withPos RangePosition(path, r shift -offs)
(offs, shifted :: olds, repl, statements)
}
}
val newSettings = settings diff replace
val oldContent = IO.readLines(writeTo)
val exist: List[String] = SbtRefactorings.applyStatements(oldContent, statements)
val adjusted = if (!newSettings.isEmpty && needsTrailingBlank(exist)) exist :+ "" else exist
val lines = adjusted ++ newSettings.flatMap(_._2 ::: "" :: Nil)
IO.writeLines(writeTo, lines)
val (newWithPos, _) = ((List[SessionSetting](), adjusted.size + 1) /: newSettings) {
case ((acc, line), (s, newLines)) =>
val endLine = line + newLines.size
((s withPos RangePosition(path, LineRange(line, endLine)), newLines) :: acc, endLine + 1)
}
(newWithPos.reverse, other ++ oldShifted)
val path = writeTo.getAbsolutePath
val (inFile, other, _) = ((List[Setting[_]](), List[Setting[_]](), Set.empty[ScopedKey[_]]) /: original.reverse) {
case ((in, oth, keys), s) =>
s.pos match {
case RangePosition(`path`, _) if !keys.contains(s.key) => (s :: in, oth, keys + s.key)
case _ => (in, s :: oth, keys)
}
}
val (_, oldShifted, replace) = ((0, List[Setting[_]](), Seq[SessionSetting]()) /: inFile) {
case ((offs, olds, repl), s) =>
val RangePosition(_, r @ LineRange(start, end)) = s.pos
settings find (_._1.key == s.key) match {
case Some(ss @ (ns, newLines)) if !ns.init.dependencies.contains(ns.key) =>
val shifted = ns withPos RangePosition(path, LineRange(start - offs, start - offs + newLines.size))
(offs + end - start - newLines.size, shifted :: olds, ss +: repl)
case _ =>
val shifted = s withPos RangePosition(path, r shift -offs)
(offs, shifted :: olds, repl)
}
}
val newSettings = settings diff replace
val oldContent = IO.readLines(writeTo)
val (_, exist) = SbtRefactorings.applySessionSettings((writeTo, oldContent), replace)
val adjusted = if (!newSettings.isEmpty && needsTrailingBlank(exist)) exist :+ "" else exist
val lines = adjusted ++ newSettings.flatMap(x => x._2 :+ "")
IO.writeLines(writeTo, lines)
val (newWithPos, _) = ((List[SessionSetting](), adjusted.size + 1) /: newSettings) {
case ((acc, line), (s, newLines)) =>
val endLine = line + newLines.size
((s withPos RangePosition(path, LineRange(line, endLine)), newLines) :: acc, endLine + 1)
}
(newWithPos.reverse, other ++ oldShifted)
}
def needsTrailingBlank(lines: Seq[String]) = !lines.isEmpty && !lines.takeRight(1).exists(_.trim.isEmpty)
/** Prints all the user-defined SessionSettings (not raw) to System.out. */
@ -216,11 +220,13 @@ object SessionSettings {
}
s
}
def printSettings(s: State): State =
withSettings(s) { session =>
printSettings(session.append.getOrElse(session.current, Nil))
s
}
def printSettings(settings: Seq[SessionSetting]): Unit =
for (((_, stringRep), index) <- settings.zipWithIndex)
println(" " + (index + 1) + ". " + stringRep.mkString("\n"))
@ -260,9 +266,13 @@ save, save-all
/** AST for the syntax of the session command. Each subclass is an action that can be performed. */
sealed trait SessionCommand
final class Print(val all: Boolean) extends SessionCommand
final class Clear(val all: Boolean) extends SessionCommand
final class Save(val all: Boolean) extends SessionCommand
final class Remove(val ranges: Seq[(Int, Int)]) extends SessionCommand
import complete._
@ -276,7 +286,9 @@ save, save-all
remove)
lazy val remove = token("remove") ~> token(Space) ~> natSelect.map(ranges => new Remove(ranges))
def natSelect = rep1sep(token(range, "<range>"), ',')
def range: Parser[(Int, Int)] = (NatBasic ~ ('-' ~> NatBasic).?).map { case lo ~ hi => (lo, hi getOrElse lo) }
/** The raw implementation of the sessoin command. */

View File

@ -7,31 +7,48 @@ import scala.reflect.runtime.universe._
private[sbt] object SbtRefactorings {
import sbt.internals.parser.SplitExpressionsNoBlankies.{ END_OF_LINE, FAKE_FILE }
import sbt.SessionSettings.{ SessionSetting, SbtConfigFile }
val EMPTY_STRING = ""
val REVERSE_ORDERING_INT = Ordering[Int].reverse
def applyStatements(lines: List[String], commands: List[List[String]]): List[String] = {
/**
* Refactoring a `.sbt` file so that the new settings are used instead of any existing settings.
* @param configFile SbtConfigFile with the lines of an sbt file as a List[String] where each string is one line
* @param commands A List of settings (space separate) that should be inserted into the current file.
* If the settings replaces a value, it will replace the original line in the .sbt file.
* If in the `.sbt` file we have multiply value for one settings -
* the first will be replaced and the other will be removed.
* @return a SbtConfigFile with new lines which represent the contents of the refactored .sbt file.
*/
def applySessionSettings(configFile: SbtConfigFile, commands: Seq[SessionSetting]): SbtConfigFile = {
val (file, lines) = configFile
val split = SplitExpressionsNoBlankies(FAKE_FILE, lines)
val recordedCommand = recordCommands(commands, split)
val sortedRecordedCommand = recordedCommand.sortBy(_._1)(REVERSE_ORDERING_INT)
val recordedCommands = recordCommands(commands, split)
val sortedRecordedCommands = recordedCommands.sortBy(_._1)(REVERSE_ORDERING_INT)
val newContent = sortedRecordedCommand.foldLeft(split.modifiedContent) {
val newContent = replaceFromBottomToTop(split.modifiedContent, sortedRecordedCommands)
(file, newContent.lines.toList)
}
private def replaceFromBottomToTop(modifiedContent: String, sortedRecordedCommands: Seq[(Int, String, String)]) = {
sortedRecordedCommands.foldLeft(modifiedContent) {
case (acc, (from, old, replacement)) =>
val before = acc.substring(0, from)
val after = acc.substring(from + old.length, acc.length)
val afterLast = emptyStringForEmptyStatement(after)
val afterLast = emptyStringForEmptyString(after)
before + replacement + afterLast
}
newContent.lines.toList
}
def emptyStringForEmptyStatement(after: String) =
if (after.trim.isEmpty) EMPTY_STRING else after
private def emptyStringForEmptyString(text: String) = {
val trimmed = text.trim
if (trimmed.isEmpty) trimmed else text
}
private def recordCommands(commands: List[List[String]], split: SplitExpressionsNoBlankies) =
private def recordCommands(commands: Seq[SessionSetting], split: SplitExpressionsNoBlankies) =
commands.flatMap {
command =>
case (_, command) =>
val map = toTreeStringMap(command)
map.flatMap {
case (name, statement) =>
@ -39,7 +56,7 @@ private[sbt] object SbtRefactorings {
}
}
private def treesToReplacements(split: SplitExpressionsNoBlankies, name: String, command: List[String]) =
private def treesToReplacements(split: SplitExpressionsNoBlankies, name: String, command: Seq[String]) =
split.settingsTrees.foldLeft(Seq.empty[(Int, String, String)]) {
case (acc, (st, tree)) =>
val treeName = extractSettingName(tree)
@ -55,7 +72,7 @@ private[sbt] object SbtRefactorings {
}
}
private def toTreeStringMap(command: List[String]) = {
private def toTreeStringMap(command: Seq[String]) = {
val split = SplitExpressionsNoBlankies(FAKE_FILE, command)
val trees = split.settingsTrees
val seq = trees.map {

View File

@ -1,4 +1,6 @@
package sbt.internals.parser
package sbt
package internals
package parser
import java.io.{ File, FilenameFilter }
@ -6,15 +8,16 @@ import org.specs2.matcher.MatchResult
import scala.collection.GenTraversableOnce
import scala.io.Source
import SessionSettings.SessionSetting
abstract class AbstractSessionSettingsSpec(folder: String, deepCompare: Boolean = false) extends AbstractSpec {
abstract class AbstractSessionSettingsSpec(folder: String) extends AbstractSpec {
protected val rootPath = getClass.getClassLoader.getResource("").getPath + folder
println(s"Reading files from: $rootPath")
protected val rootDir = new File(rootPath)
"SessionSettings " should {
"Be identical for empty map " in {
def unit(f: File) = Seq((Source.fromFile(f).getLines().toList, Nil))
def unit(f: File) = Seq((Source.fromFile(f).getLines().toList, Seq()))
runTestOnFiles(unit)
}
@ -23,7 +26,7 @@ abstract class AbstractSessionSettingsSpec(folder: String, deepCompare: Boolean
}
}
private def runTestOnFiles(expectedResultAndMap: File => Seq[(List[String], List[String])]): MatchResult[GenTraversableOnce[File]] = {
private def runTestOnFiles(expectedResultAndMap: File => Seq[(List[String], Seq[SessionSetting])]): MatchResult[GenTraversableOnce[File]] = {
val allFiles = rootDir.listFiles(new FilenameFilter() {
def accept(dir: File, name: String) = name.endsWith(".sbt.txt")
@ -33,12 +36,9 @@ abstract class AbstractSessionSettingsSpec(folder: String, deepCompare: Boolean
val originalLines = Source.fromFile(file).getLines().toList
foreach(expectedResultAndMap(file)) {
case (expectedResultList, commands) =>
val resultList = SbtRefactorings.applyStatements(originalLines, commands.map(List(_)))
val resultList = SbtRefactorings.applySessionSettings((file, originalLines), commands)
val expected = SplitExpressionsNoBlankies(file, expectedResultList)
val result = SplitExpressionsNoBlankies(file, resultList)
if (deepCompare) {
expectedResultList must_== resultList
}
val result = SplitExpressionsNoBlankies(file, resultList._2)
result.settings must_== expected.settings
}
@ -58,9 +58,10 @@ abstract class AbstractSessionSettingsSpec(folder: String, deepCompare: Boolean
override def accept(dir: File, name: String) = name.endsWith(".set")
})
files.map { file =>
val list = Source.fromFile(file).getLines().toList
val seq = Source.fromFile(file).getLines().toSeq
val result = Source.fromFile(file.getAbsolutePath + ".result").getLines().toList
(result, list)
val sessionSettings = seq.map(line => (null, Seq(line)))
(result, sessionSettings)
}
}
}