Skip to content
This repository has been archived by the owner on Sep 29, 2023. It is now read-only.

Commit

Permalink
added meta-data support at the level of the completions
Browse files Browse the repository at this point in the history
  • Loading branch information
jchapuis committed Sep 26, 2017
1 parent e3abfe2 commit f3ceb83
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 53 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
)

Expand All @@ -126,22 +130,22 @@ 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
* @param tagScore the completion tag score
* @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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -191,15 +195,16 @@ 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.
* @param meta the completion meta-data (to be used e.g. to specify the visual style for a completion entry in the menu)
* @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
Expand All @@ -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)

Expand All @@ -233,26 +258,27 @@ 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)
.toSeq)
}

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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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]())
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading

0 comments on commit f3ceb83

Please sign in to comment.