summaryrefslogtreecommitdiff
path: root/src/repl
diff options
context:
space:
mode:
authorAdriaan Moors <adriaan.moors@typesafe.com>2015-06-17 11:06:31 -0700
committerAdriaan Moors <adriaan.moors@typesafe.com>2015-06-18 10:33:33 -0700
commitdfb70b632c1e8a2c6ce27eaacb74dbbb47ce9532 (patch)
tree277124732e329eb78b6168ad8ad3a6c908fb407f /src/repl
parent43139faa4f4348b95907e06883f2fefb41ea3a3b (diff)
downloadscala-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')
-rw-r--r--src/repl/scala/tools/nsc/interpreter/ILoop.scala40
-rw-r--r--src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala93
-rw-r--r--src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala25
-rw-r--r--src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala77
-rw-r--r--src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala143
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
- }
- }
-}