From ddb4381454b80be70529c20aa29279382b333d5c Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Mon, 13 Dec 2010 22:44:25 -0500 Subject: [PATCH] fixes and improvements to tab completions combinators --- util/complete/Completions.scala | 120 +++++++++++------- util/complete/Parser.scala | 66 +++++++--- util/complete/src/test/scala/ParserTest.scala | 37 +++++- 3 files changed, 158 insertions(+), 65 deletions(-) diff --git a/util/complete/Completions.scala b/util/complete/Completions.scala index 7a5f22444..a27e5dc6f 100644 --- a/util/complete/Completions.scala +++ b/util/complete/Completions.scala @@ -19,6 +19,8 @@ sealed trait Completions override def toString = get.mkString("Completions(",",",")") final def flatMap(f: Completion => Completions): Completions = Completions(get.flatMap(c => f(c).get)) final def map(f: Completion => Completion): Completions = Completions(get map f) + override final def hashCode = get.hashCode + override final def equals(o: Any) = o match { case c: Completions => get == c.get; case _ => false } } object Completions { @@ -28,32 +30,27 @@ object Completions } /** Returns a strict Completions instance using the provided Completion Set. */ - def strict(cs: Set[Completion]): Completions = new Completions { - def get = cs - } + def strict(cs: Set[Completion]): Completions = apply(cs) /** No suggested completions, not even the empty Completion.*/ val nil: Completions = strict(Set.empty) - /** Only includes the unmarked empty Completion as a suggestion. */ + /** Only includes an empty Suggestion */ val empty: Completions = strict(Set.empty + Completion.empty) - /** Includes only the marked empty Completion as a suggestion. */ - val mark: Completions = strict(Set.empty + Completion.mark) - - /** Returns a strict Completions instance with a single Completion with `s` for `append`.*/ - def single(s: String): Completions = strict(Set.empty + Completion.strict("", s)) + /** Returns a strict Completions instance containing only the provided Completion.*/ + def single(c: Completion): Completions = strict(Set.empty + c) } /** * Represents a completion. -* The abstract members `prepend` and `append` are best explained with an example. +* The abstract members `display` and `append` are best explained with an example. * * Assuming space-delimited tokens, processing this: * am is are w * could produce these Completions: -* Completion { prepend = "w"; append = "as" } -* Completion { prepend = "w"; append = "ere" } +* Completion { display = "was"; append = "as" } +* Completion { display = "were"; append = "ere" } * to suggest the tokens "was" and "were". * * In this way, two pieces of information are preserved: @@ -62,51 +59,76 @@ object Completions */ sealed trait Completion { - /** The part of the token that was in the input.*/ - def prepend: String - /** The proposed suffix to append to the existing input to complete the last token in the input.*/ def append: String + /** The string to present to the user to represent the full token being suggested.*/ + def display: String + /** True if this Completion is suggesting the empty string.*/ + def isEmpty: Boolean - /** True if this completion has been identified with a token. - * A marked Completion will not be appended to another Completion unless that Completion is empty. - * In this way, only a single token is completed at a time.*/ - def mark: Boolean - - final def isEmpty = prepend.isEmpty && append.isEmpty - - /** Appends the completions in `o` with the completions in this unless `o` is marked and this is nonempty.*/ - final def ++(o: Completion): Completion = if(o.mark && !isEmpty) this else Completion(prepend + o.prepend, append + o.append, mark) - + /** Appends the completions in `o` with the completions in this Completion.*/ + def ++(o: Completion): Completion = Completion.concat(this, o) final def x(o: Completions): Completions = o.map(this ++ _) - - override final def toString = triple.toString - override final lazy val hashCode = triple.hashCode - override final def equals(o: Any) = o match { - case c: Completion => triple == c.triple - case _ => false - } - final def triple = (prepend, append, mark) + override final lazy val hashCode = Completion.hashCode(this) + override final def equals(o: Any) = o match { case c: Completion => Completion.equal(this, c); case _ => false } +} +final class DisplayOnly(display0: String) extends Completion +{ + lazy val display = display0 + def isEmpty = display.isEmpty + def append = "" + override def toString = "{" + display + "}" +} +final class Token(prepend0: String, append0: String) extends Completion +{ + lazy val prepend = prepend0 + lazy val append = append0 + def isEmpty = prepend.isEmpty && append.isEmpty + def display = prepend + append + override final def toString = "[" + prepend + "," + append +"]" +} +final class Suggestion(append0: String) extends Completion +{ + lazy val append = append0 + def isEmpty = append.isEmpty + def display = append + override def toString = append } object Completion { - /** Constructs a lazy Completion with the given prepend, append, and mark values. */ - def apply(d: => String, a: => String, m: Boolean = false): Completion = new Completion { - lazy val prepend = d - lazy val append = a - def mark = m - } + def concat(a: Completion, b: Completion): Completion = + (a,b) match + { + case (as: Suggestion, bs: Suggestion) => suggestion(as.append + bs.append) + case (at: Token, _) if at.append.isEmpty => b + case _ if a.isEmpty => b + case _ => a + } - /** Constructs a strict Completion with the given prepend, append, and mark values. */ - def strict(d: String, a: String, m: Boolean = false): Completion = new Completion { - def prepend = d - def append = a - def mark = m - } + def equal(a: Completion, b: Completion): Boolean = + (a,b) match + { + case (as: Suggestion, bs: Suggestion) => as.append == bs.append + case (ad: DisplayOnly, bd: DisplayOnly) => ad.display == bd.display + case (at: Token, bt: Token) => at.prepend == bt.prepend && at.append == bt.append + case _ => false + } - /** An unmarked completion with the empty string for prepend and append. */ - val empty: Completion = strict("", "", false) + def hashCode(a: Completion): Int = + a match + { + case as: Suggestion => (0, as.append).hashCode + case ad: DisplayOnly => (1, ad.display).hashCode + case at: Token => (2, at.prepend, at.append).hashCode + } - /** A marked completion with the empty string for prepend and append. */ - val mark: Completion = Completion.strict("", "", true) + val empty: Completion = suggestStrict("") + def single(c: Char): Completion = suggestStrict(c.toString) + + def displayOnly(value: => String): Completion = new DisplayOnly(value) + def displayStrict(value: String): Completion = displayOnly(value) + def token(prepend: => String, append: => String): Completion = new Token(prepend, append) + def tokenStrict(prepend: String, append: String): Completion = token(prepend, append) + def suggestion(value: => String): Completion = new Suggestion(value) + def suggestStrict(value: String): Completion = suggestion(value) } \ No newline at end of file diff --git a/util/complete/Parser.scala b/util/complete/Parser.scala index 459f7680a..f2fb6d919 100644 --- a/util/complete/Parser.scala +++ b/util/complete/Parser.scala @@ -34,6 +34,11 @@ sealed trait RichParser[A] * For example, `'c'.id` or `"asdf".id`*/ def id: Parser[A] + def ^^^[B](value: B): Parser[B] + def ??[B >: A](alt: B): Parser[B] + def <~[B](b: Parser[B]): Parser[A] + def ~>[B](b: Parser[B]): Parser[B] + def unary_- : Parser[Unit] def & (o: Parser[_]): Parser[A] def - (o: Parser[_]): Parser[A] @@ -58,7 +63,7 @@ object Parser if(p.valid) p.derive(c) else p def completions(p: Parser[_], s: String): Completions = completions( apply(p)(s) ) - def completions(p: Parser[_]): Completions = Completions.mark x p.completions + def completions(p: Parser[_]): Completions = p.completions implicit def richParser[A](a: Parser[A]): RichParser[A] = new RichParser[A] { @@ -71,6 +76,11 @@ object Parser def map[B](f: A => B) = mapParser(a, f) def id = a + def ^^^[B](value: B): Parser[B] = a map { _ => value } + def ??[B >: A](alt: B): Parser[B] = a.? map { _ getOrElse alt } + def <~[B](b: Parser[B]): Parser[A] = (a ~ b) map { case av ~ _ => av } + def ~>[B](b: Parser[B]): Parser[B] = (a ~ b) map { case _ ~ bv => bv } + def unary_- = not(a) def & (o: Parser[_]) = and(a, o) def - (o: Parser[_]) = sub(a, o) @@ -142,10 +152,11 @@ object Parser } else Invalid - def token[T](t: Parser[T]): Parser[T] = tokenStart(t, "") - def tokenStart[T](t: Parser[T], seen: String): Parser[T] = + def token[T](t: Parser[T]): Parser[T] = token(t, "", true) + def token[T](t: Parser[T], description: String): Parser[T] = token(t, description, false) + def token[T](t: Parser[T], seen: String, track: Boolean): Parser[T] = if(t.valid && !t.isTokenStart) - if(t.result.isEmpty) new TokenStart(t, seen) else t + if(t.result.isEmpty) new TokenStart(t, seen, track) else t else t @@ -205,6 +216,7 @@ object Parser def resultEmpty = result def derive(c: Char) = Invalid def completions = Completions.empty + override def toString = "success(" + value + ")" } val any: Parser[Char] = charClass(_ => true) @@ -224,14 +236,20 @@ object Parser new CharacterClass(set) examples(set.map(_.toString)) } def charClass(f: Char => Boolean): Parser[Char] = new CharacterClass(f) + implicit def literal(ch: Char): Parser[Char] = new Parser[Char] { def resultEmpty = None def derive(c: Char) = if(c == ch) success(ch) else Invalid - def completions = Completions.single(ch.toString) + def completions = Completions.single(Completion.suggestStrict(ch.toString)) + override def toString = "'" + ch + "'" } implicit def literal(s: String): Parser[String] = stringLiteral(s, s.toList) def stringLiteral(s: String, remaining: List[Char]): Parser[String] = if(s.isEmpty) error("String literal cannot be empty") else if(remaining.isEmpty) success(s) else new StringLiteral(s, remaining) + + object ~ { + def unapply[A,B](t: (A,B)): Some[(A,B)] = Some(t) + } } private final object Invalid extends Parser[Nothing] { @@ -239,6 +257,7 @@ private final object Invalid extends Parser[Nothing] def derive(c: Char) = error("Invalid.") override def valid = false def completions = Completions.nil + override def toString = "inv" } private final class SeqParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[(A,B)] { @@ -254,6 +273,7 @@ private final class SeqParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[(A } } lazy val completions = a.completions x b.completions + override def toString = "(" + a + " ~ " + b + ")" } private final class HomParser[A](a: Parser[A], b: Parser[A]) extends Parser[A] @@ -261,24 +281,28 @@ private final class HomParser[A](a: Parser[A], b: Parser[A]) extends Parser[A] def derive(c: Char) = (a derive c) | (b derive c) lazy val resultEmpty = a.resultEmpty orElse b.resultEmpty lazy val completions = a.completions ++ b.completions + override def toString = "(" + a + " | " + b + ")" } private final class HetParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[Either[A,B]] { def derive(c: Char) = (a derive c) || (b derive c) lazy val resultEmpty = a.resultEmpty.map(Left(_)) orElse b.resultEmpty.map(Right(_)) lazy val completions = a.completions ++ b.completions + override def toString = "(" + a + " || " + b + ")" } private final class BindParser[A,B](a: Parser[A], f: A => Parser[B]) extends Parser[B] { lazy val resultEmpty = a.resultEmpty match { case None => None; case Some(av) => f(av).resultEmpty } - lazy val completions = + lazy val completions = { a.completions flatMap { c => apply(a)(c.append).resultEmpty match { - case None => Completions.empty + case None => Completions.strict(Set.empty + c) case Some(av) => c x f(av).completions } } + } def derive(c: Char) = a derive c flatMap f + override def toString = "bind(" + a + ")" } private final class MapParser[A,B](a: Parser[A], f: A => B) extends Parser[B] { @@ -286,23 +310,30 @@ private final class MapParser[A,B](a: Parser[A], f: A => B) extends Parser[B] def derive(c: Char) = (a derive c) map f def completions = a.completions override def isTokenStart = a.isTokenStart + override def toString = "map(" + a + ")" } private final class Filter[T](p: Parser[T], f: T => Boolean) extends Parser[T] { lazy val resultEmpty = p.resultEmpty filter f def derive(c: Char) = (p derive c) filter f lazy val completions = p.completions filterS { s => apply(p)(s).resultEmpty.filter(f).isDefined } + override def toString = "filter(" + p + ")" } -private final class TokenStart[T](delegate: Parser[T], seen: String) extends Parser[T] +private final class TokenStart[T](delegate: Parser[T], seen: String, track: Boolean) extends Parser[T] { - def derive(c: Char) = tokenStart( delegate derive c, seen + c ) + def derive(c: Char) = token( delegate derive c, if(track) seen + c else seen, track) lazy val completions = - { - val dcs = delegate.completions - Completions( for(c <- dcs.get) yield Completion(seen, c.append, true) ) - } + if(track) + { + val dcs = delegate.completions + Completions( for(c <- dcs.get) yield Completion.token(seen, c.append) ) + } + else + Completions.single(Completion.displayStrict(seen)) + def resultEmpty = delegate.resultEmpty override def isTokenStart = true + override def toString = "token('" + seen + "', " + track + ", " + delegate + ")" } private final class And[T](a: Parser[T], b: Parser[_]) extends Parser[T] { @@ -321,26 +352,30 @@ private final class Examples[T](delegate: Parser[T], fixed: Set[String]) extends { def derive(c: Char) = examples(delegate derive c, fixed.collect { case x if x.length > 0 && x(0) == c => x.tail }) def resultEmpty = delegate.resultEmpty - lazy val completions = Completions(fixed map { ex => Completion.strict("",ex,false) } ) + lazy val completions = if(fixed.isEmpty) Completions.empty else Completions(fixed map(f => Completion.suggestion(f)) ) + override def toString = "examples(" + delegate + ", " + fixed.take(2) + ")" } private final class StringLiteral(str: String, remaining: List[Char]) extends Parser[String] { assert(str.length > 0 && !remaining.isEmpty) def resultEmpty = None def derive(c: Char) = if(remaining.head == c) stringLiteral(str, remaining.tail) else Invalid - lazy val completions = Completions.single(remaining.mkString) + lazy val completions = Completions.single(Completion.suggestion(remaining.mkString)) + override def toString = '"' + str + '"' } private final class CharacterClass(f: Char => Boolean) extends Parser[Char] { def resultEmpty = None def derive(c: Char) = if( f(c) ) success(c) else Invalid def completions = Completions.empty + override def toString = "class()" } private final class Optional[T](delegate: Parser[T]) extends Parser[Option[T]] { def resultEmpty = Some(None) def derive(c: Char) = (delegate derive c).map(Some(_)) lazy val completions = Completion.empty +: delegate.completions + override def toString = delegate.toString + "?" } private final class Repeat[T](partial: Option[Parser[T]], repeated: Parser[T], min: Int, max: UpperBound, accumulatedReverse: List[T]) extends Parser[Seq[T]] { @@ -395,4 +430,5 @@ private final class Repeat[T](partial: Option[Parser[T]], repeated: Parser[T], m for(value <- repeated.resultEmpty) yield List.make(min, value) } + override def toString = "repeat(" + min + "," + max +"," + partial + "," + repeated + ")" } \ No newline at end of file diff --git a/util/complete/src/test/scala/ParserTest.scala b/util/complete/src/test/scala/ParserTest.scala index 0fda3c6e3..3a2bc494f 100644 --- a/util/complete/src/test/scala/ParserTest.scala +++ b/util/complete/src/test/scala/ParserTest.scala @@ -1,14 +1,49 @@ package sbt.parse import Parser._ + import org.scalacheck._ +object ParserTest extends Properties("Completing Parser") +{ + val wsc = charClass(_.isWhitespace) + val ws = ( wsc + ) examples(" ") + val optWs = ( wsc * ) examples("") + + val nested = (token("a1") ~ token("b2")) ~ "c3" + val nestedDisplay = (token("a1", "") ~ token("b2", "")) ~ "c3" + + def p[T](f: T): T = { /*println(f);*/ f } + + def checkSingle(in: String, expect: Completion)(expectDisplay: Completion = expect) = + ( ("token '" + in + "'") |: checkOne(in, nested, expect)) && + ( ("display '" + in + "'") |: checkOne(in, nestedDisplay, expectDisplay) ) + + def checkOne(in: String, parser: Parser[_], expect: Completion): Prop = + p(completions(parser, in)) == Completions.single(expect) + + def checkInvalid(in: String) = + ( ("token '" + in + "'") |: checkInv(in, nested) ) && + ( ("display '" + in + "'") |: checkInv(in, nestedDisplay) ) + def checkInv(in: String, parser: Parser[_]): Prop = + p(completions(parser, in)) == Completions.nil + + property("nested tokens a") = checkSingle("", Completion.tokenStrict("","a1") )( Completion.displayStrict("")) + property("nested tokens a1") = checkSingle("a", Completion.tokenStrict("a","1") )( Completion.displayStrict("")) + property("nested tokens a inv") = checkInvalid("b") + property("nested tokens b") = checkSingle("a1", Completion.tokenStrict("","b2") )( Completion.displayStrict("")) + property("nested tokens b2") = checkSingle("a1b", Completion.tokenStrict("b","2") )( Completion.displayStrict("")) + property("nested tokens b inv") = checkInvalid("a1a") + property("nested tokens c") = checkSingle("a1b2", Completion.suggestStrict("c3") )() + property("nested tokens c3") = checkSingle("a1b2c", Completion.suggestStrict("3"))() + property("nested tokens c inv") = checkInvalid("a1b2a") +} object ParserExample { val ws = charClass(_.isWhitespace)+ val notws = charClass(!_.isWhitespace)+ val name = token("test") - val options = (ws ~ token("quick" || "failed" || "new") )* + val options = (ws ~ token("quick" | "failed" | "new") )* val include = (ws ~ token(examples(notws, Set("am", "is", "are", "was", "were") )) )* val t = name ~ options ~ include