From 6b7ff287fcd56109d1db51ff82fce0ed0cdbcca6 Mon Sep 17 00:00:00 2001 From: Paul Phillips Date: Mon, 11 Apr 2011 03:04:47 +0000 Subject: Improving the repl help infrastructure, and mad... Improving the repl help infrastructure, and made the :wrap command more robust. No review. --- .../scala/tools/nsc/interpreter/ILoop.scala | 214 ++++++++++++--------- .../scala/tools/nsc/interpreter/LoopCommands.scala | 9 +- .../tools/nsc/interpreter/MemberHandlers.scala | 19 -- .../scala/tools/nsc/interpreter/Power.scala | 3 +- .../scala/tools/nsc/interpreter/ReplStrings.scala | 45 +++++ .../scala/tools/nsc/interpreter/package.scala | 21 +- 6 files changed, 180 insertions(+), 131 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/interpreter/ReplStrings.scala (limited to 'src') diff --git a/src/compiler/scala/tools/nsc/interpreter/ILoop.scala b/src/compiler/scala/tools/nsc/interpreter/ILoop.scala index b6a6da369f..d6e447ca0a 100644 --- a/src/compiler/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/compiler/scala/tools/nsc/interpreter/ILoop.scala @@ -43,8 +43,6 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) var settings: Settings = _ var intp: IMain = _ - def codeQuoted(s: String) = intp.memberHandlers.string2codeQuoted(s) - lazy val power = { val g = intp.global Power[g.type](this, g) @@ -129,7 +127,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) |// Calling Thread.stop on runaway %s with offending code: |// scala> %s""".stripMargin - println(template.format(line.thread, line.code)) + echo(template.format(line.thread, line.code)) // XXX no way to suppress the deprecation warning line.thread.stop() in.redrawLine() @@ -150,14 +148,40 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) } /** print a friendly help message */ - def helpCommand() = { - val usageWidth = commands map (_.usageMsg.length) max - val cmds = commands map (x => (x.usageMsg, x.help)) - val formatStr = "%-" + usageWidth + "s %s" + def helpCommand(line: String): Result = { + if (line == "") helpSummary() + else uniqueCommand(line) match { + case Some(lc) => echo("\n" + lc.longHelp) + case _ => ambiguousError(line) + } + } + private def helpSummary() = { + val usageWidth = commands map (_.usageMsg.length) max + val formatStr = "%-" + usageWidth + "s %s %s" + + echo("All commands can be abbreviated, e.g. :he instead of :help.") + echo("Those marked with a * have more detailed help, e.g. :help imports.\n") - out println "All commands can be abbreviated - for example :he instead of :help.\n" - for ((use, help) <- cmds) - out println formatStr.format(use, help) + commands foreach { cmd => + val star = if (cmd.hasLongHelp) "*" else " " + echo(formatStr.format(cmd.usageMsg, star, cmd.help)) + } + } + private def ambiguousError(cmd: String): Result = { + matchingCommands(cmd) match { + case Nil => echo(cmd + ": no such command. Type :help for help.") + case xs => echo(cmd + " is ambiguous: did you mean " + xs.map(":" + _.name).mkString(" or ") + "?") + } + Result(true, None) + } + private def matchingCommands(cmd: String) = commands filter (_.name startsWith cmd) + private def uniqueCommand(cmd: String): Option[LoopCommand] = { + // this lets us add commands willy-nilly and only requires enough command to disambiguate + matchingCommands(cmd) match { + case List(x) => Some(x) + // exact match OK even if otherwise appears ambiguous + case xs => xs find (_.name == cmd) + } } /** Print a welcome message */ @@ -170,7 +194,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) stripMargin.format(versionString, javaVmName, javaVersion) val addendum = if (isReplDebug) "\n" + new java.util.Date else "" - plushln(welcomeMsg + addendum) + echo(welcomeMsg + addendum) } /** Show the history */ @@ -189,15 +213,10 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) val offset = current - lines.size + 1 for ((line, index) <- lines.zipWithIndex) - println("%3d %s".format(index + offset, line)) + echo("%3d %s".format(index + offset, line)) } } - /** Some print conveniences */ - def println(x: Any) = out println x - def plush(x: Any) = { out print x ; out.flush() } - def plushln(x: Any) = { out println x ; out.flush() } - private def echo(msg: String) = { out println msg out.flush() @@ -213,7 +232,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) val offset = history.index - history.size + 1 for ((line, index) <- history.asStrings.zipWithIndex ; if line.toLowerCase contains cmdline) - println("%d %s".format(index + offset, line)) + echo("%d %s".format(index + offset, line)) } private var currentPrompt = Properties.shellPromptString @@ -224,9 +243,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) import LoopCommand.{ cmd, nullary } /** Standard commands **/ - val standardCommands = List( - cmd("cp", "", "add an entry (jar or directory) to the classpath", addClasspath), - nullary("help", "print this help message", helpCommand), + lazy val standardCommands = List( + cmd("cp", "", "add a jar or directory to the classpath", addClasspath), + cmd("help", "[command]", "print this summary or command-specific help", helpCommand), historyCommand, cmd("h?", "", "search the history", searchHistory), cmd("imports", "[name name ...]", "show import history, identifying sources of names", importsCommand), @@ -244,15 +263,34 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) ) /** Power user commands */ - val powerCommands: List[LoopCommand] = List( + lazy val powerCommands: List[LoopCommand] = List( nullary("dump", "displays a view of the interpreter's internal state", dumpCommand), cmd("phase", "", "set the implicit phase for power commands", phaseCommand), - cmd("wrap", "", "code to wrap around all executions", wrapCommand) + cmd("wrap", "", "name of method to wrap around each repl line", wrapCommand) withLongHelp (""" + |:wrap + |:wrap + | + |Installs a wrapper around each line entered into the repl. + |Currently it must be the simple name of an existing method + |with the specific signature shown in the following example. + | + |def timed[T](body: => T): T = { + | val start = System.nanoTime + | try body + | finally println((System.nanoTime - start) + " nanos elapsed.") + |} + |:wrap timed + | + |If given no argument, :wrap removes any wrapper present. + |Note that wrappers do not compose (a new one replaces the old + |one) and also that the :phase command uses the same machinery, + |so setting :wrap will clear any :phase setting. + """.stripMargin.trim) ) private def dumpCommand(): Result = { - println(power) - history.asStrings takeRight 30 foreach println + echo("" + power) + history.asStrings takeRight 30 foreach echo in.redrawLine() } @@ -385,16 +423,35 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) private def keybindingsCommand(): Result = { if (in.keyBindings.isEmpty) "Key bindings unavailable." else { - println("Reading jline properties for default key bindings.") - println("Accuracy not guaranteed: treat this as a guideline only.\n") - in.keyBindings foreach println + echo("Reading jline properties for default key bindings.") + echo("Accuracy not guaranteed: treat this as a guideline only.\n") + in.keyBindings foreach (x => echo ("" + x)) } } private def wrapCommand(line: String): Result = { - intp setExecutionWrapper line - if (line == "") "Cleared wrapper." - else "Set wrapper to '" + line + "'" + def failMsg = "Argument to :wrap must be the name of a method with signature [T](=> T): T" + val intp = ILoop.this.intp + val g: intp.global.type = intp.global + import g._ + + words(line) match { + case Nil => + intp setExecutionWrapper "" + "Cleared wrapper." + case wrapper :: Nil => + intp.typeOfExpression(wrapper) match { + case Some(PolyType(List(targ), MethodType(List(arg), restpe))) => + intp setExecutionWrapper intp.pathToTerm(wrapper) + "Set wrapper to '" + wrapper + "'" + case Some(x) => + failMsg + "\nFound: " + x + case _ => + failMsg + "\nFound: " + } + case _ => failMsg + } } + private def pathToPhaseWrapper = intp.pathToTerm("$r") + ".phased.atCurrent" private def phaseCommand(name: String): Result = { // This line crashes us in TreeGen: @@ -451,15 +508,15 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) case ex: Throwable => if (settings.YrichExes.value) { val sources = implicitly[Sources] - out.println("\n" + ex.getMessage) - out.println( + echo("\n" + ex.getMessage) + echo( if (isReplDebug) "[searching " + sources.path + " for exception contexts...]" else "[searching for exception contexts...]" ) - out.println(Exceptional(ex).force().context()) + echo(Exceptional(ex).force().context()) } else { - out.println(util.stackTraceString(ex)) + echo(util.stackTraceString(ex)) } ex match { case _: NoSuchMethodError | _: NoClassDefFoundError => @@ -467,7 +524,6 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) throw ex case _ => def fn(): Boolean = in.readYesOrNo(replayQuestionMessage, { echo("\nYou must enter y or n.") ; fn() }) - if (fn()) replay() else echo("\nAbandoning crashed session.") } @@ -504,7 +560,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) try file applyReader { reader => in = SimpleReader(reader, out, false) - plushln("Loading " + file + "...") + echo("Loading " + file + "...") loop() } finally { @@ -518,9 +574,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) closeInterpreter() createInterpreter() for (cmd <- replayCommands) { - plushln("Replaying: " + cmd) // flush because maybe cmd will have its own output + echo("Replaying: " + cmd) // flush because maybe cmd will have its own output command(cmd) - out.println + echo("") } } @@ -530,7 +586,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) def apply(line: String): Result = line match { case "" => showUsage() case _ => - val toRun = classOf[ProcessResult].getName + "(" + codeQuoted(line) + ")" + val toRun = classOf[ProcessResult].getName + "(" + string2codeQuoted(line) + ")" intp interpret toRun () } @@ -540,7 +596,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) val f = File(filename) if (f.exists) action(f) - else out.println("That file does not exist") + else echo("That file does not exist") } def loadCommand(arg: String) = { @@ -557,10 +613,10 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) if (f.exists) { addedClasspath = ClassPath.join(addedClasspath, f.path) val totalClasspath = ClassPath.join(settings.classpath.value, addedClasspath) - println("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, totalClasspath)) + echo("Added '%s'. Your new classpath is:\n\"%s\"".format(f.path, totalClasspath)) replay() } - else out.println("The path '" + f + "' doesn't seem to exist.") + else echo("The path '" + f + "' doesn't seem to exist.") } def powerCmd(): Result = { @@ -570,48 +626,28 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) def enablePowerMode() = { replProps.power setValue true power.unleash() - out.println(power.banner) + echo(power.banner) } def verbosity() = { val old = intp.printResults intp.printResults = !old - out.println("Switched " + (if (old) "off" else "on") + " result printing.") + echo("Switched " + (if (old) "off" else "on") + " result printing.") } /** Run one command submitted by the user. Two values are returned: * (1) whether to keep running, (2) the line to record for replay, * if any. */ def command(line: String): Result = { - def withError(msg: String) = { - out println msg - Result(true, None) - } - def ambiguous(cmds: List[LoopCommand]) = "Ambiguous: did you mean " + cmds.map(":" + _.name).mkString(" or ") + "?" - - // not a command - if (!line.startsWith(":")) { - // Notice failure to create compiler - if (intp.global == null) return Result(false, None) - else return Result(true, interpretStartingWith(line)) - } - - val line2 = line.tail - val (cmd, rest) = line2 indexWhere (_.isWhitespace) match { - case -1 => (line2, "") - case idx => (line2 take idx, line2 drop idx dropWhile (_.isWhitespace)) - } - - // this lets us add commands willy-nilly and only requires enough command to disambiguate - commands.filter(_.name startsWith cmd) match { - case List(f) => f(rest) - case Nil => withError("Unknown command. Type :help for help.") - case fs => - fs find (_.name == cmd) match { - case Some(exact) => exact(rest) - case _ => withError(ambiguous(fs)) - } + if (line startsWith ":") { + val cmd = line.tail takeWhile (x => !x.isWhitespace) + uniqueCommand(cmd) match { + case Some(lc) => lc(line.tail stripPrefix cmd dropWhile (_.isWhitespace)) + case _ => ambiguousError(cmd) + } } + else if (intp.global == null) Result(false, None) // Notice failure to create compiler + else Result(true, interpretStartingWith(line)) } private def readWhile(cond: String => Boolean) = { @@ -619,10 +655,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) } def pasteCommand(): Result = { - out.println("// Entering paste mode (ctrl-D to finish)\n") - out.flush() + echo("// Entering paste mode (ctrl-D to finish)\n") val code = readWhile(_ => true) mkString "\n" - out.println("\n// Exiting paste mode, now interpreting.\n") + echo("\n// Exiting paste mode, now interpreting.\n") intp interpret code () } @@ -632,9 +667,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) val PromptString = "scala> " def interpret(line: String): Unit = { - out.println(line.trim) + echo(line.trim) intp interpret line - out.println("") + echo("") } def transcript(start: String) = { @@ -642,7 +677,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) // transcript they just pasted. Todo: a short timer goes off when // lines stop coming which tells them to hit ctrl-D. // - // out.println("// Detected repl transcript paste: ctrl-D to finish.") + // echo("// Detected repl transcript paste: ctrl-D to finish.") apply(Iterator(start) ++ readWhile(_.trim != PromptString.trim)) } } @@ -665,7 +700,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) case IR.Success => Some(code) case IR.Incomplete => if (in.interactive && code.endsWith("\n\n")) { - out.println("You typed two blank lines. Starting a new command.") + echo("You typed two blank lines. Starting a new command.") None } else in.readLine(ContinueString) match { @@ -730,7 +765,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) val cmd = ":load " + filename command(cmd) addReplay(cmd) - out.println() + echo("") } case _ => } @@ -748,8 +783,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) ) catch { case ex @ (_: Exception | _: NoClassDefFoundError) => - out.println("Failed to created JLineReader: " + ex) - out.println("Falling back to SimpleReader.") + echo("Failed to created JLineReader: " + ex + "\nFalling back to SimpleReader.") SimpleReader() } } @@ -778,7 +812,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) // they type a command the compiler is ready to roll. intp.initialize() if (isReplPower) { - plushln("Starting in power mode, one moment...\n") + echo("Starting in power mode, one moment...\n") enablePowerMode() } loop() @@ -789,8 +823,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) /** process command-line arguments and do as they request */ def process(args: Array[String]): Boolean = { - def error1(msg: String) = out println ("scala: " + msg) - val command = new CommandLine(args.toList, error1) + val command = new CommandLine(args.toList, msg => echo("scala: " + msg)) def neededHelp(): String = (if (command.settings.help.value) command.usageMsg + "\n" else "") + (if (command.settings.Xhelp.value) command.xusageMsg + "\n" else "") @@ -798,7 +831,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) // if they asked for no help and command is valid, we call the real main neededHelp() match { case "" => command.ok && process(command.settings) - case help => plush(help) ; true + case help => echoNoNL(help) ; true } } @@ -815,6 +848,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) object ILoop { implicit def loopToInterpreter(repl: ILoop): IMain = repl.intp + private def echo(msg: String) = Console println msg // Designed primarily for use by test code: take a String with a // bunch of code, and prints out a transcript of what it would look @@ -882,11 +916,11 @@ object ILoop { val msg = if (args.isEmpty) "" else " Binding " + args.size + " value%s.".format( if (args.size == 1) "" else "s" ) - Console.println("Debug repl starting." + msg) + echo("Debug repl starting." + msg) val repl = new ILoop { override def prompt = "\ndebug> " } - repl.settings = new Settings(Console println _) + repl.settings = new Settings(echo) repl.settings.embeddedDefaults[T] repl.createInterpreter() repl.in = JLineReader(repl) @@ -896,7 +930,7 @@ object ILoop { args foreach (p => repl.bind(p.name, p.tpe, p.value)) repl.loop() - Console.println("\nDebug repl exiting.") + echo("\nDebug repl exiting.") repl.closeInterpreter() } } diff --git a/src/compiler/scala/tools/nsc/interpreter/LoopCommands.scala b/src/compiler/scala/tools/nsc/interpreter/LoopCommands.scala index 2fa7f1501b..196d8b882f 100644 --- a/src/compiler/scala/tools/nsc/interpreter/LoopCommands.scala +++ b/src/compiler/scala/tools/nsc/interpreter/LoopCommands.scala @@ -31,12 +31,19 @@ trait LoopCommands { // a single interpreter command abstract class LoopCommand(val name: String, val help: String) extends (String => Result) { + private var _longHelp: String = null + final def defaultHelp = usageMsg + " (no extended help available.)" + def hasLongHelp = _longHelp != null || longHelp != defaultHelp + def withLongHelp(text: String): this.type = { _longHelp = text ; this } + def longHelp = _longHelp match { + case null => defaultHelp + case text => text + } def usage: String = "" def usageMsg: String = ":" + name + ( if (usage == "") "" else " " + usage ) def apply(line: String): Result - protected def words(line: String): List[String] = line split "\\s+" toList // called if no args are given def showUsage(): Result = { diff --git a/src/compiler/scala/tools/nsc/interpreter/MemberHandlers.scala b/src/compiler/scala/tools/nsc/interpreter/MemberHandlers.scala index 208b033afb..330294f823 100644 --- a/src/compiler/scala/tools/nsc/interpreter/MemberHandlers.scala +++ b/src/compiler/scala/tools/nsc/interpreter/MemberHandlers.scala @@ -18,25 +18,6 @@ trait MemberHandlers { import global._ import naming._ - def string2codeQuoted(str: String) = - "\"" + string2code(str) + "\"" - - def any2stringOf(x: Any, maxlen: Int) = - "scala.runtime.ScalaRunTime.replStringOf(%s, %s)".format(x, maxlen) - - /** Convert a string into code that can recreate the string. - * This requires replacing all special characters by escape - * codes. It does not add the surrounding " marks. */ - def string2code(str: String): String = { - val res = new StringBuilder - for (c <- str) c match { - case '"' | '\'' | '\\' => res += '\\' ; res += c - case _ if c.isControl => res ++= Chars.char2uescape(c) - case _ => res += c - } - res.toString - } - private def codegenln(leadingPlus: Boolean, xs: String*): String = codegen(leadingPlus, (xs ++ Array("\n")): _*) private def codegenln(xs: String*): String = codegenln(true, xs: _*) diff --git a/src/compiler/scala/tools/nsc/interpreter/Power.scala b/src/compiler/scala/tools/nsc/interpreter/Power.scala index 1d68c14d4a..c2f72e9f37 100644 --- a/src/compiler/scala/tools/nsc/interpreter/Power.scala +++ b/src/compiler/scala/tools/nsc/interpreter/Power.scala @@ -135,6 +135,7 @@ abstract class Power[G <: Global]( |import scala.tools.nsc._ |import scala.collection.JavaConverters._ |import global._ + |import power.Implicits._ """.stripMargin /** Starts up power mode and runs whatever is in init. @@ -318,7 +319,7 @@ abstract class Power[G <: Global]( implicit def replInputStreamURL(url: URL)(implicit codec: Codec) = replInputStream(url.openStream()) } object Implicits extends Implicits2 { - val global = Power.this.global + val global: G = Power.this.global } trait ReplUtilities { diff --git a/src/compiler/scala/tools/nsc/interpreter/ReplStrings.scala b/src/compiler/scala/tools/nsc/interpreter/ReplStrings.scala new file mode 100644 index 0000000000..cef46c963a --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/ReplStrings.scala @@ -0,0 +1,45 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2011 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.collection.{ mutable, immutable } +import scala.PartialFunction.cond +import scala.reflect.NameTransformer +import util.Chars + +trait ReplStrings { + // Longest common prefix + def longestCommonPrefix(xs: List[String]): String = { + if (xs.isEmpty || xs.contains("")) "" + else xs.head.head match { + case ch => + if (xs.tail forall (_.head == ch)) "" + ch + longestCommonPrefix(xs map (_.tail)) + else "" + } + } + /** Convert a string into code that can recreate the string. + * This requires replacing all special characters by escape + * codes. It does not add the surrounding " marks. */ + def string2code(str: String): String = { + val res = new StringBuilder + for (c <- str) c match { + case '"' | '\'' | '\\' => res += '\\' ; res += c + case _ if c.isControl => res ++= Chars.char2uescape(c) + case _ => res += c + } + res.toString + } + + def string2codeQuoted(str: String) = + "\"" + string2code(str) + "\"" + + def any2stringOf(x: Any, maxlen: Int) = + "scala.runtime.ScalaRunTime.replStringOf(%s, %s)".format(x, maxlen) + + def words(s: String) = s.trim split "\\s+" filterNot (_ == "") toList + def isQuoted(s: String) = (s.length >= 2) && (s.head == s.last) && ("\"'" contains s.head) +} diff --git a/src/compiler/scala/tools/nsc/interpreter/package.scala b/src/compiler/scala/tools/nsc/interpreter/package.scala index aaf07343cb..1865e90e92 100644 --- a/src/compiler/scala/tools/nsc/interpreter/package.scala +++ b/src/compiler/scala/tools/nsc/interpreter/package.scala @@ -22,7 +22,7 @@ package scala.tools.nsc * InteractiveReader contains { history: History, completion: Completion } * IMain contains { global: Global } */ -package object interpreter extends ReplConfig { +package object interpreter extends ReplConfig with ReplStrings { type JFile = java.io.File type JClass = java.lang.Class[_] type JList[T] = java.util.List[T] @@ -35,23 +35,4 @@ package object interpreter extends ReplConfig { import collection.JavaConverters._ xs.asScala.toList map ("" + _) } - - // Longest common prefix - def longestCommonPrefix(xs: List[String]): String = { - if (xs.isEmpty || xs.contains("")) "" - else xs.head.head match { - case ch => - if (xs.tail forall (_.head == ch)) "" + ch + longestCommonPrefix(xs map (_.tail)) - else "" - } - } - - private[nsc] def words(s: String) = s.trim split "\\s+" toList - private[nsc] def isQuoted(s: String) = - (s.length >= 2) && (s.head == s.last) && ("\"'" contains s.head) - - /** Class objects */ - private[nsc] def classForName(name: String): Option[JClass] = - try Some(Class forName name) - catch { case _: ClassNotFoundException | _: SecurityException => None } } -- cgit v1.2.3