From a67c5fd1874d26b83c23ad648113366f359f7826 Mon Sep 17 00:00:00 2001 From: "andrzej.jozwik@gmail.com" Date: Tue, 30 Sep 2014 23:58:05 +0200 Subject: [PATCH] Comments #1630 --- main/src/main/scala/sbt/SessionSettings.scala | 150 ++++++++++-------- .../internals/parser/SbtRefactorings.scala | 41 +++-- .../parser/SessionSettingsSpec.scala | 23 +-- 3 files changed, 122 insertions(+), 92 deletions(-) diff --git a/main/src/main/scala/sbt/SessionSettings.scala b/main/src/main/scala/sbt/SessionSettings.scala index 109b17442..2ca37ffac 100755 --- a/main/src/main/scala/sbt/SessionSettings.scala +++ b/main/src/main/scala/sbt/SessionSettings.scala @@ -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, ""), ',') + def range: Parser[(Int, Int)] = (NatBasic ~ ('-' ~> NatBasic).?).map { case lo ~ hi => (lo, hi getOrElse lo) } /** The raw implementation of the sessoin command. */ diff --git a/main/src/main/scala/sbt/internals/parser/SbtRefactorings.scala b/main/src/main/scala/sbt/internals/parser/SbtRefactorings.scala index 1acd3ae4f..6074d8611 100644 --- a/main/src/main/scala/sbt/internals/parser/SbtRefactorings.scala +++ b/main/src/main/scala/sbt/internals/parser/SbtRefactorings.scala @@ -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 { diff --git a/main/src/test/scala/sbt/internals/parser/SessionSettingsSpec.scala b/main/src/test/scala/sbt/internals/parser/SessionSettingsSpec.scala index 64bf370ca..51a75edd8 100644 --- a/main/src/test/scala/sbt/internals/parser/SessionSettingsSpec.scala +++ b/main/src/test/scala/sbt/internals/parser/SessionSettingsSpec.scala @@ -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) } } }