diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala index 6ddaaa3e9..365cb1349 100644 --- a/main/CommandSupport.scala +++ b/main/CommandSupport.scala @@ -62,8 +62,11 @@ SetCommand + """ This command does not rebuild the build definitions, plugins, or configurations. It does not automatically persist the setting. - This is done by running 'save-settings'. + This is done by running 'settings save' or 'settings save-all'. """ + + def SessionCommand = "session" + def sessionBrief = (SessionCommand + " ...", "Manipulates session settings. For details, run 'help " + SessionCommand + "'..") /** The command name to terminate the program.*/ val TerminateAction: String = Exit diff --git a/main/Main.scala b/main/Main.scala index 68064356a..80c9a02d3 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -60,7 +60,7 @@ class xMain extends xsbti.AppMain object Commands { def DefaultCommands: Seq[Command] = Seq(ignore, help, reload, read, history, continuous, exit, loadCommands, loadProject, compile, discover, - projects, project, setOnFailure, ifLast, multi, shell, set, get, eval, alias, append, nop, act) + projects, project, setOnFailure, ifLast, multi, shell, set, get, eval, alias, append, nop, sessionCommand, act) def nop = Command.custom(s => success(() => s), Nil) def ignore = Command.command(FailureWall)(identity) @@ -221,15 +221,20 @@ object Commands log.info("ans: " + result.tpe + " = " + result.value.toString) s } + def sessionCommand = Command(SessionCommand, sessionBrief, SessionSettings.Help)(SessionSettings.command) + def reapply(newSession: SessionSettings, structure: Load.BuildStructure, s: State): State = + { + logger(s).info("Reapplying settings...") + val newStructure = Load.reapply(newSession.mergeSettings, structure) + setProject(newSession, newStructure, s) + } def set = Command.single(SetCommand, setBrief, setDetailed) { (s, arg) => val extracted = Project extract s import extracted._ val setting = EvaluateConfigurations.evaluateSetting(session.currentEval(), "", imports(extracted), arg, 0) val append = Load.transformSettings(Load.projectScope(curi, cid), curi, rootProject, setting :: Nil) val newSession = session.appendSettings( append map (a => (a, arg))) - logger(s).info("Reapplying settings...") - val newStructure = Load.reapply(newSession.mergeSettings, structure) - setProject(newSession, newStructure, s) + reapply(newSession, structure, s) } def get = Command.single(GetCommand, getBrief, getDetailed) { (s, arg) => val extracted = Project extract s diff --git a/main/Project.scala b/main/Project.scala index b78401005..33a2aba17 100644 --- a/main/Project.scala +++ b/main/Project.scala @@ -117,6 +117,7 @@ final case class SessionSettings(currentBuild: URI, currentProject: Map[URI, Str def appendSettings(s: Seq[SessionSetting]): SessionSettings = copy(append = modify(append, _ ++ s)) def prependSettings(s: Seq[SessionSetting]): SessionSettings = copy(prepend = modify(prepend, s ++ _)) def mergeSettings: Seq[Setting[_]] = merge(prepend) ++ original ++ merge(append) + def clearExtraSettings: SessionSettings = copy(prepend = Map.empty, append = Map.empty) 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 = @@ -129,6 +130,131 @@ object SessionSettings { type SessionSetting = (Setting[_], String) type SessionMap = Map[(URI, String), Seq[SessionSetting]] + + def reapply(session: SessionSettings, s: State): State = + Commands.reapply(session, Project.structure(s), s) + + def clearSettings(s: State): State = + withSettings(s)(session => reapply(session.copy(append = session.append - session.current), s)) + def clearAllSettings(s: State): State = + withSettings(s)(session => reapply(session.clearExtraSettings, s)) + + def withSettings(s: State)(f: SessionSettings => State): State = + { + val extracted = Project extract s + import extracted._ + if(session.prepend.isEmpty && session.append.isEmpty) + { + logger(s).info("No session settings defined.") + s + } + else + f(session) + } + + 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) + } + def saveAllSettings(s: State): State = saveSomeSettings(s)((_,_) => true) + def saveSettings(s: State): State = + { + val (curi,cid) = Project.session(s).current + saveSomeSettings(s)( (uri,id) => uri == curi && id == cid) + } + def saveSomeSettings(s: State)(include: (URI,String) => Boolean): State = + withSettings(s){session => + for( ((uri,id), settings) <- session.append if !settings.isEmpty && include(uri,id) ) + writeSettings(ProjectRef(uri, id), settings, Project.structure(s)) + reapply(session.copy(original = session.mergeSettings, append = Map.empty, prepend = Map.empty), s) + } + def writeSettings(pref: ProjectRef, settings: Seq[SessionSetting], structure: Load.BuildStructure) + { + val project = Project.getProject(pref, structure).getOrElse(error("Invalid project reference " + pref)) + val appendTo: File = BuildPaths.configurationSources(project.base).headOption.getOrElse(new File(project.base, "build.sbt")) + val sbtAppend = settingStrings(settings).flatMap("" :: _ :: Nil) + IO.writeLines(appendTo, sbtAppend, append = true) + } + def printAllSettings(s: State): State = + withSettings(s){ session => + for( ((uri,id), settings) <- session.append if !settings.isEmpty) { + println("In " + Project.display(ProjectRef(uri,id))) + printSettings(settings) + } + 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) <- settingStrings(settings).zipWithIndex) + println(" " + (index+1) + ". " + stringRep) + + def settingStrings(s: Seq[SessionSetting]): Seq[String] = s.map(_._2) + + def Help = """session + +Manipulates session settings, which are temporary settings that do not persist past the current sbt execution (that is, the current session). +Valid commands are: + +clear, clear-all + Removes temporary settings added using 'set' and re-evaluates all settings. + For 'clear', only the settings defined for the current project are cleared. + For 'clear-all', all settings in all projects are cleared. + +list, list-all + Prints a numbered list of session settings defined. + The numbers may be used to remove individual settings or ranges of settings using 'remove'. + For 'list', only the settings for the current project are printed. + For 'list-all', all settings in all projets are printed. + +remove + is a comma-separated list of individual numbers or ranges of numbers. + For example, 'remove 1,3,5-7'. + The temporary settings at the given indices for the current project are removed and all settings are re-evaluated. + Use the 'list' command to see a numbered list of settings for the current project. + +save, save-all + Makes the session settings permanent by writing them to a '.sbt' configuration file. + For 'save', only the current project's settings are saved (the settings for other projects are left alone). + For 'save-all', the session settings are saved for all projects. + The session settings defined for a project are appended to the first '.sbt' configuration file in that project. + If no '.sbt' configuration file exists, the settings are written to 'build.sbt' in the project's base directory.""" + + 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._ + import DefaultParsers._ + + lazy val parser = + token(Space) ~> + (token("list-all" ^^^ new Print(true)) | token("list" ^^^ new Print(false)) | token("clear" ^^^ new Clear(false)) | + token("save-all" ^^^ new Save(true)) | token("save" ^^^ new Save(false)) | token("clear-all" ^^^ new Clear(true)) | + 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)} + + def command(s: State) = Command.applyEffect(parser){ + case p: Print => if(p.all) printAllSettings(s) else printSettings(s) + case v: Save => if(v.all) saveAllSettings(s) else saveSettings(s) + case c: Clear => if(c.all) clearAllSettings(s) else clearSettings(s) + case r: Remove => removeSettings(s,r.ranges) + } } trait ProjectConstructors diff --git a/util/complete/Parsers.scala b/util/complete/Parsers.scala index 2b8957a54..f8dee3aaa 100644 --- a/util/complete/Parsers.scala +++ b/util/complete/Parsers.scala @@ -39,9 +39,13 @@ trait Parsers lazy val Port = token(IntBasic, "") lazy val IntBasic = mapOrFail( '-'.? ~ Digit.+ )( Function.tupled(toInt) ) + lazy val NatBasic = mapOrFail( Digit.+ )( _.mkString.toInt ) private[this] def toInt(neg: Option[Char], digits: Seq[Char]): Int = (neg.toSeq ++ digits).mkString.toInt + def rep1sep[T](rep: Parser[T], sep: Parser[_]): Parser[Seq[T]] = + (rep ~ (sep ~> rep).*).map { case (x ~ xs) => x +: xs } + def mapOrFail[S,T](p: Parser[S])(f: S => T): Parser[T] = p flatMap { s => try { success(f(s)) } catch { case e: Exception => failure(e.toString) } }