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

Commit f4946bd

Browse files
author
Jonas Chapuis
committed
improvements in meta-data handling: renamed to meta & now supporting merging JSON meta-data
1 parent 73d17e8 commit f4946bd

File tree

9 files changed

+134
-100
lines changed

9 files changed

+134
-100
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Add the following lines to your `build.sbt` file:
2020

2121
```
2222
resolvers += Resolver.bintrayRepo("jchapuis", "maven")
23-
libraryDependencies += "com.nexthink" %% "scala-parser-combinators-completion" % "1.0.2"
23+
libraryDependencies += "com.nexthink" %% "scala-parser-combinators-completion" % "1.0.3"
2424
```
2525

2626
## Completing on a grammar
@@ -228,8 +228,8 @@ Completing on the same expressions (`2`, `2+`, `(10*2`) now leads to the followi
228228
|`%` |defines the completions tag|<code>("+" &#124; "-") % "operators"</code>|
229229
|`%` |followed by an `Int`, defines the completion tag score|<code>("+" &#124; "-") % 10</code>|
230230
|`%?` |defines the description of the completions tag|<code>("+" &#124; "-") %? "arithmetic operators"</code>|
231-
|`%%` |defines the completions tag meta-data (can be used to encode properties for the tag, e.g. visual decorations)|<code>("+" &#124; "-") %% "style: highlight"</code>|
232-
|`%-%` |defines the completion meta-data (can be used to encode properties for each completion entry, e.g. visual decorations)|<code>("+" %-% "style: highlight" &#124; "-")</code>|
231+
|`%%` |defines the completions tag meta-data (can be used to encode properties for the tag in JSON, e.g. visual decorations)|<code>("+" &#124; "-") %% "{\\"style\\": \\"highlight\\"}"</code>|
232+
|`%-%` |defines the completion meta-data (can be used to encode properties for each completion entry in JSON, e.g. visual decorations)|<code>("+" %-% "{\\"style\\": \\"highlight\\"}" &#124; "-")</code>|
233233

234234
## Fuzzy completion
235235

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name := "scala-parser-combinators-completion"
22
organization := "com.nexthink"
33
licenses += ("MIT", url("http://opensource.org/licenses/MIT"))
4-
version := "1.0.2"
4+
version := "1.0.3"
55
scalaVersion := "2.12.2"
66
bintrayRepository := "maven"
77
bintrayVcsUrl := Some("jchapuis@github.com:jchapuis/scala-parser-combinators-completion")

src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionExpansionSupport.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ trait CompletionExpansionSupport extends RegexCompletionSupport {
5656
else
5757
completions.allSets
5858
.map(cSet => {
59-
cSet.completions
59+
cSet.entries
6060
.map(c => {
6161
val completedInput = completeString(str, completions.position.column, c)
6262
if (stop(new CharSequenceReader(completedInput)).successful) {

src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionSupport.scala

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,7 @@ trait CompletionSupport extends Parsers with CompletionTypes {
115115
this,
116116
in => {
117117
val completions = this.completions(in)
118-
Completions(completions.position,
119-
completions.sets.mapValues(s =>
120-
CompletionSet(s.tag, s.completions.toList.sortBy(_.score).reverse.take(n).toSet)))
118+
Completions(completions.position, completions.sets.mapValues(s => CompletionSet(s.tag, s.entries.toList.sortBy(_.score).reverse.take(n).toSet)).toSeq)
121119
}
122120
)
123121

@@ -150,8 +148,7 @@ trait CompletionSupport extends Parsers with CompletionTypes {
150148
* @return wrapper `Parser` instance specifying completion tag
151149
*/
152150
def %(tag: String, tagScore: Int, tagDescription: String): Parser[T] =
153-
Parser(this,
154-
in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), None))
151+
Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), None))
155152

156153
/** An operator to specify the completion tag, score, description and meta of a parser
157154
* @param tag the completion tag
@@ -161,19 +158,14 @@ trait CompletionSupport extends Parsers with CompletionTypes {
161158
* @return wrapper `Parser` instance specifying completion tag
162159
*/
163160
def %(tag: String, tagScore: Int, tagDescription: String, tagKind: String): Parser[T] =
164-
Parser(
165-
this,
166-
in =>
167-
updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), Some(tagKind)))
161+
Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag), Some(tagScore), Some(tagDescription), Some(tagKind)))
168162

169163
/** An operator to specify the completion tag
170164
* @param tag the completion tag
171165
* @return wrapper `Parser` instance specifying completion tag
172166
*/
173167
def %(tag: CompletionTag): Parser[T] =
174-
Parser(
175-
this,
176-
in => updateCompletionsTag(this.completions(in), Some(tag.label), Some(tag.score), tag.description, tag.meta))
168+
Parser(this, in => updateCompletionsTag(this.completions(in), Some(tag.label), Some(tag.score), tag.description, tag.meta))
177169

178170
/** An operator to specify the completion tag description of a parser (empty by default)
179171
* @param tagDescription the completion description (to be used e.g. to add information to a completion entry)
@@ -182,14 +174,20 @@ trait CompletionSupport extends Parsers with CompletionTypes {
182174
def %?(tagDescription: String): Parser[T] =
183175
Parser(this, in => updateCompletionsTag(this.completions(in), None, None, Some(tagDescription), None))
184176

185-
/** An operator to specify the completion tag meta of a parser (empty by default)
177+
/** An operator to specify the completion tag meta-data of a parser (empty by default).
178+
* Note that if the meta-data is encoded in JSON, it is automatically merged when combining two equivalent tags
179+
* (i.e. bearing the same label, but with a different payload). This allows for more flexibility when defining the grammar:
180+
* various parsers can return the same completion tags with an additive effect on the meta-data (and the entries).
186181
* @param tagMeta the completion tag meta-data (to be used e.g. to specify the visual style for a completion tag in the menu)
187182
* @return wrapper `Parser` instance specifying the completion tag meta-data
188183
*/
189184
def %%(tagMeta: String): Parser[T] =
190185
Parser(this, in => updateCompletionsTag(this.completions(in), None, None, None, Some(tagMeta)))
191186

192-
/** An operator to specify the meta for completions of a parser (empty by default)
187+
/** An operator to specify the meta-data for completions of a parser (empty by default)
188+
* Note that if the meta-data is encoded in JSON, it is automatically merged when combining two equivalent tags
189+
* (i.e. bearing the same label, but with a different payload). This allows for more flexibility when defining the grammar:
190+
* various parsers can return the same completion entries with an additive effect on the meta-data.
193191
* @param meta the completion meta-data (to be used e.g. to specify the visual style for a completion entry in the menu)
194192
* @return wrapper `Parser` instance specifying the completion meta-data
195193
*/
@@ -222,7 +220,7 @@ trait CompletionSupport extends Parsers with CompletionTypes {
222220
completions.sets.values
223221
.map(updateSet)
224222
.map(s => s.tag.label -> s)
225-
.toMap)
223+
.toSeq)
226224
}
227225

228226
private def updateCompletionsTag(completions: Completions,
@@ -231,15 +229,14 @@ trait CompletionSupport extends Parsers with CompletionTypes {
231229
newTagDescription: Option[String],
232230
newTagKind: Option[String]) = {
233231
def updateSet(existingSet: CompletionSet) =
234-
CompletionSet(existingSet.tag.update(newTagLabel, newTagScore, newTagDescription, newTagKind),
235-
existingSet.completions)
232+
CompletionSet(existingSet.tag.update(newTagLabel, newTagScore, newTagDescription, newTagKind), existingSet.completions)
236233

237234
updateCompletionsSets(completions, updateSet)
238235
}
239236

240237
private def updateCompletions(completions: Completions, newCompletionKind: Option[String]) = {
241238
def updateSet(existingSet: CompletionSet) =
242-
CompletionSet(existingSet.tag, existingSet.completions.map(e => e.updateKind(newCompletionKind)))
239+
CompletionSet(existingSet.tag, existingSet.entries.map(e => e.updateKind(newCompletionKind)))
243240
updateCompletionsSets(completions, updateSet)
244241
}
245242

@@ -898,5 +895,4 @@ trait CompletionSupport extends Parsers with CompletionTypes {
898895
def phrase[T](p: Parser[T]): Parser[T] =
899896
Parser(super.phrase(p), p.completions)
900897

901-
902898
}

src/main/scala/com/nexthink/utils/parsing/combinator/completion/CompletionTypes.scala

Lines changed: 63 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ package com.nexthink.utils.parsing.combinator.completion
1010
import org.json4s
1111

1212
import scala.util.parsing.input.{NoPosition, Position}
13-
import org.json4s._
1413
import org.json4s.JsonDSL._
1514
import org.json4s.native.JsonMethods._
15+
import scala.collection.immutable
1616

1717
/** Collection of data types allowing definition of structured parser completions.
1818
* A `Completions` instance can contain multiple `CompletionSet`s instances. A `CompletionSet` provides a set of
@@ -73,45 +73,52 @@ trait CompletionTypes {
7373
* @param tag set tag
7474
* @param completions set of unique completion entries
7575
*/
76-
case class CompletionSet(tag: CompletionTag, completions: Set[Completion]) {
77-
def label: String = tag.label
78-
def score: Int = tag.score
79-
def description: Option[String] = tag.description
80-
def meta: Option[String] = tag.meta
81-
def completionStrings: Seq[String] =
82-
completions.toSeq.sorted.map(_.value.toString)
76+
case class CompletionSet(tag: CompletionTag, completions: immutable.HashMap[Elems, Completion]) {
77+
def label: String = tag.label
78+
def score: Int = tag.score
79+
def description: Option[String] = tag.description
80+
def meta: Option[String] = tag.meta
81+
def entries: Iterable[Completion] = completions.values
82+
def sortedEntries: Seq[Completion] = entries.toSeq.sorted
83+
def stringEntries: Seq[String] = sortedEntries.map(_.value.toString)
8384

8485
private[CompletionTypes] def serializeJson =
85-
("tag" -> tag.serializeJson) ~ ("completions" -> completions.map(_.serializeJson).toList)
86+
("tag" -> tag.serializeJson) ~ ("completions" -> entries.map(_.serializeJson).toList)
8687

8788
override def toString: String = pretty(render(serializeJson))
8889
def toJson: String = compact(render(serializeJson))
8990
}
9091

9192
case object CompletionSet {
93+
def apply(tag: CompletionTag, completions: Seq[(Elems, Completion)]): CompletionSet =
94+
CompletionSet(tag, immutable.HashMap(completions: _*))
95+
9296
def apply(tag: String, el: Elem): CompletionSet =
93-
CompletionSet(CompletionTag(tag), Set(Completion(el)))
97+
CompletionSet(CompletionTag(tag), Seq(Seq(el) -> Completion(el)))
9498

9599
def apply(tag: String, elems: Elems): CompletionSet =
96-
CompletionSet(CompletionTag(tag), Set(Completion(elems)))
100+
CompletionSet(CompletionTag(tag), Seq(elems -> Completion(elems)))
97101

98102
def apply(tag: String, completion: Completion): CompletionSet =
99-
CompletionSet(CompletionTag(tag), Set(completion))
103+
CompletionSet(CompletionTag(tag), Seq(completion.value -> completion))
104+
105+
def apply(tag: CompletionTag, completions: Iterable[Completion]): CompletionSet =
106+
CompletionSet(tag, completions.map(c => c.value -> c).toSeq)
100107

101108
def apply(tag: String, completions: Iterable[Completion]): CompletionSet =
102-
CompletionSet(CompletionTag(tag), completions.toSet)
109+
CompletionSet(CompletionTag(tag), completions.map(c => c.value -> c).toSeq)
103110

104111
def apply(completions: Iterable[Completion]): CompletionSet =
105-
CompletionSet(CompletionTag.Default, completions.toSet)
112+
CompletionSet(CompletionTag.Default, completions.map(c => c.value -> c).toSeq)
106113

107114
def apply(completions: Completion*): CompletionSet =
108-
CompletionSet(CompletionTag.Default, completions.toSet)
115+
CompletionSet(CompletionTag.Default, completions.map(c => c.value -> c))
109116

110117
def apply(el: Elem): CompletionSet =
111-
CompletionSet(CompletionTag.Default, Set(Completion(el)))
118+
CompletionSet(CompletionTag.Default, Seq(Seq(el) -> Completion(el)))
112119

113120
def apply(completions: Traversable[Elems]): CompletionSet =
114-
CompletionSet(CompletionTag.Default, completions.map(Completion(_)).toSet)
121+
CompletionSet(CompletionTag.Default, completions.map(c => c -> Completion(c)).toSeq)
115122
}
116123

117124
type Elems = Seq[Elem]
@@ -142,46 +149,45 @@ trait CompletionTypes {
142149
* @param position position in the input where completion entries apply
143150
* @param sets completion entries, grouped per tag
144151
*/
145-
case class Completions(position: Position, sets: Map[String, CompletionSet]) {
152+
case class Completions(position: Position, sets: immutable.HashMap[String, CompletionSet]) {
146153
def isEmpty: Boolean = sets.isEmpty
147154
def nonEmpty: Boolean = !isEmpty
148155
def setWithTag(tag: String): Option[CompletionSet] = sets.get(tag)
149156
def allSets: Iterable[CompletionSet] = sets.values
150-
def allCompletions: Iterable[Completion] = allSets.flatMap(_.completions)
157+
def allCompletions: Iterable[Completion] = allSets.flatMap(_.sortedEntries)
151158
def defaultSet: Option[CompletionSet] = sets.get("")
152159

153160
private def serializeJson = ("position" -> (("line" -> position.line) ~ ("column" -> position.column))) ~ ("sets" -> allSets.map(_.serializeJson))
154161

155162
override def toString: String = pretty(render(serializeJson))
156163
def toJson: String = compact(render(serializeJson))
157164

158-
private def unionSets(left: CompletionSet, right: CompletionSet): CompletionSet = {
159-
def offsetCompletions(set: CompletionSet) = {
160-
val isOffsetRequired =
161-
set.completions.map(_.score).exists(_ < set.score)
162-
if (isOffsetRequired)
163-
set.completions.map(c => Completion(c.value, set.score + c.score, c.meta))
164-
else set.completions
165-
}
166-
CompletionSet(
167-
CompletionTag(left.tag.label, left.score.min(right.score), left.description, left.meta.orElse(right.meta)),
168-
offsetCompletions(left) ++ offsetCompletions(right)
165+
private def mergeMetaData(left: Option[String], right: Option[String]) = (left, right) match {
166+
case (Some(l), Some(r)) =>
167+
(parseOpt(l), parseOpt(r)) match {
168+
case (Some(lJson), Some(rJson)) => Some(compact(render(lJson merge rJson)))
169+
case _ => Some(l + r)
170+
}
171+
case (Some(l), None) => Some(l)
172+
case (None, Some(r)) => Some(r)
173+
case (None, None) => None
174+
}
175+
176+
private def mergeCompletion(left: Completion, right: Completion): Completion = {
177+
assert(left.value == right.value, "Attempt to merge different completion entries")
178+
Completion(
179+
left.value,
180+
left.score.max(right.score),
181+
mergeMetaData(left.meta, right.meta)
169182
)
170183
}
171184

172-
private def mergeCompletions(other: Completions) = {
173-
val overlappingSetTags = sets.keySet.intersect(other.sets.keySet)
174-
val unions =
175-
overlappingSetTags.map(name => (sets(name), other.sets(name))).map {
176-
case (left, right) => unionSets(left, right)
177-
}
178-
val leftExclusive = sets.keySet.diff(overlappingSetTags).map(sets(_))
179-
val rightExclusive =
180-
other.sets.keySet.diff(overlappingSetTags).map(other.sets(_))
181-
Completions(position,
182-
(unions ++ leftExclusive ++ rightExclusive)
183-
.map(s => s.tag.label -> s)
184-
.toMap)
185+
private def mergeSets(left: CompletionSet, right: CompletionSet): CompletionSet = {
186+
assert(left.label == right.label, "Attempt to merge sets with different completion tags")
187+
CompletionSet(
188+
CompletionTag(left.tag.label, left.score.max(right.score), left.description.orElse(right.description), mergeMetaData(left.meta, right.meta)),
189+
left.completions.merged(right.completions)((l, r) => (l._1, mergeCompletion(l._2, r._2)))
190+
)
185191
}
186192

187193
def |(other: Completions): Completions = {
@@ -190,7 +196,7 @@ trait CompletionTypes {
190196
case _ =>
191197
other.position match {
192198
case otherPos if otherPos < position => this
193-
case otherPos if otherPos == position => mergeCompletions(other)
199+
case otherPos if otherPos == position => Completions(position, sets.merged(other.sets)((l, r) => (l._1, mergeSets(l._2, r._2))))
194200
case _ => other
195201
}
196202
}
@@ -200,17 +206,17 @@ trait CompletionTypes {
200206
sets.values.toSeq
201207
.sortBy(_.score)
202208
.reverse
203-
.flatMap(_.completionStrings)
209+
.flatMap(_.stringEntries)
204210
.toList
205211

206212
def takeTop(count: Int): Completions = {
207213
val allEntries = allSets
208-
.flatMap(s => s.completions.map((_, s.tag)))
214+
.flatMap(s => s.completions.values.map((_, s.tag)))
209215
.toList
210216
val sortedEntries =
211217
allEntries
212218
.sortBy {
213-
case (Completion(_, score, meta), CompletionTag(_, tagScore, _, _)) =>
219+
case (Completion(_, score, _), CompletionTag(_, tagScore, _, _)) =>
214220
(tagScore, score)
215221
}
216222
.reverse
@@ -219,27 +225,31 @@ trait CompletionTypes {
219225
.groupBy { case (_, tag) => tag }
220226
.map {
221227
case (groupTag, completions) =>
222-
CompletionSet(groupTag, completions.map(_._1).toSet)
228+
CompletionSet(groupTag, completions.map(c => c._1))
223229
}
224-
copy(sets = regroupedSets.map(s => (s.tag.label, s)).toMap)
230+
Completions(position, regroupedSets.map(s => s.tag.label -> s).toSeq)
225231
}
226232

227233
def setsScoredWithMaxCompletion(): Completions = {
228-
Completions(position, sets.mapValues(s => CompletionSet(s.tag.copy(score = s.completions.map(_.score).max), s.completions)))
234+
Completions(position, sets.mapValues(s => CompletionSet(s.tag.copy(score = s.completions.values.map(_.score).max), s.completions)).toSeq)
229235
}
230236
}
231237

232238
case object Completions {
239+
def apply(position: Position, completionSets: Seq[(String, CompletionSet)]): Completions =
240+
Completions(position, immutable.HashMap(completionSets: _*))
233241
def apply(position: Position, completionSet: CompletionSet): Completions =
234-
Completions(position, Map(completionSet.tag.label -> completionSet))
242+
Completions(position, Seq(completionSet.tag.label -> completionSet))
235243
def apply(position: Position, completions: Traversable[Elems]): Completions =
236244
Completions(position, CompletionSet(completions))
237245
def apply(completionSet: CompletionSet): Completions =
238246
Completions(NoPosition, completionSet)
247+
def apply(position: Position, completionSets: Iterable[CompletionSet]): Completions =
248+
Completions(position, completionSets.map(s => s.tag.label -> s).toSeq)
239249
def apply(completionSets: Iterable[CompletionSet]): Completions =
240-
Completions(NoPosition, completionSets.map(s => s.tag.label -> s).toMap)
250+
Completions(NoPosition, completionSets.map(s => s.tag.label -> s).toSeq)
241251

242-
val empty = Completions(NoPosition, Map[String, CompletionSet]())
252+
val empty = Completions(NoPosition, immutable.HashMap[String, CompletionSet]())
243253
}
244254

245255
}

0 commit comments

Comments
 (0)