fixes and improvements to tab completions combinators

This commit is contained in:
Mark Harrah 2010-12-13 22:44:25 -05:00
parent c436a1d3eb
commit ddb4381454
3 changed files with 158 additions and 65 deletions

View File

@ -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<TAB>
* 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)
}

View File

@ -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 + ")"
}

View File

@ -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", "<a1>") ~ token("b2", "<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("<a1>"))
property("nested tokens a1") = checkSingle("a", Completion.tokenStrict("a","1") )( Completion.displayStrict("<a1>"))
property("nested tokens a inv") = checkInvalid("b")
property("nested tokens b") = checkSingle("a1", Completion.tokenStrict("","b2") )( Completion.displayStrict("<b2>"))
property("nested tokens b2") = checkSingle("a1b", Completion.tokenStrict("b","2") )( Completion.displayStrict("<b2>"))
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