mirror of https://github.com/sbt/sbt.git
fixes and improvements to tab completions combinators
This commit is contained in:
parent
c436a1d3eb
commit
ddb4381454
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 + ")"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue