From e9f1ccb0308f207303af2507415379c4d8dbcd6a Mon Sep 17 00:00:00 2001 From: Paul Phillips Date: Fri, 11 Feb 2011 04:00:46 +0000 Subject: This addresses a few long standing irritations ... This addresses a few long standing irritations with jline, rewriting chunks of it along the way. No longer does columnar output spill over and double space everything if you're unlucky with the chosen widths. Pagination works for a higher definition of work. Etc. Also, for those who enjoy operating missile systems from their repls, crash recovery now requests your permission before replaying the session. Closes #4194, no review. --- lib/jline.jar.desired.sha1 | 2 +- .../nsc/interpreter/ConsoleReaderHelper.scala | 66 ++++++++++++++++++++++ .../scala/tools/nsc/interpreter/ILoop.scala | 31 ++++++---- .../scala/tools/nsc/interpreter/IMain.scala | 26 ++++++--- .../tools/nsc/interpreter/InteractiveReader.scala | 17 ++++-- .../scala/tools/nsc/interpreter/JLineReader.scala | 42 ++++++++------ .../scala/tools/nsc/interpreter/SimpleReader.scala | 2 + .../scala/tools/nsc/interpreter/package.scala | 18 +++++- .../tools/nsc/interpreter/session/package.scala | 1 - src/jline/project/build.properties | 4 +- src/jline/project/plugins/project/build.properties | 2 +- .../java/scala/tools/jline/WindowsTerminal.java | 2 + .../scala/tools/jline/console/ConsoleReader.java | 10 ++-- 13 files changed, 172 insertions(+), 51 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala diff --git a/lib/jline.jar.desired.sha1 b/lib/jline.jar.desired.sha1 index b20982df23..d672b233ac 100644 --- a/lib/jline.jar.desired.sha1 +++ b/lib/jline.jar.desired.sha1 @@ -1 +1 @@ -50e6e3fad054ce6d0a077e85b457817039ede3c8 ?jline.jar +80aedc428de3beae3608772523a8d29ffcef0e5b ?jline.jar diff --git a/src/compiler/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala b/src/compiler/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala new file mode 100644 index 0000000000..d0d3349038 --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala @@ -0,0 +1,66 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2011 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.tools.jline.console.{ ConsoleReader, CursorBuffer } +import scala.tools.jline.console.completer.CompletionHandler +import Completion._ + +trait ConsoleReaderHelper extends ConsoleReader { + def currentLine = "" + getCursorBuffer.buffer + def currentPos = getCursorBuffer.cursor + def terminal = getTerminal() + def width = terminal.getWidth() + def height = terminal.getHeight() + def paginate = isPaginationEnabled() + def paginate_=(value: Boolean) = setPaginationEnabled(value) + + def goBack(num: Int): Unit + def readOneKey(prompt: String): Int + def eraseLine(): Unit + + private val marginSize = 3 + private def morePrompt = "--More--" + private def emulateMore(): Int = { + val key = readOneKey(morePrompt) + try key match { + case '\r' | '\n' => 1 + case 'q' => -1 + case _ => height - 1 + } + finally { + eraseLine() + // TODO: still not quite managing to erase --More-- and get + // back to a scala prompt without another keypress. + if (key == 'q') { + putString(getPrompt()) + redrawLine() + flush() + } + } + } + + override def printColumns(items: JCollection[_ <: CharSequence]): Unit = + printColumns(items: List[String]) + + def printColumns(items: List[String]): Unit = { + val longest = items map (_.length) max + var linesLeft = if (isPaginationEnabled()) height - 1 else Int.MaxValue + val columnSize = longest + marginSize + val padded = items map ("%-" + columnSize + "s" format _) + + padded grouped (width / columnSize) foreach { xs => + println(xs.mkString) + linesLeft -= 1 + if (linesLeft <= 0) { + linesLeft = emulateMore() + if (linesLeft < 0) + return + } + } + } +} diff --git a/src/compiler/scala/tools/nsc/interpreter/ILoop.scala b/src/compiler/scala/tools/nsc/interpreter/ILoop.scala index 90346c480f..38084af016 100644 --- a/src/compiler/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/compiler/scala/tools/nsc/interpreter/ILoop.scala @@ -249,13 +249,6 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) import global.Symbol def p(x: Any) = intp.reporter.printMessage("" + x) - def toDefString(sym: Symbol) = { - TypeStrings.quieter( - intp.afterTyper(sym.defString), - sym.owner.name + ".this.", - sym.owner.fullName + "." - ) - } // If an argument is given, only show a source with that // in its name somewhere. @@ -300,7 +293,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) p(" /* " + members.size + ownerMessage + owner.fullName + " */") memberGroups foreach { group => - group foreach (s => p(" " + toDefString(s))) + group foreach (s => p(" " + intp.symbolDefString(s))) p("") } } @@ -392,6 +385,12 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) else powerCommands ) + val replayQuestionMessage = + """|The repl compiler has crashed spectacularly. Shall I replay your + |session? I can re-run all lines except the last one. + |[y/n] + """.trim.stripMargin + private val crashRecovery: PartialFunction[Throwable, Unit] = { case ex: Throwable => if (settings.YrichExes.value) { @@ -406,9 +405,19 @@ class ILoop(in0: Option[BufferedReader], protected val out: PrintWriter) else { out.println(util.stackTraceString(ex)) } - out.println("Attempting session recovery...") - - replay() + ex match { + case _: NoSuchMethodError | _: NoClassDefFoundError => + out.println("Unrecoverable error.") + throw ex + case _ => + out.print(replayQuestionMessage) + out.flush() + if (in.readAssumingNo("")) { + out.println("\nAttempting session recovery with replay.") + replay() + } + else out.println("\nAbandoning crashed session.") + } } /** The main read-eval-print loop for the repl. It calls diff --git a/src/compiler/scala/tools/nsc/interpreter/IMain.scala b/src/compiler/scala/tools/nsc/interpreter/IMain.scala index c3cc0f9e03..33dcfa59e6 100644 --- a/src/compiler/scala/tools/nsc/interpreter/IMain.scala +++ b/src/compiler/scala/tools/nsc/interpreter/IMain.scala @@ -83,6 +83,7 @@ class IMain(val settings: Settings, protected val out: PrintWriter) { /** reporter */ lazy val reporter: ConsoleReporter = new IMain.ReplReporter(this) import reporter.{ printMessage, withoutTruncating } + // not sure if we have some motivation to print directly to console private def echo(msg: String) { Console println msg } @@ -1088,6 +1089,14 @@ class IMain(val settings: Settings, protected val out: PrintWriter) { finally isettings.unwrapStrings = saved } + def symbolDefString(sym: Symbol) = { + TypeStrings.quieter( + afterTyper(sym.defString), + sym.owner.name + ".this.", + sym.owner.fullName + "." + ) + } + def showCodeIfDebugging(code: String) { /** Secret bookcase entrance for repl debuggers: end the line * with "// show" and see what's going on. @@ -1110,6 +1119,14 @@ class IMain(val settings: Settings, protected val out: PrintWriter) { /** Utility methods for the Interpreter. */ object IMain { + // The two name forms this is catching are the two sides of this assignment: + // + // $line3.$read.$iw.$iw.Bippy = + // $line3.$read$$iw$$iw$Bippy@4a6a00ca + private def removeLineWrapper(s: String) = s.replaceAll("""\$line\d+[./]\$(read|eval|print)[$.]""", "") + private def removeIWPackages(s: String) = s.replaceAll("""\$(iw|read|eval|print)[$.]""", "") + def stripString(s: String) = removeIWPackages(removeLineWrapper(s)) + trait CodeAssembler[T] { def preamble: String def generate: T => String @@ -1152,7 +1169,7 @@ object IMain { def isTruncating = reporter.truncationOK def stripImpl(str: String): String = { - val cleaned = removeIWPackages(removeLineWrapper(str)) + val cleaned = stripString(str) var ctrlChars = 0 cleaned map { ch => if (ch.isControl && !ch.isWhitespace) { @@ -1163,13 +1180,6 @@ object IMain { else ch } } - - // The two name forms this is catching are the two sides of this assignment: - // - // $line3.$read.$iw.$iw.Bippy = - // $line3.$read$$iw$$iw$Bippy@4a6a00ca - private def removeLineWrapper(s: String) = s.replaceAll("""\$line\d+[./]\$(read|eval|print)[$.]""", "") - private def removeIWPackages(s: String) = s.replaceAll("""\$(iw|read|eval|print)[$.]""", "") } class ReplReporter(intp: IMain) extends ConsoleReporter(intp.settings, null, new ReplStrippingWriter(intp)) { diff --git a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala index 782fd52ab2..1430cbe55c 100644 --- a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -17,16 +17,25 @@ import Properties.isMac trait InteractiveReader { val interactive: Boolean + def init(): Unit + def reset(): Unit + def history: History def completion: Completion def keyBindings: List[KeyBinding] + def eraseLine(): Unit + def redrawLine(): Unit + def currentLine: String - def init(): Unit - def reset(): Unit + def readYesOrNo(prompt: String) = readOneKey(prompt) match { + case 'y' => true + case 'n' => false + } + def readAssumingNo(prompt: String) = try readYesOrNo(prompt) catch { case _: MatchError => false } + def readAssumingYes(prompt: String) = try readYesOrNo(prompt) catch { case _: MatchError => true } protected def readOneLine(prompt: String): String - def redrawLine(): Unit - def currentLine: String + protected def readOneKey(prompt: String): Int def readLine(prompt: String): String = // hack necessary for OSX jvm suspension because read calls are not restarted after SIGTSTP diff --git a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala index 9ca10b9e6b..d9260427b3 100644 --- a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala @@ -35,33 +35,41 @@ class JLineReader(val completion: Completion) extends InteractiveReader { } } - def argCompletor: ArgumentCompleter = { - val c = new ArgumentCompleter(new JLineDelimiter, scalaToJline(completion.completer())) - c setStrict false - c - } + class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper { + // working around protected/trait/java insufficiencies. + def goBack(num: Int): Unit = back(num) + def readOneKey(prompt: String) = { + this.print(prompt) + this.flush() + this.readVirtualKey() + } + def eraseLine() = consoleReader.resetPromptLine("", "", 0) + def redrawLineAndFlush(): Unit = { flush() ; drawLine() ; flush() } - val consoleReader = { - val r = new ConsoleReader() - r setBellEnabled false + this setBellEnabled false if (history ne NoHistory) - r setHistory history + this setHistory history if (completion ne NoCompletion) { - r addCompleter argCompletor - r setAutoprintThreshold 400 // max completion candidates without warning - } + val argCompletor: ArgumentCompleter = + new ArgumentCompleter(new JLineDelimiter, scalaToJline(completion.completer())) + argCompletor setStrict false - r + this addCompleter argCompletor + this setAutoprintThreshold 400 // max completion candidates without warning + } } + val consoleReader: JLineConsoleReader = new JLineConsoleReader() + def currentLine: String = consoleReader.getCursorBuffer.buffer.toString - def redrawLine() = { - consoleReader.flush() - consoleReader.drawLine() - consoleReader.flush() + def redrawLine() = consoleReader.redrawLineAndFlush() + def eraseLine() = { + while (consoleReader.delete()) { } + // consoleReader.eraseLine() } def readOneLine(prompt: String) = consoleReader readLine prompt + def readOneKey(prompt: String) = consoleReader readOneKey prompt } object JLineReader { diff --git a/src/compiler/scala/tools/nsc/interpreter/SimpleReader.scala b/src/compiler/scala/tools/nsc/interpreter/SimpleReader.scala index d8181e1793..5249f89dfb 100644 --- a/src/compiler/scala/tools/nsc/interpreter/SimpleReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/SimpleReader.scala @@ -22,6 +22,7 @@ extends InteractiveReader def init() = () def reset() = () + def eraseLine() = () def redrawLine() = () def currentLine = "" def readOneLine(prompt: String): String = { @@ -31,6 +32,7 @@ extends InteractiveReader } in.readLine() } + def readOneKey(prompt: String) = sys.error("No char-based input in SimpleReader") } object SimpleReader { diff --git a/src/compiler/scala/tools/nsc/interpreter/package.scala b/src/compiler/scala/tools/nsc/interpreter/package.scala index 42826e9c6d..e2e6cb6651 100644 --- a/src/compiler/scala/tools/nsc/interpreter/package.scala +++ b/src/compiler/scala/tools/nsc/interpreter/package.scala @@ -23,7 +23,9 @@ package scala.tools.nsc * IMain contains { global: Global } */ package object interpreter { - type JClass = java.lang.Class[_] + type JClass = java.lang.Class[_] + type JList[T] = java.util.List[T] + type JCollection[T] = java.util.Collection[T] private[nsc] val DebugProperty = "scala.repl.debug" private[nsc] val TraceProperty = "scala.repl.trace" @@ -31,6 +33,10 @@ package object interpreter { private[nsc] var isReplDebug = sys.props contains DebugProperty // Also set by -Yrepl-debug private[nsc] implicit def enrichClass[T](clazz: Class[T]) = new RichClass[T](clazz) + private[interpreter] implicit def javaCharSeqCollectionToScala(xs: JCollection[_ <: CharSequence]): List[String] = { + import collection.JavaConverters._ + xs.asScala.toList map ("" + _) + } /** Debug output */ private[nsc] def repldbg(msg: String) = if (isReplDebug) Console println msg @@ -43,6 +49,16 @@ package object interpreter { x } + // 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) diff --git a/src/compiler/scala/tools/nsc/interpreter/session/package.scala b/src/compiler/scala/tools/nsc/interpreter/session/package.scala index e6ff2c3e03..8fbba2f05e 100644 --- a/src/compiler/scala/tools/nsc/interpreter/session/package.scala +++ b/src/compiler/scala/tools/nsc/interpreter/session/package.scala @@ -12,7 +12,6 @@ package interpreter package object session { type JIterator[T] = java.util.Iterator[T] type JListIterator[T] = java.util.ListIterator[T] - type JList[T] = java.util.List[T] type JEntry = scala.tools.jline.console.history.History.Entry type JHistory = scala.tools.jline.console.history.History diff --git a/src/jline/project/build.properties b/src/jline/project/build.properties index c6143fe8df..3ecffcd808 100644 --- a/src/jline/project/build.properties +++ b/src/jline/project/build.properties @@ -3,6 +3,6 @@ project.organization=org.improving project.name=jline sbt.version=0.7.5.RC0 project.version=0.98 -#build.scala.versions=2.8.1 -build.scala.versions=2.9.0-SNAPSHOT +build.scala.versions=2.8.1 +/*build.scala.versions=2.9.0-SNAPSHOT*/ project.initialize=false diff --git a/src/jline/project/plugins/project/build.properties b/src/jline/project/plugins/project/build.properties index f39984bd73..0b7014c531 100644 --- a/src/jline/project/plugins/project/build.properties +++ b/src/jline/project/plugins/project/build.properties @@ -1,3 +1,3 @@ #Project properties -#Fri Jan 21 08:49:59 PST 2011 +#Thu Feb 10 14:58:03 PST 2011 plugin.uptodate=true diff --git a/src/jline/src/main/java/scala/tools/jline/WindowsTerminal.java b/src/jline/src/main/java/scala/tools/jline/WindowsTerminal.java index 66d2c69f8d..4c70155f59 100644 --- a/src/jline/src/main/java/scala/tools/jline/WindowsTerminal.java +++ b/src/jline/src/main/java/scala/tools/jline/WindowsTerminal.java @@ -196,6 +196,8 @@ public class WindowsTerminal if (indicator == SPECIAL_KEY_INDICATOR.code || indicator == NUMPAD_KEY_INDICATOR.code) { int c = readCharacter(in); WindowsKey key = WindowsKey.valueOf(c); + if (key == null) + return 0; switch (key) { case UP_ARROW_KEY: diff --git a/src/jline/src/main/java/scala/tools/jline/console/ConsoleReader.java b/src/jline/src/main/java/scala/tools/jline/console/ConsoleReader.java index bff0a10648..861c2d58bd 100644 --- a/src/jline/src/main/java/scala/tools/jline/console/ConsoleReader.java +++ b/src/jline/src/main/java/scala/tools/jline/console/ConsoleReader.java @@ -219,7 +219,7 @@ public class ConsoleReader * * @return false if we failed (e.g., the buffer was empty) */ - final boolean resetLine() throws IOException { + protected final boolean resetLine() throws IOException { if (buf.cursor == 0) { return false; } @@ -378,7 +378,7 @@ public class ConsoleReader * @param str * @return */ - final String expandEvents(String str) throws IOException { + protected String expandEvents(String str) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); @@ -650,7 +650,7 @@ public class ConsoleReader /** * Move the visual cursor backwards without modifying the buffer cursor. */ - private void back(final int num) throws IOException { + protected void back(final int num) throws IOException { if (num == 0) return; if (terminal.isAnsiSupported()) { int width = getTerminal().getWidth(); @@ -736,7 +736,7 @@ public class ConsoleReader return backspace(1) == 1; } - private boolean moveToEnd() throws IOException { + protected boolean moveToEnd() throws IOException { return moveCursor(buf.length() - buf.cursor) > 0; } @@ -1502,7 +1502,7 @@ public class ConsoleReader * * @return true if successful */ - private boolean complete() throws IOException { + protected boolean complete() throws IOException { // debug ("tab for (" + buf + ")"); if (completers.size() == 0) { return false; -- cgit v1.2.3