Skip to content

Commit

Permalink
SI-8308 Fix REPL tab completion after suspend/resume
Browse files Browse the repository at this point in the history
Implementation borrowed from SBT. For best results, we need to use
`sun.misc.SignalHandler` to repair JLine immediately after SIGCONT.
If that isn't avaialable, we avoid crashing, and get the terminal
working after the next newline is entered.

The signal handler registration can be disabled with
`-Dscala.repl.disable.cont`.

Access to `sun.misc` is performed via reflection.

Tested manually on MacOS X: http://recordit.co/mjOTxvu84K
  • Loading branch information
retronym committed Mar 16, 2016
1 parent fd7a159 commit bfdee84
Showing 1 changed file with 79 additions and 5 deletions.
84 changes: 79 additions & 5 deletions src/repl-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** NSC -- new Scala compiler
*
* Copyright 2005-2015 LAMP/EPFL
*
* @author Stepan Koltsov
* @author Adriaan Moors
*/
Expand Down Expand Up @@ -33,7 +34,7 @@ class InteractiveReader(completer: () => Completion) extends interpreter.Interac
private val consoleReader = {
val reader = new JLineConsoleReader()

reader setPaginationEnabled interpreter.`package`.isPaged
reader setPaginationEnabled interpreter.isPaged

// ASAP
reader setExpandEvents false
Expand All @@ -53,14 +54,41 @@ class InteractiveReader(completer: () => Completion) extends interpreter.Interac
}

def reset() = consoleReader.getTerminal().reset()
def redrawLine() = consoleReader.redrawLineAndFlush()
def readOneLine(prompt: String) = consoleReader.readLine(prompt)
def readOneKey(prompt: String) = consoleReader.readOneKey(prompt)
def redrawLine() = withReader(_.redrawLineAndFlush())
def readOneLine(prompt: String) = withReader(_.readLine(prompt))
def readOneKey(prompt: String) = withReader(_.readOneKey(prompt))
private def withReader[T](f: JLineConsoleReader => T): T = {
// SI-8308 Get JLine working after suspend/resume
//
// Borrowed this logic from SBT.
// My testing on MacOS X suggests that only the call to `resume` in the
// signal handler for SIGCONT is needed to make things work. The line is redrawn
// and tab completion works immediately.
//
// However, the `init` / `restore` pair will get JLine back up and running on
// systems that don't support signal handlers (sun.misc.SignalHandler isn't portable)
// so it seems to serve as a useful backstop. I'm not sure whether or not it serves
// some other purpose.
consoleReader.getTerminal.init()
try {
Signalling().withSigContHandler(() => resume()){
f(consoleReader)
}
} finally {
consoleReader.getTerminal.restore()
}
}

private def resume() {
jline.TerminalFactory.reset()
redrawLine()
consoleReader.getTerminal.init()
}
}

// implements a jline interface
private class JLineConsoleReader extends jconsole.ConsoleReader with interpreter.VariColumnTabulator {
val isAcross = interpreter.`package`.isAcross
val isAcross = interpreter.isAcross
val marginSize = 3

def width = getTerminal.getWidth()
Expand Down Expand Up @@ -167,3 +195,49 @@ private class JLineConsoleReader extends jconsole.ConsoleReader with interpreter
setAutoprintThreshold(400) // max completion candidates without warning
}
}

trait Signalling {
def withSigContHandler[T](handler: () => Unit)(action: => T): T
}
object Signalling {
def apply(): Signalling = instance

private lazy val instance: Signalling =
if (DisableSigCont) NoSignalling
else try { new SunMiscSignalViaReflection } catch { case _: Throwable => NoSignalling}

// Disable unconditionally on J9 as it prints a stacktrace to console (e.g. https://github.com/sbt/sbt/issues/1027)
private val DisableSigCont = sys.props.contains("scala.repl.disable.cont") || (sys.props("java.vm.name") == "IBM J9 VM")
private object NoSignalling extends Signalling {
override def withSigContHandler[T](handler: () => Unit)(action: => T): T = action
}
private class SunMiscSignalViaReflection extends Signalling {
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.{Method, Proxy, InvocationHandler, Constructor}

def withSigContHandler[T](handler: () => Unit)(action: => T): T = {
val newHandler = newSignalHandler(handler)
val oldHandler = Signal_handle(sigCont, newHandler)
try action finally Signal_handle(sigCont, oldHandler)
}

@inline private def unwrapITE[T](f: => T): T =
try { f } catch { case ite: InvocationTargetException => throw ite.getTargetException }
val sun_misc_Signal: Class[_] = Class.forName("sun.misc.Signal")
val sun_misc_SignalHandler: Class[_] = Class.forName("sun.misc.SignalHandler")
val sun_misc_Signal_handle: Method = sun_misc_Signal.getDeclaredMethod("handle", sun_misc_Signal, sun_misc_SignalHandler)
val sun_misc_Signal_init: Constructor[_] = sun_misc_Signal.getConstructor(classOf[String])
val sigCont = newSignal("CONT") // create the signal here (within a try/catch) to avoid crashing on windows.

def newSignal(s: String): AnyRef = unwrapITE(sun_misc_Signal_init.newInstance(s).asInstanceOf[AnyRef])
def newSignalHandler(s: () => Unit): AnyRef = {
val ih = new InvocationHandler {
override def invoke(proxy: scala.Any, method: Method, args: Array[AnyRef]): AnyRef = {s(); null}
}
Proxy.newProxyInstance(getClass.getClassLoader, Array(sun_misc_SignalHandler), ih)
}
def Signal_handle(signal: AnyRef, handler: AnyRef): AnyRef = unwrapITE {
sun_misc_Signal_handle.invoke(null, signal, handler)
}
}
}

0 comments on commit bfdee84

Please sign in to comment.