diff --git a/README.md b/README.md index df187b4d..054ed7ac 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,16 @@ This library is now community-maintained. If you are interested in helping pleas As of Scala 2.11, this library is a separate jar that can be omitted from Scala projects that do not use Parser Combinators. +#### New: completion parsers +Mixing-in the `CompletionSupport` trait enables completion support for a grammar (use `RegexCompletionSupport` for `RegexParsers`): + +```scala +object MyParsers extends RegexParsers with RegexCompletionSupport +``` + +Parsers are thus 'augmented' with a `completions` method which returns possible entry completions for a certain input. This can be used to elaborate as-you-type completions menus or tab-completion experiences, and is e.g. easy to plug with readline to implement a console application. +A set of additional operators also allow overriding completions and specifying ordering and grouping properties for completions. + ## Documentation * [Latest version](http://www.scala-lang.org/files/archive/api/2.11.x/scala-parser-combinators/) diff --git a/shared/src/main/scala/scala/util/parsing/combinator/completion/CompletionSupport.scala b/shared/src/main/scala/scala/util/parsing/combinator/completion/CompletionSupport.scala new file mode 100644 index 00000000..45419165 --- /dev/null +++ b/shared/src/main/scala/scala/util/parsing/combinator/completion/CompletionSupport.scala @@ -0,0 +1,893 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import scala.annotation.tailrec +import scala.util.parsing.combinator.Parsers +import scala.util.parsing.input.Positional + +/** `CompletionSupport` adds completion capability to parsers. + * + * When mixed-in, this trait extends + * the [[scala.util.parsing.combinator.Parsers.Parser]] type with the abstract method + * [[scala.util.parsing.combinator.completion.CompletionSupport.Parser#completions]] + * which returns a instance of [[scala.util.parsing.combinator.completion.CompletionTypes.Completions]] + * for a certain input. + * + * Combinators are overloaded to cover the additional completion aspect, so that no change is required in the grammar. + * + * Note that the derived trait [[scala.util.parsing.combinator.completion.RegexCompletionSupport]] can be mixed-in + * with `RegexParsers` to automatically obtain completion behavior for string literals. + * + * A set of additional operators allow defining completions and specifying structural properties of completions + * (tag, score, kind, etc.) for a `Parser`. + * + * @author Jonas Chapuis + */ +trait CompletionSupport extends Parsers with CompletionTypes { + def Parser[T](f: Input => ParseResult[T], c: Input => Completions) = new Parser[T] { + def apply(in: Input) = f(in) + def completions(in: Input) = c(in) + } + + /** The root class of completion parsers, overloading the `Parser` class. + * Completion parsers are functions from the Input type to ParseResult, with the + * addition of a `completions` function from the Input type to an instance of `Completions` + */ + abstract class Parser[+T] extends super.Parser[T] { + + def append[U >: T](p0: => Parser[U]): Parser[U] = { + lazy val p = p0 + Parser( + in => super.append(p)(in), + in => { + val thisCompletions = this.completions(in) + lazy val combinedCompletions = thisCompletions | p.completions(in) + this(in) match { + case Success(_, rest) => + // only return any completions if they start at the last position, otherwise it can behave badly e.g. with fuzzy matching + if (combinedCompletions.position < rest.pos) Completions.empty + else combinedCompletions + case Failure(_, rest) => combinedCompletions + case Error(_, _) => + thisCompletions // avoids backtracking completions in the case of error, e.g. when using the ~! operator + } + } + ) + } + + /** An unspecified method that defines the possible completions for this parser + * + * @param in the input + * @return an instance of [[scala.util.parsing.combinator.completion.CompletionTypes.Completions]] + */ + def completions(in: Input): Completions + + /** An operator to specify completions of a parser + * @param completions possible completions for this parser + * @return a `Parser` that upon invocation of the `completions` method returns the passed completions + */ + def %>(completions: Elems*): Parser[T] = + %>(completions.map(el => Completion(el))) + + /** An operator to specify completion of a parser + * @param completion completion for this parser + * @return a `Parser` that upon invocation of the `completions` method returns the passed completion + */ + def %>(completion: Completion): Parser[T] = + %>(Set(completion)) + + /** An operator to specify completions of a parser + * @param completions possible completions for this parser + * @return a `Parser` that upon invocation of the `completions` method returns the passed completions + */ + def %>(completions: Iterable[Completion]): Parser[T] = + Parser(this, in => { + this(in) match { + case Failure(_, rest) if rest.atEnd => + Completions(rest.pos, CompletionSet(completions)) + case _ => Completions.empty + } + }) + + /** An operator to specify completions of a parser + * @param completioner function of input to completions + * @return a `Parser` that upon invocation of the `completions` method will invoke the passed function + */ + def %>(completioner: Input => Completions): Parser[T] = + Parser(this, completioner) + + /** Limits completions to the top `n` completions ordered by their score + * @param n the limit + * @return wrapper `Parser` instance limiting the number of completions + */ + def topCompletions(n: Int): Parser[T] = + Parser( + this, + in => { + val completions = this.completions(in) + Completions(completions.position, + completions.sets.mapValues(s => + CompletionSet(s.tag, s.completions.toList.sortBy(_.score).reverse.take(n).toSet))) + } + ) + + /** An operator to specify the completion tag of a parser (empty tag by default) + * @param tag the completion tag (to be used e.g. to structure a completion menu) + * @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)) + + /** 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)) + + /** 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)) + + /** An operator to specify the completion tag, score and description of a parser + * @param tag the completion tag + * @param tagScore the completion tag score + * @param tagDescription the completion tag description + * @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)) + + /** An operator to specify the completion tag, score, description and kind 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 kind + * @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))) + + /** An operator to specify the completion tag + * @param tag the completion tag + * @return wrapper `Parser` instance specifying completion tag + */ + def %(tag: CompletionTag): Parser[T] = + Parser( + this, + in => updateCompletionsTag(this.completions(in), Some(tag.label), Some(tag.score), tag.description, tag.kind)) + + /** An operator to specify the completion tag description of a parser (empty by default) + * @param tagDescription the completion description (to be used e.g. to add information to a completion entry) + * @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)) + + /** An operator to specify the completion tag kind of a parser (empty by default) + * @param tagKind the completion tag kind (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 kind + */ + def %%(tagKind: String): Parser[T] = + Parser(this, in => updateCompletionsTag(this.completions(in), None, None, None, Some(tagKind))) + + /** An operator to specify the kind for completions of a parser (empty by default) + * @param kind the completion kind (to be used e.g. to specify the visual style for a completion entry in the menu) + * @return wrapper `Parser` instance specifying the completion kind + */ + def %-%(kind: String): Parser[T] = + Parser(this, in => updateCompletions(this.completions(in), Some(kind))) + + def flatMap[U](f: T => Parser[U]): Parser[U] = + Parser(super.flatMap(f), completions) + + override def map[U](f: T => U): Parser[U] = + Parser(super.map(f), completions) + + override def filter(p: T => Boolean): Parser[T] = withFilter(p) + + override def withFilter(p: T => Boolean): Parser[T] = + Parser(super.withFilter(p), completions) + + private def seqCompletions[U](in: Input, other: => Parser[U]): Completions = { + lazy val thisCompletions = this.completions(in) + this(in) match { + case Success(_, rest) => + thisCompletions | other.completions(rest) + case NoSuccess(_, _) => + thisCompletions + } + } + + private def updateCompletionsSets(completions: Completions, updateSet: CompletionSet => CompletionSet) = { + Completions(completions.position, + completions.sets.values + .map(updateSet) + .map(s => s.tag.label -> s) + .toMap) + } + + private def updateCompletionsTag(completions: Completions, + newTagLabel: Option[String], + newTagScore: Option[Int], + newTagDescription: Option[String], + newTagKind: Option[String]) = { + 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]) = { + def updateSet(existingSet: CompletionSet) = + CompletionSet(existingSet.tag, existingSet.completions.map(e => e.updateKind(newCompletionKind))) + updateCompletionsSets(completions, updateSet) + } + + /** A parser combinator for sequential composition. + * + * `p ~ q` succeeds if `p` succeeds and `q` succeeds on the input left over by `p`. + * + * @param q a parser that will be executed after `p` (this parser) + * succeeds -- evaluated at most once, and only when necessary. + * @return a `Parser` that -- on success -- returns a `~` (like a `Pair`, + * but easier to pattern match on) that contains the result of `p` and + * that of `q`. The resulting parser fails if either `p` or `q` fails. + */ + def ~[U](q: => Parser[U]): Parser[~[T, U]] = { + lazy val p = q + Parser(super.~(q), in => seqCompletions(in, p)) + }.named("~") + + /** A parser combinator for sequential composition which keeps only the right result. + * + * `p ~> q` succeeds if `p` succeeds and `q` succeeds on the input left over by `p`. + * + * @param q a parser that will be executed after `p` (this parser) + * succeeds -- evaluated at most once, and only when necessary. + * @return a `Parser` that -- on success -- returns the result of `q`. + */ + def ~>[U](q: => Parser[U]): Parser[U] = { + lazy val p = q + Parser(super.~>(q), in => seqCompletions(in, p)) + }.named("~>") + + /** A parser combinator for sequential composition which keeps only the left result. + * + * `p <~ q` succeeds if `p` succeeds and `q` succeeds on the input + * left over by `p`. + * + * @note <~ has lower operator precedence than ~ or ~>. + * + * @param q a parser that will be executed after `p` (this parser) succeeds -- evaluated at most once, and only when necessary + * @return a `Parser` that -- on success -- returns the result of `p`. + */ + def <~[U](q: => Parser[U]): Parser[T] = { + lazy val p = q + Parser(super.<~(q), in => seqCompletions(in, p)) + }.named("<~") + + /** A parser combinator for non-back-tracking sequential composition. + * + * `p ~! q` succeeds if `p` succeeds and `q` succeeds on the input left over by `p`. + * In case of failure, no back-tracking is performed (in an earlier parser produced by the `|` combinator). + * + * @param q a parser that will be executed after `p` (this parser) succeeds + * @return a `Parser` that -- on success -- returns a `~` (like a Pair, but easier to pattern match on) + * that contains the result of `p` and that of `q`. + * The resulting parser fails if either `p` or `q` fails, this failure is fatal. + */ + def ~![U](q: => Parser[U]): Parser[~[T, U]] = { + lazy val p = q + Parser(super.~!(q), in => seqCompletions(in, p)) + }.named("<~") + + /** A parser combinator for non-back-tracking sequential composition which only keeps the right result. + * + * `p ~>! q` succeeds if `p` succeds and `q` succeds on the input left over by `p`. + * In case of failure, no back-tracking is performed (in an earlier parser produced by the `|` combinator). + * + * @param q a parser that will be executed after `p` (this parser) succeeds -- evaluated at most once, and only when necessary + * @return a `Parser` that -- on success -- reutrns the result of `q`. + * The resulting parser fails if either `p` or `q` fails, this failure is fatal. + */ + def ~>![U](q: => Parser[U]): Parser[U] = { + lazy val p = q + Parser(super.~>!(q), in => seqCompletions(in, p)) + }.named("~>!") + + /** A parser combinator for non-back-tracking sequential composition which only keeps the left result. + * + * `p <~! q` succeeds if `p` succeds and `q` succeds on the input left over by `p`. + * In case of failure, no back-tracking is performed (in an earlier parser produced by the `|` combinator). + * + * @param q a parser that will be executed after `p` (this parser) succeeds -- evaluated at most once, and only when necessary + * @return a `Parser` that -- on success -- reutrns the result of `p`. + * The resulting parser fails if either `p` or `q` fails, this failure is fatal. + */ + def <~![U](q: => Parser[U]): Parser[T] = { + lazy val p = q + Parser(super.<~!(q), in => seqCompletions(in, p)) + }.named("<~!") + + /** A parser combinator for alternative composition. + * + * `p | q` succeeds if `p` succeeds or `q` succeeds. + * Note that `q` is only tried if `p`s failure is non-fatal (i.e., back-tracking is allowed). + * + * @param q a parser that will be executed if `p` (this parser) fails (and allows back-tracking) + * @return a `Parser` that returns the result of the first parser to succeed (out of `p` and `q`) + * The resulting parser succeeds if (and only if) + * - `p` succeeds, ''or'' + * - if `p` fails allowing back-tracking and `q` succeeds. + */ + def |[U >: T](q: => Parser[U]): Parser[U] = + append(q).named("|") + + /** A parser combinator for alternative with longest match composition. + * + * `p ||| q` succeeds if `p` succeeds or `q` succeeds. + * If `p` and `q` both succeed, the parser that consumed the most characters accepts. + * + * @param q a parser that accepts if p consumes less characters. -- evaluated at most once, and only when necessary + * @return a `Parser` that returns the result of the parser consuming the most characters (out of `p` and `q`). + */ + def |||[U >: T](q: => Parser[U]): Parser[U] = { + lazy val p = q + Parser(super.|||(q), in => this.completions(in) | p.completions(in)) + } + + /** A parser combinator for function application. + * + * `p ^^ f` succeeds if `p` succeeds; it returns `f` applied to the result of `p`. + * + * @param f a function that will be applied to this parser's result (see `map` in `ParseResult`). + * @return a parser that has the same behaviour as the current parser, but whose result is + * transformed by `f`. + */ + override def ^^[U](f: T => U): Parser[U] = + Parser(super.^^(f), completions).named(toString + "^^") + + /** A parser combinator that changes a successful result into the specified value. + * + * `p ^^^ v` succeeds if `p` succeeds; discards its result, and returns `v` instead. + * + * @param v The new result for the parser, evaluated at most once (if `p` succeeds), not evaluated at all if `p` fails. + * @return a parser that has the same behaviour as the current parser, but whose successful result is `v` + */ + override def ^^^[U](v: => U): Parser[U] = { + Parser(super.^^^(v), completions) + }.named(toString + "^^^") + + /** A parser combinator for partial function application. + * + * `p ^? (f, error)` succeeds if `p` succeeds AND `f` is defined at the result of `p`; + * in that case, it returns `f` applied to the result of `p`. If `f` is not applicable, + * error(the result of `p`) should explain why. + * + * @param f a partial function that will be applied to this parser's result + * (see `mapPartial` in `ParseResult`). + * @param error a function that takes the same argument as `f` and produces an error message + * to explain why `f` wasn't applicable + * @return a parser that succeeds if the current parser succeeds and `f` is applicable + * to the result. If so, the result will be transformed by `f`. + */ + override def ^?[U](f: PartialFunction[T, U], error: T => String): Parser[U] = + Parser(super.^?(f, error), completions).named(toString + "^?") + + /** A parser combinator for partial function application. + * + * `p ^? f` succeeds if `p` succeeds AND `f` is defined at the result of `p`; + * in that case, it returns `f` applied to the result of `p`. + * + * @param f a partial function that will be applied to this parser's result + * (see `mapPartial` in `ParseResult`). + * @return a parser that succeeds if the current parser succeeds and `f` is applicable + * to the result. If so, the result will be transformed by `f`. + */ + override def ^?[U](f: PartialFunction[T, U]): Parser[U] = + Parser(super.^?(f), completions) + + /** A parser combinator that parameterizes a subsequent parser with the + * result of this one. + * + * Use this combinator when a parser depends on the result of a previous + * parser. `p` should be a function that takes the result from the first + * parser and returns the second parser. + * + * `p into fq` (with `fq` typically `{x => q}`) first applies `p`, and + * then, if `p` successfully returned result `r`, applies `fq(r)` to the + * rest of the input. + * + * ''From: G. Hutton. Higher-order functions for parsing. J. Funct. Program., 2(3):323--343, 1992.'' + * + * @example {{{ + * def perlRE = "m" ~> (".".r into (separator => """[^%s]*""".format(separator).r <~ separator)) + * }}} + * + * @param fq a function that, given the result from this parser, returns + * the second parser to be applied + * @return a parser that succeeds if this parser succeeds (with result `x`) + * and if then `fq(x)` succeeds + */ + def into[U](fq: T => Parser[U]): Parser[U] = + Parser(super.into(fq), in => { + this(in) match { + case Success(result, next) => fq(result).completions(next) + case _: NoSuccess => this.completions(in) + } + }) + + /** Returns `into(fq)`. */ + def >>[U](fq: T => Parser[U]) = into(fq) + + /** Returns a parser that repeatedly parses what this parser parses. + * + * @return rep(this) + */ + override def * = rep(this) + + /** Returns a parser that repeatedly parses what this parser parses, + * interleaved with the `sep` parser. The `sep` parser specifies how + * the results parsed by this parser should be combined. + * + * @return chainl1(this, sep) + */ + def *[U >: T](sep: => Parser[(U, U) => U]) = chainl1(this, sep) + + /** Returns a parser that repeatedly (at least once) parses what this parser parses. + * + * @return rep1(this) + */ + override def + = rep1(this) + + /** Returns a parser that optionally parses what this parser parses. + * + * @return opt(this) + */ + override def ? = opt(this) + + /** Changes the failure message produced by a parser. + * + * This doesn't change the behavior of a parser on neither + * success nor error, just on failure. The semantics are + * slightly different than those obtained by doing `| failure(msg)`, + * in that the message produced by this method will always + * replace the message produced, which is not guaranteed + * by that idiom. + * + * For example, parser `p` below will always produce the + * designated failure message, while `q` will not produce + * it if `sign` is parsed but `number` is not. + * + * {{{ + * def p = sign.? ~ number withFailureMessage "Number expected!" + * def q = sign.? ~ number | failure("Number expected!") + * }}} + * + * @param msg The message that will replace the default failure message. + * @return A parser with the same properties and different failure message. + */ + override def withFailureMessage(msg: String) = + Parser(super.withFailureMessage(msg), completions) + + /** Changes the failure message produced by a parser. + * + * This doesn't change the behavior of a parser on neither + * success nor error, just on failure. The semantics are + * slightly different than those obtained by doing `| failure(msg)`, + * in that the message produced by this method will always + * replace the message produced, which is not guaranteed + * by that idiom. + * + * For example, parser `p` below will always produce the + * designated failure message, while `q` will not produce + * it if `sign` is parsed but `number` is not. + * + * {{{ + * def p = sign.? ~ number withFailureMessage "Number expected!" + * def q = sign.? ~ number | failure("Number expected!") + * }}} + * + * @param msg The message that will replace the default failure message. + * @return A parser with the same properties and different failure message. + */ + override def withErrorMessage(msg: String) = + Parser(super.withErrorMessage(msg), completions) + + } + + /** Wrap a parser so that its failures become errors (the `|` combinator + * will give up as soon as it encounters an error, on failure it simply + * tries the next alternative). + */ + def commit[T](p: => Parser[T]): Parser[T] = + Parser(super.commit(p), p.completions) + + /** A parser matching input elements that satisfy a given predicate. + * + * `elem(kind, p)` succeeds if the input starts with an element `e` for which `p(e)` is true. + * + * @param kind The element kind, used for error messages + * @param p A predicate that determines which elements match. + * @param completions Possible alternatives (for completion) + * @return + */ + def elem(kind: String, p: Elem => Boolean, completions: Set[Elem] = Set()): Parser[Elem] = + acceptIf(p, completions)(inEl => kind + " expected") + + /** A parser that matches only the given element `e`. + * + * `elem(e)` succeeds if the input starts with an element `e`. + * + * @param e the `Elem` that must be the next piece of input for the returned parser to succeed + * @return a `Parser` that succeeds if `e` is the next available input (and returns it). + */ + override def elem(e: Elem): Parser[Elem] = accept(e) + + /** A parser that matches only the given element `e`. + * + * The method is implicit so that elements can automatically be lifted to their parsers. + * For example, when parsing `Token`s, `Identifier("new")` (which is a `Token`) can be used directly, + * instead of first creating a `Parser` using `accept(Identifier("new"))`. + * + * @param e the `Elem` that must be the next piece of input for the returned parser to succeed + * @return a `tParser` that succeeds if `e` is the next available input. + */ + override implicit def accept(e: Elem): Parser[Elem] = + acceptIf(_ == e, Set(e))("'" + e + "' expected but " + _ + " found") + + /** A parser that matches only the given list of element `es`. + * + * `accept(es)` succeeds if the input subsequently provides the elements in the list `es`. + * + * @param es the list of expected elements + * @return a Parser that recognizes a specified list of elements + */ + override def accept[ES <% List[Elem]](es: ES): Parser[List[Elem]] = + acceptSeq(es) + + /** The parser that matches an element in the domain of the partial function `f`. + * + * If `f` is defined on the first element in the input, `f` is applied + * to it to produce this parser's result. + * + * Example: The parser `accept("name", {case Identifier(n) => Name(n)})` + * accepts an `Identifier(n)` and returns a `Name(n)` + * + * @param expected a description of the kind of element this parser expects (for error messages) + * @param f a partial function that determines when this parser is successful and what its output is + * @param completions Possible alternatives (for completion) + * @return A parser that succeeds if `f` is applicable to the first element of the input, + * applying `f` to it to produce the result. + */ + def accept[U](expected: String, f: PartialFunction[Elem, U], completions: Set[Elem] = Set()): Parser[U] = + acceptMatch(expected, f, completions.map(Completion(_))) + + /** A parser matching input elements that satisfy a given predicate. + * + * `acceptIf(p)(el => "Unexpected "+el)` succeeds if the input starts with an element `e` for which `p(e)` is true. + * + * @param err A function from the received element into an error message. + * @param p A predicate that determines which elements match. + * @param completions Possible completions + * @return A parser for elements satisfying p(e). + */ + def acceptIf(p: Elem => Boolean, completions: Set[Elem])(err: Elem => String): Parser[Elem] = { + lazy val completionSet = + if (completions.isEmpty) + None + else + Some(CompletionSet(completions.map(c => Completion(c)))) + Parser( + super.acceptIf(p)(err), + in => + completionSet match { + case None => Completions.empty + case Some(c) => + super.acceptIf(p)(err)(in) match { + case Success(_, _) => Completions.empty + case _ => Completions(in.pos, c) + } + } + ) + } + + def acceptMatch[U](expected: String, f: PartialFunction[Elem, U], completions: Set[Completion]): Parser[U] = { + lazy val completionSet = + if (completions.nonEmpty) + Some(CompletionSet(CompletionTag(expected), completions)) + else None + Parser( + super.acceptMatch(expected, f), + in => + completionSet match { + case None => Completions.empty + case Some(c) => + super.acceptMatch(expected, f)(in) match { + case Success(_, _) => Completions.empty + case _ => Completions(in.pos, c) + } + } + ).named(expected) + } + + /** A parser that matches only the given [[scala.collection.Iterable]] collection of elements `es`. + * + * `acceptSeq(es)` succeeds if the input subsequently provides the elements in the iterable `es`. + * + * @param es the list of expected elements + * @return a Parser that recognizes a specified list of elements + */ + override def acceptSeq[ES <% Iterable[Elem]](es: ES): Parser[List[Elem]] = + Parser(super.acceptSeq(es), + in => + es.tail + .foldLeft(accept(es.head))((a, b) => a ~> accept(b)) + .completions(in)) + + /** A parser that always fails. + * + * @param msg The error message describing the failure. + * @return A parser that always fails with the specified error message. + */ + override def failure(msg: String): Parser[Nothing] = + Parser(super.failure(msg), _ => Completions.empty) + + /** A parser that always succeeds. + * + * @param v The result for the parser + * @return A parser that always succeeds, with the given result `v` + */ + override def success[T](v: T): Parser[T] = + Parser(super.success(v), _ => Completions.empty) + + /** A parser that results in an error. + * + * @param msg The error message describing the failure. + * @return A parser that always fails with the specified error message. + */ + override def err(msg: String): Parser[Nothing] = + Parser(super.err(msg), _ => Completions.empty) + + /** A helper method that turns a `Parser` into one that will + * print debugging information to stdout before and after + * being applied. + */ + def log[T](p: => Parser[T])(name: String): Parser[T] = + Parser(super.log(p)(name), p.completions) + + /** A parser generator for repetitions. + * + * `rep(p)` repeatedly uses `p` to parse the input until `p` fails + * (the result is a List of the consecutive results of `p`). + * + * @param p a `Parser` that is to be applied successively to the input + * @return A parser that returns a list of results produced by repeatedly applying `p` to the input. + */ + def rep[T](p: => Parser[T]): Parser[List[T]] = + rep1(p) | success(List()) + + /** A parser generator for interleaved repetitions. + * + * `repsep(p, q)` repeatedly uses `p` interleaved with `q` to parse the input, until `p` fails. + * (The result is a `List` of the results of `p`.) + * + * Example: `repsep(term, ",")` parses a comma-separated list of term's, yielding a list of these terms. + * + * @param p a `Parser` that is to be applied successively to the input + * @param q a `Parser` that parses the elements that separate the elements parsed by `p` + * @return A parser that returns a list of results produced by repeatedly applying `p` (interleaved with `q`) to the input. + * The results of `p` are collected in a list. The results of `q` are discarded. + */ + def repsep[T](p: => Parser[T], q: => Parser[Any]): Parser[List[T]] = + rep1sep(p, q) | success(List()) + + /** A parser generator for non-empty repetitions. + * + * `rep1(p)` repeatedly uses `p` to parse the input until `p` fails -- `p` must succeed at least + * once (the result is a `List` of the consecutive results of `p`) + * + * @param p a `Parser` that is to be applied successively to the input + * @return A parser that returns a list of results produced by repeatedly applying `p` to the input + * (and that only succeeds if `p` matches at least once). + */ + def rep1[T](p: => Parser[T]): Parser[List[T]] = + rep1(p, p) + + /** A parser generator for non-empty repetitions. + * + * `rep1(f, p)` first uses `f` (which must succeed) and then repeatedly + * uses `p` to parse the input until `p` fails + * (the result is a `List` of the consecutive results of `f` and `p`) + * + * @param first a `Parser` that parses the first piece of input + * @param p0 a `Parser` that is to be applied successively to the rest of the input (if any) -- evaluated at most once, and only when necessary + * @return A parser that returns a list of results produced by first applying `f` and then + * repeatedly `p` to the input (it only succeeds if `f` matches). + */ + def rep1[T](first: => Parser[T], p0: => Parser[T]): Parser[List[T]] = { + lazy val p = p0 // lazy argument + Parser( + super.rep1(first, p0), + in => { + def continue(in: Input): Completions = { + val currentCompletions = p.completions(in) + p(in) match { + case Success(_, rest) => currentCompletions | continue(rest) + case NoSuccess(_, _) => currentCompletions + } + } + val firstCompletions = first.completions(in) + first(in) match { + case Success(_, rest) => firstCompletions | continue(rest) + case NoSuccess(_, _) => firstCompletions + } + } + ) + } + + /** A parser generator for a specified number of repetitions. + * + * `repN(n, p)` uses `p` exactly `n` time to parse the input + * (the result is a `List` of the `n` consecutive results of `p`). + * + * @param p0 a `Parser` that is to be applied successively to the input + * @param num the exact number of times `p` must succeed + * @return A parser that returns a list of results produced by repeatedly applying `p` to the input + * (and that only succeeds if `p` matches exactly `n` times). + */ + def repN[T](num: Int, p0: => Parser[T]): Parser[List[T]] = { + lazy val p = p0 // lazy argument + if (num == 0) { success(Nil) } else { + Parser( + super.repN(num, p0), + in => { + var parsedCount = 0 + def completions(in0: Input): Completions = + if (parsedCount == num) { + Completions.empty + } else { + val currentCompletions = p.completions(in0) + p(in0) match { + case Success(_, rest) => parsedCount += 1; currentCompletions | completions(rest) + case ns: NoSuccess => currentCompletions + } + } + + val result = completions(in) + if (parsedCount < num) result else Completions.empty + } + ) + } + } + + /** A parser generator for non-empty repetitions. + * + * `rep1sep(p, q)` repeatedly applies `p` interleaved with `q` to parse the + * input, until `p` fails. The parser `p` must succeed at least once. + * + * @param p a `Parser` that is to be applied successively to the input + * @param q a `Parser` that parses the elements that separate the elements parsed by `p` + * (interleaved with `q`) + * @return A parser that returns a list of results produced by repeatedly applying `p` to the input + * (and that only succeeds if `p` matches at least once). + * The results of `p` are collected in a list. The results of `q` are discarded. + */ + def rep1sep[T](p: => Parser[T], q: => Parser[Any]): Parser[List[T]] = + p ~ rep(q ~> p) ^^ { case x ~ y => x :: y } + + /** A parser generator that, roughly, generalises the rep1sep generator so + * that `q`, which parses the separator, produces a left-associative + * function that combines the elements it separates. + * + * ''From: J. Fokker. Functional parsers. In J. Jeuring and E. Meijer, editors, Advanced Functional Programming, + * volume 925 of Lecture Notes in Computer Science, pages 1--23. Springer, 1995.'' + * + * @param p a parser that parses the elements + * @param q a parser that parses the token(s) separating the elements, yielding a left-associative function that + * combines two elements into one + */ + def chainl1[T](p: => Parser[T], q: => Parser[(T, T) => T]): Parser[T] = + chainl1(p, p, q) + + /** A parser generator that, roughly, generalises the `rep1sep` generator + * so that `q`, which parses the separator, produces a left-associative + * function that combines the elements it separates. + * + * @param first a parser that parses the first element + * @param p a parser that parses the subsequent elements + * @param q a parser that parses the token(s) separating the elements, + * yielding a left-associative function that combines two elements + * into one + */ + def chainl1[T, U](first: => Parser[T], p: => Parser[U], q: => Parser[(T, U) => T]): Parser[T] = + first ~ rep(q ~ p) ^^ { + case x ~ xs => + xs.foldLeft(x: T) { case (a, f ~ b) => f(a, b) } // x's type annotation is needed to deal with changed type inference due to SI-5189 + } + + /** A parser generator that generalises the `rep1sep` generator so that `q`, + * which parses the separator, produces a right-associative function that + * combines the elements it separates. Additionally, the right-most (last) + * element and the left-most combining function have to be supplied. + * + * rep1sep(p: Parser[T], q) corresponds to chainr1(p, q ^^ cons, cons, Nil) (where val cons = (x: T, y: List[T]) => x :: y) + * + * @param p a parser that parses the elements + * @param q a parser that parses the token(s) separating the elements, yielding a right-associative function that + * combines two elements into one + * @param combine the "last" (left-most) combination function to be applied + * @param first the "first" (right-most) element to be combined + */ + def chainr1[T, U](p: => Parser[T], q: => Parser[(T, U) => U], combine: (T, U) => U, first: U): Parser[U] = + p ~ rep(q ~ p) ^^ { + case x ~ xs => + (new ~(combine, x) :: xs).foldRight(first) { case (f ~ a, b) => f(a, b) } + } + + /** A parser generator for optional sub-phrases. + * + * `opt(p)` is a parser that returns `Some(x)` if `p` returns `x` and `None` if `p` fails. + * + * @param p A `Parser` that is tried on the input + * @return a `Parser` that always succeeds: either with the result provided by `p` or + * with the empty result + */ + def opt[T](p: => Parser[T]): Parser[Option[T]] = + p ^^ (x => Some(x)) | success(None) + + /** Wrap a parser so that its failures and errors become success and + * vice versa -- it never consumes any input. + */ + def not[T](p: => Parser[T]): Parser[Unit] = + Parser(super.not(p), _ => Completions.empty) + + /** A parser generator for guard expressions. The resulting parser will + * fail or succeed just like the one given as parameter but it will not + * consume any input. + * + * @param p a `Parser` that is to be applied to the input + * @return A parser that returns success if and only if `p` succeeds but + * never consumes any input + */ + def guard[T](p: => Parser[T]): Parser[T] = + Parser(super.guard(p), p.completions) + + /** `positioned` decorates a parser's result with the start position of the + * input it consumed. + * + * @param p a `Parser` whose result conforms to `Positional`. + * @return A parser that has the same behaviour as `p`, but which marks its + * result with the start position of the input it consumed, + * if it didn't already have a position. + */ + def positioned[T <: Positional](p: => Parser[T]): Parser[T] = + Parser(super.positioned(p), p.completions) + + /** A parser generator delimiting whole phrases (i.e. programs). + * + * `phrase(p)` succeeds if `p` succeeds and no input is left over after `p`. + * + * @param p the parser that must consume all input for the resulting parser + * to succeed. + * @return a parser that has the same result as `p`, but that only succeeds + * if `p` consumed all the input. + */ + def phrase[T](p: Parser[T]) = + Parser(super.phrase(p), p.completions) +} diff --git a/shared/src/main/scala/scala/util/parsing/combinator/completion/CompletionTypes.scala b/shared/src/main/scala/scala/util/parsing/combinator/completion/CompletionTypes.scala new file mode 100644 index 00000000..10302d14 --- /dev/null +++ b/shared/src/main/scala/scala/util/parsing/combinator/completion/CompletionTypes.scala @@ -0,0 +1,223 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import scala.util.parsing.input.{NoPosition, Position} + +/** Collection of data types allowing definition of structured parser completions. + * A `Completions` instance can contain multiple `CompletionSet`s instances. A `CompletionSet` provides a set of + * `Completion` entries and is tagged with a `CompletionTag`. + * + * Sets allow structuring the completion entries into groups, each group tagged with a `label` (plus optional + * `description` and `kind`, the latter allowing e.g. encoding visual attributes for the set). + * Sets also feature a score, which defines the order between sets within the `Completions` instance. + * + * Each `Completion` entry within a set has a `value`, a `score` and a `kind`: + * the score allows ordering the entries within a set, and the kind can e.g. be used to assign a representation style + * for a particular completion entry. + * + * Note that specifying tags and sets is optional: if no tag is specified upon creation, + * `Completions` instances create a unique default set with an empty tag. + * + * @author Jonas Chapuis + */ +trait CompletionTypes { + type Elem + + val DefaultCompletionTag = "" + val DefaultCompletionScore = 0 + + /** Tag defining identification and attributes of a set of completion entries + * @param label tag label + * @param score tag score (the higher the better, 0 by default) + * @param description tag description (optional) - can be used for additional information e.g. for a tooltip + * @param kind tag kind (optional) - can be used e.g. to define visual style + */ + case class CompletionTag(label: String, score: Int, description: Option[String], kind: Option[String]) { + def update(newTag: Option[String], + newScore: Option[Int], + newDescription: Option[String], + newKind: Option[String]) = + copy( + label = newTag.getOrElse(label), + score = newScore.getOrElse(score), + description = newDescription.map(Some(_)).getOrElse(description), + kind = newKind.map(Some(_)).getOrElse(kind) + ) + + override def toString: String = label + } + + case object CompletionTag { + val Default = + CompletionTag(DefaultCompletionTag, DefaultCompletionScore, None, None) + def apply(label: String): CompletionTag = + CompletionTag(label, DefaultCompletionScore, None, None) + def apply(label: String, score: Int): CompletionTag = + CompletionTag(label, score, None, None) + } + + /** Set of related completion entries + * @param tag set tag + * @param completions set of unique completion entries + */ + case class CompletionSet(tag: CompletionTag, completions: Set[Completion]) { + require(completions.nonEmpty, "empty completions set") + def label: String = tag.label + def score: Int = tag.score + def description: Option[String] = tag.description + def kind: Option[String] = tag.kind + def completionStrings: Seq[String] = + completions.toSeq.sorted.map(_.value.toString) + } + + case object CompletionSet { + def apply(tag: String, el: Elem): CompletionSet = + CompletionSet(CompletionTag(tag), Set(Completion(el))) + + def apply(tag: String, elems: Elems): CompletionSet = + CompletionSet(CompletionTag(tag), Set(Completion(elems))) + + def apply(tag: String, completion: Completion): CompletionSet = + CompletionSet(CompletionTag(tag), Set(completion)) + + def apply(tag: String, completions: Iterable[Completion]): CompletionSet = + CompletionSet(CompletionTag(tag), completions.toSet) + + def apply(completions: Iterable[Completion]): CompletionSet = + CompletionSet(CompletionTag.Default, completions.toSet) + + def apply(completions: Completion*): CompletionSet = + CompletionSet(CompletionTag.Default, completions.toSet) + + def apply(el: Elem): CompletionSet = + CompletionSet(CompletionTag.Default, Set(Completion(el))) + + def apply(completions: Traversable[Elems]): CompletionSet = + CompletionSet(CompletionTag.Default, completions.map(Completion(_)).toSet) + } + + type Elems = Seq[Elem] + + /** Completion entry + * @param value entry value (e.g. string literal) + * @param score entry score (defines the order of entries within a set, the higher the better) + * @param kind entry kind (e.g. visual style) + */ + case class Completion(value: Elems, score: Int = DefaultCompletionScore, kind: Option[String] = None) { + require(value.nonEmpty, "empty completion") + def updateKind(newKind: Option[String]) = + copy(kind = newKind.map(Some(_)).getOrElse(kind)) + } + case object Completion { + def apply(el: Elem): Completion = Completion(Seq(el)) + implicit def orderingByScoreAndThenAlphabetical: Ordering[Completion] = + Ordering.by(c => (-c.score, c.value.toString)) + } + + /** Result of parser completion, listing the possible entry alternatives at a certain input position + * @param position position in the input where completion entries apply + * @param sets completion entries, grouped per tag + */ + case class Completions(position: Position, sets: Map[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 + def allCompletions: Iterable[Completion] = allSets.flatMap(_.completions) + def defaultSet: Option[CompletionSet] = sets.get("") + + private def unionSets(left: CompletionSet, right: CompletionSet): CompletionSet = { + def offsetCompletions(set: CompletionSet) = { + val isOffsetRequired = + set.completions.map(_.score).exists(_ < set.score) + if (isOffsetRequired) + set.completions.map(c => Completion(c.value, set.score + c.score, c.kind)) + else set.completions + } + CompletionSet( + CompletionTag(left.tag.label, left.score.min(right.score), left.description, left.kind.orElse(right.kind)), + offsetCompletions(left) ++ offsetCompletions(right) + ) + } + + private def mergeCompletions(other: Completions) = { + val overlappingSetTags = sets.keySet.intersect(other.sets.keySet) + val unions = + overlappingSetTags.map(name => (sets(name), other.sets(name))).map { + case (left, right) => unionSets(left, right) + } + val leftExclusive = sets.keySet.diff(overlappingSetTags).map(sets(_)) + val rightExclusive = + other.sets.keySet.diff(overlappingSetTags).map(other.sets(_)) + Completions(position, + (unions ++ leftExclusive ++ rightExclusive) + .map(s => s.tag.label -> s) + .toMap) + } + + def |(other: Completions): Completions = { + other match { + case Completions.empty => this + case _ => + other.position match { + case otherPos if otherPos < position => this + case otherPos if otherPos == position => mergeCompletions(other) + case _ => other + } + } + } + + def completionStrings: Seq[String] = + sets.values.toSeq + .sortBy(_.score) + .reverse + .flatMap(_.completionStrings) + .toList + + def takeTop(count: Int): Completions = { + val allEntries = allSets + .flatMap(s => s.completions.map((_, s.tag))) + .toList + val sortedEntries = + allEntries + .sortBy { + case (Completion(_, score, kind), CompletionTag(_, tagScore, _, _)) => + (tagScore, score) + } + .reverse + .take(count) + val regroupedSets = sortedEntries + .groupBy { case (_, tag) => tag } + .map { + case (groupTag, completions) => + CompletionSet(groupTag, completions.map(_._1).toSet) + } + copy(sets = regroupedSets.map(s => (s.tag.label, s)).toMap) + } + + def setsScoredWithMaxCompletion(): Completions = { + Completions( + position, + sets.mapValues(s => CompletionSet(s.tag.copy(score = s.completions.map(_.score).max), s.completions))) + } + } + + case object Completions { + def apply(position: Position, completionSet: CompletionSet): Completions = + Completions(position, Map(completionSet.tag.label -> completionSet)) + def apply(position: Position, completions: Traversable[Elems]): Completions = + Completions(position, CompletionSet(completions)) + def apply(completionSet: CompletionSet): Completions = + Completions(NoPosition, completionSet) + def apply(completionSets: Iterable[CompletionSet]): Completions = + Completions(NoPosition, completionSets.map(s => s.tag.label -> s).toMap) + + val empty = Completions(NoPosition, Map[String, CompletionSet]()) + } + +} diff --git a/shared/src/main/scala/scala/util/parsing/combinator/completion/RegexCompletionSupport.scala b/shared/src/main/scala/scala/util/parsing/combinator/completion/RegexCompletionSupport.scala new file mode 100644 index 00000000..8fbbc955 --- /dev/null +++ b/shared/src/main/scala/scala/util/parsing/combinator/completion/RegexCompletionSupport.scala @@ -0,0 +1,83 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion +import scala.util.matching.Regex +import scala.util.parsing.combinator.RegexParsers +import scala.util.parsing.input.{CharSequenceReader, OffsetPosition, Positional, Reader} + +/** This component extends `RegexParsers` with completion capability. In particular, + * it provides completions for the `literal` parser. + * Note that completions for the `regex` parser are undefined by default and can be specified + * with the `%>` operator. + * + * @author Jonas Chapuis + */ +trait RegexCompletionSupport extends RegexParsers with CompletionSupport { + protected val areLiteralsCaseSensitive = false + + protected def dropWhiteSpace(input: Input): Input = + input.drop(handleWhiteSpace(input.source, input.offset) - input.offset) + + protected def handleWhiteSpace(input: Input): Int = + handleWhiteSpace(input.source, input.offset) + + protected def findMatchOffsets(s: String, in: Input): (Int, Int) = { + val source = in.source + val offset = in.offset + val start = handleWhiteSpace(source, offset) + var literalPos = 0 + var sourcePos = start + def charsEqual(a: Char, b: Char) = + if (areLiteralsCaseSensitive) a == b else a.toLower == b.toLower + while (literalPos < s.length && sourcePos < source.length && charsEqual(s.charAt(literalPos), + source.charAt(sourcePos))) { + literalPos += 1 + sourcePos += 1 + } + (literalPos, sourcePos) + } + + abstract override implicit def literal(s: String): Parser[String] = + Parser[String]( + super.literal(s), + (in: Input) => { + lazy val literalCompletion = + Completions(OffsetPosition(in.source, handleWhiteSpace(in)), CompletionSet(Completion(s))) + val (literalOffset, sourceOffset) = findMatchOffsets(s, in) + lazy val inputAtEnd = sourceOffset == in.source.length + literalOffset match { + case 0 if inputAtEnd => + literalCompletion // whitespace, free entry possible + case someOffset + if inputAtEnd & someOffset > 0 & someOffset < s.length => // partially entered literal, we are at the end + literalCompletion + case _ => Completions.empty + } + } + ) + + abstract override implicit def regex(r: Regex): Parser[String] = + Parser(super.regex(r), _ => Completions.empty) + + override def positioned[T <: Positional](p: => Parser[T]): Parser[T] = { + lazy val q = p + Parser[T](super.positioned(p), in => q.completions(in)) + } + + /** Returns completions for read `in` with parser `p`. */ + def complete[T](p: Parser[T], in: Reader[Char]): Completions = + p.completions(in) + + /** Returns completions for character sequence `in` with parser `p`. */ + def complete[T](p: Parser[T], in: CharSequence): Completions = + p.completions(new CharSequenceReader(in)) + + /** Returns flattened string completions for character sequence `in` with parser `p`. */ + def completeString[T](p: Parser[T], input: String): Seq[String] = + complete(p, input).completionStrings + +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForAcceptAndElemTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForAcceptAndElemTest.scala new file mode 100644 index 00000000..dca1edc0 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForAcceptAndElemTest.scala @@ -0,0 +1,82 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.syntactical.StandardTokenParsers + +class CompletionForAcceptAndElemTest { + + object TestParser extends StandardTokenParsers with CompletionSupport + import TestParser.lexical._ + + @Test + def elemCompletesToPassedCompletions(): Unit = { + // Arrange + val tokens = Set[Token](NumericLit("1"), NumericLit("2"), NumericLit("3")) + val parser = + TestParser.elem("test", _ => true, completions = tokens) + + // Act + val result = parser.completions(new Scanner("")) + + // Assert + Assert.assertArrayEquals(tokens.toArray[AnyRef], result.allCompletions.map(_.value.head).toArray[AnyRef]) + } + + @Test + def acceptElemCompletesToElem(): Unit = { + // Arrange + val elem = NumericLit("1") + val parser = TestParser.elem(elem) + + // Act + val result = parser.completions(new Scanner("")) + + // Assert + Assert.assertEquals(elem, headToken(result.allCompletions)) + } + + @Test + def acceptElemListCompletesToNextInList(): Unit = { + // Arrange + val one = NumericLit("1") + val two = NumericLit("2") + val three = NumericLit("3") + val seq = List(one, two, three) + val parser = TestParser.accept(seq) + + // Act + val result1 = parser.completions(new Scanner("")) + val result2 = parser.completions(new Scanner("1")) + val result3 = parser.completions(new Scanner("1 2")) + val emptyResult = parser.completions(new Scanner("1 2 3")) + + // Assert + Assert.assertEquals(one, headToken(result1.allCompletions)) + Assert.assertEquals(two, headToken(result2.allCompletions)) + Assert.assertEquals(three, headToken(result3.allCompletions)) + Assert.assertTrue(emptyResult.allCompletions.isEmpty) + } + + @Test + def acceptWithPartialFunctionCompletesToPassedCompletions(): Unit = { + // Arrange + case class Number(n: Int) + val tokens = Set[Token](NumericLit("1"), NumericLit("2"), NumericLit("3")) + val parser = TestParser.accept("number", {case NumericLit(n) => Number(n.toInt)}, tokens) + + // Act + val result = parser.completions(new Scanner("")) + + // Assert + Assert.assertArrayEquals(tokens.toArray[AnyRef], result.allCompletions.map(_.value.head).toArray[AnyRef]) + } + + def headToken(completions: Iterable[TestParser.Completion]) = completions.map(_.value).head.head +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForAlternativesTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForAlternativesTest.scala new file mode 100644 index 00000000..437accf8 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForAlternativesTest.scala @@ -0,0 +1,35 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.Parsers + +class CompletionForAlternativesTest { + val left = "left" + val right = "right" + val common = "common" + + object TestParser extends Parsers with RegexCompletionSupport { + val alternativesWithCommonFirstParser = common ~ left | common ~! right + val alternativesWithCommonPrefix = (common+left) | common ~ right + } + + @Test + def emptyCompletesToCommon = + Assert.assertEquals(Seq(common), TestParser.completeString(TestParser.alternativesWithCommonFirstParser, "")) + + @Test + def commonCompletesToLeftAndRight = + Assert.assertEquals(Seq(left, right), TestParser.completeString(TestParser.alternativesWithCommonFirstParser, common)) + + @Test + def commonPrefixCompletesToRightSinceCompletionPositionsAreDifferent = + Assert.assertEquals(Seq(right), TestParser.completeString(TestParser.alternativesWithCommonPrefix, common)) + +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForChainTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForChainTest.scala new file mode 100644 index 00000000..953a0447 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForChainTest.scala @@ -0,0 +1,41 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.RegexParsers + +class CompletionForChainTest { + val repeated = "rep" + val separator = "," + object TestParser extends RegexParsers with RegexCompletionSupport { + val chainlParser = literal(repeated) * (separator ^^ (_ => (a: String, b: String) => a)) + val chainrParser = + chainr1(literal(repeated), separator ^^ (_ => (a: String, b: String) => a), (a: String, b: String) => a, "") + } + + @Test + def repeaterCompletesToParserAndSeparatorAlternatively(): Unit = chainTest(TestParser.chainlParser) + + @Test + def chainr1CompletesToParserAndSeparatorAlternatively(): Unit = + chainTest(TestParser.chainrParser) + + def chainTest[T](parser: TestParser.Parser[T]) = { + val resultRep = TestParser.completeString(parser, "") + val resultSep = TestParser.completeString(parser, repeated) + val resultRep2 = TestParser.completeString(parser, s"$repeated,") + val resultSep2 = TestParser.completeString(parser, s"$repeated,$repeated") + + // Assert + Assert.assertEquals(resultRep.head, repeated) + Assert.assertEquals(resultSep.head, separator) + Assert.assertEquals(resultRep2.head, repeated) + Assert.assertEquals(resultSep2.head, separator) + } +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForIntoTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForIntoTest.scala new file mode 100644 index 00000000..84388062 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForIntoTest.scala @@ -0,0 +1,38 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.RegexParsers + +class CompletionForIntoTest { + val animal = "animal" + val machine = "machine" + val bear = "bear" + val lion = "lion" + + object TestParser extends RegexParsers with RegexCompletionSupport { + val animalParser = bear | lion + val machineParser = "plane" | "car" + val test = (animal | machine) >> { kind: String => + if (kind == animal) animalParser else machineParser + } + } + + @Test + def intoParserWithoutSuccessCompletesToParser(): Unit = { + val completions = TestParser.completeString(TestParser.test, "") + Assert.assertEquals(Seq(animal, machine), completions) + } + + @Test + def intoParserWithSuccessCompletesResultingParser(): Unit = { + val completions = TestParser.completeString(TestParser.test, animal) + Assert.assertEquals(Seq(bear, lion), completions) + } +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForLiteralTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForLiteralTest.scala new file mode 100644 index 00000000..84b4b773 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForLiteralTest.scala @@ -0,0 +1,74 @@ +/* *\ +** scala-parser-combinators completion fork ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +** Author: jonas.chapuis@nexthink.com ** +\* */ +package scala.util.parsing.combinator.completion + +import org.junit.Assert._ +import org.junit.Test + +import scala.util.parsing.combinator.Parsers + +class CompletionForLiteralTest { + val someLiteral = "literal" + val otherLiteralWithSamePrefix = "litOther" + val someLiteralPrefix = "lit" + + object Parser extends Parsers with RegexCompletionSupport { + val literal: Parser[String] = someLiteral + + val combination = someLiteral | otherLiteralWithSamePrefix + } + + @Test + def prefixCompletesToLiteral = { + val completion = Parser.complete(Parser.literal, " " + someLiteralPrefix) + assertEquals(2, completion.position.column) + assertEquals(Seq(someLiteral), completion.completionStrings) + } + + @Test + def prefixCombinationCompletesToBothAlternatives = { + val completion = + Parser.completeString(Parser.combination, someLiteralPrefix) + assertEquals(Seq(otherLiteralWithSamePrefix, someLiteral), completion) + } + + @Test + def partialOtherCompletesToOther = { + val completion = Parser.completeString( + Parser.combination, + someLiteralPrefix + otherLiteralWithSamePrefix + .stripPrefix(someLiteralPrefix) + .head) + assertEquals(Seq(otherLiteralWithSamePrefix), completion) + } + + @Test + def whitespaceCompletesToLiteral = { + val completion = + Parser.complete(Parser.literal, List.fill(2)(" ").mkString) + assertEquals(3, completion.position.column) + assertEquals(Seq(someLiteral), completion.completionStrings) + } + + @Test + def emptyCompletesToLiteral = { + val completion = Parser.complete(Parser.literal, "") + assertEquals(1, completion.position.column) + assertEquals(Seq(someLiteral), completion.completionStrings) + } + + @Test + def otherCompletesToNothing = + assertEquals( + Map(), + Parser.complete(Parser.literal, otherLiteralWithSamePrefix).sets) + + @Test + def completeLiteralCompletesToEmpty = + assertTrue(Parser.complete(Parser.literal, someLiteral).sets.isEmpty) + +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForLongestMatchTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForLongestMatchTest.scala new file mode 100644 index 00000000..c5023cef --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForLongestMatchTest.scala @@ -0,0 +1,41 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ +package scala.util.parsing.combinator.completion + +import org.junit.Assert._ +import org.junit.Test + +import scala.util.parsing.combinator.Parsers + +class CompletionForLongestMatchTest { + val foo = "foo" + val bar = "bar" + + object Parsers extends Parsers with RegexCompletionSupport { + val samePrefix = foo ||| foo ~ bar + val constrainedAndOpenAlternatives = foo ~ bar ||| (".{5,}".r %> Completion("sample string longer than 5 char")) + } + + @Test + def normallyProblematicallyOrderedAlternativesParseCorrectly = { + assertTrue(Parsers.parseAll(Parsers.samePrefix, foo).successful) + assertTrue(Parsers.parseAll(Parsers.samePrefix, foo + bar).successful) // would be false with | + } + + @Test + def emptyCompletesToAlternatives = + assertEquals(Seq(foo), Parsers.completeString(Parsers.samePrefix, "")) + + @Test + def partialLongerAlternativeCompletesToLongerAlternative = + assertEquals(Seq(bar), Parsers.completeString(Parsers.samePrefix, foo)) + + @Test + def longestParseProvidesCompletion = + assertEquals(Seq(bar), Parsers.completeString(Parsers.constrainedAndOpenAlternatives, foo)) + + +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForRepetitionTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForRepetitionTest.scala new file mode 100644 index 00000000..d8107127 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForRepetitionTest.scala @@ -0,0 +1,82 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.Parsers + +class CompletionForRepetitionTest { + val repeated = "repeated" + val separator = "separator" + val n = 5 + + object TestParser extends Parsers with RegexCompletionSupport { + val repSequence = rep(repeated) + val repSepSequence = repsep(repeated, separator) + val error = repsep(repeated, err("some error")) + val repNSequence = repN(5, repeated) + + val subSeqLeft = "foo" ~ "bar" | "foo" + val subSeqRight = "as" ~ "df" | "df" ~ "as" + val composedSequence = subSeqLeft ~ subSeqRight + val repAlternatives = rep1sep("foo" | composedSequence, "and") + val repNAlternatives = repN(5, "foo" | composedSequence) + } + + @Test + def emptyRepCompletesToRepeated = + Assert.assertEquals(Seq(repeated), TestParser.completeString(TestParser.repSequence, "")) + + @Test + def nInstancesAndPartialRepCompletesToRepeated = + Assert.assertEquals( + Seq(repeated), + TestParser.completeString(TestParser.repSequence, List.fill(3)(repeated).mkString + repeated.dropRight(3))) + + @Test + def nInstancesOfRepeatedRepNCompletesToRepeated = + Assert.assertEquals(Seq(repeated), + TestParser.completeString(TestParser.repNSequence, List.fill(3)(repeated).mkString)) + + @Test + def nInstancesPartialCompleteRepNCompletesToRepeated = + Assert.assertEquals( + Seq(repeated), + TestParser.completeString(TestParser.repNSequence, List.fill(3)(repeated).mkString + repeated.dropRight(3))) + + @Test + def nInstancesFollowedByErrorRepCompletesToNothing = + Assert.assertEquals(Nil, + TestParser.completeString(TestParser.repSequence, List.fill(3)(repeated).mkString + "error")) + + @Test + def emptyRepSepCompletesToRepeated = + Assert.assertEquals(Seq(repeated), TestParser.completeString(TestParser.repSepSequence, "")) + + @Test + def repeatedAndSeparatorRepSepCompletesToRepeated = + Assert.assertEquals(Seq(repeated), TestParser.completeString(TestParser.repSepSequence, repeated+separator)) + + @Test + def errorRepSepCompletesToNothing = + Assert.assertEquals(Nil, TestParser.completeString(TestParser.error, repeated)) + + @Test + def emptyRepNCompletesToRepeated = + Assert.assertEquals(Seq(repeated), TestParser.completeString(TestParser.repNSequence, "")) + + @Test + def repAlternativesCompletesToAlternatives(): Unit = + Assert.assertEquals(Seq("and", "as", "bar", "df"), + TestParser.completeString(TestParser.repAlternatives, s"foo and foo")) + + @Test + def repNAlternativesCompletesToAlternatives(): Unit = + Assert.assertEquals(Seq("as", "bar", "df", "foo"), + TestParser.completeString(TestParser.repNAlternatives, s"foo foo")) +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForSequenceTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForSequenceTest.scala new file mode 100644 index 00000000..be816550 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForSequenceTest.scala @@ -0,0 +1,59 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.Parsers + +class CompletionForSequenceTest { + val left = "left" + val foo = "foo" + val bar = "bar" + val as = "as" + val df = "df" + + object TestParser extends Parsers with RegexCompletionSupport { + val sequence = left ~> (foo | bar) + + val subSeqLeft = foo ~ bar | foo + val subSeqRight = as ~ df | df ~ as + val composedSequence = subSeqLeft ~ subSeqRight + } + + @Test + def emptyCompletesToLeft = + Assert.assertEquals(Seq(left), TestParser.completeString(TestParser.sequence, "")) + + @Test + def partialLeftCompletesToLeft = + Assert.assertEquals(Seq(left), TestParser.completeString(TestParser.sequence, left.dropRight(2))) + + @Test + def completeLeftcompletesToRightAlternatives = { + val completion = TestParser.complete(TestParser.sequence, left) + Assert.assertEquals(left.length + 1, completion.position.column) + Assert.assertEquals(Seq(bar, foo), completion.completionStrings) + } + + @Test + def completeLeftAndRightCompletesToNothing = + Assert.assertEquals(Nil, TestParser.completeString(TestParser.sequence, left + " " + bar)) + + + @Test + def emptyComposedCompletesToLeft = + Assert.assertEquals(Seq(foo), TestParser.completeString(TestParser.composedSequence, "")) + + @Test + def leftComposedCompletesToLeftRemainingAlternativeAndRight = + Assert.assertEquals(Seq(as, bar, df), TestParser.completeString(TestParser.composedSequence, foo)) + + @Test + def completeLeftComposedCompletesToCorrectRightAlternative = + Assert.assertEquals(Seq(df), TestParser.completeString(TestParser.composedSequence, foo + " "+ as)) +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForSimpleGrammarTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForSimpleGrammarTest.scala new file mode 100644 index 00000000..ecf4927e --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionForSimpleGrammarTest.scala @@ -0,0 +1,69 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.Test + +class CompletionForSimpleGrammarTest { + import CompletionTestDefinitions._ + + object SimpleGrammar extends CompletionTestParser { + val number = "[0-9]+".r %> ("1", "10", "99") % "number" %? "any number" + + def expr: Parser[Int] = term | "(" ~> term <~ ")" + def term: Parser[Int] = number ^^ { + _.toInt + } + } + + @Test + def emptyCompletesToNumberOrParen() = + SimpleGrammar.assertHasCompletions( + Set(Tagged("number", Some("any number"), 0, "1", "10", "99"), Default("(")), + SimpleGrammar.complete(SimpleGrammar.expr, "")) + + @Test + def invalidCompletesToNothing() = + SimpleGrammar.assertHasCompletions( + Set(), + SimpleGrammar.complete(SimpleGrammar.expr, "invalid")) + + + @Test + def leftParenCompletesToNumber() = + SimpleGrammar.assertHasCompletions( + Set(Tagged("number", Some("any number"), 0, "1", "10", "99")), + SimpleGrammar.complete(SimpleGrammar.log(SimpleGrammar.expr)("expr"), + "(")) + + @Test + def leftParenAndNumberCompletesToRightParen() = + SimpleGrammar.assertHasCompletions( + Set(Default(")")), + SimpleGrammar.complete(SimpleGrammar.log(SimpleGrammar.expr)("expr"), + "(8")) + + @Test + def leftParenAndInvalidCompletesToNothing() = + SimpleGrammar.assertHasCompletions( + Set(), + SimpleGrammar.complete(SimpleGrammar.log(SimpleGrammar.expr)("expr"), + "(invalid")) + + @Test + def parenNumberCompletesToEmpty() = + SimpleGrammar.assertHasCompletions( + Set(), + SimpleGrammar.complete(SimpleGrammar.expr, "(56) ")) + + @Test + def numberCompletesToEmpty() = + SimpleGrammar.assertHasCompletions( + Set(), + SimpleGrammar.complete(SimpleGrammar.expr, "28 ")) + +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionOperatorsTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionOperatorsTest.scala new file mode 100644 index 00000000..6c1c4e5e --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionOperatorsTest.scala @@ -0,0 +1,122 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.{Assert, Test} + +import scala.util.parsing.combinator.Parsers + +class CompletionOperatorsTest { + + object TestParser extends Parsers with RegexCompletionSupport { + val someParser: Parser[String] = "parser" + } + + @Test + def completionSpecifiedWithBuilderIsCorrect = { + // Arrange + val completions: Seq[Seq[Char]] = Seq("one", "two", "three") + val score = 10 + val description = "some description" + val tag = "some tag" + val kind = "some kind" + + assertCompletionsMatch(TestParser.someParser %> (completions: _*) % (tag, score) %? description %% kind, + completions, + Some(tag), + Some(score), + Some(description), + Some(kind)) + + assertCompletionsMatch(TestParser.someParser %> (completions: _*) % (tag, score, description), + completions, + Some(tag), + Some(score), + Some(description), + None) + + assertCompletionsMatch(TestParser.someParser %> (completions: _*) % (tag, score, description, kind), + completions, + Some(tag), + Some(score), + Some(description), + Some(kind)) + + assertCompletionsMatch( + TestParser.someParser %> (completions: _*) % TestParser.CompletionTag(tag, score, Some(description), Some(kind)), + completions, + Some(tag), + Some(score), + Some(description), + Some(kind) + ) + + assertCompletionsMatch(TestParser.someParser %> (completions: _*) % tag %? description % score %% kind, + completions, + Some(tag), + Some(score), + Some(description), + Some(kind)) + + assertCompletionsMatch(TestParser.someParser %> (completions: _*) % tag % score %? description %% kind, + completions, + Some(tag), + Some(score), + Some(description), + Some(kind)) + } + + def assertCompletionsMatch[T](sut: TestParser.Parser[T], + completions: Seq[Seq[Char]], + tag: Option[String], + score: Option[Int], + description: Option[String], + kind: Option[String]) = { + // Act + val result = TestParser.complete(sut, "") + + // Assert + val completionSet: TestParser.CompletionSet = + tag.flatMap(n => result.setWithTag(n)).orElse(result.defaultSet).get + Assert.assertEquals(tag.getOrElse(""), completionSet.tag.label) + Assert.assertEquals(score.getOrElse(0), completionSet.tag.score) + Assert.assertEquals(description, completionSet.tag.description) + Assert.assertEquals(kind, completionSet.tag.kind) + Assert.assertEquals(completions.toSet, completionSet.completions.map(_.value)) + } + + @Test + def unioningCompletionSetsScoresMergedItemsOffsetBySetScore = { + // Arrange + val a = Seq(TestParser.Completion("one", 10), TestParser.Completion("two")) + val b = Seq(TestParser.Completion("three", 5), TestParser.Completion("five")) + val c = Seq(TestParser.Completion("four")) + val sut = TestParser.someParser %> a % 10 | TestParser.someParser %> b | TestParser.someParser %> c % 3 + + // Act + val result = TestParser.complete(sut, "") + + // Assert + Assert.assertArrayEquals(Seq("one", "two", "three", "four", "five").toArray[AnyRef], + result.completionStrings.toArray[AnyRef]) + } + + @Test + def topCompletionsLimitsCompletionsAccordingToScore(): Unit = { + // Arrange + val completions = Seq("one", "two", "three", "four").zipWithIndex.map { + case (c, s) => TestParser.Completion(c, s) + } + val sut = (TestParser.someParser %> completions).topCompletions(2) + + // Act + val result = TestParser.complete(sut, "") + + // Assert + Assert.assertArrayEquals(Seq("four", "three").toArray[AnyRef], result.completionStrings.toArray[AnyRef]) + } +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionTestDefinitions.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionTestDefinitions.scala new file mode 100644 index 00000000..89002a1c --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionTestDefinitions.scala @@ -0,0 +1,52 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.Assert._ + +import scala.util.parsing.combinator.Parsers + +object CompletionTestDefinitions { + trait AssertionSet{ + def tag: String + } + case class Default(strings: String*) extends AssertionSet { + def tag: String = "" + } + case class Tagged(tag: String, desc: Option[String], score: Int, strings: String*) + extends AssertionSet + case object Tagged { + def apply(name: String, strings: String*): Tagged = + Tagged(name, None, 0, strings: _*) + } +} + +trait CompletionTestParser extends Parsers with RegexCompletionSupport { + import CompletionTestDefinitions._ + def assertSetEquals(expected: AssertionSet, actual: CompletionSet): Unit = + expected match { + case default @ Default(_ *) => { + default.strings.zip(actual.completionStrings).foreach { + case (expected, actual) => assertEquals(expected, actual) + } + } + case named @ Tagged(name, desc, score, _ *) => { + assertEquals(name, actual.tag.label) + assertEquals(score, actual.tag.score) + assertEquals(desc, actual.tag.description) + named.strings.zip(actual.completionStrings).foreach { + case (expected, actual) => assertEquals(expected, actual) + } + } + } + def assertHasCompletions(expected: Set[AssertionSet], + actual: Completions) = { + expected.toList.sortBy(_.tag).zip(actual.allSets.toList.sortBy(_.tag.label)).foreach{ + case (expected, actual) => assertSetEquals(expected, actual) + } + } +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionTypesTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionTypesTest.scala new file mode 100644 index 00000000..21f492f5 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/CompletionTypesTest.scala @@ -0,0 +1,40 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.Assert._ +import org.junit.Test + +import scala.util.parsing.input.NoPosition + +class CompletionTypesTest extends CompletionTypes { + override type Elem = Char + + val setA = CompletionSet(CompletionTag("A", 10), Set(Completion("a", 2), Completion("b", 1))) + val setB = CompletionSet(CompletionTag("B", 5), Set(Completion("c", 4), Completion("d", 3))) + val setC = CompletionSet("C", Completion("e", 10)) + + @Test + def completionsTakeTopWorks() = { + // Arrange + val compl = Completions(Seq(setA, setB, setC)) + + // Act + val lettersInOrder = Seq("a", "b", "c", "d", "e") + val letterSets = for (i <- 1 until lettersInOrder.length) yield lettersInOrder.take(i) + letterSets.foreach(set => assertEquals(set, compl.takeTop(set.length).completionStrings)) + } + + @Test + def completionsSetsScoredWithMaxCompletionWorks() = { + // Arrange + val compl = Completions(Seq(setA, setB, setC)) + + // Act + assertEquals(Seq("e", "c", "d", "a", "b"), compl.setsScoredWithMaxCompletion().completionStrings) + } +} diff --git a/shared/src/test/scala/scala/util/parsing/combinator/completion/RecursiveGrammarTest.scala b/shared/src/test/scala/scala/util/parsing/combinator/completion/RecursiveGrammarTest.scala new file mode 100644 index 00000000..4bc95799 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/completion/RecursiveGrammarTest.scala @@ -0,0 +1,73 @@ +/* *\ +** scala-parser-combinators completion extensions ** +** Copyright (c) by Nexthink S.A. ** +** Lausanne, Switzerland (http://www.nexthink.com) ** +\* */ + +package scala.util.parsing.combinator.completion + +import org.junit.Assert._ +import org.junit.Test + +class RecursiveGrammarTest { + import CompletionTestDefinitions._ + + object ExprParser extends CompletionTestParser { + val number = "[0-9]+".r %> ("1", "10", "99") % "number" %? "any number" + lazy val expr: Parser[Int] = term ~ rep( + (("+" | "-") % "operators" %? "arithmetic operators" % 10) ~! term ^^ { + case "+" ~ t => t + case "-" ~ t => -t + }) ^^ { case t ~ r => t + r.sum } + lazy val multiplicationDivisionOperators = ("*" | "/") % "operators" %? "arithmetic operators" % 10 + lazy val term: Parser[Int] = factor ~ rep(multiplicationDivisionOperators ~! factor) ^^ { + case f ~ Nil => f + case f ~ r => + r.foldLeft(f) { + case (prev, "*" ~ next) => prev * next + case (prev, "/" ~ next) => prev / next + } + } + lazy val factor: Parser[Int] = number ^^ { _.toInt } | "(" ~> expr <~ ")" + } + + @Test + def expressionsParseCorrectly() = { + assertEquals(1 + 2 + 3, ExprParser.parseAll(ExprParser.expr, "1+2+3").get) + assertEquals(2 * 3, ExprParser.parseAll(ExprParser.expr, "2*3").get) + assertEquals(10 / (3 + 2), ExprParser.parseAll(ExprParser.expr, "(5+5)/(3+2)").get) + assertEquals(5 * 2 / 2, ExprParser.parseAll(ExprParser.expr, "(5*2/2)").get) + assertEquals(3 - 4 - 5, ExprParser.parseAll(ExprParser.expr, "3-4-5").get) + } + + @Test + def emptyCompletesToNumberOrParen() = + ExprParser.assertHasCompletions(Set(Tagged("number", Some("any number"), 0, "1", "10", "99"), Default("(")), + ExprParser.complete(ExprParser.expr, "")) + + @Test + def numberCompletesToOperators() = + ExprParser.assertHasCompletions(Set(Tagged("operators", Some("arithmetic operators"), 10, "*", "+", "-", "/")), + ExprParser.complete(ExprParser.expr, "2")) + + @Test + def numberAndOperationCompletesToNumberOrParen() = + ExprParser.assertHasCompletions(Set(Tagged("number", Some("any number"), 0, "1", "10", "99"), Default("(")), + ExprParser.complete(ExprParser.expr, "2*")) + + @Test + def parenCompletesToNumberAndParen() = + ExprParser.assertHasCompletions(Set(Tagged("number", Some("any number"), 0, "1", "10", "99"), Default("(")), + ExprParser.complete(ExprParser.expr, "(")) + + @Test + def recursiveParenAndNumberCompletesToOperatorsOrParen() = + ExprParser.assertHasCompletions( + Set(Tagged("operators", Some("arithmetic operators"), 10, "*", "+", "-", "/"), Default(")")), + ExprParser.complete(ExprParser.expr, "(((2")) + + @Test + def closedParentCompletesToOperators() = + ExprParser.assertHasCompletions(Set(Tagged("operators", Some("arithmetic operators"), 10, "*", "+", "-", "/")), + ExprParser.complete(ExprParser.expr, "(5*2/2)")) +}