mirror of https://github.com/sbt/sbt.git
418 lines
20 KiB
Scala
418 lines
20 KiB
Scala
package sbt
|
|
|
|
import java.io.File
|
|
import java.net.URI
|
|
import Project._
|
|
import Scope.{GlobalScope,ThisScope}
|
|
import Load.BuildStructure
|
|
import Types.{const, idFun, Id}
|
|
import complete._
|
|
import DefaultParsers._
|
|
|
|
/** The resulting `session` and verbose and quiet summaries of the result of a set operation.
|
|
* The verbose summary will typically use more vertical space and show full details,
|
|
* while the quiet summary will be a couple of lines and truncate information. */
|
|
private[sbt] class SetResult(val session: SessionSettings, val verboseSummary: String, val quietSummary: String)
|
|
|
|
/** Defines methods for implementing the `set` command.*/
|
|
private[sbt] object SettingCompletions
|
|
{
|
|
/** Implementation of the `set every` command. Each setting in the provided `settings` sequence will be applied in all scopes,
|
|
* overriding all previous definitions of the underlying AttributeKey.
|
|
* The settings injected by this method cannot be later persisted by the `session save` command. */
|
|
def setAll(extracted: Extracted, settings: Seq[Setting[_]]): SetResult =
|
|
{
|
|
import extracted._
|
|
val r = relation(extracted.structure, true)
|
|
val allDefs = r._1s.toSeq
|
|
val projectScope = Load.projectScope(currentRef)
|
|
def resolve(s: Setting[_]): Seq[Setting[_]] = Load.transformSettings(projectScope, currentRef.build, rootProject, s :: Nil)
|
|
def rescope[T](setting: Setting[T]): Seq[Setting[_]] =
|
|
{
|
|
val akey = setting.key.key
|
|
val global = ScopedKey(Global, akey)
|
|
val globalSetting = resolve( Project.setting(global, setting.init, setting.pos) )
|
|
globalSetting ++ allDefs.flatMap { d =>
|
|
if(d.key == akey)
|
|
Seq( SettingKey(akey) in d.scope <<= global)
|
|
else
|
|
Nil
|
|
}
|
|
}
|
|
val redefined = settings.flatMap(x => rescope(x))
|
|
val session = extracted.session.appendRaw(redefined)
|
|
setResult(session, r, redefined)
|
|
}
|
|
|
|
/** Implementation of the `set` command that will reload the current project with `settings` appended to the current settings. */
|
|
def setThis(s: State, extracted: Extracted, settings: Seq[Project.Setting[_]], arg: String): SetResult =
|
|
{
|
|
import extracted._
|
|
val append = Load.transformSettings(Load.projectScope(currentRef), currentRef.build, rootProject, settings)
|
|
val r = relation(extracted.structure, true)
|
|
val newSession = session.appendSettings( append map (a => (a, arg.split('\n').toList)))
|
|
setResult(newSession, r, append)
|
|
}
|
|
|
|
private[this] def setResult(session: SessionSettings, r: Relation[ScopedKey[_], ScopedKey[_]], redefined: Seq[Setting[_]])(implicit show: Show[ScopedKey[_]]): SetResult =
|
|
{
|
|
val redefinedKeys = redefined.map(_.key).toSet
|
|
val affectedKeys = redefinedKeys.flatMap(r.reverse)
|
|
def summary(verbose: Boolean): String = setSummary(redefinedKeys, affectedKeys, verbose)
|
|
new SetResult(session, summary(true), summary(false))
|
|
}
|
|
|
|
private[this] def setSummary(redefined: Set[ScopedKey[_]], affected: Set[ScopedKey[_]], verbose: Boolean)(implicit display: Show[ScopedKey[_]]): String =
|
|
{
|
|
val QuietLimit = 3
|
|
def strings(in: Set[ScopedKey[_]]): Seq[String] = in.toSeq.map(sk => display(sk)).sorted
|
|
def lines(in: Seq[String]): (String, Boolean) =
|
|
if(in.isEmpty)
|
|
("no settings or tasks.", false)
|
|
else if(verbose)
|
|
(in.mkString("\n\t", "\n\t", "\n"), false)
|
|
else
|
|
quietList(in)
|
|
def quietList(in: Seq[String]): (String, Boolean) =
|
|
{
|
|
val (first, last) = in.splitAt(QuietLimit)
|
|
if(last.isEmpty)
|
|
(first.mkString(", "), false)
|
|
else
|
|
{
|
|
val s = first.take(QuietLimit - 1).mkString("", ", ", " and " + last.size + " others.")
|
|
(s, true)
|
|
}
|
|
}
|
|
if(redefined.isEmpty)
|
|
"No settings or tasks were redefined."
|
|
else
|
|
{
|
|
val (redef, trimR) = lines(strings(redefined))
|
|
val (used, trimU) = lines(strings(affected))
|
|
val details = if(trimR || trimU) "\n\tRun `last` for details." else ""
|
|
val valuesString = if(redefined.size == 1) "value" else "values"
|
|
"Defining %s\nThe new %s will be used by %s%s".format(redef, valuesString, used, details)
|
|
}
|
|
}
|
|
|
|
/** Parser that provides tab completion for the main argument to the `set` command.
|
|
* `settings` are the evaluated settings for the build, `rawKeyMap` maps the hypenated key identifier to the key object,
|
|
* and `context` is the current project.
|
|
* The tab completion will try to present the most relevant information first, with additional descriptions or keys available
|
|
* when there are fewer choices or tab is pressed multiple times.
|
|
* The last part of the completion will generate a template for the value or function literal that will initialize the setting or task. */
|
|
def settingParser(settings: Settings[Scope], rawKeyMap: Map[String, AttributeKey[_]], context: ResolvedProject): Parser[String] =
|
|
{
|
|
val cutoff = KeyRanks.MainCutoff
|
|
val keyMap: Map[String, AttributeKey[_]] = rawKeyMap.map { case (k,v) => (keyScalaID(k), v) } toMap ;
|
|
def inputScopedKey(pred: AttributeKey[_] => Boolean): Parser[ScopedKey[_]] =
|
|
scopedKeyParser(keyMap.filter { case (_, k) => pred(k) }, settings, context)
|
|
val full = for {
|
|
defineKey <- scopedKeyParser(keyMap, settings, context)
|
|
a <- assign(defineKey)
|
|
deps <- valueParser(defineKey, a, inputScopedKey(keyFilter(defineKey.key)))
|
|
} yield
|
|
() // parser is currently only for completion and the parsed data structures are not used
|
|
|
|
matched( full ) | any.+.string
|
|
}
|
|
|
|
/** Parser for a Scope+AttributeKey (ScopedKey). */
|
|
def scopedKeyParser(keyMap: Map[String, AttributeKey[_]], settings: Settings[Scope], context: ResolvedProject): Parser[ScopedKey[_]] =
|
|
{
|
|
val cutoff = KeyRanks.MainCutoff
|
|
val keyCompletions = fixedCompletions { (seen, level) => completeKey(seen, keyMap, level, cutoff, 10).toSet }
|
|
val keyID: Parser[AttributeKey[_]] = scalaID(keyMap, "key")
|
|
val keyParser = token(keyID, keyCompletions)
|
|
for(key <- keyParser; scope <- scopeParser(key, settings, context) ) yield
|
|
ScopedKey(scope, key)
|
|
}
|
|
|
|
/** Parser for the `in` method name that slightly augments the naive completion to give a hint of the purpose of `in`.*/
|
|
val inParser = tokenDisplay(Space ~> InMethod, "%s <scope>".format(InMethod))
|
|
|
|
/** Parser for the initialization expression for the assignment method `assign` on the key `sk`.
|
|
* `scopedKeyP` is used to parse and complete the input keys for an initialization that depends on other keys. */
|
|
def valueParser(sk: ScopedKey[_], assign: Assign.Value, scopedKeyP: Parser[ScopedKey[_]]): Parser[Seq[ScopedKey[_]]] =
|
|
{
|
|
val fullTypeString = keyTypeString(sk.key)
|
|
val typeString = if(assignNoAppend(assign)) fullTypeString else "..."
|
|
if( assign == Assign.Update )
|
|
{
|
|
val function = "{(prev: " + typeString + ") => /*" + typeString + "*/ }"
|
|
token(OptSpace ~ function) ^^^ Nil
|
|
}
|
|
else if( assignNoInput(assign) )
|
|
{
|
|
val value = "/* value of type " + typeString + " */"
|
|
token(Space ~ value) ^^^ Nil
|
|
}
|
|
else
|
|
{
|
|
val open = token(OptSpace ~ '(')
|
|
val quietOptSpace = SpaceClass.*
|
|
val inputKeys = rep1sep(scopedKeyP, token(quietOptSpace ~ ',' ~ OptSpace))
|
|
val close = token(quietOptSpace ~ ')')
|
|
val method = token(Space ~ assignMethodName(sk.key))
|
|
val start = open ~> inputKeys <~ (close ~ method)
|
|
for(in <- start; _ <- initializeFunction(in, typeString,true)) yield in
|
|
}
|
|
}
|
|
|
|
/** For a setting definition `definingKey <<= (..., in, ...) { ... }`,
|
|
* `keyFilter(definingKey)(in)` returns true when `in` is an allowed input for `definingKey` based on whether they are settings or not.
|
|
* For example, if `definingKey` is for a setting, `in` may only be a setting itself. */
|
|
def keyFilter(definingKey: AttributeKey[_]): AttributeKey[_] => Boolean =
|
|
if(isSetting(definingKey)) isSetting _ else isTaskOrSetting _
|
|
|
|
/** Parser for the function literal part of an initialization expression given inputs `in` and
|
|
* the underlying type of the setting or task `typeString`.
|
|
* This parser is not useful for actually parsing; it mainly provides a completion suggestion
|
|
* that is a template for implementing the function. The parameter names are autogenerated mnemonics
|
|
* for the input keys they correspond to. If `withTypes` is true, explicit types will be given for the parameters. */
|
|
def initializeFunction(in: Seq[ScopedKey[_]], typeString: String, withTypes: Boolean): Parser[Seq[ScopedKey[_]]] =
|
|
{
|
|
val names = mnemonics(in.map(_.key))
|
|
val types = in.map(sk => keyTypeString(sk.key))
|
|
val params = if(withTypes) (names, types).zipped.map(_ + ": " + _) else names
|
|
val function = params.mkString("{(", ", ", ") => /* " + typeString + " */ }" )
|
|
token( OptSpace ~ function ) ^^^ in
|
|
}
|
|
|
|
/** Parser for a Scope for a `key` given the current project `context` and evaluated `settings`.
|
|
* The completions are restricted to be more useful. Currently, this parser will suggest
|
|
* only known axis values for configurations and tasks and only in that order.*/
|
|
def scopeParser(key: AttributeKey[_], settings: Settings[Scope], context: ResolvedProject): Parser[Scope] = {
|
|
val data = settings.data
|
|
val allScopes = data.keys.toSeq
|
|
val definedScopes = data.toSeq flatMap { case (scope, attrs) => if(attrs contains key) scope :: Nil else Nil }
|
|
scope(key, allScopes, definedScopes, context)
|
|
}
|
|
|
|
private[this] def scope(key: AttributeKey[_], allScopes: Seq[Scope], definedScopes: Seq[Scope], context: ResolvedProject): Parser[Scope] =
|
|
{
|
|
def axisParser[T](axis: Scope => ScopeAxis[T], name: T => String, description: T => Option[String], label: String): Parser[ScopeAxis[T]] =
|
|
{
|
|
def getChoice(s: Scope): Seq[(String,T)] = axis(s) match {
|
|
case Select(t) => (name(t), t) :: Nil
|
|
case _ => Nil
|
|
}
|
|
def getChoices(scopes: Seq[Scope]): Map[String,T] = scopes.flatMap(getChoice).toMap
|
|
val definedChoices: Set[String] = definedScopes.flatMap(s => axis(s).toOption.map(name)).toSet
|
|
val fullChoices: Map[String,T] = getChoices(allScopes.toSeq)
|
|
val completions = fixedCompletions { (seen, level) => completeScope(seen, level, definedChoices, fullChoices)(description).toSet }
|
|
Act.optionalAxis(inParser ~> token(Space) ~> token(scalaID(fullChoices, label), completions), This)
|
|
}
|
|
val configurations: Map[String, Configuration] = context.configurations.map(c => (configScalaID(c.name), c)).toMap
|
|
val configParser = axisParser[ConfigKey](_.config, c => configScalaID(c.name), ck => configurations.get(ck.name).map(_.description), "configuration")
|
|
val taskParser = axisParser[AttributeKey[_]](_.task, k => keyScalaID(k.label), _.description, "task")
|
|
val nonGlobal = (configParser ~ taskParser) map { case (c,t) => Scope(This, c, t, Global) }
|
|
val global = inParser ~> token( (Space ~ GlobalID) ^^^ GlobalScope)
|
|
global | nonGlobal
|
|
}
|
|
|
|
/** Parser for the assignment method (such as `:=`) for defining `key`. */
|
|
def assign(key: ScopedKey[_]): Parser[Assign.Value] =
|
|
{
|
|
val completions = fixedCompletions { (seen, level) => completeAssign(seen, level, key).toSet }
|
|
val identifier = Act.filterStrings(Op, Assign.values.map(_.toString), "assignment method") map Assign.withName
|
|
token(Space) ~> token(optionallyQuoted(identifier), completions)
|
|
}
|
|
|
|
private[this] def fixedCompletions(f: (String, Int) => Set[Completion]): TokenCompletions =
|
|
TokenCompletions.fixed( (s,l) => Completions( f(s,l) ) )
|
|
|
|
private[this] def scalaID[T](keyMap: Map[String, T], label: String): Parser[T] =
|
|
{
|
|
val identifier = Act.filterStrings(ScalaID, keyMap.keySet, label) map keyMap
|
|
optionallyQuoted(identifier)
|
|
}
|
|
|
|
/** Produce a new parser that allows the input accepted by `p` to be quoted in backticks. */
|
|
def optionallyQuoted[T](p: Parser[T]): Parser[T] =
|
|
(Backtick.? ~ p) flatMap { case (quote, id) => if(quote.isDefined) Backtick.? ^^^ id else success(id) }
|
|
|
|
/** Completions for an assignment method for `key` given the tab completion `level` and existing partial string `seen`.
|
|
* This will filter possible assignment methods based on the underlying type of `key`, so that only `<<=` is shown for input tasks, for example. */
|
|
def completeAssign(seen: String, level: Int, key: ScopedKey[_]): Seq[Completion] =
|
|
{
|
|
val allowed: Iterable[Assign.Value] =
|
|
if(isInputTask(key.key)) Assign.DefineDep :: Nil
|
|
else if(appendable(key.key)) Assign.values
|
|
else assignNoAppend
|
|
val applicable = allowed.toSeq.flatMap { a =>
|
|
val s = a.toString
|
|
if(s startsWith seen) (s, a) :: Nil else Nil
|
|
}
|
|
completeDescribed(seen, true, applicable)(assignDescription)
|
|
}
|
|
|
|
def completeKey(seen: String, keys: Map[String, AttributeKey[_]], level: Int, prominentCutoff: Int, detailLimit: Int): Seq[Completion] =
|
|
completeSelectDescribed(seen, level, keys, detailLimit)(_.description) { case (k,v) => v.rank <= prominentCutoff }
|
|
|
|
def completeScope[T](seen: String, level: Int, definedChoices: Set[String], allChoices: Map[String,T])(description: T => Option[String]): Seq[Completion] =
|
|
completeSelectDescribed(seen, level, allChoices, 10)(description) { case (k,v) => definedChoices(k) }
|
|
|
|
def completeSelectDescribed[T](seen: String, level: Int, all: Map[String,T], detailLimit: Int)(description: T => Option[String])(prominent: (String, T) => Boolean): Seq[Completion] =
|
|
{
|
|
val applicable = all.toSeq.filter { case (k,v) => k startsWith seen }
|
|
val prominentOnly = applicable filter { case (k,v) => prominent(k,v) }
|
|
|
|
val showAll = (level >= 3) || (level == 2 && prominentOnly.size <= detailLimit) || prominentOnly.isEmpty
|
|
val showKeys = if(showAll) applicable else prominentOnly
|
|
val showDescriptions = (level >= 2) || (showKeys.size <= detailLimit)
|
|
completeDescribed(seen, showDescriptions, showKeys)(s => description(s).toList.mkString)
|
|
}
|
|
def completeDescribed[T](seen: String, showDescriptions: Boolean, in: Seq[(String,T)])(description: T => String): Seq[Completion] =
|
|
{
|
|
def appendString(id: String): String = id.stripPrefix(seen) + " "
|
|
if(in.isEmpty)
|
|
Nil
|
|
else if(showDescriptions)
|
|
{
|
|
val withDescriptions = in map { case (id, key) => (id, description(key)) }
|
|
val padded = CommandUtil.aligned("", " ", withDescriptions)
|
|
(padded, in).zipped.map { case (line, (id, key)) =>
|
|
Completion.tokenDisplay(append = appendString(id), display = line + "\n")
|
|
}
|
|
}
|
|
else
|
|
in map { case (id, key) =>
|
|
Completion.tokenDisplay(display= id, append = appendString(id))
|
|
}
|
|
}
|
|
|
|
/** Transforms the hypenated key label `k` into camel-case and quotes it with backticks if it is a Scala keyword.
|
|
* This is intended to be an estimate of the Scala identifier that may be used to reference the keyword in the default sbt context. */
|
|
def keyScalaID(k: String): String = Util.quoteIfKeyword(Util.hypenToCamel(k))
|
|
|
|
/** Transforms the configuration name `c` so that the first letter is capitalized and the name is quoted with backticks if it is a Scala keyword.
|
|
* This is intended to be an estimate of the Scala identifier that may be used to reference the keyword in the default sbt context. */
|
|
def configScalaID(c: String): String = Util.quoteIfKeyword(c.capitalize)
|
|
|
|
/** Returns unambiguous mnemonics for the provided sequence of `keys`.
|
|
* The key labels are first transformed to the shortest unambiguous prefix.
|
|
* Then, duplicate keys in the sequence are given a unique numeric suffix.
|
|
* The results are returned in the same order as the corresponding input keys. */
|
|
def mnemonics(keys: Seq[AttributeKey[_]]): Seq[String] =
|
|
{
|
|
val names = keys.map(key => keyScalaID(key.label))
|
|
val shortened = shortNames(names.toSet)
|
|
val repeated = collection.mutable.Map[String,Int]()
|
|
names.map { name =>
|
|
val base = shortened(name)
|
|
val occurs = repeated.getOrElse(base, 1)
|
|
repeated.put(base, occurs+1)
|
|
if(occurs > 1) base + occurs.toString else base
|
|
}
|
|
}
|
|
|
|
/** For a set of strings, generates a mapping from the original string to the shortest unambiguous prefix.
|
|
* For example, for `Set("blue", "black", "green"), this returns `Map("blue" -> "blu", "black" -> "bla", "green" -> "g")` */
|
|
def shortNames(names: Set[String]): Map[String,String] =
|
|
{
|
|
def loop(i:Int, current: Set[String]): Map[String,String] = {
|
|
val (unambiguous, ambiguous) = current.groupBy(_ charAt i).partition { case (_, v) => v.size <= 1 }
|
|
val thisLevel = unambiguous.values.flatten.map(s => (s, s.substring(0,i+1)))
|
|
val down = ambiguous.values.flatMap { (vs: Set[String]) => loop(i+1, vs) }
|
|
(thisLevel ++ down).toMap
|
|
}
|
|
loop(0, names)
|
|
}
|
|
|
|
/** Applies a function on the underlying manifest for T for `key` depending if it is for a `Setting[T]`, `Task[T]`, or `InputTask[T]`.*/
|
|
def keyType[S](key: AttributeKey[_])(onSetting: Manifest[_] => S, onTask: Manifest[_] => S, onInput: Manifest[_] => S)(implicit tm: Manifest[Task[_]], im: Manifest[InputTask[_]]): S =
|
|
{
|
|
def argTpe = key.manifest.typeArguments.head
|
|
val e = key.manifest.erasure
|
|
if(e == tm.erasure) onTask(argTpe)
|
|
else if(e == im.erasure) onInput(argTpe)
|
|
else onSetting(key.manifest)
|
|
}
|
|
|
|
/** For a Task[T], InputTask[T], or Setting[T], this returns the manifest for T. */
|
|
def keyUnderlyingType(key: AttributeKey[_]): Manifest[_] = keyType(key)(idFun, idFun, idFun)
|
|
|
|
/** Returns a string representation of the underlying type T for a `key` representing a `Setting[T]`, `Task[T]`, or `InputTask[T]`.
|
|
* This string representation is currently a cleaned up toString of the underlying Manifest. */
|
|
def keyTypeString[T](key: AttributeKey[_]): String =
|
|
{
|
|
val mfToString = (mf: Manifest[_]) => complete.TypeString.cleanup(mf.toString)
|
|
keyType(key)(mfToString, mfToString ,mfToString)
|
|
}
|
|
|
|
/** True if the `key` represents an input task, false if it represents a task or setting. */
|
|
def isInputTask(key: AttributeKey[_]): Boolean = keyType(key)(const(false), const(false), const(true))
|
|
|
|
/** True if the `key` represents a setting, false if it represents a task or an input task.*/
|
|
def isSetting(key: AttributeKey[_]): Boolean = keyType(key)(const(true), const(false), const(false))
|
|
|
|
/** True if the `key` represents a setting or task, false if it is for an input task. */
|
|
def isTaskOrSetting(key: AttributeKey[_]): Boolean = keyType(key)(const(true), const(true), const(false))
|
|
|
|
/** True if the `key` represents a setting or task that may be appended using an assignment method such as `+=`. */
|
|
def appendable(key: AttributeKey[_]): Boolean =
|
|
{
|
|
val underlying = keyUnderlyingType(key).erasure
|
|
appendableClasses.exists(_ isAssignableFrom underlying)
|
|
}
|
|
|
|
|
|
/** Name of the method to apply a function to several inputs depending on whether `key` is for a setting or task. */
|
|
def assignMethodName(key: AttributeKey[_]): String =
|
|
if(isSetting(key)) SettingAppName else TaskAppName
|
|
|
|
/** The simple name of the global scope axis, which can be used to reference it in the default setting context. */
|
|
final val GlobalID = Global.getClass.getSimpleName.stripSuffix("$")
|
|
|
|
/** Character used to quote a Scala identifier that would otherwise be interpreted as a keyword.*/
|
|
final val Backtick = '`'
|
|
|
|
/** Name of the method that modifies the scope of a key. */
|
|
final val InMethod = "in"
|
|
|
|
/** Name of the method that applies a function to several setting inputs (type Initialize[T]), producing a new setting.*/
|
|
final val SettingAppName = "apply"
|
|
|
|
/** Name of the method used to apply a function to several task inputs (type Initialize[Task[T]]), producing a new task*/
|
|
final val TaskAppName = "map"
|
|
|
|
/** Assignment methods that may be called on a setting or task. */
|
|
object Assign extends Enumeration {
|
|
val AppendValue = Value("+=")
|
|
val AppendValueDep = Value("<+=")
|
|
val AppendValues = Value("++=")
|
|
val AppendValuesDep = Value("<++=")
|
|
val Define = Value(":=")
|
|
val DefineDep = Value("<<=")
|
|
val Update = Value("~=")
|
|
}
|
|
import Assign._
|
|
/** Returns the description associated with the provided assignment method. */
|
|
def assignDescription(a: Assign.Value): String = a match {
|
|
case AppendValue => "append value"
|
|
case AppendValueDep => "append dependent value"
|
|
case AppendValues => "append values"
|
|
case AppendValuesDep => "append dependent values"
|
|
case Define => "define value, overwriting any existing value"
|
|
case DefineDep => "define dependent value, overwriting any existing value"
|
|
case Update => "transform existing value"
|
|
}
|
|
/** The assignment methods except for the ones that append. */
|
|
val assignNoAppend: Set[Assign.Value] = Set(Define, DefineDep, Update)
|
|
|
|
/** The assignment methods that do not accept an Initialize (that is, don't use values from other settings/tasks). */
|
|
val assignNoInput: Set[Assign.Value] = Set(AppendValue, AppendValues, Define, Update)
|
|
|
|
/** Class values to approximate which types can be appended*/
|
|
val appendableClasses = Seq(
|
|
classOf[Seq[_]],
|
|
classOf[Map[_,_]],
|
|
classOf[Set[_]],
|
|
classOf[Int],
|
|
classOf[Double],
|
|
classOf[Long],
|
|
classOf[String]
|
|
)
|
|
} |