sbt.internal.SettingCompletions.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of main_2.12 Show documentation
Show all versions of main_2.12 Show documentation
sbt is an interactive build tool
The newest version!
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import sbt.internal.util.{ AttributeKey, complete, Relation, Settings, Types, Util }
import sbt.util.Show
import sbt.librarymanagement.Configuration
import Project._
import Def.{ ScopedKey, Setting }
import Scope.Global
import Types.idFun
import complete._
import DefaultParsers._
import scala.annotation.nowarn
/**
* 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 = Def
.flattenLocals(
Def.compiled(extracted.structure.settings, true)(
structure.delegates,
structure.scopeLocal,
implicitly[Show[ScopedKey[_]]]
)
)
.keys
val projectScope = Load.projectScope(currentRef)
def resolve(s: Setting[_]): Seq[Setting[_]] =
Load.transformSettings(projectScope, currentRef.build, rootProject, s :: Nil)
@nowarn
def rescope[T](setting: Setting[T]): Seq[Setting[_]] = {
val akey = setting.key.key
val global = ScopedKey(Global, akey)
val globalSetting = resolve(Def.setting(global, setting.init, setting.pos))
globalSetting ++ allDefs.flatMap { d =>
if (d.key == akey)
Seq(SettingKey(akey) in d.scope := { global.value })
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(extracted: Extracted, settings: Seq[Def.Setting[_]], arg: String): SetResult = {
import extracted._
val append =
Load.transformSettings(Load.projectScope(currentRef), currentRef.build, rootProject, settings)
val newSession = session.appendSettings(append map (a => (a, arg.split('\n').toList)))
val r = relation(newSession.mergeSettings, true)(
structure.delegates,
structure.scopeLocal,
implicitly
)
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.show(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 hyphenated 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 keyMap: Map[String, AttributeKey[_]] =
rawKeyMap.map { case (k, v) => (keyScalaID(k), v) }.toMap
val full = for {
defineKey <- scopedKeyParser(keyMap, settings, context)
a <- assign(defineKey)
_ <- valueParser(defineKey, a)
} 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 ".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): 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 {
val value = "/* value of type " + typeString + " */"
token(Space ~ value) ^^^ Nil
}
}
/**
* 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(allScopes, definedScopes, context)
}
private[this] def scope(
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)
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, Zero) }
val global = inParser ~> token((Space ~ GlobalID) ^^^ Global)
global | nonGlobal
}
/** Parser for the assignment method (such as `:=`) for defining `key`. */
def assign(key: ScopedKey[_]): Parser[Assign.Value] = {
val completions = fixedCompletions { (seen, _) =>
completeAssign(seen, 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, key: ScopedKey[_]): Seq[Completion] = {
val allowed: Iterable[Assign.Value] =
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 (_, 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, _) => 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, _) => k startsWith seen }
val prominentOnly = applicable filter { case (k, v) => prominent(k, v) }
val showAll = (level >= 3) || (level == 2 && prominentOnly.lengthCompare(detailLimit) <= 0) || prominentOnly.isEmpty
val showKeys = if (showAll) applicable else prominentOnly
val showDescriptions = (level >= 2) || showKeys.lengthCompare(detailLimit) <= 0
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, _)) =>
Completion.tokenDisplay(append = appendString(id), display = line + "\n")
}
} else
in map { case (id, _) => Completion.tokenDisplay(display = id, append = appendString(id)) }
}
/**
* Transforms the hyphenated 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.hyphenToCamel(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)
/** 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 TaskClass = tm.runtimeClass
val InputTaskClass = im.runtimeClass
key.manifest.runtimeClass match {
case TaskClass => onTask(argTpe)
case InputTaskClass => onInput(argTpe)
case _ => 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 a setting or task that may be appended using an assignment method such as `+=`. */
def appendable(key: AttributeKey[_]): Boolean = {
val underlying = keyUnderlyingType(key).runtimeClass
appendableClasses.exists(_ isAssignableFrom underlying)
}
/** The simple name of the Global scope, which can be used to reference it in the default setting context. */
final val GlobalID = Scope.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"
/** Assignment methods that may be called on a setting or task. */
object Assign extends Enumeration {
val AppendValue = Value("+=")
val AppendValues = Value("++=")
val Define = 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 AppendValues => "append values"
case Define => "define 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, 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]
)
}