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.

git-svn-id: http://lampsvn.epfl.ch/svn-repos/scala/scala/trunk@24266 5e8d7ff9-d8ef-0310-90f0-a4852d11357a
This commit is contained in:
extempore 2011-02-11 04:00:46 +00:00
parent bc11cc8b02
commit 572ff48e9b
13 changed files with 174 additions and 53 deletions

Binary file not shown.

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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)) {

View File

@ -17,17 +17,26 @@ import Properties.isMac
trait InteractiveReader {
val interactive: Boolean
def history: History
def completion: Completion
def keyBindings: List[KeyBinding]
def init(): Unit
def reset(): Unit
protected def readOneLine(prompt: String): String
def history: History
def completion: Completion
def keyBindings: List[KeyBinding]
def eraseLine(): Unit
def redrawLine(): Unit
def currentLine: String
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
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
if (isMac) restartSysCalls(readOneLine(prompt), reset())

View File

@ -34,34 +34,42 @@ class JLineReader(val completion: Completion) extends InteractiveReader {
newCursor
}
}
def argCompletor: ArgumentCompleter = {
val c = new ArgumentCompleter(new JLineDelimiter, scalaToJline(completion.completer()))
c setStrict false
c
}
val consoleReader = {
val r = new ConsoleReader()
r setBellEnabled false
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() }
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
this addCompleter argCompletor
this setAutoprintThreshold 400 // max completion candidates without warning
}
r
}
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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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;