mirror of https://github.com/sbt/sbt.git
Comments #1630
This commit is contained in:
parent
97a96d5bf8
commit
a67c5fd187
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue