From db0577c840425e699197c409b09674d5ce2039ac Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Mon, 8 Feb 2016 19:48:38 +1000 Subject: Add SBT tab completion for scala{,c,doc} Also refactor the partest parser to use the improved tab completion for test paths. --- build.sbt | 25 +++++--- project/ParserUtil.scala | 54 +++++++++++++++++ project/PartestUtil.scala | 32 ++++++---- project/ScalaOptionParser.scala | 128 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 project/ParserUtil.scala create mode 100644 project/ScalaOptionParser.scala diff --git a/build.sbt b/build.sbt index a752a33315..f9538d0b60 100644 --- a/build.sbt +++ b/build.sbt @@ -831,13 +831,24 @@ def generateServiceProviderResources(services: (String, String)*): Setting[_] = buildDirectory in ThisBuild := (baseDirectory in ThisBuild).value / "build-sbt" -// TODO custom argument parsers for these to autocomplete compiler options and paths -addCommandAlias("scalac", "compiler/compile:runMain scala.tools.nsc.Main -usejavacp") -addCommandAlias("scala", "repl-jline-embedded/compile:runMain scala.tools.nsc.MainGenericRunner -usejavacp") -addCommandAlias("scaladoc", "scaladoc/compile:runMain scala.tools.nsc.ScalaDoc -usejavacp") -addCommandAlias("scalap", "scalap/compile:runMain scala.tools.scalap.Main -usejavacp") - // Add tab completion to partest commands += Command("partest")(_ => PartestUtil.partestParser((baseDirectory in ThisBuild).value, (baseDirectory in ThisBuild).value / "test")) { (state, parsed) => - ("test/it:testOnly -- " + parsed):: state + ("test/it:testOnly -- " + parsed) :: state +} + +// Add tab completion to scalac et al. +commands ++= { + val commands = + List(("scalac", "compiler", "scala.tools.nsc.Main"), + ("scala", "repl-jline-embedded", "scala.tools.nsc.MainGenericRunner"), + ("scaladoc", "scaladoc", "scala.tools.nsc.ScalaDoc")) + + commands.map { + case (entryPoint, projectRef, mainClassName) => + Command(entryPoint)(_ => ScalaOptionParser.scalaParser(entryPoint, (baseDirectory in ThisBuild).value)) { (state, parsedOptions) => + (projectRef + "/runMain " + mainClassName + " -usejavacp " + parsedOptions) :: state + } + } } + +addCommandAlias("scalap", "scalap/compile:runMain scala.tools.scalap.Main -usejavacp") diff --git a/project/ParserUtil.scala b/project/ParserUtil.scala new file mode 100644 index 0000000000..f6658b146b --- /dev/null +++ b/project/ParserUtil.scala @@ -0,0 +1,54 @@ +import sbt._ +import sbt.complete.Parser._ +import sbt.complete.Parsers._ +import sbt.complete._ + +object ParserUtil { + def notStartingWith(parser: Parser[String], c: Char): Parser[String] = parser & not(c ~> any.*, "value cannot start with " + c + ".") + def concat(p: Parser[(String, String)]): Parser[String] = { + p.map(x => x._1 + x._2) + } + + def EitherOr(a: Parser[String], b: Parser[String]): Parser[String] = { + a.flatMap[String] { + case "" => b + case x: String => + concat(Space.string ~ b).map[String]((s: String) => x + s) + } + } + def Opt(a: Parser[String]) = a.?.map(_.getOrElse("")) + + val StringBasicNotStartingWithDash = notStartingWith(StringBasic, '-') + val IsDirectoryFilter = new SimpleFileFilter(_.isDirectory) + val JarOrDirectoryParser = FileParser(GlobFilter("*.jar") || IsDirectoryFilter) + def FileParser(filter: FileFilter, dirFilter: FileFilter = AllPassFilter, base: File = file(".")) = { + def matching(prefix: String): List[String] = { + val preFile = file(prefix) + val cwd = base + val parent = Option(preFile.getParentFile).getOrElse(cwd) + if (preFile.exists) { + if (preFile.isDirectory) { + preFile.*(IsDirectoryFilter.&&(dirFilter) || filter).get.map(_.getPath).toList + } else { + List(preFile).filter(filter.accept).map(_.getPath) + } + } + else if (parent != null) { + def ensureSuffix(s: String, suffix: String) = if (s.endsWith(suffix)) s else s + suffix + def pathOf(f: File): String = if (f.isDirectory && !filter.accept(f)) ensureSuffix(f.getPath, "/") else f.getPath + parent.*(GlobFilter(preFile.name + "*") && ((IsDirectoryFilter && dirFilter) || filter)).get.map(x => pathOf(if (parent == cwd) x.relativeTo(cwd).get else x)).toList + } else Nil + } + def displayPath = Completions.single(Completion.displayOnly("")) + token(StringBasic, TokenCompletions.fixed((seen, level) => if (seen.isEmpty) displayPath else matching(seen) match { + case Nil => displayPath + case x :: Nil => + if (filter.accept(file(x))) + Completions.strict(Set(Completion.tokenDisplay(x.stripPrefix(seen), x))) + else + Completions.strict(Set(Completion.suggestion(x.stripPrefix(seen)))) + case xs => + Completions.strict(xs.map(x => Completion.tokenDisplay(x.stripPrefix(seen), x)).toSet) + })).filter(!_.startsWith("-"), x => x) + } +} \ No newline at end of file diff --git a/project/PartestUtil.scala b/project/PartestUtil.scala index 7efe6185e5..0c0c677a6f 100644 --- a/project/PartestUtil.scala +++ b/project/PartestUtil.scala @@ -4,11 +4,25 @@ import sbt.complete._, Parser._, Parsers._ object PartestUtil { private case class TestFiles(srcPath: String, globalBase: File, testBase: File) { private val testCaseDir = new SimpleFileFilter(f => f.isDirectory && f.listFiles.nonEmpty && !(f.getParentFile / (f.name + ".res")).exists) - private def testCaseFinder = (testBase / srcPath).*(AllPassFilter).*(GlobFilter("*.scala") | GlobFilter("*.java") | GlobFilter("*.res") || testCaseDir) + private val testCaseFilter = GlobFilter("*.scala") | GlobFilter("*.java") | GlobFilter("*.res") || testCaseDir + private def testCaseFinder = (testBase / srcPath).*(AllPassFilter).*(testCaseFilter) private val basePaths = allTestCases.map(_._2.split('/').take(3).mkString("/") + "/").distinct def allTestCases = testCaseFinder.pair(relativeTo(globalBase)) def basePathExamples = new FixedSetExamples(basePaths) + private def equiv(f1: File, f2: File) = f1.getCanonicalFile == f2.getCanonicalFile + def parentChain(f: File): Iterator[File] = + if (f == null || !f.exists) Iterator() + else Iterator(f) ++ (if (f.getParentFile == null) Nil else parentChain(f.getParentFile)) + def isParentOf(parent: File, f2: File, maxDepth: Int) = + parentChain(f2).take(maxDepth).exists(p1 => equiv(p1, parent)) + def isTestCase(f: File) = { + val grandParent = if (f != null && f.getParentFile != null) f.getParentFile.getParentFile else null + grandParent != null && equiv(grandParent, testBase / srcPath) && testCaseFilter.accept(f) + } + def mayContainTestCase(f: File) = { + isParentOf(testBase / srcPath, f, 2) || isParentOf(f, testBase / srcPath, Int.MaxValue) + } } /** A parser for the custom `partest` command */ def partestParser(globalBase: File, testBase: File): Parser[String] = { @@ -28,11 +42,9 @@ object PartestUtil { if (_testFiles == null || _testFiles.srcPath != srcPath) _testFiles = new TestFiles(srcPath, globalBase, testBase) _testFiles } - val TestPathParser = token(NotSpace & not('-' ~> any.*, "File name cannot start with '-'."), TokenCompletions.fixed({ - (seen, level) => - val suggestions = testFiles.allTestCases.map(_._2).filter(_.startsWith(seen)).map(_.stripPrefix(seen)) - Completions.strict(suggestions.map(s => Completion.token(seen, s)).toSet) - })) + val TestPathParser = ParserUtil.FileParser( + new SimpleFileFilter(f => testFiles.isTestCase(f)), + new SimpleFileFilter(f => testFiles.mayContainTestCase(f)), globalBase) // allow `--grep "is unchecked" | --grep *t123*, in the spirit of ./bin/partest-ack // superset of the --grep built into partest itself. @@ -66,7 +78,7 @@ object PartestUtil { case Seq() => failure("no tests match pattern / glob") case x => success(x.mkString(" ")) } - ((token(grepOption <~ Space)) ~> token(globOrPattern, tokenCompletion)) + token(grepOption <~ Space) ~> token(globOrPattern, tokenCompletion) } val SrcPath = ((token(srcPathOption) <~ Space) ~ token(StringBasic.examples(Set("files", "pending", "scaladoc")))) map { @@ -74,9 +86,7 @@ object PartestUtil { srcPath = path opt + " " + path } - // allow the user to additional abitrary arguments, in case our parser is incomplete. - val WhatEver = token(NotSpace, _ => true).filter(x => !knownUnaryOptions.contains(x) && !Set(grepOption, srcPathOption).contains(x), x => x) - val P = oneOf(knownUnaryOptions.map(x => token(x))) | SrcPath | TestPathParser | Grep //| WhatEver - (Space ~> repsep(P, oneOrMore(Space))).map(_.mkString(" ")) + val P = oneOf(knownUnaryOptions.map(x => token(x))) | SrcPath | TestPathParser | Grep + (Space ~> repsep(P, oneOrMore(Space))).map(_.mkString(" ")).?.map(_.getOrElse("")) } } diff --git a/project/ScalaOptionParser.scala b/project/ScalaOptionParser.scala new file mode 100644 index 0000000000..a11bd93d82 --- /dev/null +++ b/project/ScalaOptionParser.scala @@ -0,0 +1,128 @@ +import ParserUtil._ +import sbt._ +import sbt.complete.Parser._ +import sbt.complete.Parsers._ +import sbt.complete._ + +object ScalaOptionParser { + /** A SBT parser for the Scala command line runners (scala, scalac, etc) */ + def scalaParser(entryPoint: String, globalBase: File): Parser[String] = { + def BooleanSetting(name: String): Parser[String] = + token(name) + def StringSetting(name: String): Parser[String] = { + val valueParser = name match { + case "-d" => JarOrDirectoryParser + case _ => token(StringBasic, TokenCompletions.displayOnly("")) + } + concat(concat(token(name ~ Space.string)) ~ valueParser) + } + def MultiStringSetting(name: String): Parser[String] = + concat(concat(token(name ~ ":")) ~ repsep(token(StringBasicNotStartingWithDash, TokenCompletions.displayOnly("")), token(",")).map(_.mkString)) + def IntSetting(name: String): Parser[String] = + concat(concat(token(name ~ ":")) ~ token(IntBasic.map(_.toString), TokenCompletions.displayOnly(""))) + def ChoiceSetting(name: String, choices: List[String]): Parser[String] = + concat(token(concat(name ~ ":")) ~ token(StringBasic.examples(choices: _*)).map(_.mkString)) + def MultiChoiceSetting(name: String, choices: List[String]): Parser[String] = + concat(token(concat(name ~ ":")) ~ rep1sep(token(StringBasic.examples(choices: _*)), token(",")).map(_.mkString)) + def PathSetting(name: String): Parser[String] = { + concat(concat(token(name) ~ Space.string) ~ rep1sep(JarOrDirectoryParser.filter(!_.contains(":"), x => x), token(java.io.File.pathSeparator)).map(_.mkString)) + } + def FileSetting(name: String): Parser[String] = { + concat(concat(token(name) ~ Space.string) ~ rep1sep(JarOrDirectoryParser.filter(!_.contains(":"), x => x), token(java.io.File.pathSeparator)).map(_.mkString)) + } + val Phase = token(NotSpace.examples(phases: _*)) + def PhaseSettingParser(name: String): Parser[String] = { + MultiChoiceSetting(name, phases) + } + def ScalaVersionSetting(name: String): Parser[String] = { + concat(concat(token(name ~ Space.string)) ~ token(StringBasic, TokenCompletions.displayOnly(""))) + } + val Property: Parser[String] = { + val PropName = concat(token("-D" ~ oneOrMore(NotSpaceClass & not('=', "not =")).string, TokenCompletions.displayOnly("-D"))) + val EqualsValue = concat("=" ~ token(OptNotSpace, TokenCompletions.displayOnly(""))) + concat(PropName ~ EqualsValue.?.map(_.getOrElse(""))) + } + + val sourceFile = FileParser(GlobFilter("*.scala") | GlobFilter("*.java")) + + // TODO Allow JVM settings via -J-... and temporarily add them to the ForkOptions + val UniversalOpt = Property | oneOf(pathSettingNames.map(PathSetting) ++ phaseSettings.map(PhaseSettingParser) ++ booleanSettingNames.map(BooleanSetting) ++ stringSettingNames.map(StringSetting) ++ multiStringSettingNames.map(MultiStringSetting) ++ intSettingNames.map(IntSetting) ++ choiceSettingNames.map { case (k, v) => ChoiceSetting(k, v) } ++ multiChoiceSettingNames.map { case (k, v) => MultiChoiceSetting(k, v) } ++ scalaVersionSettings.map(ScalaVersionSetting)) + val ScalacOpt = sourceFile | UniversalOpt + + val ScalaExtraSettings = oneOf( + scalaChoiceSettingNames.map { case (k, v) => ChoiceSetting(k,v)}.toList + ++ scalaStringSettingNames.map(StringSetting) + ++ scalaBooleanSettingNames.map(BooleanSetting)) + val ScalaOpt = UniversalOpt | ScalaExtraSettings + + val ScalaDocExtraSettings = oneOf( + scalaDocBooleanSettingNames.map(BooleanSetting) + ++ scalaDocIntSettingNames.map(IntSetting) + ++ scalaDocChoiceSettingNames.map { case (k, v) => ChoiceSetting(k, v)} + ++ scaladocStringSettingNames.map(StringSetting) + ++ scaladocPathSettingNames.map(PathSetting) + ++ scaladocMultiStringSettingNames.map(MultiStringSetting) + ) + val ScalaDocOpt = sourceFile | ScalaOpt | ScalaDocExtraSettings + + entryPoint match { + case "scala" => + val runnable = token(StringBasicNotStartingWithDash, TokenCompletions.displayOnly("")).filter(!_.startsWith("-"), x => x) + val runnableAndArgs = concat(runnable ~ Opt(concat(Space.string ~ repsep(token(StringBasic, TokenCompletions.displayOnly("")), Space).map(_.mkString(" "))))) + val options = repsep(ScalaOpt, Space).map(_.mkString(" ")) + Opt(Space ~> EitherOr(options, runnableAndArgs)) + case "scaladoc" => + Opt(Space ~> Opt(repsep(ScalaDocOpt, Space).map(_.mkString(" ")))) + case "scalac" => + Opt(Space ~> repsep(ScalacOpt, Space).map(_.mkString(" "))) + } + } + + // TODO retrieve this data programatically, ala https://github.com/scala/scala-tool-support/blob/master/bash-completion/src/main/scala/BashCompletion.scala + private def booleanSettingNames = List("-X", "-Xcheckinit", "-Xdev", "-Xdisable-assertions", "-Xexperimental", "-Xfatal-warnings", "-Xfull-lubs", "-Xfuture", "-Xlog-free-terms", "-Xlog-free-types", "-Xlog-implicit-conversions", "-Xlog-implicits", "-Xlog-reflective-calls", + "-Xno-forwarders", "-Xno-patmat-analysis", "-Xno-uescape", "-Xnojline", "-Xprint-pos", "-Xprint-types", "-Xprompt", "-Xresident", "-Xshow-phases", "-Xstrict-inference", "-Xverify", "-Y", + "-Ybreak-cycles", "-Yclosure-elim", "-Yconst-opt", "-Ydead-code", "-Ydebug", "-Ycompact-trees", "-Ydisable-unreachable-prevention", "-YdisableFlatCpCaching", "-Ydoc-debug", + "-Yeta-expand-keeps-star", "-Yide-debug", "-Yinfer-argument-types", "-Yinfer-by-name", "-Yinfer-debug", "-Yinline", "-Yinline-handlers", + "-Yinline-warnings", "-Yissue-debug", "-Ylog-classpath", "-Ymacro-debug-lite", "-Ymacro-debug-verbose", "-Ymacro-no-expand", + "-Yno-completion", "-Yno-generic-signatures", "-Yno-imports", "-Yno-load-impl-class", "-Yno-predef", "-Ynooptimise", + "-Yoverride-objects", "-Yoverride-vars", "-Ypatmat-debug", "-Yno-adapted-args", "-Ypos-debug", "-Ypresentation-debug", + "-Ypresentation-strict", "-Ypresentation-verbose", "-Yquasiquote-debug", "-Yrangepos", "-Yreify-copypaste", "-Yreify-debug", "-Yrepl-class-based", + "-Yrepl-sync", "-Yshow-member-pos", "-Yshow-symkinds", "-Yshow-symowners", "-Yshow-syms", "-Yshow-trees", "-Yshow-trees-compact", "-Yshow-trees-stringified", "-Ytyper-debug", + "-Ywarn-adapted-args", "-Ywarn-dead-code", "-Ywarn-inaccessible", "-Ywarn-infer-any", "-Ywarn-nullary-override", "-Ywarn-nullary-unit", "-Ywarn-numeric-widen", "-Ywarn-unused", "-Ywarn-unused-import", "-Ywarn-value-discard", + "-deprecation", "-explaintypes", "-feature", "-help", "-no-specialization", "-nobootcp", "-nowarn", "-optimise", "-print", "-unchecked", "-uniqid", "-usejavacp", "-usemanifestcp", "-verbose", "-version") + private def stringSettingNames = List("-Xgenerate-phase-graph", "-Xmain-class", "-Xpluginsdir", "-Xshow-class", "-Xshow-object", "-Xsource-reader", "-Ydump-classes", "-Ygen-asmp", + "-Ygen-javap", "-Ypresentation-log", "-Ypresentation-replay", "-Yrepl-outdir", "-d", "-dependencyfile", "-encoding", "-Xscript") + private def pathSettingNames = List("-bootclasspath", "-classpath", "-extdirs", "-javabootclasspath", "-javaextdirs", "-sourcepath", "-toolcp") + private val phases = List("all", "parser", "namer", "packageobjects", "typer", "patmat", "superaccessors", "extmethods", "pickler", "refchecks", "uncurry", "tailcalls", "specialize", "explicitouter", "erasure", "posterasure", "lazyvals", "lambdalift", "constructors", "flatten", "mixin", "cleanup", "delambdafy", "icode", "jvm", "terminal") + private val phaseSettings = List("-Xprint-icode", "-Ystop-after", "-Yskip", "-Yshow", "-Ystop-before", "-Ybrowse", "-Ylog", "-Ycheck", "-Xprint") + private def multiStringSettingNames = List("-Xmacro-settings", "-Xplugin", "-Xplugin-disable", "-Xplugin-require") + private def intSettingNames = List("-Xmax-classfile-name", "-Xelide-below", "-Ypatmat-exhaust-depth", "-Ypresentation-delay", "-Yrecursion") + private def choiceSettingNames = Map[String, List[String]]( + "-Ybackend" -> List("GenASM", "GenBCode"), + "-YclasspathImpl" -> List("flat", "recursive"), + "-Ydelambdafy" -> List("inline", "method"), + "-Ylinearizer" -> List("dfs", "dump", "normal", "rpo"), + "-Ymacro-expand" -> List("discard", "none"), + "-Yresolve-term-conflict" -> List("error", "object", "package"), + "-g" -> List("line", "none", "notailcails", "source", "vars"), + "-target" -> List("jvm-1.5", "jvm-1.6", "jvm-1.7", "jvm-1.8")) + private def multiChoiceSettingNames = Map[String, List[String]]( + "-Xlint" -> List("adapted-args", "nullary-unit", "inaccessible", "nullary-override", "infer-any", "missing-interpolator", "doc-detached", "private-shadow", "type-parameter-shadow", "poly-implicit-overload", "option-implicit", "delayedinit-select", "by-name-right-associative", "package-object-classes", "unsound-match", "stars-align"), + "-language" -> List("help", "_", "dynamics", "postfixOps", "reflectiveCalls", "implicitConversions", "higherKinds", "existentials", "experimental.macros"), + "-Yopt" -> List("l:none", "l:default", "l:method", "l:project", "l:classpath", "unreachable-code", "simplify-jumps", "empty-line-numbers", "empty-labels", "compact-locals", "nullness-tracking", "closure-elimination", "inline-project", "inline-global"), + "-Ystatistics" -> List("parser", "typer", "patmat", "erasure", "cleanup", "jvm") + ) + private def scalaVersionSettings = List("-Xmigration", "-Xsource") + + private def scalaChoiceSettingNames = Map("-howtorun" -> List("object", "script", "jar", "guess")) + private def scalaStringSettingNames = List("-i", "-e") + private def scalaBooleanSettingNames = List("-nc", "-save") + + private def scalaDocBooleanSettingNames = List("-Yuse-stupid-types", "-implicits", "-implicits-debug", "-implicits-show-all", "-implicits-sound-shadowing", "-implicits-hide", "-author", "-diagrams", "-diagrams-debug", "-raw-output", "-no-prefixes", "-no-link-warnings", "-expand-all-types", "-groups") + private def scalaDocIntSettingNames = List("-diagrams-max-classes", "-diagrams-max-implicits", "-diagrams-dot-timeout", "-diagrams-dot-restart") + private def scalaDocChoiceSettingNames = Map("-doc-format" -> List("html")) + private def scaladocStringSettingNames = List("-doc-title", "-doc-version", "-doc-footer", "-doc-no-compile", "-doc-source-url", "-doc-generator", "-skip-packages") + private def scaladocPathSettingNames = List("-doc-root-content", "-diagrams-dot-path") + private def scaladocMultiStringSettingNames = List("-doc-external-doc") + +} -- cgit v1.2.3