diff --git a/main/src/main/scala/sbt/Act.scala b/main/src/main/scala/sbt/Act.scala index b4365b442..28d016142 100644 --- a/main/src/main/scala/sbt/Act.scala +++ b/main/src/main/scala/sbt/Act.scala @@ -48,22 +48,12 @@ object Act { new ParsedKey(makeScopedKey(proj, conf, task, extra, key), mask) } - val projectKeys = - for { - rawProject <- optProjectRef(index, current) - proj = resolveProject(rawProject, current) - confAmb <- config(index configs proj) - partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false) - } yield taskKeyExtra(proj, confAmb, partialMask) - - val build = Some(BuildRef(current.build)) - val buildKeys = - for { - confAmb <- config(index configs build) - partialMask = ScopeMask(false, confAmb.isExplicit, false, false) - } yield taskKeyExtra(build, confAmb, partialMask) - - buildKeys combinedWith projectKeys map (_.flatten) + for { + rawProject <- optProjectRef(index, current) + proj = resolveProject(rawProject, current) + confAmb <- config(index configs proj) + partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false) + } yield taskKeyExtra(proj, confAmb, partialMask) } def makeScopedKey(proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]], extra: ScopeAxis[AttributeMap], key: AttributeKey[_]): ScopedKey[_] = ScopedKey(Scope(toAxis(proj, Global), toAxis(conf map ConfigKey.apply, Global), toAxis(task, Global), extra), key) @@ -77,16 +67,11 @@ object Act { selectFromValid(ss filter isValid(data), default) } def selectFromValid(ss: Seq[ParsedKey], default: Parser[ParsedKey])(implicit show: Show[ScopedKey[_]]): Parser[ParsedKey] = - selectByTask(selectByConfig(ss)) partition isBuildKey match { - case (_, Seq(single)) => success(single) - case (Seq(single), Seq()) => success(single) - case (Seq(), Seq()) => default - case (buildKeys, projectKeys) => failure("Ambiguous keys: " + showAmbiguous(keys(buildKeys ++ projectKeys))) + selectByTask(selectByConfig(ss)) match { + case Seq() => default + case Seq(single) => success(single) + case multi => failure("Ambiguous keys: " + showAmbiguous(keys(multi))) } - private def isBuildKey(parsed: ParsedKey): Boolean = parsed.key.scope.project match { - case Select(_: BuildReference) => true - case _ => false - } private[this] def keys(ss: Seq[ParsedKey]): Seq[ScopedKey[_]] = ss.map(_.key) def selectByConfig(ss: Seq[ParsedKey]): Seq[ParsedKey] = ss match { @@ -148,7 +133,16 @@ object Act { token(ID !!! "Expected key" examples dropHyphenated(keys)) flatMap { keyString => getKey(keyMap, keyString, idFun) } - keyParser(index.keys(proj, conf, task)) + // Fixes sbt/sbt#2460 and sbt/sbt#2851 + // The parser already accepts build-level keys. + // This queries the key index so tab completion will list the build-level keys. + val buildKeys: Set[String] = + proj match { + case Some(ProjectRef(uri, id)) => index.keys(Some(BuildRef(uri)), conf, task) + case _ => Set() + } + val keys: Set[String] = index.keys(proj, conf, task) ++ buildKeys + keyParser(keys) } def getKey[T](keyMap: Map[String, AttributeKey[_]], keyString: String, f: AttributeKey[_] => T): Parser[T] = @@ -315,4 +309,4 @@ object Act { final object Omitted extends ParsedAxis[Nothing] final class ParsedValue[T](val value: T) extends ParsedAxis[T] def value[T](t: Parser[T]): Parser[ParsedAxis[T]] = t map { v => new ParsedValue(v) } -} +} \ No newline at end of file diff --git a/main/src/test/scala/ParseKey.scala b/main/src/test/scala/ParseKey.scala index 1081f3517..d9607e2de 100644 --- a/main/src/test/scala/ParseKey.scala +++ b/main/src/test/scala/ParseKey.scala @@ -31,7 +31,7 @@ object ParseKey extends Properties("Key parser test") { parseExpected(structure, string, expected, mask) } - property("An unspecified project axis resolves to the current project or the build of the current project") = + property("An unspecified project axis resolves to the current project") = forAllNoShrink(structureDefinedKey) { (skm: StructureKeyMask) => import skm.{ structure, key } @@ -43,7 +43,7 @@ object ParseKey extends Properties("Key parser test") { ("Current: " + structure.current) |: parse(structure, string) { case Left(err) => false - case Right(sk) => sk.scope.project == Select(structure.current) || sk.scope.project == Select(BuildRef(structure.current.build)) + case Right(sk) => sk.scope.project == Select(structure.current) } } diff --git a/notes/0.13.14/buildlevelkey.md b/notes/0.13.14/buildlevelkey.md new file mode 100644 index 000000000..e5ee44518 --- /dev/null +++ b/notes/0.13.14/buildlevelkey.md @@ -0,0 +1,9 @@ +### Bug fixes + +- Fixes regressions in sbt 0.13.11 - 0.13.13 that processed build-level keys incorrectly. [#2851][2851]/[#2460][2460] by [@eed3si9n] + + [#2851]: https://github.com/sbt/sbt/issues/2851 + [#2460]: https://github.com/sbt/sbt/issues/2460 + [@eed3si9n]: https://github.com/eed3si9n + [@dwijnand]: https://github.com/dwijnand + [@Duhemm]: https://github.com/Duhemm diff --git a/sbt/src/sbt-test/actions/completions/build.sbt b/sbt/src/sbt-test/actions/completions/build.sbt new file mode 100644 index 000000000..072ab22e7 --- /dev/null +++ b/sbt/src/sbt-test/actions/completions/build.sbt @@ -0,0 +1,29 @@ +import complete.{ Completion, Completions, DefaultParsers, Parser } +import DefaultParsers._ +import Command.applyEffect +import CommandUtil._ + +lazy val root = (project in file(".")). + enablePlugins(FooPlugin). + settings( + commands += checkCompletionsCommand + ) + +// This checks the tab completion lists build-level keys +def checkCompletionsCommand = Command.make("checkCompletions")(completionsParser) +def completionsParser(state: State) = + { + val notQuoted = (NotQuoted ~ any.*) map { case (nq, s) => (nq +: s).mkString } + val quotedOrUnquotedSingleArgument = Space ~> (StringVerbatim | StringEscapable | notQuoted) + applyEffect(token(quotedOrUnquotedSingleArgument ?? "" examples ("", " ")))(runCompletions(state)) + } +def runCompletions(state: State)(input: String): State = { + val xs = Parser.completions(state.combinedParser, input, 9).get map { + c => if (c.isEmpty) input else input + c.append + } map { c => + c.replaceAll("\n", " ") + } + println(xs) + assert(xs == Set("myTask")) + state +} diff --git a/sbt/src/sbt-test/actions/completions/project/FooPlugin.scala b/sbt/src/sbt-test/actions/completions/project/FooPlugin.scala new file mode 100644 index 000000000..4bef386d9 --- /dev/null +++ b/sbt/src/sbt-test/actions/completions/project/FooPlugin.scala @@ -0,0 +1,14 @@ +import sbt._ + +object FooPlugin extends AutoPlugin { + override def trigger = noTrigger + object autoImport { + val myTask = taskKey[Unit]("My task") + } + + import autoImport._ + + override def buildSettings = super.buildSettings ++ Seq( + myTask := println("Called my task") + ) +} diff --git a/sbt/src/sbt-test/actions/completions/test b/sbt/src/sbt-test/actions/completions/test new file mode 100644 index 000000000..942db912e --- /dev/null +++ b/sbt/src/sbt-test/actions/completions/test @@ -0,0 +1 @@ +> checkCompletions my diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index 1c2b1cd7d..9b8aff7d8 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -126,9 +126,6 @@ sealed trait RichParser[A] { /** Applies the original parser, applies `f` to the result to get the next parser, and applies that parser and uses its result for the overall result. */ def flatMap[B](f: A => Parser[B]): Parser[B] - - /** Applied both the original parser and `b` on the same input and returns the results produced by each parser */ - def combinedWith(b: Parser[A]): Parser[Seq[A]] } /** Contains Parser implementation helper methods not typically needed for using parsers. */ @@ -312,11 +309,6 @@ trait ParserMain { def filter(f: A => Boolean, msg: String => String): Parser[A] = filterParser(a, f, "", msg) def string(implicit ev: A <:< Seq[Char]): Parser[String] = map(_.mkString) def flatMap[B](f: A => Parser[B]) = bindParser(a, f) - def combinedWith(b: Parser[A]): Parser[Seq[A]] = - if (a.valid) - if (b.valid) new CombiningParser(a, b) else a.map(Seq(_)) - else - b.map(Seq(_)) } implicit def literalRichCharParser(c: Char): RichParser[Char] = richParser(c) @@ -641,24 +633,6 @@ private final class HetParser[A, B](a: Parser[A], b: Parser[B]) extends ValidPar def completions(level: Int) = a.completions(level) ++ b.completions(level) override def toString = "(" + a + " || " + b + ")" } -private final class CombiningParser[T](a: Parser[T], b: Parser[T]) extends ValidParser[Seq[T]] { - lazy val result: Option[Seq[T]] = (a.result.toSeq ++ b.result.toSeq) match { case Seq() => None; case seq => Some(seq) } - def completions(level: Int) = a.completions(level) ++ b.completions(level) - def derive(i: Char) = - (a.valid, b.valid) match { - case (true, true) => new CombiningParser(a derive i, b derive i) - case (true, false) => a derive i map (Seq(_)) - case (false, true) => b derive i map (Seq(_)) - case (false, false) => new Invalid(mkFailure("No valid parser available.")) - } - def resultEmpty = - (a.resultEmpty, b.resultEmpty) match { - case (Value(ra), Value(rb)) => Value(Seq(ra, rb)) - case (Value(ra), _) => Value(Seq(ra)) - case (_, Value(rb)) => Value(Seq(rb)) - case _ => Value(Nil) - } -} private final class ParserSeq[T](a: Seq[Parser[T]], errors: => Seq[String]) extends ValidParser[Seq[T]] { assert(a.nonEmpty) lazy val resultEmpty: Result[Seq[T]] = diff --git a/util/complete/src/test/scala/ParserTest.scala b/util/complete/src/test/scala/ParserTest.scala index 8bcffad47..f02431e53 100644 --- a/util/complete/src/test/scala/ParserTest.scala +++ b/util/complete/src/test/scala/ParserTest.scala @@ -107,15 +107,6 @@ object ParserTest extends Properties("Completing Parser") { property("repeatDep requires at least one token") = !matches(repeat, "") property("repeatDep accepts one token") = matches(repeat, colors.toSeq.head) property("repeatDep accepts two tokens") = matches(repeat, colors.toSeq.take(2).mkString(" ")) - property("combined parser gives completion of both parsers") = { - val prefix = "fix" - val p1Suffixes = Set("", "ated", "ation") - val p2Suffixes = Set("es", "ed") - val p1: Parser[String] = p1Suffixes map (suffix => (prefix + suffix): Parser[String]) reduce (_ | _) - val p2: Parser[String] = p2Suffixes map (suffix => (prefix + suffix): Parser[String]) reduce (_ | _) - val suggestions: Set[Completion] = p1Suffixes ++ p2Suffixes map (new Suggestion(_)) - checkAll(prefix, p1 combinedWith p2, Completions(suggestions)) - } } object ParserExample { val ws = charClass(_.isWhitespace)+