From 143993fdc1d96fc4223f7fd3a3b6daff14ff2f7f Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sat, 5 Apr 2014 19:46:27 +0100 Subject: [PATCH 01/11] Implemented a file parser. Added SourceOfExamples for lazy example listing (especially useful when lazily searching for files that match a certain prefix). --- .../src/main/scala/sbt/complete/Parser.scala | 43 +++++++++++++++++-- .../src/main/scala/sbt/complete/Parsers.scala | 35 ++++++++++++--- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index 798ea6d49..3ace05d26 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -70,7 +70,7 @@ sealed trait RichParser[A] /** If an exception is thrown by the original Parser, * capture it and fail locally instead of allowing the exception to propagate up and terminate parsing.*/ def failOnException: Parser[A] - + @deprecated("Use `not` and explicitly provide the failure message", "0.12.2") def unary_- : Parser[Unit] @@ -87,6 +87,9 @@ 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: SourceOfExamples, maxNumberOfExamples: Int): Parser[A] + /** Converts a Parser returning a Char sequence to a Parser returning a String.*/ def string(implicit ev: A <:< Seq[Char]): Parser[String] @@ -239,7 +242,7 @@ object Parser extends ParserMain case None => if(max.isZero) success(revAcc.reverse) else new Repeat(partial, repeated, min, max, revAcc) } } - + partial match { case Some(part) => @@ -285,6 +288,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: SourceOfExamples, maxNumberOfExamples: Int): Parser[A] = Parser.examples(a, s, maxNumberOfExamples) 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) @@ -295,7 +299,7 @@ trait ParserMain /** Construct a parser that is valid, but has no valid result. This is used as a way * to provide a definitive Failure when a parser doesn't match empty input. For example, - * in `softFailure(...) | p`, if `p` doesn't match the empty sequence, the failure will come + * in `softFailure(...) | p`, if `p` doesn't match the empty sequence, the failure will come * from the Parser constructed by the `softFailure` method. */ private[sbt] def softFailure(msg: => String, definitive: Boolean = false): Parser[Nothing] = SoftInvalid( mkFailures(msg :: Nil, definitive) ) @@ -430,6 +434,17 @@ trait ParserMain } else a + def examples[A](a: Parser[A], completions: SourceOfExamples, maxNumberOfExamples: Int): Parser[A] = + if(a.valid) { + a.result match + { + case Some(av) => success( av ) + case None => + new DynamicExamples(a, completions, maxNumberOfExamples) + } + } + else a + def matched(t: Parser[_], seen: Vector[Char] = Vector.empty, partial: Boolean = false): Parser[String] = t match { @@ -442,7 +457,7 @@ trait ParserMain } /** Establishes delegate parser `t` as a single token of tab completion. - * When tab completion of part of this token is requested, the completions provided by the delegate `t` or a later derivative are appended to + * When tab completion of part of this token is requested, the completions provided by the delegate `t` or a later derivative are appended to * the prefix String already seen by this parser. */ def token[T](t: Parser[T]): Parser[T] = token(t, TokenCompletions.default) @@ -703,6 +718,26 @@ 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) + ")" } +abstract class SourceOfExamples +{ + def apply(): Iterable[String] + def withAddedPrefix(addedPrefix: String): SourceOfExamples +} +private final class DynamicExamples[T](delegate: Parser[T], sourceOfExamples: SourceOfExamples, maxNumberOfExamples: Int = 10) extends ValidParser[T] +{ + def derive(c: Char) = examples(delegate derive c, sourceOfExamples.withAddedPrefix(c.toString), maxNumberOfExamples) + def result = delegate.result + lazy val resultEmpty = delegate.resultEmpty + def completions(level: Int) = { + if(sourceOfExamples().isEmpty) + if(resultEmpty.isValid) Completions.nil else Completions.empty + else { + val examplesBasedOnTheResult = sourceOfExamples().take(maxNumberOfExamples).toSet + Completions(examplesBasedOnTheResult.map(ex => Completion.suggestion(ex))) + } + } + override def toString = "examples(" + delegate + ", " + sourceOfExamples().take(2).toList + ")" +} private final class StringLiteral(str: String, start: Int) extends ValidParser[String] { assert(0 <= start && start < str.length) diff --git a/util/complete/src/main/scala/sbt/complete/Parsers.scala b/util/complete/src/main/scala/sbt/complete/Parsers.scala index 6bc745285..0ae64fb44 100644 --- a/util/complete/src/main/scala/sbt/complete/Parsers.scala +++ b/util/complete/src/main/scala/sbt/complete/Parsers.scala @@ -7,6 +7,7 @@ package sbt.complete import java.io.File import java.net.URI import java.lang.Character.{getType, MATH_SYMBOL, OTHER_SYMBOL, DASH_PUNCTUATION, OTHER_PUNCTUATION, MODIFIER_SYMBOL, CURRENCY_SYMBOL} + import java.nio.file.{Files, Path} /** Provides standard implementations of commonly useful [[Parser]]s. */ trait Parsers @@ -78,7 +79,7 @@ trait Parsers def isScalaIDChar(c: Char) = c.isLetterOrDigit || c == '_' def isDelimiter(c: Char) = c match { case '`' | '\'' | '\"' | /*';' | */',' | '.' => true ; case _ => false } - + /** Matches a single character that is not a whitespace character. */ lazy val NotSpaceClass = charClass(!_.isWhitespace, "non-whitespace character") @@ -128,8 +129,32 @@ 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') - // TODO: implement - def fileParser(base: File): Parser[File] = token(mapOrFail(NotSpace)(s => new File(s.mkString)), "") + class FileExamples(base: Path, prefix: String = "") extends SourceOfExamples { + private val prefixPath: String = "." + File.separator + prefix + + override def apply(): Iterable[String] = files(base).map(base.relativize).map(_.toString.substring(prefix.length)) + + override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix) + + protected def fileStartsWithPrefix(path: Path): Boolean = path.toString.startsWith(prefixPath) + + protected def directoryStartsWithPrefix(path: Path): Boolean = { + val pathString = path.toString + pathString.startsWith(prefixPath) || prefixPath.startsWith(pathString) + } + + protected def files(directory: Path): Iterable[Path] = { + import scala.collection.JavaConversions._ + val subPathStream = Files.newDirectoryStream(directory).toStream + val (subDirectories, filesOnly) = subPathStream.partition(path => Files.isDirectory(path)) + filesOnly.filter(fileStartsWithPrefix) ++ subDirectories.filter(directoryStartsWithPrefix).flatMap(files) + } + } + + def fileParser(base: File, maxNumberOfExamples: Int = 25): Parser[File] = + OptSpace ~> StringBasic + .examples(new FileExamples(base.toPath), maxNumberOfExamples) + .map(new File(_)) /** Parses a port number. Currently, this accepts any integer and presents a tab completion suggestion of ``. */ lazy val Port = token(IntBasic, "") @@ -153,7 +178,7 @@ trait Parsers /** Parses a verbatim quoted String value, discarding the quotes in the result. This kind of quoted text starts with triple quotes `"""` * and ends at the next triple quotes and may contain any character in between. */ lazy val StringVerbatim: Parser[String] = VerbatimDQuotes ~> - any.+.string.filter(!_.contains(VerbatimDQuotes), _ => "Invalid verbatim string") <~ + any.+.string.filter(!_.contains(VerbatimDQuotes), _ => "Invalid verbatim string") <~ VerbatimDQuotes /** Parses a string value, interpreting escapes and discarding the surrounding quotes in the result. @@ -168,7 +193,7 @@ trait Parsers BackslashChar ~> ('b' ^^^ '\b' | 't' ^^^ '\t' | 'n' ^^^ '\n' | 'f' ^^^ '\f' | 'r' ^^^ '\r' | '\"' ^^^ '\"' | '\'' ^^^ '\'' | '\\' ^^^ '\\' | UnicodeEscape) - /** Parses a single unicode escape sequence into the represented Char. + /** Parses a single unicode escape sequence into the represented Char. * A unicode escape begins with a backslash, followed by a `u` and 4 hexadecimal digits representing the unicode value. */ lazy val UnicodeEscape: Parser[Char] = ("u" ~> repeat(HexDigit, 4, 4)) map { seq => Integer.parseInt(seq.mkString, 16).toChar } From 60c6460755a616b4da2f716fbc7a9854a6566214 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sun, 6 Apr 2014 00:01:30 +0100 Subject: [PATCH 02/11] Ported the file search with pre-Java 7 API. --- .../src/main/scala/sbt/complete/Parsers.scala | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/util/complete/src/main/scala/sbt/complete/Parsers.scala b/util/complete/src/main/scala/sbt/complete/Parsers.scala index 0ae64fb44..bac5db0c2 100644 --- a/util/complete/src/main/scala/sbt/complete/Parsers.scala +++ b/util/complete/src/main/scala/sbt/complete/Parsers.scala @@ -7,7 +7,6 @@ package sbt.complete import java.io.File import java.net.URI import java.lang.Character.{getType, MATH_SYMBOL, OTHER_SYMBOL, DASH_PUNCTUATION, OTHER_PUNCTUATION, MODIFIER_SYMBOL, CURRENCY_SYMBOL} - import java.nio.file.{Files, Path} /** Provides standard implementations of commonly useful [[Parser]]s. */ trait Parsers @@ -129,31 +128,29 @@ 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') - class FileExamples(base: Path, prefix: String = "") extends SourceOfExamples { - private val prefixPath: String = "." + File.separator + prefix + class FileExamples(base: File, prefix: String = "") extends SourceOfExamples { + private val relativizedPrefix: String = "." + File.separator + prefix - override def apply(): Iterable[String] = files(base).map(base.relativize).map(_.toString.substring(prefix.length)) + override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length)) override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix) - protected def fileStartsWithPrefix(path: Path): Boolean = path.toString.startsWith(prefixPath) + protected def fileStartsWithPrefix(path: File): Boolean = path.toString.startsWith(relativizedPrefix) - protected def directoryStartsWithPrefix(path: Path): Boolean = { + protected def directoryStartsWithPrefix(path: File): Boolean = { val pathString = path.toString - pathString.startsWith(prefixPath) || prefixPath.startsWith(pathString) + pathString.startsWith(relativizedPrefix) || relativizedPrefix.startsWith(pathString) } - protected def files(directory: Path): Iterable[Path] = { - import scala.collection.JavaConversions._ - val subPathStream = Files.newDirectoryStream(directory).toStream - val (subDirectories, filesOnly) = subPathStream.partition(path => Files.isDirectory(path)) + protected def files(directory: File): Iterable[File] = { + val (subDirectories, filesOnly) = directory.listFiles().toStream.partition(_.isDirectory) filesOnly.filter(fileStartsWithPrefix) ++ subDirectories.filter(directoryStartsWithPrefix).flatMap(files) } } def fileParser(base: File, maxNumberOfExamples: Int = 25): Parser[File] = OptSpace ~> StringBasic - .examples(new FileExamples(base.toPath), maxNumberOfExamples) + .examples(new FileExamples(base), maxNumberOfExamples) .map(new File(_)) /** Parses a port number. Currently, this accepts any integer and presents a tab completion suggestion of ``. */ From 4d6a5472d8b43a4015d6ac9cda5545970e86b96b Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sun, 6 Apr 2014 22:39:10 +0100 Subject: [PATCH 03/11] Overloaded the the fileParser method. Renamed SourceOfExamples to ExampleSource. Documented fileParser, FileExamples, and ExampleSource. --- .../src/main/scala/sbt/complete/Parser.scala | 36 +++++++++++++------ .../src/main/scala/sbt/complete/Parsers.scala | 19 ++++++++-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index 3ace05d26..48d137e00 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -88,7 +88,7 @@ sealed trait RichParser[A] def examples(s: Set[String], check: Boolean = false): Parser[A] /** Explicitly defines the completions for the original Parser.*/ - def examples(s: SourceOfExamples, maxNumberOfExamples: Int): Parser[A] + def examples(s: ExampleSource, maxNumberOfExamples: Int): Parser[A] /** Converts a Parser returning a Char sequence to a Parser returning a String.*/ def string(implicit ev: A <:< Seq[Char]): Parser[String] @@ -288,7 +288,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: SourceOfExamples, maxNumberOfExamples: Int): Parser[A] = Parser.examples(a, s, maxNumberOfExamples) + def examples(s: ExampleSource, maxNumberOfExamples: Int): Parser[A] = Parser.examples(a, s, maxNumberOfExamples) 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,7 +434,7 @@ trait ParserMain } else a - def examples[A](a: Parser[A], completions: SourceOfExamples, maxNumberOfExamples: Int): Parser[A] = + def examples[A](a: Parser[A], completions: ExampleSource, maxNumberOfExamples: Int): Parser[A] = if(a.valid) { a.result match { @@ -718,25 +718,41 @@ 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) + ")" } -abstract class SourceOfExamples + +/** + * These sources of examples are used in parsers for user input completion. An example of such a source is the + * [[sbt.complete.Parsers.FileExamples]] class, which provides a list of suggested files to the user as they press the + * TAB key in the console. + */ +abstract class 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] - def withAddedPrefix(addedPrefix: String): SourceOfExamples + + /** + * @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 } -private final class DynamicExamples[T](delegate: Parser[T], sourceOfExamples: SourceOfExamples, maxNumberOfExamples: Int = 10) extends ValidParser[T] +private final class DynamicExamples[T](delegate: Parser[T], exampleSource: ExampleSource, maxNumberOfExamples: Int) extends ValidParser[T] { - def derive(c: Char) = examples(delegate derive c, sourceOfExamples.withAddedPrefix(c.toString), maxNumberOfExamples) + def derive(c: Char) = examples(delegate derive c, exampleSource.withAddedPrefix(c.toString), maxNumberOfExamples) def result = delegate.result lazy val resultEmpty = delegate.resultEmpty def completions(level: Int) = { - if(sourceOfExamples().isEmpty) + if(exampleSource().isEmpty) if(resultEmpty.isValid) Completions.nil else Completions.empty else { - val examplesBasedOnTheResult = sourceOfExamples().take(maxNumberOfExamples).toSet + val examplesBasedOnTheResult = exampleSource().take(maxNumberOfExamples).toSet Completions(examplesBasedOnTheResult.map(ex => Completion.suggestion(ex))) } } - override def toString = "examples(" + delegate + ", " + sourceOfExamples().take(2).toList + ")" + override def toString = "examples(" + delegate + ", " + exampleSource().take(2).toList + ")" } 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 bac5db0c2..375a622f9 100644 --- a/util/complete/src/main/scala/sbt/complete/Parsers.scala +++ b/util/complete/src/main/scala/sbt/complete/Parsers.scala @@ -128,7 +128,12 @@ 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') - class FileExamples(base: File, prefix: String = "") extends SourceOfExamples { + /** + * Provides path completion examples based on files in the base directory. + * @param base the directory within which this class will search for completion examples. + * @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 override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length)) @@ -148,11 +153,21 @@ trait Parsers } } - def fileParser(base: File, maxNumberOfExamples: Int = 25): 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, maxNumberOfExamples: Int): Parser[File] = OptSpace ~> StringBasic .examples(new FileExamples(base), maxNumberOfExamples) .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, "") From ac4712cac72fb73b7725f53b42db7484ec609102 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sun, 6 Apr 2014 22:48:22 +0100 Subject: [PATCH 04/11] Moved ExampleSource into a separate file. --- .../scala/sbt/complete/ExampleSource.scala | 49 +++++++++++++++++++ .../src/main/scala/sbt/complete/Parser.scala | 21 -------- .../src/main/scala/sbt/complete/Parsers.scala | 25 ---------- 3 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 util/complete/src/main/scala/sbt/complete/ExampleSource.scala diff --git a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala new file mode 100644 index 000000000..be46bc587 --- /dev/null +++ b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala @@ -0,0 +1,49 @@ +package sbt.complete + +import java.io.File + +/** + * These sources of examples are used in parsers for user input completion. An example of such a source is the + * [[sbt.complete.FileExamples]] class, which provides a list of suggested files to the user as they press the + * TAB key in the console. + */ +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] + + /** + * @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 +} + +/** + * Provides path completion examples based on files in the base directory. + * @param base the directory within which this class will search for completion examples. + * @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 + + override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length)) + + override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix) + + 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 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 48d137e00..83ac69f2d 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -718,27 +718,6 @@ 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) + ")" } - -/** - * These sources of examples are used in parsers for user input completion. An example of such a source is the - * [[sbt.complete.Parsers.FileExamples]] class, which provides a list of suggested files to the user as they press the - * TAB key in the console. - */ -abstract class 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] - - /** - * @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 -} private final class DynamicExamples[T](delegate: Parser[T], exampleSource: ExampleSource, maxNumberOfExamples: Int) extends ValidParser[T] { def derive(c: Char) = examples(delegate derive c, exampleSource.withAddedPrefix(c.toString), maxNumberOfExamples) diff --git a/util/complete/src/main/scala/sbt/complete/Parsers.scala b/util/complete/src/main/scala/sbt/complete/Parsers.scala index 375a622f9..911253332 100644 --- a/util/complete/src/main/scala/sbt/complete/Parsers.scala +++ b/util/complete/src/main/scala/sbt/complete/Parsers.scala @@ -128,31 +128,6 @@ 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') - /** - * Provides path completion examples based on files in the base directory. - * @param base the directory within which this class will search for completion examples. - * @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 - - override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length)) - - override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix) - - 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 files(directory: File): Iterable[File] = { - val (subDirectories, filesOnly) = directory.listFiles().toStream.partition(_.isDirectory) - filesOnly.filter(fileStartsWithPrefix) ++ subDirectories.filter(directoryStartsWithPrefix).flatMap(files) - } - } - /** * @param base the directory used for completion proposals (when the user presses the TAB key). Only paths under this * directory will be proposed. From fc6b2e087dd8cbe0bc2a4bd00198780f0a224db5 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sun, 6 Apr 2014 23:49:15 +0100 Subject: [PATCH 05/11] 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, "") From d00037263bc1b81d4ac0e2226704f3133343e760 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Tue, 8 Apr 2014 20:40:51 +0100 Subject: [PATCH 06/11] Documented the DynamicExamples and FixedSetExamples classes. --- .../main/scala/sbt/complete/ExampleSource.scala | 3 ++- .../src/main/scala/sbt/complete/Parser.scala | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala index f576f1bff..e13626083 100644 --- a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala +++ b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala @@ -24,7 +24,8 @@ trait ExampleSource } /** - * @param examples the source of examples that will be displayed to the user when they press the TAB key. + * A convenience example source that wraps any collection of strings into a source of examples. + * @param examples the 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)) diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index eb1844db6..c76c2f06a 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -744,6 +744,22 @@ 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) + ")" } + +/** + * This class wraps an existing parser (the delegate), and replaces the delegate's completions with examples from + * the given example source. + * + * This class asks the example source for a limited amount of examples (to prevent lengthy and expensive + * computations and large amounts of allocated data). It then passes these examples on to the UI. + * + * @param delegate the parser to decorate with completion examples (i.e., completion of user input). + * @param exampleSource the source from which this class will take examples (potentially filter them with the delegate + * parser), and pass them to the UI. + * @param maxNumberOfExamples the maximum number of completions to read from the example source and pass to the UI. This + * limit prevents lengthy example generation and allocation of large amounts of memory. + * @param removeInvalidExamples indicates whether to remove examples that are deemed invalid by the delegate parser. + * @tparam T the type of value produced by the parser. + */ 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, removeInvalidExamples) From 5b40b0c73aec41a1ce2e60391ae87b22150efc63 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Tue, 8 Apr 2014 21:27:27 +0100 Subject: [PATCH 07/11] Now using ExampleSource in collection-based completion parsers. Removed the Examples parser. Renamed DynamicExamples to ParserWithExamples. --- .../src/main/scala/sbt/complete/Parser.scala | 37 ++----------------- util/complete/src/test/scala/ParserTest.scala | 3 +- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index c76c2f06a..8a54b09fd 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -174,16 +174,9 @@ object Parser extends ParserMain def mkFailures(errors: => Seq[String], definitive: Boolean = false): Failure = new Failure(errors.distinct, definitive) def mkFailure(error: => String, definitive: Boolean = false): Failure = new Failure(error :: Nil, definitive) - def checkMatches(a: Parser[_], completions: Seq[String]) - { - val bad = completions.filter( apply(a)(_).resultEmpty.isFailure) - if(!bad.isEmpty) sys.error("Invalid example completions: " + bad.mkString("'", "', '", "'")) - } - def tuple[A,B](a: Option[A], b: Option[B]): Option[(A,B)] = (a,b) match { case (Some(av), Some(bv)) => Some((av, bv)); case _ => None } - def mapParser[A,B](a: Parser[A], f: A => B): Parser[B] = a.ifValid { a.result match @@ -302,7 +295,7 @@ trait ParserMain def & (o: Parser[_]) = and(a, o) 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: Set[String], check: Boolean = false): Parser[A] = examples(new FixedSetExamples(s), s.size, check) 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) @@ -437,18 +430,6 @@ trait ParserMain // The x Completions.empty removes any trailing token completions where append.isEmpty apply(p)(s).completions(level) x Completions.empty - def examples[A](a: Parser[A], completions: Set[String], check: Boolean = false): Parser[A] = - if(a.valid) { - a.result match - { - case Some(av) => success( av ) - case None => - if(check) checkMatches(a, completions.toSeq) - new Examples(a, completions) - } - } - else 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. @@ -466,7 +447,7 @@ trait ParserMain { case Some(av) => success( av ) case None => - new DynamicExamples(a, completions, maxNumberOfExamples, removeInvalidExamples) + new ParserWithExamples(a, completions, maxNumberOfExamples, removeInvalidExamples) } } else a @@ -732,18 +713,6 @@ private final class Not(delegate: Parser[_], failMessage: String) extends ValidP } override def toString = " -(%s)".format(delegate) } -private final class Examples[T](delegate: Parser[T], fixed: Set[String]) extends ValidParser[T] -{ - def derive(c: Char) = examples(delegate derive c, fixed.collect { case x if x.length > 0 && x(0) == c => x substring 1 }) - def result = delegate.result - lazy val resultEmpty = delegate.resultEmpty - def completions(level: Int) = - if(fixed.isEmpty) - if(resultEmpty.isValid) Completions.nil else Completions.empty - else - Completions(fixed map(f => Completion.suggestion(f)) ) - override def toString = "examples(" + delegate + ", " + fixed.take(2) + ")" -} /** * This class wraps an existing parser (the delegate), and replaces the delegate's completions with examples from @@ -760,7 +729,7 @@ private final class Examples[T](delegate: Parser[T], fixed: Set[String]) extends * @param removeInvalidExamples indicates whether to remove examples that are deemed invalid by the delegate parser. * @tparam T the type of value produced by the parser. */ -private final class DynamicExamples[T](delegate: Parser[T], exampleSource: ExampleSource, maxNumberOfExamples: Int, removeInvalidExamples: Boolean) extends ValidParser[T] +private final class ParserWithExamples[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, removeInvalidExamples) def result = delegate.result diff --git a/util/complete/src/test/scala/ParserTest.scala b/util/complete/src/test/scala/ParserTest.scala index 7a5d20b23..78ee28dc0 100644 --- a/util/complete/src/test/scala/ParserTest.scala +++ b/util/complete/src/test/scala/ParserTest.scala @@ -118,7 +118,8 @@ object ParserExample val name = token("test") val options = (ws ~> token("quick" | "failed" | "new") )* - val include = (ws ~> token(examples(notws.string, Set("am", "is", "are", "was", "were") )) )* + val exampleSet = Set("am", "is", "are", "was", "were") + val include = (ws ~> token(examples(notws.string, new FixedSetExamples(exampleSet), exampleSet.size, false )) )* val t = name ~ options ~ include From 447ab8d3342a8f2213f1cbe4650d6d6bf0fc4d7f Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sat, 12 Apr 2014 20:16:58 +0100 Subject: [PATCH 08/11] Created unit tests for ParserWithExamples and FixedSetExampleSource. --- .../src/main/scala/sbt/complete/Parser.scala | 2 +- .../sbt/complete/FixedSetExamplesTest.scala | 26 ++++++ .../sbt/complete/ParserWithExamplesTest.scala | 93 +++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 util/complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala create mode 100644 util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index 8a54b09fd..faea14d9a 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -752,7 +752,7 @@ private final class ParserWithExamples[T](delegate: Parser[T], exampleSource: Ex } private def isExampleValid(example: String): Boolean = { - apply(delegate)(example).resultEmpty.isFailure + apply(delegate)(example).resultEmpty.isValid } } private final class StringLiteral(str: String, start: Int) extends ValidParser[String] diff --git a/util/complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala b/util/complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala new file mode 100644 index 000000000..b9a5b2de2 --- /dev/null +++ b/util/complete/src/test/scala/sbt/complete/FixedSetExamplesTest.scala @@ -0,0 +1,26 @@ +package sbt.complete + +import org.specs2.mutable.Specification +import org.specs2.specification.Scope + +class FixedSetExamplesTest extends Specification { + + "adding a prefix" should { + "produce a smaller set of examples with the prefix removed" in new examples { + fixedSetExamples.withAddedPrefix("f")() must containTheSameElementsAs(List("oo", "ool", "u")) + fixedSetExamples.withAddedPrefix("fo")() must containTheSameElementsAs(List("o", "ol")) + fixedSetExamples.withAddedPrefix("b")() must containTheSameElementsAs(List("ar")) + } + } + + "without a prefix" should { + "produce the original set" in new examples { + fixedSetExamples() mustEqual exampleSet + } + } + + trait examples extends Scope { + val exampleSet = List("foo", "bar", "fool", "fu") + val fixedSetExamples = FixedSetExamples(exampleSet) + } +} diff --git a/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala b/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala new file mode 100644 index 000000000..664018f8f --- /dev/null +++ b/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala @@ -0,0 +1,93 @@ +package sbt.complete + +import org.specs2.mutable.Specification +import org.specs2.specification.Scope +import Completion._ + +class ParserWithExamplesTest extends Specification { + + "listing a limited number of completions" should { + "grab only the needed number of elements the iterable source of examples" in new parserWithLazyExamples { + parserWithExamples.completions(0) + examples.size shouldEqual maxNumberOfExamples + } + } + + "listing only valid completions" should { + "remove invalid examples" in new parserWithValidExamples { + val validCompletions = Completions(Set( + suggestion("blue"), + suggestion("red") + )) + parserWithExamples.completions(0) shouldEqual validCompletions + } + } + + "listing completions in a derived parser" should { + "produce only examples that match the derivation" in new parserWithValidExamples { + val derivedCompletions = Completions(Set( + suggestion("lue") + )) + parserWithExamples.derive('b').completions(0) shouldEqual derivedCompletions + } + } + + "listing unfiltered completions" should { + "produce all examples" in new parserWithAllExamples { + val completions = Completions(examples.map(suggestion(_)).toSet) + parserWithExamples.completions(0) shouldEqual completions + } + } + + "listing completions in a derived parser" should { + "produce only examples that match the derivation" in new parserWithAllExamples { + val derivedCompletions = Completions(Set( + suggestion("lue"), + suggestion("lock") + )) + parserWithExamples.derive('b').completions(0) shouldEqual derivedCompletions + } + } + + class parserWithLazyExamples extends parser(GrowableSourceOfExamples(), maxNumberOfExamples = 5, removeInvalidExamples = false) + + class parserWithValidExamples extends parser(removeInvalidExamples = true) + + class parserWithAllExamples extends parser(removeInvalidExamples = false) + + case class parser(examples: Iterable[String] = Set("blue", "yellow", "greeen", "block", "red"), + maxNumberOfExamples: Int = 25, + removeInvalidExamples: Boolean) extends Scope { + + import DefaultParsers._ + + val colorParser = "blue" | "green" | "black" | "red" + val parserWithExamples = new ParserWithExamples[String]( + colorParser, + FixedSetExamples(examples), + maxNumberOfExamples, + removeInvalidExamples + ) + } + + case class GrowableSourceOfExamples() extends Iterable[String] { + var numberOfIteratedElements: Int = 0 + + override def iterator: Iterator[String] = { + new Iterator[String] { + var currentElement = 0 + + override def next(): String = { + currentElement += 1 + numberOfIteratedElements = Math.max(currentElement, numberOfIteratedElements) + numberOfIteratedElements.toString + } + + override def hasNext: Boolean = true + } + } + + override def size: Int = numberOfIteratedElements + } + +} From 7aa58fd784f9e0573f16828eb114bdb95813fc96 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Mon, 14 Apr 2014 08:24:02 +0100 Subject: [PATCH 09/11] Improved the description of ParserWithExamples tests. --- .../sbt/complete/ParserWithExamplesTest.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala b/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala index 664018f8f..1151e1b0d 100644 --- a/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala +++ b/util/complete/src/test/scala/sbt/complete/ParserWithExamplesTest.scala @@ -7,14 +7,14 @@ import Completion._ class ParserWithExamplesTest extends Specification { "listing a limited number of completions" should { - "grab only the needed number of elements the iterable source of examples" in new parserWithLazyExamples { + "grab only the needed number of elements from the iterable source of examples" in new parserWithLazyExamples { parserWithExamples.completions(0) examples.size shouldEqual maxNumberOfExamples } } "listing only valid completions" should { - "remove invalid examples" in new parserWithValidExamples { + "use the delegate parser to remove invalid examples" in new parserWithValidExamples { val validCompletions = Completions(Set( suggestion("blue"), suggestion("red") @@ -23,8 +23,8 @@ class ParserWithExamplesTest extends Specification { } } - "listing completions in a derived parser" should { - "produce only examples that match the derivation" in new parserWithValidExamples { + "listing valid completions in a derived parser" should { + "produce only valid examples that start with the character of the derivation" in new parserWithValidExamples { val derivedCompletions = Completions(Set( suggestion("lue") )) @@ -32,15 +32,15 @@ class ParserWithExamplesTest extends Specification { } } - "listing unfiltered completions" should { - "produce all examples" in new parserWithAllExamples { + "listing valid and invalid completions" should { + "produce the entire source of examples" in new parserWithAllExamples { val completions = Completions(examples.map(suggestion(_)).toSet) parserWithExamples.completions(0) shouldEqual completions } } - "listing completions in a derived parser" should { - "produce only examples that match the derivation" in new parserWithAllExamples { + "listing valid and invalid completions in a derived parser" should { + "produce only examples that start with the character of the derivation" in new parserWithAllExamples { val derivedCompletions = Completions(Set( suggestion("lue"), suggestion("lock") @@ -62,7 +62,7 @@ class ParserWithExamplesTest extends Specification { import DefaultParsers._ val colorParser = "blue" | "green" | "black" | "red" - val parserWithExamples = new ParserWithExamples[String]( + val parserWithExamples: Parser[String] = new ParserWithExamples[String]( colorParser, FixedSetExamples(examples), maxNumberOfExamples, @@ -71,7 +71,7 @@ class ParserWithExamplesTest extends Specification { } case class GrowableSourceOfExamples() extends Iterable[String] { - var numberOfIteratedElements: Int = 0 + private var numberOfIteratedElements: Int = 0 override def iterator: Iterator[String] = { new Iterator[String] { From 134b0322f68d629a9f965a383b2143225736ce01 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Wed, 16 Apr 2014 08:36:27 +0100 Subject: [PATCH 10/11] Added tests for FileExamples. Improved the file-searching in FileExamples. --- .../scala/sbt/complete/ExampleSource.scala | 31 ++++--- .../scala/sbt/complete/FileExamplesTest.scala | 92 +++++++++++++++++++ 2 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 util/complete/src/test/scala/sbt/complete/FileExamplesTest.scala diff --git a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala index e13626083..565a8c3f1 100644 --- a/util/complete/src/main/scala/sbt/complete/ExampleSource.scala +++ b/util/complete/src/main/scala/sbt/complete/ExampleSource.scala @@ -1,6 +1,7 @@ package sbt.complete import java.io.File +import sbt.IO._ /** * These sources of examples are used in parsers for user input completion. An example of such a source is the @@ -27,12 +28,15 @@ trait ExampleSource * A convenience example source that wraps any collection of strings into a source of examples. * @param examples the examples that will be displayed to the user when they press the TAB key. */ -sealed case class FixedSetExamples(examples: Iterable[String]) extends ExampleSource { +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 } + private def examplesWithRemovedPrefix(prefix: String) = examples.collect { + case example if example startsWith prefix => example substring prefix.length + } } /** @@ -40,22 +44,19 @@ sealed case class FixedSetExamples(examples: Iterable[String]) extends ExampleSo * @param base the directory within which this class will search for completion examples. * @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 - - override def apply(): Iterable[String] = files(base).map(_.toString.substring(relativizedPrefix.length)) +class FileExamples(base: File, prefix: String = "") extends ExampleSource +{ + override def apply(): Stream[String] = files(base).map(_ substring prefix.length) override def withAddedPrefix(addedPrefix: String): FileExamples = new FileExamples(base, prefix + addedPrefix) - 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 files(directory: File): Stream[String] = { + val childPaths = directory.listFiles().toStream + val prefixedDirectChildPaths = childPaths.map(relativize(base, _).get).filter(_ startsWith prefix) + val dirsToRecurseInto = childPaths.filter(_.isDirectory).map(relativize(base, _).get).filter(dirStartsWithPrefix) + prefixedDirectChildPaths append dirsToRecurseInto.flatMap(dir => files(new File(base, dir))) } - protected def files(directory: File): Iterable[File] = { - val (subDirectories, filesOnly) = directory.listFiles().toStream.partition(_.isDirectory) - filesOnly.filter(fileStartsWithPrefix) ++ subDirectories.filter(directoryStartsWithPrefix).flatMap(files) - } + private def dirStartsWithPrefix(relativizedPath: String): Boolean = + (relativizedPath startsWith prefix) || (prefix startsWith relativizedPath) } \ No newline at end of file diff --git a/util/complete/src/test/scala/sbt/complete/FileExamplesTest.scala b/util/complete/src/test/scala/sbt/complete/FileExamplesTest.scala new file mode 100644 index 000000000..08c9a5884 --- /dev/null +++ b/util/complete/src/test/scala/sbt/complete/FileExamplesTest.scala @@ -0,0 +1,92 @@ +package sbt.complete + +import org.specs2.mutable.Specification +import org.specs2.specification.Scope +import sbt.IO.withTemporaryDirectory +import java.io.File +import sbt.IO._ + +class FileExamplesTest extends Specification +{ + + "listing all files in an absolute base directory" should { + "produce the entire base directory's contents" in new directoryStructure { + fileExamples().toList should containTheSameElementsAs(allRelativizedPaths) + } + } + + "listing files with a prefix that matches none" should { + "produce an empty list" in new directoryStructure(withCompletionPrefix = "z") { + fileExamples().toList should beEmpty + } + } + + "listing single-character prefixed files" should { + "produce matching paths only" in new directoryStructure(withCompletionPrefix = "f") { + fileExamples().toList should containTheSameElementsAs(prefixedPathsOnly) + } + } + + "listing directory-prefixed files" should { + "produce matching paths only" in new directoryStructure(withCompletionPrefix = "far") { + fileExamples().toList should containTheSameElementsAs(prefixedPathsOnly) + } + + "produce sub-dir contents only when appending a file separator to the directory" in new directoryStructure(withCompletionPrefix = "far" + File.separator) { + fileExamples().toList should containTheSameElementsAs(prefixedPathsOnly) + } + } + + "listing files with a sub-path prefix" should { + "produce matching paths only" in new directoryStructure(withCompletionPrefix = "far" + File.separator + "ba") { + fileExamples().toList should containTheSameElementsAs(prefixedPathsOnly) + } + } + + "completing a full path" should { + "produce a list with an empty string" in new directoryStructure(withCompletionPrefix = "bazaar") { + fileExamples().toList shouldEqual List("") + } + } + + class directoryStructure(withCompletionPrefix: String = "") extends Scope with DelayedInit + { + var fileExamples: FileExamples = _ + var baseDir: File = _ + var childFiles: List[File] = _ + var childDirectories: List[File] = _ + var nestedFiles: List[File] = _ + var nestedDirectories: List[File] = _ + + def allRelativizedPaths: List[String] = + (childFiles ++ childDirectories ++ nestedFiles ++ nestedDirectories).map(relativize(baseDir, _).get) + + def prefixedPathsOnly: List[String] = + allRelativizedPaths.filter(_ startsWith withCompletionPrefix).map(_ substring withCompletionPrefix.length) + + override def delayedInit(testBody: => Unit): Unit = { + withTemporaryDirectory { + tempDir => + createSampleDirStructure(tempDir) + fileExamples = new FileExamples(baseDir, withCompletionPrefix) + testBody + } + } + + private def createSampleDirStructure(tempDir: File): Unit = { + childFiles = toChildFiles(tempDir, List("foo", "bar", "bazaar")) + childDirectories = toChildFiles(tempDir, List("moo", "far")) + nestedFiles = toChildFiles(childDirectories(1), List("farfile1", "barfile2")) + nestedDirectories = toChildFiles(childDirectories(1), List("fardir1", "bardir2")) + + (childDirectories ++ nestedDirectories).map(_.mkdirs()) + (childFiles ++ nestedFiles).map(_.createNewFile()) + + // NOTE: Creating a new file here because `tempDir.listFiles()` returned an empty list. + baseDir = new File(tempDir.getCanonicalPath) + } + + private def toChildFiles(baseDir: File, files: List[String]): List[File] = files.map(new File(baseDir, _)) + } + +} From 247252fac14c7bb70389126e8adab7114f2de0bc Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Fri, 18 Apr 2014 16:41:05 +0100 Subject: [PATCH 11/11] Reintroduced the `examples` method. Reintroduced and deprecated the `checkMatches` method. --- .../src/main/scala/sbt/complete/Parser.scala | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/util/complete/src/main/scala/sbt/complete/Parser.scala b/util/complete/src/main/scala/sbt/complete/Parser.scala index faea14d9a..575cc5ec6 100644 --- a/util/complete/src/main/scala/sbt/complete/Parser.scala +++ b/util/complete/src/main/scala/sbt/complete/Parser.scala @@ -174,6 +174,13 @@ object Parser extends ParserMain def mkFailures(errors: => Seq[String], definitive: Boolean = false): Failure = new Failure(errors.distinct, definitive) def mkFailure(error: => String, definitive: Boolean = false): Failure = new Failure(error :: Nil, definitive) + @deprecated("This method is deprecated and will be removed in the next major version. Use the parser directly to check for invalid completions.", since = "0.13.2") + def checkMatches(a: Parser[_], completions: Seq[String]) + { + val bad = completions.filter( apply(a)(_).resultEmpty.isFailure) + if(!bad.isEmpty) sys.error("Invalid example completions: " + bad.mkString("'", "', '", "'")) + } + def tuple[A,B](a: Option[A], b: Option[B]): Option[(A,B)] = (a,b) match { case (Some(av), Some(bv)) => Some((av, bv)); case _ => None } @@ -430,6 +437,9 @@ trait ParserMain // The x Completions.empty removes any trailing token completions where append.isEmpty apply(p)(s).completions(level) x Completions.empty + def examples[A](a: Parser[A], completions: Set[String], check: Boolean = false): Parser[A] = + examples(a, new FixedSetExamples(completions), completions.size, check) + /** * @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. @@ -437,7 +447,7 @@ trait ParserMain * @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. + * 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 */ @@ -731,9 +741,13 @@ private final class Not(delegate: Parser[_], failMessage: String) extends ValidP */ private final class ParserWithExamples[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, removeInvalidExamples) + 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 @@ -742,6 +756,7 @@ private final class ParserWithExamples[T](delegate: Parser[T], exampleSource: Ex Completions(examplesBasedOnTheResult.map(ex => Completion.suggestion(ex))) } } + override def toString = "examples(" + delegate + ", " + exampleSource().take(2).toList + ")" private def filteredExamples: Iterable[String] = {