From f3ceb83a7c9a421676ad57ef7c3779260fe99575 Mon Sep 17 00:00:00 2001 From: jchapuis Date: Tue, 26 Sep 2017 11:15:53 +0200 Subject: [PATCH] added meta-data support at the level of the completions --- build.sbt | 2 +- .../CompletionExpansionSupport.scala | 4 +- .../completion/CompletionSupport.scala | 60 ++++++++++++----- .../completion/CompletionTypes.scala | 65 ++++++++++++------- .../CompletionExpansionSupportTest.scala | 8 ++- .../completion/CompletionOperatorsTest.scala | 4 +- .../completion/CompletionTypesTest.scala | 17 +++-- 7 files changed, 107 insertions(+), 53 deletions(-) diff --git a/build.sbt b/build.sbt index 06b4ffd..d2fecea 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ name := "scala-parser-combinators-completion" organization := "com.nexthink" licenses += ("MIT", url("http://opensource.org/licenses/MIT")) -version := "1.0.5" +version := "1.0.6" scalaVersion := "2.12.2" bintrayRepository := "maven" bintrayVcsUrl := Some("jchapuis@github.com:jchapuis/scala-parser-combinators-completion") diff --git a/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupport.scala b/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupport.scala index 813ea9a..3555f5a 100644 --- a/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupport.scala +++ b/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupport.scala @@ -39,8 +39,8 @@ trait CompletionExpansionSupport extends RegexCompletionSupport { in => { lazy val isAtInputEnd = dropAnyWhiteSpace(in).atEnd if (!onlyAtInputEnd || isAtInputEnd) { - val Completions(_, sets) = exploreCompletions(p, limiter, in) - Completions(OffsetPosition(in.source, handleWhiteSpace(in)), sets) + val Completions(_, meta, sets) = exploreCompletions(p, limiter, in) + Completions(OffsetPosition(in.source, handleWhiteSpace(in)), meta, sets) } else p.completions(in) } diff --git a/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionSupport.scala b/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionSupport.scala index ac48023..a484029 100644 --- a/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionSupport.scala +++ b/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionSupport.scala @@ -117,7 +117,11 @@ trait CompletionSupport extends Parsers with CompletionTypes { this, in => { val completions = this.completions(in) - Completions(completions.position, completions.sets.mapValues(s => CompletionSet(s.tag, s.entries.toList.sortBy(_.score).reverse.take(n).toSet)).toSeq) + Completions( + completions.position, + completions.meta, + completions.sets.mapValues(s => CompletionSet(s.tag, s.entries.toList.sortBy(_.score).reverse.take(n).toSet)).toSeq + ) } ) @@ -126,14 +130,14 @@ trait CompletionSupport extends Parsers with CompletionTypes { * @return wrapper `Parser` instance specifying the completion tag */ def %(tag: String): Parser[T] = - Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), None, None, None)) + Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag))) /** An operator to specify the completions tag score of a parser (0 by default) * @param tagScore the completion tag score (to be used e.g. to order sections in a completion menu) * @return wrapper `Parser` instance specifying the completion tag score */ def %(tagScore: Int): Parser[T] = - Parser(this, in => updateCompletionsTag(this.completions(in), None, Some(tagScore), None, None)) + Parser(this, in => updateCompletionsTag(this.completions(in), None, Some(tagScore))) /** An operator to specify the completion tag and score of a parser * @param tag the completion tag @@ -141,7 +145,7 @@ trait CompletionSupport extends Parsers with CompletionTypes { * @return wrapper `Parser` instance specifying the completion tag */ def %(tag: String, tagScore: Int): Parser[T] = - Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), None, None)) + Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore))) /** An operator to specify the completion tag, score and description of a parser * @param tag the completion tag @@ -150,17 +154,17 @@ trait CompletionSupport extends Parsers with CompletionTypes { * @return wrapper `Parser` instance specifying completion tag */ def %(tag: String, tagScore: Int, tagDescription: String): Parser[T] = - Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), None)) + Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription))) /** An operator to specify the completion tag, score, description and meta of a parser * @param tag the completion tag * @param tagScore the completion tag score * @param tagDescription the completion tag description - * @param tagKind the completion tag meta + * @param tagMeta the completion tag meta * @return wrapper `Parser` instance specifying completion tag */ - def %(tag: String, tagScore: Int, tagDescription: String, tagKind: String): Parser[T] = - Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), Some(tagKind))) + def %(tag: String, tagScore: Int, tagDescription: String, tagMeta: String): Parser[T] = + Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), Some(tagMeta))) /** An operator to specify the completion tag * @param tag the completion tag @@ -174,7 +178,7 @@ trait CompletionSupport extends Parsers with CompletionTypes { * @return wrapper `Parser` instance specifying the completion description */ def %?(tagDescription: String): Parser[T] = - Parser(this, in => updateCompletionsTag(this.completions(in), None, None, Some(tagDescription), None)) + Parser(this, in => updateCompletionsTag(this.completions(in), None, None, Some(tagDescription))) /** An operator to specify the completion tag meta-data of a parser (empty by default). * Note that meta-data is merged with comma separations when combining two equivalent entries. @@ -191,7 +195,8 @@ trait CompletionSupport extends Parsers with CompletionTypes { * @param tagMeta the JValue for completion tag meta-data (to be used e.g. to specify the visual style for a completion tag in the menu) * @return wrapper `Parser` instance specifying the completion tag meta-data */ - def %%(tagMeta: JValue): Parser[T] = %%(compact(render(tagMeta))) + def %%(tagMeta: JValue): Parser[T] = + Parser(this, in => updateCompletionsSets(this.completions(in), set => CompletionSet(set.tag.updateMeta(tagMeta), set.completions))) /** An operator to specify the meta-data for completions of a parser (empty by default). * Note that meta-data is merged with comma separations when combining two equivalent entries. @@ -199,7 +204,7 @@ trait CompletionSupport extends Parsers with CompletionTypes { * @return wrapper `Parser` instance specifying the completion meta-data */ def %-%(meta: String): Parser[T] = - Parser(this, in => updateCompletions(this.completions(in), Some(meta))) + Parser(this, in => updateCompletionsSets(this.completions(in), set => CompletionSet(set.tag, set.entries.map(e => e.updateMeta(meta))))) /** An operator to specify the meta-data for completions of a parser in JSON format (empty by default) * Note that if the meta-data is encoded in JSON, it is automatically merged when combining two equivalent entries @@ -210,6 +215,26 @@ trait CompletionSupport extends Parsers with CompletionTypes { */ def %-%(meta: JValue): Parser[T] = %-%(compact(render(meta))) + /** + * An operator to specify the meta-data for the whole set of completions (empty by default) + * Note that meta-data is merged with comma separations when combining two equivalent entries. + * @param globalMeta the meta-data (to be used e.g. to specify the visual style for the completion menu) + * @return wrapper `Parser` instance specifying the completions meta-data + */ + def %%%(globalMeta: String): Parser[T] = + Parser(this, in => this.completions(in).updateMeta(globalMeta)) + + /** + * An operator to specify the meta-data for the whole set of completions (empty by default) + * Note that if the meta-data is encoded in JSON, it is automatically merged when combining multiple completion sets. + * This allows for more flexibility when defining the grammar: various parsers can define the global completion meta-data + * with an additive effect. + * @param globalMeta the JValue for completions meta-data (to be used e.g. to specify the visual style for the completion menu) + * @return wrapper `Parser` instance specifying the completions meta-data + */ + def %%%(globalMeta: JValue): Parser[T] = + Parser(this, in => this.completions(in).updateMeta(globalMeta)) + def flatMap[U](f: T => Parser[U]): Parser[U] = Parser(super.flatMap(f), completions) @@ -233,6 +258,7 @@ trait CompletionSupport extends Parsers with CompletionTypes { private def updateCompletionsSets(completions: Completions, updateSet: CompletionSet => CompletionSet) = { Completions(completions.position, + completions.meta, completions.sets.values .map(updateSet) .map(s => s.tag.label -> s) @@ -240,19 +266,19 @@ trait CompletionSupport extends Parsers with CompletionTypes { } private def updateCompletionsTag(completions: Completions, - newTagLabel: Option[String], - newTagScore: Option[Int], - newTagDescription: Option[String], - newTagKind: Option[String]) = { + newTagLabel: Option[String] = None, + newTagScore: Option[Int] = None, + newTagDescription: Option[String] = None, + newTagKind: Option[String] = None) = { def updateSet(existingSet: CompletionSet) = CompletionSet(existingSet.tag.update(newTagLabel, newTagScore, newTagDescription, newTagKind), existingSet.completions) updateCompletionsSets(completions, updateSet) } - private def updateCompletions(completions: Completions, newCompletionKind: Option[String]) = { + private def updateCompletionsMeta(completions: Completions, newMeta: String) = { def updateSet(existingSet: CompletionSet) = - CompletionSet(existingSet.tag, existingSet.entries.map(e => e.updateKind(newCompletionKind))) + CompletionSet(existingSet.tag, existingSet.entries.map(e => e.updateMeta(newMeta))) updateCompletionsSets(completions, updateSet) } diff --git a/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypes.scala b/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypes.scala index 63e386a..22d2a02 100644 --- a/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypes.scala +++ b/src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypes.scala @@ -55,11 +55,13 @@ trait CompletionTypes { meta = newMeta.map(Some(_)).getOrElse(meta) ) + def updateMeta(newMeta: JValue): CompletionTag = copy(meta = Some(encodeJson(newMeta))) + private[CompletionTypes] def serializeJson: json4s.JObject = { ("label" -> label) ~ ("score" -> score) ~ ("description" -> description) ~ ("meta" -> meta) } - override def toString: String = pretty(render(serializeJson)) + override def toString: String = printJson(serializeJson) def toJson: JValue = serializeJson } @@ -70,6 +72,8 @@ trait CompletionTypes { CompletionTag(label, DefaultCompletionScore, None, None) def apply(label: String, score: Int): CompletionTag = CompletionTag(label, score, None, None) + def apply(label: String, score: Int, description: String): CompletionTag = + CompletionTag(label, score, Some(description), None) } /** Set of related completion entries @@ -88,7 +92,7 @@ trait CompletionTypes { private[CompletionTypes] def serializeJson = ("tag" -> tag.serializeJson) ~ ("completions" -> entries.map(_.serializeJson).toList) - override def toString: String = pretty(render(serializeJson)) + override def toString: String = printJson(serializeJson) def toJson: JValue = serializeJson } @@ -133,14 +137,14 @@ trait CompletionTypes { */ case class Completion(value: Elems, score: Int = DefaultCompletionScore, meta: Option[String] = None) { require(value.nonEmpty, "empty completion") - def updateKind(newMeta: Option[String]): Completion = - copy(meta = newMeta.map(Some(_)).getOrElse(meta)) + def updateMeta(newMeta: JValue): Completion = updateMeta(encodeJson(newMeta)) + def updateMeta(newMeta: String): Completion = copy(meta = Some(newMeta)) private[CompletionTypes] def serializeJson = ("value" -> value.toString()) ~ ("score" -> score) ~ ("meta" -> meta) - def toJson: String = compact(render(serializeJson)) + def toJson: String = encodeJson(serializeJson) - override def toString: String = pretty(render(serializeJson)) + override def toString: String = printJson(serializeJson) } case object Completion { def apply(el: Elem): Completion = Completion(Seq(el)) @@ -152,24 +156,27 @@ trait CompletionTypes { * @param position position in the input where completion entries apply * @param sets completion entries, grouped per tag */ - case class Completions(position: Position, sets: immutable.HashMap[String, CompletionSet]) { + case class Completions(position: Position, meta: Option[String], sets: immutable.HashMap[String, CompletionSet]) { def isEmpty: Boolean = sets.isEmpty def nonEmpty: Boolean = !isEmpty def setWithTag(tag: String): Option[CompletionSet] = sets.get(tag) def allSets: Iterable[CompletionSet] = sets.values.toSeq.sorted def allCompletions: Iterable[Completion] = allSets.flatMap(_.sortedEntries) def defaultSet: Option[CompletionSet] = sets.get("") + def updateMeta(newMeta: JValue): Completions = updateMeta(encodeJson(newMeta)) + def updateMeta(newMeta: String): Completions = copy(meta = Some(newMeta)) - private def serializeJson = ("position" -> (("line" -> position.line) ~ ("column" -> position.column))) ~ ("sets" -> allSets.map(_.serializeJson)) + private def serializeJson = + ("position" -> (("line" -> position.line) ~ ("column" -> position.column))) ~ ("meta" -> meta) ~ ("sets" -> allSets.map(_.serializeJson)) - override def toString: String = pretty(render(serializeJson)) + override def toString: String = printJson(serializeJson) def toJson: JValue = serializeJson def setsToJson: JArray = allSets.map(_.serializeJson) private def mergeMetaData(left: Option[String], right: Option[String]) = (left, right) match { case (Some(l), Some(r)) => (parseOpt(l), parseOpt(r)) match { - case (Some(lJson), Some(rJson)) => Some(compact(render(lJson merge rJson))) + case (Some(lJson), Some(rJson)) => Some(encodeJson(lJson merge rJson)) case _ => Some(Seq(l, r).mkString(", ")) } case (Some(l), None) => Some(l) @@ -199,9 +206,10 @@ trait CompletionTypes { case Completions.empty => this case _ => other.position match { - case otherPos if otherPos < position => this - case otherPos if otherPos == position => Completions(position, sets.merged(other.sets)((l, r) => (l._1, mergeSets(l._2, r._2)))) - case _ => other + case otherPos if otherPos < position => this + case otherPos if otherPos == position => + Completions(position, mergeMetaData(meta, other.meta), sets.merged(other.sets)((l, r) => (l._1, mergeSets(l._2, r._2)))) + case _ => other } } } @@ -231,29 +239,38 @@ trait CompletionTypes { case (groupTag, completions) => CompletionSet(groupTag, completions.map(c => c._1)) } - Completions(position, regroupedSets.map(s => s.tag.label -> s).toSeq) + Completions(position, meta, regroupedSets.map(s => s.tag.label -> s).toSeq) } def setsScoredWithMaxCompletion(): Completions = { - Completions(position, sets.mapValues(s => CompletionSet(s.tag.copy(score = s.completions.values.map(_.score).max), s.completions)).toSeq) + Completions(position, meta, sets.mapValues(s => CompletionSet(s.tag.copy(score = s.completions.values.map(_.score).max), s.completions)).toSeq) } } + private def encodeJson(meta: JValue) = compact(render(meta)) + private def printJson(meta: JValue) = pretty(render(meta)) + case object Completions { - def apply(position: Position, completionSets: Seq[(String, CompletionSet)]): Completions = - Completions(position, immutable.HashMap(completionSets: _*)) + def apply(position: Position, meta: Option[String], completionSets: Seq[(String, CompletionSet)]): Completions = + Completions(position, meta, immutable.HashMap(completionSets: _*)) def apply(position: Position, completionSet: CompletionSet): Completions = - Completions(position, Seq(completionSet.tag.label -> completionSet)) + Completions(position, None, Seq(completionSet.tag.label -> completionSet)) + def apply(position: Position, meta: Option[String], completionSet: CompletionSet): Completions = + Completions(position, None, Seq(completionSet.tag.label -> completionSet)) + def apply(position: Position, meta: Option[String], completions: Traversable[Elems]): Completions = + Completions(position, meta, CompletionSet(completions)) def apply(position: Position, completions: Traversable[Elems]): Completions = - Completions(position, CompletionSet(completions)) - def apply(completionSet: CompletionSet): Completions = - Completions(NoPosition, completionSet) + Completions(position, None, CompletionSet(completions)) + def apply(position: Position, meta:Option[String], completionSets: Iterable[CompletionSet]): Completions = + Completions(position, meta, completionSets.map(s => s.tag.label -> s).toSeq) def apply(position: Position, completionSets: Iterable[CompletionSet]): Completions = - Completions(position, completionSets.map(s => s.tag.label -> s).toSeq) + Completions(position, None, completionSets.map(s => s.tag.label -> s).toSeq) + def apply(completionSet: CompletionSet): Completions = + Completions(NoPosition, None, completionSet) def apply(completionSets: Iterable[CompletionSet]): Completions = - Completions(NoPosition, completionSets.map(s => s.tag.label -> s).toSeq) + Completions(NoPosition, None, completionSets.map(s => s.tag.label -> s).toSeq) - val empty = Completions(NoPosition, immutable.HashMap[String, CompletionSet]()) + val empty = Completions(NoPosition, None, immutable.HashMap[String, CompletionSet]()) } } diff --git a/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupportTest.scala b/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupportTest.scala index 82f9675..3d62bd5 100644 --- a/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupportTest.scala +++ b/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupportTest.scala @@ -41,7 +41,7 @@ class CompletionExpansionSupportTest { val jumpsOver = "which jumps over the lazy" % "action" val jumpsOverDogOrCat = jumpsOver ~ ("dog" | "cat") % "animal" %? "dogs and cats" % 10 lazy val parser = jumpsOverDogOrCat | jumpsOverDogOrCat ~ which() - def which(): Parser[Any] = expandedCompletionsWithLimiter(parser, limiter = jumpsOverDogOrCat ~ jumpsOverDogOrCat) + def which(): Parser[Any] = expandedCompletionsWithLimiter(parser, limiter = jumpsOverDogOrCat ~ jumpsOverDogOrCat) %%% "expansions" lazy val infiniteDogsAndCats = fox ~ which } @@ -73,6 +73,12 @@ class CompletionExpansionSupportTest { ) } + @Test + def infiniteExpressionExpansionIncludesGlobalMeta(): Unit = { + val completions = InfiniteExpressionParser.complete(InfiniteExpressionParser.infiniteDogsAndCats, "the quick brown fox ") + Assert.assertEquals(Some("expansions"), completions.meta) + } + @Test def expanderWithOnlyAtInputEndDoesNotExpandElsewhere(): Unit = { val completions = InfiniteExpressionParser.completeString(InfiniteExpressionParser.infiniteDogsAndCats, "the quick brown fox which jumps over the lazy") diff --git a/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionOperatorsTest.scala b/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionOperatorsTest.scala index a1f7b9c..3c2c6aa 100644 --- a/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionOperatorsTest.scala +++ b/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionOperatorsTest.scala @@ -94,15 +94,17 @@ class CompletionOperatorsTest { @Test def topCompletionsLimitsCompletionsAccordingToScore(): Unit = { // Arrange + val meta = "meta" val completions = Seq("one", "two", "three", "four").zipWithIndex.map { case (c, s) => TestParser.Completion(c, s) } - val sut = (TestParser.someParser %> completions).topCompletions(2) + val sut = (TestParser.someParser %> completions %%% meta).topCompletions(2) // Act val result = TestParser.complete(sut, "") // Assert Assert.assertArrayEquals(Seq("four", "three").toArray[AnyRef], result.completionStrings.toArray[AnyRef]) + Assert.assertEquals(Some(meta), result.meta) } } diff --git a/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypesTest.scala b/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypesTest.scala index 4864f59..12d5010 100644 --- a/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypesTest.scala +++ b/src/test/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypesTest.scala @@ -16,16 +16,17 @@ class CompletionTypesTest extends CompletionTypes { override type Elem = Char val setA = CompletionSet( - CompletionTag("A", 10, Some("description"), Some(compact(render("type" -> "a-type")))), - Set(Completion("a", 2, Some("meta1")), Completion("b", 1, Some(compact(render(("objects" -> Seq("devices")) ~ ("themes" -> Seq("some"))))))) + CompletionTag("A", 10, "description").updateMeta("type" -> "a-type"), + Set(Completion("a", 2, Some("meta1")), Completion("b", 1).updateMeta(("objects" -> Seq("devices")) ~ ("themes" -> Seq("some")))) ) + val setB = CompletionSet(CompletionTag("B", 5), Set(Completion("c", 4), Completion("d", 3))) val setC = CompletionSet("C", Completion("e", 10)) val setAPrime = CompletionSet( - CompletionTag("A", 10, None, Some(compact(render("style" -> "highlight")))), + CompletionTag("A", 10).updateMeta("style" -> "highlight"), Set( Completion("a", 4, Some("meta2")), - Completion("b", 1, Some(compact(render(("objects" -> Seq("users", "packages")) ~ ("themes" -> Seq("other")))))), + Completion("b", 1).updateMeta(("objects" -> Seq("users", "packages")) ~ ("themes" -> Seq("other"))), Completion("aa") ) ) @@ -53,17 +54,18 @@ class CompletionTypesTest extends CompletionTypes { @Test def completionsAtSamePositionAreMerged(): Unit = { // Act - val merged = Completions(Seq(setA, setB)) | Completions(Seq(setAPrime, setC)) + val merged = Completions(Seq(setA, setB)).updateMeta("context" -> Seq("contextA")) | Completions(Seq(setAPrime, setC)) + .updateMeta("context" -> Seq("contextB")) // Assert assertArrayEquals( merged.allSets.toArray[AnyRef], Seq( CompletionSet( - CompletionTag("A", 10, Some("description"), Some(compact(render(("type" -> "a-type") ~ ("style" -> "highlight"))))), + CompletionTag("A", 10, "description").updateMeta(("type" -> "a-type") ~ ("style" -> "highlight")), Set( Completion("a", 4, Some("meta1, meta2")), - Completion("b", 1, Some(compact(render(("objects" -> Seq("devices", "users", "packages")) ~ ("themes" -> Seq("some", "other")))))), + Completion("b", 1).updateMeta(("objects" -> Seq("devices", "users", "packages")) ~ ("themes" -> Seq("some", "other"))), Completion("aa") ) ), @@ -71,6 +73,7 @@ class CompletionTypesTest extends CompletionTypes { setC ).toArray[AnyRef] ) + assertEquals(Some(compact(render("context" -> Seq("contextA", "contextB")))), merged.meta) } @Test