From 6a4eb92ee500ae88d30021ab9a0f392c037ce2dc Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sun, 6 Apr 2014 23:49:15 +0100 Subject: [PATCH] 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. --- .../scala/sbt/complete/ExampleSource.scala | 57 +++++++++++-------- .../src/main/scala/sbt/complete/Parser.scala | 55 +++++++++++++++--- .../src/main/scala/sbt/complete/Parsers.scala | 19 +++---- 3 files changed, 87 insertions(+), 44 deletions(-) diff --git a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala index be46bc587..f576f1bff 100644 --- a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala +++ b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala @@ -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) + } } \ No newline at end of file diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index 83ac69f2d..eb1844db6 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -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] { diff --git a/util/complete/src/main/scala/sbt/complete/Parsers.scala b/util/complete/src/main/scala/sbt/complete/Parsers.scala index 911253332..cb1b15d1a 100644 --- a/util/complete/src/main/scala/sbt/complete/Parsers.scala +++ b/util/complete/src/main/scala/sbt/complete/Parsers.scala @@ -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 ``. */ lazy val Port = token(IntBasic, "")