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(",",",")") override def toString = get.mkString("Completions(",",",")")
final def flatMap(f: Completion => Completions): Completions = Completions(get.flatMap(c => f(c).get)) 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) 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 object Completions
{ {
@ -28,32 +30,27 @@ object Completions
} }
/** Returns a strict Completions instance using the provided Completion Set. */ /** Returns a strict Completions instance using the provided Completion Set. */
def strict(cs: Set[Completion]): Completions = new Completions { def strict(cs: Set[Completion]): Completions = apply(cs)
def get = cs
}
/** No suggested completions, not even the empty Completion.*/ /** No suggested completions, not even the empty Completion.*/
val nil: Completions = strict(Set.empty) 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) val empty: Completions = strict(Set.empty + Completion.empty)
/** Includes only the marked empty Completion as a suggestion. */ /** Returns a strict Completions instance containing only the provided Completion.*/
val mark: Completions = strict(Set.empty + Completion.mark) def single(c: Completion): Completions = strict(Set.empty + c)
/** Returns a strict Completions instance with a single Completion with `s` for `append`.*/
def single(s: String): Completions = strict(Set.empty + Completion.strict("", s))
} }
/** /**
* Represents a completion. * 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: * Assuming space-delimited tokens, processing this:
* am is are w<TAB> * am is are w<TAB>
* could produce these Completions: * could produce these Completions:
* Completion { prepend = "w"; append = "as" } * Completion { display = "was"; append = "as" }
* Completion { prepend = "w"; append = "ere" } * Completion { display = "were"; append = "ere" }
* to suggest the tokens "was" and "were". * to suggest the tokens "was" and "were".
* *
* In this way, two pieces of information are preserved: * In this way, two pieces of information are preserved:
@ -62,51 +59,76 @@ object Completions
*/ */
sealed trait Completion 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.*/ /** The proposed suffix to append to the existing input to complete the last token in the input.*/
def append: String 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. /** Appends the completions in `o` with the completions in this Completion.*/
* A marked Completion will not be appended to another Completion unless that Completion is empty. def ++(o: Completion): Completion = Completion.concat(this, o)
* 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)
final def x(o: Completions): Completions = o.map(this ++ _) final def x(o: Completions): Completions = o.map(this ++ _)
override final lazy val hashCode = Completion.hashCode(this)
override final def toString = triple.toString override final def equals(o: Any) = o match { case c: Completion => Completion.equal(this, c); case _ => false }
override final lazy val hashCode = triple.hashCode }
override final def equals(o: Any) = o match { final class DisplayOnly(display0: String) extends Completion
case c: Completion => triple == c.triple {
case _ => false lazy val display = display0
} def isEmpty = display.isEmpty
final def triple = (prepend, append, mark) 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 object Completion
{ {
/** Constructs a lazy Completion with the given prepend, append, and mark values. */ def concat(a: Completion, b: Completion): Completion =
def apply(d: => String, a: => String, m: Boolean = false): Completion = new Completion { (a,b) match
lazy val prepend = d {
lazy val append = a case (as: Suggestion, bs: Suggestion) => suggestion(as.append + bs.append)
def mark = m 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 equal(a: Completion, b: Completion): Boolean =
def strict(d: String, a: String, m: Boolean = false): Completion = new Completion { (a,b) match
def prepend = d {
def append = a case (as: Suggestion, bs: Suggestion) => as.append == bs.append
def mark = m 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. */ def hashCode(a: Completion): Int =
val empty: Completion = strict("", "", false) 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 empty: Completion = suggestStrict("")
val mark: Completion = Completion.strict("", "", true) 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`*/ * For example, `'c'.id` or `"asdf".id`*/
def id: Parser[A] 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 unary_- : Parser[Unit]
def & (o: Parser[_]): Parser[A] def & (o: Parser[_]): Parser[A]
def - (o: Parser[_]): Parser[A] def - (o: Parser[_]): Parser[A]
@ -58,7 +63,7 @@ object Parser
if(p.valid) p.derive(c) else p if(p.valid) p.derive(c) else p
def completions(p: Parser[_], s: String): Completions = completions( apply(p)(s) ) 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] 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 map[B](f: A => B) = mapParser(a, f)
def id = a 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 unary_- = not(a)
def & (o: Parser[_]) = and(a, o) def & (o: Parser[_]) = and(a, o)
def - (o: Parser[_]) = sub(a, o) def - (o: Parser[_]) = sub(a, o)
@ -142,10 +152,11 @@ object Parser
} }
else Invalid else Invalid
def token[T](t: Parser[T]): Parser[T] = tokenStart(t, "") def token[T](t: Parser[T]): Parser[T] = token(t, "", true)
def tokenStart[T](t: Parser[T], seen: String): Parser[T] = 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.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 else
t t
@ -205,6 +216,7 @@ object Parser
def resultEmpty = result def resultEmpty = result
def derive(c: Char) = Invalid def derive(c: Char) = Invalid
def completions = Completions.empty def completions = Completions.empty
override def toString = "success(" + value + ")"
} }
val any: Parser[Char] = charClass(_ => true) val any: Parser[Char] = charClass(_ => true)
@ -224,14 +236,20 @@ object Parser
new CharacterClass(set) examples(set.map(_.toString)) new CharacterClass(set) examples(set.map(_.toString))
} }
def charClass(f: Char => Boolean): Parser[Char] = new CharacterClass(f) def charClass(f: Char => Boolean): Parser[Char] = new CharacterClass(f)
implicit def literal(ch: Char): Parser[Char] = new Parser[Char] { implicit def literal(ch: Char): Parser[Char] = new Parser[Char] {
def resultEmpty = None def resultEmpty = None
def derive(c: Char) = if(c == ch) success(ch) else Invalid 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) implicit def literal(s: String): Parser[String] = stringLiteral(s, s.toList)
def stringLiteral(s: String, remaining: List[Char]): Parser[String] = 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) 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] private final object Invalid extends Parser[Nothing]
{ {
@ -239,6 +257,7 @@ private final object Invalid extends Parser[Nothing]
def derive(c: Char) = error("Invalid.") def derive(c: Char) = error("Invalid.")
override def valid = false override def valid = false
def completions = Completions.nil def completions = Completions.nil
override def toString = "inv"
} }
private final class SeqParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[(A,B)] 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 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] 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) def derive(c: Char) = (a derive c) | (b derive c)
lazy val resultEmpty = a.resultEmpty orElse b.resultEmpty lazy val resultEmpty = a.resultEmpty orElse b.resultEmpty
lazy val completions = a.completions ++ b.completions 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]] 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) def derive(c: Char) = (a derive c) || (b derive c)
lazy val resultEmpty = a.resultEmpty.map(Left(_)) orElse b.resultEmpty.map(Right(_)) lazy val resultEmpty = a.resultEmpty.map(Left(_)) orElse b.resultEmpty.map(Right(_))
lazy val completions = a.completions ++ b.completions 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] 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 resultEmpty = a.resultEmpty match { case None => None; case Some(av) => f(av).resultEmpty }
lazy val completions = lazy val completions = {
a.completions flatMap { c => a.completions flatMap { c =>
apply(a)(c.append).resultEmpty match { 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 case Some(av) => c x f(av).completions
} }
} }
}
def derive(c: Char) = a derive c flatMap f 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] 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 derive(c: Char) = (a derive c) map f
def completions = a.completions def completions = a.completions
override def isTokenStart = a.isTokenStart override def isTokenStart = a.isTokenStart
override def toString = "map(" + a + ")"
} }
private final class Filter[T](p: Parser[T], f: T => Boolean) extends Parser[T] private final class Filter[T](p: Parser[T], f: T => Boolean) extends Parser[T]
{ {
lazy val resultEmpty = p.resultEmpty filter f lazy val resultEmpty = p.resultEmpty filter f
def derive(c: Char) = (p derive c) 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 } 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 = lazy val completions =
{ if(track)
val dcs = delegate.completions {
Completions( for(c <- dcs.get) yield Completion(seen, c.append, true) ) 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 def resultEmpty = delegate.resultEmpty
override def isTokenStart = true override def isTokenStart = true
override def toString = "token('" + seen + "', " + track + ", " + delegate + ")"
} }
private final class And[T](a: Parser[T], b: Parser[_]) extends Parser[T] 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 derive(c: Char) = examples(delegate derive c, fixed.collect { case x if x.length > 0 && x(0) == c => x.tail })
def resultEmpty = delegate.resultEmpty 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] private final class StringLiteral(str: String, remaining: List[Char]) extends Parser[String]
{ {
assert(str.length > 0 && !remaining.isEmpty) assert(str.length > 0 && !remaining.isEmpty)
def resultEmpty = None def resultEmpty = None
def derive(c: Char) = if(remaining.head == c) stringLiteral(str, remaining.tail) else Invalid 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] private final class CharacterClass(f: Char => Boolean) extends Parser[Char]
{ {
def resultEmpty = None def resultEmpty = None
def derive(c: Char) = if( f(c) ) success(c) else Invalid def derive(c: Char) = if( f(c) ) success(c) else Invalid
def completions = Completions.empty def completions = Completions.empty
override def toString = "class()"
} }
private final class Optional[T](delegate: Parser[T]) extends Parser[Option[T]] private final class Optional[T](delegate: Parser[T]) extends Parser[Option[T]]
{ {
def resultEmpty = Some(None) def resultEmpty = Some(None)
def derive(c: Char) = (delegate derive c).map(Some(_)) def derive(c: Char) = (delegate derive c).map(Some(_))
lazy val completions = Completion.empty +: delegate.completions 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]] 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 for(value <- repeated.resultEmpty) yield
List.make(min, value) List.make(min, value)
} }
override def toString = "repeat(" + min + "," + max +"," + partial + "," + repeated + ")"
} }

View File

@ -1,14 +1,49 @@
package sbt.parse package sbt.parse
import Parser._ 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 object ParserExample
{ {
val ws = charClass(_.isWhitespace)+ val ws = charClass(_.isWhitespace)+
val notws = charClass(!_.isWhitespace)+ val notws = charClass(!_.isWhitespace)+
val name = token("test") 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 include = (ws ~ token(examples(notws, Set("am", "is", "are", "was", "were") )) )*
val t = name ~ options ~ include val t = name ~ options ~ include