From fdfbaf99d4037dbc79f09ed576220d65be38cea8 Mon Sep 17 00:00:00 2001 From: Matej Urbas Date: Sat, 5 Apr 2014 19:46:27 +0100 Subject: [PATCH] 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 }