summaryrefslogtreecommitdiff
path: root/src/repl-jline
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-jline
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-jline')
-rw-r--r--src/repl-jline/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala93
-rw-r--r--src/repl-jline/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala25
-rw-r--r--src/repl-jline/scala/tools/nsc/interpreter/jline/JLineHistory.scala77
-rw-r--r--src/repl-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala143
4 files changed, 338 insertions, 0 deletions
diff --git a/src/repl-jline/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala b/src/repl-jline/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala
new file mode 100644
index 0000000000..b6c9792ec0
--- /dev/null
+++ b/src/repl-jline/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala
@@ -0,0 +1,93 @@
+/* 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-jline/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala b/src/repl-jline/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala
new file mode 100644
index 0000000000..c18a9809a0
--- /dev/null
+++ b/src/repl-jline/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala
@@ -0,0 +1,25 @@
+/* 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-jline/scala/tools/nsc/interpreter/jline/JLineHistory.scala b/src/repl-jline/scala/tools/nsc/interpreter/jline/JLineHistory.scala
new file mode 100644
index 0000000000..1f6a1f7022
--- /dev/null
+++ b/src/repl-jline/scala/tools/nsc/interpreter/jline/JLineHistory.scala
@@ -0,0 +1,77 @@
+/* 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-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala b/src/repl-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala
new file mode 100644
index 0000000000..f0fce13fe8
--- /dev/null
+++ b/src/repl-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala
@@ -0,0 +1,143 @@
+/** 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 InteractiveReader(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
+ }
+ }
+}