Documented the new Parsers API a bit. Prepared the new API so that we can port the old ones to the new. Added support for filtering erroneous examples.

This commit is contained in:
Matej Urbas 2014-04-06 23:49:15 +01:00
parent b9e37107b2
commit 6a4eb92ee5
3 changed files with 87 additions and 44 deletions

View File

@ -9,18 +9,29 @@ import java.io.File
*/
trait ExampleSource
{
/**
* @return a (possibly lazy) list of completion example strings. These strings are continuations of user's input. The
* user's input is incremented with calls to [[withAddedPrefix]].
*/
def apply(): Iterable[String]
/**
* @return a (possibly lazy) list of completion example strings. These strings are continuations of user's input. The
* user's input is incremented with calls to [[withAddedPrefix]].
*/
def apply(): Iterable[String]
/**
* @param addedPrefix a string that just typed in by the user.
* @return a new source of only those examples that start with the string typed by the user so far (with addition of
* the just added prefix).
*/
def withAddedPrefix(addedPrefix: String): ExampleSource
/**
* @param addedPrefix a string that just typed in by the user.
* @return a new source of only those examples that start with the string typed by the user so far (with addition of
* the just added prefix).
*/
def withAddedPrefix(addedPrefix: String): ExampleSource
}
/**
* @param examples the source of examples that will be displayed to the user when they press the TAB key.
*/
sealed case class FixedSetExamples(examples: Iterable[String]) extends ExampleSource {
override def withAddedPrefix(addedPrefix: String): ExampleSource = FixedSetExamples(examplesWithRemovedPrefix(addedPrefix))
override def apply(): Iterable[String] = examples
private def examplesWithRemovedPrefix(prefix: String) = examples.collect { case example if example startsWith prefix => example substring prefix.length }
}
/**
@ -29,21 +40,21 @@ trait ExampleSource
* @param prefix the part of the path already written by the user.
*/
class FileExamples(base: File, prefix: String = "") extends ExampleSource {
private val relativizedPrefix: String = "." + File.separator + prefix
private val relativizedPrefix: String = "." + File.separator + prefix
override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length))
override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length))
override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix)
override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix)
protected def fileStartsWithPrefix(path: File): Boolean = path.toString.startsWith(relativizedPrefix)
protected def fileStartsWithPrefix(path: File): Boolean = path.toString.startsWith(relativizedPrefix)
protected def directoryStartsWithPrefix(path: File): Boolean = {
val pathString = path.toString
pathString.startsWith(relativizedPrefix) || relativizedPrefix.startsWith(pathString)
}
protected def directoryStartsWithPrefix(path: File): Boolean = {
val pathString = path.toString
pathString.startsWith(relativizedPrefix) || relativizedPrefix.startsWith(pathString)
}
protected def files(directory: File): Iterable[File] = {
val (subDirectories, filesOnly) = directory.listFiles().toStream.partition(_.isDirectory)
filesOnly.filter(fileStartsWithPrefix) ++ subDirectories.filter(directoryStartsWithPrefix).flatMap(files)
}
protected def files(directory: File): Iterable[File] = {
val (subDirectories, filesOnly) = directory.listFiles().toStream.partition(_.isDirectory)
filesOnly.filter(fileStartsWithPrefix) ++ subDirectories.filter(directoryStartsWithPrefix).flatMap(files)
}
}

View File

@ -4,7 +4,7 @@
package sbt.complete
import Parser._
import sbt.Types.{const, left, right, some}
import sbt.Types.{left, right, some}
import sbt.Util.{makeList,separate}
/** A String parser that provides semi-automatic tab completion.
@ -87,8 +87,23 @@ sealed trait RichParser[A]
/** Explicitly defines the completions for the original Parser.*/
def examples(s: Set[String], check: Boolean = false): Parser[A]
/** Explicitly defines the completions for the original Parser.*/
def examples(s: ExampleSource, maxNumberOfExamples: Int): Parser[A]
/**
* @param exampleSource the source of examples when displaying completions to the user.
* @param maxNumberOfExamples limits the number of examples that the source of examples should return. This can
* prevent lengthy pauses and avoids bad interactive user experience.
* @param removeInvalidExamples indicates whether completion examples should be checked for validity (against the
* given parser). Invalid examples will be filtered out and only valid suggestions will
* be displayed.
* @return a new parser with a new source of completions.
*/
def examples(exampleSource: ExampleSource, maxNumberOfExamples: Int, removeInvalidExamples: Boolean): Parser[A]
/**
* @param exampleSource the source of examples when displaying completions to the user.
* @return a new parser with a new source of completions. It displays at most 25 completion examples and does not
* remove invalid examples.
*/
def examples(exampleSource: ExampleSource): Parser[A] = examples(exampleSource, maxNumberOfExamples = 25, removeInvalidExamples = false)
/** Converts a Parser returning a Char sequence to a Parser returning a String.*/
def string(implicit ev: A <:< Seq[Char]): Parser[String]
@ -288,7 +303,7 @@ trait ParserMain
def - (o: Parser[_]) = sub(a, o)
def examples(s: String*): Parser[A] = examples(s.toSet)
def examples(s: Set[String], check: Boolean = false): Parser[A] = Parser.examples(a, s, check)
def examples(s: ExampleSource, maxNumberOfExamples: Int): Parser[A] = Parser.examples(a, s, maxNumberOfExamples)
def examples(s: ExampleSource, maxNumberOfExamples: Int, removeInvalidExamples: Boolean): Parser[A] = Parser.examples(a, s, maxNumberOfExamples, removeInvalidExamples)
def filter(f: A => Boolean, msg: String => String): Parser[A] = filterParser(a, f, "", msg)
def string(implicit ev: A <:< Seq[Char]): Parser[String] = map(_.mkString)
def flatMap[B](f: A => Parser[B]) = bindParser(a, f)
@ -434,13 +449,24 @@ trait ParserMain
}
else a
def examples[A](a: Parser[A], completions: ExampleSource, maxNumberOfExamples: Int): Parser[A] =
/**
* @param a the parser to decorate with a source of examples. All validation and parsing is delegated to this parser,
* only [[Parser.completions]] is modified.
* @param completions the source of examples when displaying completions to the user.
* @param maxNumberOfExamples limits the number of examples that the source of examples should return. This can
* prevent lengthy pauses and avoids bad interactive user experience.
* @param removeInvalidExamples indicates whether completion examples should be checked for validity (against the given parser). An
* exception is thrown if the example source contains no valid completion suggestions.
* @tparam A the type of values that are returned by the parser.
* @return
*/
def examples[A](a: Parser[A], completions: ExampleSource, maxNumberOfExamples: Int, removeInvalidExamples: Boolean): Parser[A] =
if(a.valid) {
a.result match
{
case Some(av) => success( av )
case None =>
new DynamicExamples(a, completions, maxNumberOfExamples)
new DynamicExamples(a, completions, maxNumberOfExamples, removeInvalidExamples)
}
}
else a
@ -718,20 +744,31 @@ private final class Examples[T](delegate: Parser[T], fixed: Set[String]) extends
Completions(fixed map(f => Completion.suggestion(f)) )
override def toString = "examples(" + delegate + ", " + fixed.take(2) + ")"
}
private final class DynamicExamples[T](delegate: Parser[T], exampleSource: ExampleSource, maxNumberOfExamples: Int) extends ValidParser[T]
private final class DynamicExamples[T](delegate: Parser[T], exampleSource: ExampleSource, maxNumberOfExamples: Int, removeInvalidExamples: Boolean) extends ValidParser[T]
{
def derive(c: Char) = examples(delegate derive c, exampleSource.withAddedPrefix(c.toString), maxNumberOfExamples)
def derive(c: Char) = examples(delegate derive c, exampleSource.withAddedPrefix(c.toString), maxNumberOfExamples, removeInvalidExamples)
def result = delegate.result
lazy val resultEmpty = delegate.resultEmpty
def completions(level: Int) = {
if(exampleSource().isEmpty)
if(resultEmpty.isValid) Completions.nil else Completions.empty
else {
val examplesBasedOnTheResult = exampleSource().take(maxNumberOfExamples).toSet
val examplesBasedOnTheResult = filteredExamples.take(maxNumberOfExamples).toSet
Completions(examplesBasedOnTheResult.map(ex => Completion.suggestion(ex)))
}
}
override def toString = "examples(" + delegate + ", " + exampleSource().take(2).toList + ")"
private def filteredExamples: Iterable[String] = {
if (removeInvalidExamples)
exampleSource().filter(isExampleValid)
else
exampleSource()
}
private def isExampleValid(example: String): Boolean = {
apply(delegate)(example).resultEmpty.isFailure
}
}
private final class StringLiteral(str: String, start: Int) extends ValidParser[String]
{

View File

@ -128,21 +128,16 @@ trait Parsers
/** Returns true if `c` is an ASCII letter or digit. */
def alphanum(c: Char) = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')
/**
* @param base the directory used for completion proposals (when the user presses the TAB key). Only paths under this
* directory will be proposed.
* @return the file that was parsed from the input string. The returned path may or may not exist.
*/
def fileParser(base: File, maxNumberOfExamples: Int): Parser[File] =
/**
* @param base the directory used for completion proposals (when the user presses the TAB key). Only paths under this
* directory will be proposed.
* @return the file that was parsed from the input string. The returned path may or may not exist.
*/
def fileParser(base: File): Parser[File] =
OptSpace ~> StringBasic
.examples(new FileExamples(base), maxNumberOfExamples)
.examples(new FileExamples(base))
.map(new File(_))
/**
* See the overloaded [[fileParser]] method.
*/
def fileParser(base: File): Parser[File] = fileParser(base, 25)
/** Parses a port number. Currently, this accepts any integer and presents a tab completion suggestion of `<port>`. */
lazy val Port = token(IntBasic, "<port>")