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, _)) + } + +}