From dfb70b632c1e8a2c6ce27eaacb74dbbb47ce9532 Mon Sep 17 00:00:00 2001 From: Adriaan Moors Date: Wed, 17 Jun 2015 11:06:31 -0700 Subject: 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. --- .../nsc/interpreter/jline/FileBackedHistory.scala | 93 ++++++++++++++ .../nsc/interpreter/jline/JLineDelimiter.scala | 25 ++++ .../tools/nsc/interpreter/jline/JLineHistory.scala | 77 +++++++++++ .../tools/nsc/interpreter/jline/JLineReader.scala | 143 +++++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 src/repl-jline/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala create mode 100644 src/repl-jline/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala create mode 100644 src/repl-jline/scala/tools/nsc/interpreter/jline/JLineHistory.scala create mode 100644 src/repl-jline/scala/tools/nsc/interpreter/jline/JLineReader.scala (limited to 'src/repl-jline') 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 + } + } +} -- cgit v1.2.3