diff options
author | Adriaan Moors <adriaan.moors@typesafe.com> | 2015-06-17 11:06:31 -0700 |
---|---|---|
committer | Adriaan Moors <adriaan.moors@typesafe.com> | 2015-06-18 10:33:33 -0700 |
commit | dfb70b632c1e8a2c6ce27eaacb74dbbb47ce9532 (patch) | |
tree | 277124732e329eb78b6168ad8ad3a6c908fb407f /src/repl | |
parent | 43139faa4f4348b95907e06883f2fefb41ea3a3b (diff) | |
download | scala-dfb70b632c1e8a2c6ce27eaacb74dbbb47ce9532.tar.gz scala-dfb70b632c1e8a2c6ce27eaacb74dbbb47ce9532.tar.bz2 scala-dfb70b632c1e8a2c6ce27eaacb74dbbb47ce9532.zip |
SI-9339 Support classpaths with no single compatible jline
As usual, the repl will use whatever jline 2 jar on the classpath,
if there is one. Failing that, there's a fallback and an override.
If instantiating the standard `jline.InteractiveReader` fails,
we fall back to an embedded, shaded, version of jline,
provided by `jline_embedded.InteractiveReader`.
(Assume `import scala.tools.nsc.interpreter._` for this message.)
The instantiation of `InteractiveReader` eagerly exercises jline,
so that a linkage error will result if jline is missing or if the
provided one is not binary compatible.
The property `scala.repl.reader` overrides this behavior, if set to
the FQN of a class that looks like `YourInteractiveReader` below.
```
class YourInteractiveReader(completer: () => Completion) extends InteractiveReader
```
The repl logs which classes it tried to instantiate under `-Ydebug`.
# Changes to source & build
The core of the repl (`src/repl`) no longer depends on jline.
The jline interface is now in `src/repl-jline`.
The embedded jline + our interface to it are generated by the `quick.repl` target.
The build now also enforces that only `src/repl-jline` depends on jline.
The sources in `src/repl` are now sure to be independent of it,
though they do use reflection to instantiate a suitable subclass
of `InteractiveReader`, as explained above.
The `quick.repl` target builds the sources in `src/repl` and `src/repl-jline`,
producing a jar for the `repl-jline` classes, which is then transformed using
jarjar to obtain a shaded copy of the `scala.tools.nsc.interpreter.jline` package.
Jarjar is used to combine the `jline` jar and the `repl-jline` into a new jar,
rewriting package names as follows:
- `org.fusesource` -> `scala.tools.fusesource_embedded`
- `jline` -> `scala.tools.jline_embedded`
- `scala.tools.nsc.interpreter.jline` -> `scala.tools.nsc.interpreter.jline_embedded`
Classes not reachable from `scala.tools.**` are pruned, as well as empty dirs.
The classes in the `repl-jline` jar as well as those in the rewritten one
are copied to the repl's output directory.
PS: The sbt build is not updated, sorry.
PPS: A more recent fork of jarjar: https://github.com/shevek/jarjar.
Diffstat (limited to 'src/repl')
5 files changed, 30 insertions, 348 deletions
diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala index 3ce9668b97..a3047ccc8e 100644 --- a/src/repl/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -26,6 +26,8 @@ import scala.concurrent.{ ExecutionContext, Await, Future, future } import ExecutionContext.Implicits._ import java.io.{ BufferedReader, FileReader } +import scala.util.{Try, Success, Failure} + /** The Scala interactive shell. It provides a read-eval-print loop * around the Interpreter class. * After instantiation, clients should call the main() method. @@ -860,18 +862,36 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) * with SimpleReader. */ def chooseReader(settings: Settings): InteractiveReader = { - def mkJLineReader(completer: () => Completion): InteractiveReader = - try new jline.JLineReader(completer) - catch { - case ex@(_: Exception | _: NoClassDefFoundError) => - Console.println(f"Failed to created JLineReader: ${ex}%nFalling back to SimpleReader.") - SimpleReader() - } - if (settings.Xnojline || Properties.isEmacsShell) SimpleReader() else { - if (settings.noCompletion) mkJLineReader(() => NoCompletion) - else mkJLineReader(() => new JLineCompletion(intp)) + type Completer = () => Completion + type ReaderMaker = Completer => InteractiveReader + + def instantiate(className: String): ReaderMaker = completer => { + if (settings.debug) Console.println(s"Trying to instantiate a InteractiveReader from $className") + Class.forName(className).getConstructor(classOf[Completer]). + newInstance(completer). + asInstanceOf[InteractiveReader] + } + + def mkReader(maker: ReaderMaker) = + if (settings.noCompletion) maker(() => NoCompletion) + else maker(() => new JLineCompletion(intp)) // JLineCompletion is a misnomer -- it's not tied to jline + + def internalClass(kind: String) = s"scala.tools.nsc.interpreter.$kind.InteractiveReader" + val readerClasses = sys.props.get("scala.repl.reader").toStream ++ Stream(internalClass("jline"), internalClass("jline_embedded")) + val readers = readerClasses map (cls => Try { mkReader(instantiate(cls)) }) + + val reader = (readers collect { case Success(reader) => reader } headOption) getOrElse SimpleReader() + + if (settings.debug) { + val readerDiags = (readerClasses, readers).zipped map { + case (cls, Failure(e)) => s" - $cls --> " + e.getStackTrace.mkString(e.toString+"\n\t", "\n\t","\n") + case (cls, Success(_)) => s" - $cls OK" + } + Console.println(s"All InteractiveReaders tried: ${readerDiags.mkString("\n","\n","\n")}") + } + reader } } diff --git a/src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala b/src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala deleted file mode 100644 index b6c9792ec0..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala +++ /dev/null @@ -1,93 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2015 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc.interpreter.jline - -import _root_.jline.console.history.PersistentHistory - - -import scala.tools.nsc.interpreter -import scala.tools.nsc.io.{File, Path} - -/** TODO: file locking. - */ -trait FileBackedHistory extends JLineHistory with PersistentHistory { - def maxSize: Int - - protected lazy val historyFile: File = FileBackedHistory.defaultFile - private var isPersistent = true - - locally { - load() - } - - def withoutSaving[T](op: => T): T = { - val saved = isPersistent - isPersistent = false - try op - finally isPersistent = saved - } - - def addLineToFile(item: CharSequence): Unit = { - if (isPersistent) - append(item + "\n") - } - - /** Overwrites the history file with the current memory. */ - protected def sync(): Unit = { - val lines = asStrings map (_ + "\n") - historyFile.writeAll(lines: _*) - } - - /** Append one or more lines to the history file. */ - protected def append(lines: String*): Unit = { - historyFile.appendAll(lines: _*) - } - - def load(): Unit = { - if (!historyFile.canRead) - historyFile.createFile() - - val lines: IndexedSeq[String] = { - try historyFile.lines().toIndexedSeq - catch { - // It seems that control characters in the history file combined - // with the default codec can lead to nio spewing exceptions. Rather - // than abandon hope we'll try to read it as ISO-8859-1 - case _: Exception => - try historyFile.lines("ISO-8859-1").toIndexedSeq - catch { - case _: Exception => Vector() - } - } - } - - interpreter.repldbg("Loading " + lines.size + " into history.") - - // avoid writing to the history file - withoutSaving(lines takeRight maxSize foreach add) - // truncate the history file if it's too big. - if (lines.size > maxSize) { - interpreter.repldbg("File exceeds maximum size: truncating to " + maxSize + " entries.") - sync() - } - moveToEnd() - } - - def flush(): Unit = () - - def purge(): Unit = historyFile.truncate() -} - -object FileBackedHistory { - // val ContinuationChar = '\003' - // val ContinuationNL: String = Array('\003', '\n').mkString - - import scala.tools.nsc.Properties.userHome - - def defaultFileName = ".scala_history" - - def defaultFile: File = File(Path(userHome) / defaultFileName) -} diff --git a/src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala b/src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala deleted file mode 100644 index c18a9809a0..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala +++ /dev/null @@ -1,25 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc.interpreter.jline - -import scala.tools.nsc.interpreter - -import _root_.jline.console.completer.ArgumentCompleter.{ ArgumentDelimiter, ArgumentList } - -// implements a jline interface -class JLineDelimiter extends ArgumentDelimiter { - def toJLine(args: List[String], cursor: Int) = args match { - case Nil => new ArgumentList(new Array[String](0), 0, 0, cursor) - case xs => new ArgumentList(xs.toArray, xs.size - 1, xs.last.length, cursor) - } - - def delimit(buffer: CharSequence, cursor: Int) = { - val p = interpreter.Parsed(buffer.toString, cursor) - toJLine(p.args, cursor) - } - - def isDelimiter(buffer: CharSequence, cursor: Int) = interpreter.Parsed(buffer.toString, cursor).isDelimiter -} diff --git a/src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala b/src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala deleted file mode 100644 index 1f6a1f7022..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala +++ /dev/null @@ -1,77 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc.interpreter.jline - -import java.util.{Iterator => JIterator, ListIterator => JListIterator} - -import _root_.jline.{console => jconsole} -import jconsole.history.History.{Entry => JEntry} -import jconsole.history.{History => JHistory} - -import scala.tools.nsc.interpreter -import scala.tools.nsc.interpreter.session.{History, SimpleHistory} - - -/** A straight scalification of the jline interface which mixes - * in the sparse jline-independent one too. - */ -trait JLineHistory extends JHistory with History { - def size: Int - def isEmpty: Boolean - def index: Int - def clear(): Unit - def get(index: Int): CharSequence - def add(line: CharSequence): Unit - def replace(item: CharSequence): Unit - - def entries(index: Int): JListIterator[JEntry] - def entries(): JListIterator[JEntry] - def iterator: JIterator[JEntry] - - def current(): CharSequence - def previous(): Boolean - def next(): Boolean - def moveToFirst(): Boolean - def moveToLast(): Boolean - def moveTo(index: Int): Boolean - def moveToEnd(): Unit - - override def historicize(text: String): Boolean = { - text.lines foreach add - moveToEnd() - true - } -} - -object JLineHistory { - class JLineFileHistory extends SimpleHistory with FileBackedHistory { - override def add(item: CharSequence): Unit = { - if (!isEmpty && last == item) - interpreter.repldbg("Ignoring duplicate entry '" + item + "'") - else { - super.add(item) - addLineToFile(item) - } - } - override def toString = "History(size = " + size + ", index = " + index + ")" - - import scala.collection.JavaConverters._ - - override def asStrings(from: Int, to: Int): List[String] = - entries(from).asScala.take(to - from).map(_.value.toString).toList - - case class Entry(index: Int, value: CharSequence) extends JEntry { - override def toString = value.toString - } - - private def toEntries(): Seq[JEntry] = buf.zipWithIndex map { case (x, i) => Entry(i, x)} - def entries(idx: Int): JListIterator[JEntry] = toEntries().asJava.listIterator(idx) - def entries(): JListIterator[JEntry] = toEntries().asJava.listIterator() - def iterator: JIterator[JEntry] = toEntries().iterator.asJava - } - - def apply(): History = try new JLineFileHistory catch { case x: Exception => new SimpleHistory() } -} diff --git a/src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala b/src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala deleted file mode 100644 index 414868a7e5..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala +++ /dev/null @@ -1,143 +0,0 @@ -/** NSC -- new Scala compiler - * - * Copyright 2005-2015 LAMP/EPFL - * @author Stepan Koltsov - * @author Adriaan Moors - */ - -package scala.tools.nsc.interpreter.jline - -import java.util.{Collection => JCollection, List => JList} - -import _root_.jline.{console => jconsole} -import jconsole.completer.{Completer, ArgumentCompleter} -import jconsole.history.{History => JHistory} - - -import scala.tools.nsc.interpreter -import scala.tools.nsc.interpreter.Completion -import scala.tools.nsc.interpreter.Completion.Candidates -import scala.tools.nsc.interpreter.session.History - -/** - * Reads from the console using JLine. - * - * Eagerly instantiates all relevant JLine classes, so that we can detect linkage errors on `new JLineReader` and retry. - */ -class JLineReader(completer: () => Completion) extends interpreter.InteractiveReader { - val interactive = true - - val history: History = new JLineHistory.JLineFileHistory() - - private val consoleReader = { - val reader = new JLineConsoleReader() - - reader setPaginationEnabled interpreter.`package`.isPaged - - // ASAP - reader setExpandEvents false - - reader setHistory history.asInstanceOf[JHistory] - - reader - } - - private[this] var _completion: Completion = interpreter.NoCompletion - def completion: Completion = _completion - - override def postInit() = { - _completion = completer() - - consoleReader.initCompletion(completion) - } - - def reset() = consoleReader.getTerminal().reset() - def redrawLine() = consoleReader.redrawLineAndFlush() - def readOneLine(prompt: String) = consoleReader.readLine(prompt) - def readOneKey(prompt: String) = consoleReader.readOneKey(prompt) -} - -// implements a jline interface -private class JLineConsoleReader extends jconsole.ConsoleReader with interpreter.VariColumnTabulator { - val isAcross = interpreter.`package`.isAcross - val marginSize = 3 - - def width = getTerminal.getWidth() - def height = getTerminal.getHeight() - - 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 = { - import scala.tools.nsc.interpreter.javaCharSeqCollectionToScala - printColumns_(items: List[String]) - } - - private def printColumns_(items: List[String]): Unit = if (items exists (_ != "")) { - val grouped = tabulate(items) - var linesLeft = if (isPaginationEnabled()) height - 1 else Int.MaxValue - grouped foreach { xs => - println(xs.mkString) - linesLeft -= 1 - if (linesLeft <= 0) { - linesLeft = emulateMore() - if (linesLeft < 0) - return - } - } - } - - def readOneKey(prompt: String) = { - this.print(prompt) - this.flush() - this.readCharacter() - } - - def eraseLine() = resetPromptLine("", "", 0) - - def redrawLineAndFlush(): Unit = { - flush(); drawLine(); flush() - } - - // A hook for running code after the repl is done initializing. - def initCompletion(completion: Completion): Unit = { - this setBellEnabled false - - if (completion ne interpreter.NoCompletion) { - val jlineCompleter = new ArgumentCompleter(new JLineDelimiter, - new Completer { - val tc = completion.completer() - def complete(_buf: String, cursor: Int, candidates: JList[CharSequence]): Int = { - val buf = if (_buf == null) "" else _buf - val Candidates(newCursor, newCandidates) = tc.complete(buf, cursor) - newCandidates foreach (candidates add _) - newCursor - } - } - ) - - jlineCompleter setStrict false - - this addCompleter jlineCompleter - this setAutoprintThreshold 400 // max completion candidates without warning - } - } -} |