From e35f9bb11ef851590811e1bccfcb082b8a27fecf Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 22 Feb 2016 12:12:32 +0100 Subject: [PATCH] Completion for build-level keys sbt's shell provided completion only for keys that were relative to a defined project, but didn't provide completion for keys that belong to the build definition only. This commit fixes this issue by defining a new kind of `Parser` (from which completions are generated) which runs its input simultaneously on distinct parsers. We now define a parser for project-level keys and another parser for build-level keys. These two parsers are eventually combined, and we get the completions of both parsers. Fixes sbt/sbt#2460 --- main/src/main/scala/sbt/Act.scala | 22 +++++++++++---- .../src/main/scala/sbt/complete/Parser.scala | 28 +++++++++++++++++++ util/complete/src/test/scala/ParserTest.scala | 9 ++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/main/src/main/scala/sbt/Act.scala b/main/src/main/scala/sbt/Act.scala index 3606de881..cde3a73e5 100644 --- a/main/src/main/scala/sbt/Act.scala +++ b/main/src/main/scala/sbt/Act.scala @@ -48,12 +48,22 @@ object Act { new ParsedKey(makeScopedKey(proj, conf, task, extra, key), mask) } - 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 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) } 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) diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index 892119f6c..4b87e876e 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -126,6 +126,9 @@ 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. */ @@ -229,6 +232,12 @@ object Parser extends ParserMain { } } + def combinedParser[A](a: Parser[A], b: Parser[A]): Parser[Seq[A]] = + if (a.valid) + if (b.valid) new CombiningParser(a, b) else a.map(Seq(_)) + else + b.map(Seq(_)) + def choiceParser[A, B](a: Parser[A], b: Parser[B]): Parser[Either[A, B]] = if (a.valid) if (b.valid) new HetParser(a, b) else a.map(left.fn) @@ -309,6 +318,7 @@ 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]] = combinedParser(a, b) } implicit def literalRichCharParser(c: Char): RichParser[Char] = richParser(c) @@ -633,6 +643,24 @@ 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 f02431e53..8bcffad47 100644 --- a/util/complete/src/test/scala/ParserTest.scala +++ b/util/complete/src/test/scala/ParserTest.scala @@ -107,6 +107,15 @@ 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)+