From 43139faa4f4348b95907e06883f2fefb41ea3a3b Mon Sep 17 00:00:00 2001 From: Adriaan Moors Date: Fri, 12 Jun 2015 15:19:00 +0200 Subject: Centralize dependencies on jline Code that depends on jline is now in package `scala.tools.nsc.interpreter.jline`. To make this possible, remove the `entries` functionality from `History`, and add the `historicize` method. Also provide an overload for `asStrings`. Clean up a little along the way in `JLineHistory.scala` and `JLineReader.scala`. Next step: fall back to an embedded jline when the expected jline jar is not on the classpath. The gist of the refactor: https://gist.github.com/adriaanm/02e110d4da0a585480c1 --- .../nsc/interpreter/ConsoleReaderHelper.scala | 165 --------------------- .../scala/tools/nsc/interpreter/Delimited.scala | 41 ----- src/repl/scala/tools/nsc/interpreter/ILoop.scala | 41 +++-- .../tools/nsc/interpreter/InteractiveReader.scala | 2 + .../tools/nsc/interpreter/JLineCompletion.scala | 1 + .../scala/tools/nsc/interpreter/JLineReader.scala | 75 ---------- src/repl/scala/tools/nsc/interpreter/Parsed.scala | 19 +++ .../scala/tools/nsc/interpreter/Tabulators.scala | 112 ++++++++++++++ .../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 ++++++++++++++++++ .../interpreter/session/FileBackedHistory.scala | 84 ----------- .../tools/nsc/interpreter/session/History.scala | 3 + .../nsc/interpreter/session/JLineHistory.scala | 49 ------ .../nsc/interpreter/session/SimpleHistory.scala | 12 +- .../tools/nsc/interpreter/session/package.scala | 5 - 17 files changed, 494 insertions(+), 453 deletions(-) delete mode 100644 src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/Delimited.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/JLineReader.scala create mode 100644 src/repl/scala/tools/nsc/interpreter/Tabulators.scala create mode 100644 src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala create mode 100644 src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala create mode 100644 src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala create mode 100644 src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala (limited to 'src') diff --git a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala deleted file mode 100644 index a8d537e314..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala +++ /dev/null @@ -1,165 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc -package interpreter - -import jline.console.{ ConsoleReader, CursorBuffer } - -trait ConsoleReaderHelper { _: ConsoleReader with Tabulator => - def isAcross: Boolean - - def terminal = getTerminal() - def width = terminal.getWidth() - def height = terminal.getHeight() - - def readOneKey(prompt: String): Int - def eraseLine(): Unit - - val marginSize = 3 - - 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 = - 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 - } - } - } -} - -trait Tabulator { - def isAcross: Boolean - def width: Int - def marginSize: Int - - protected def fits(items: Seq[String], width: Int): Boolean = ( - (items map (_.length)).sum + (items.length - 1) * marginSize < width - ) - def tabulate(items: Seq[String]): Seq[Seq[String]] = ( - if (fits(items, width)) Seq(Seq(items mkString " " * marginSize)) - else printMultiLineColumns(items) - ) - protected def columnize(ss: Seq[String]): Seq[Seq[String]] = ss map (s => Seq(s)) - protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = { - import SimpleMath._ - val longest = (items map (_.length)).max - val columnWidth = longest + marginSize - val maxcols = ( - if (columnWidth >= width) 1 - else 1 max (width / columnWidth) // make sure it doesn't divide to 0 - ) - val nrows = items.size /% maxcols - val ncols = items.size /% nrows - val groupSize = ncols - val padded = items map (s"%-${columnWidth}s" format _) - val xwise = isAcross || ncols >= items.length - val grouped: Seq[Seq[String]] = - if (groupSize == 1) columnize(items) - else if (xwise) (padded grouped groupSize).toSeq - else { - val h = 1 max padded.size /% groupSize - val cols = (padded grouped h).toList - for (i <- 0 until h) yield - for (j <- 0 until groupSize) yield - if (i < cols(j).size) cols(j)(i) else "" - } - grouped - } -} - -/** Adjust the column width and number of columns to minimize the row count. */ -trait VariColumnTabulator extends Tabulator { - override protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = { - import SimpleMath._ - val longest = (items map (_.length)).max - val shortest = (items map (_.length)).min - val fattest = longest + marginSize - val skinny = shortest + marginSize - - // given ncols, calculate nrows and a list of column widths, or none if not possible - // if ncols > items.size, then columnWidths.size == items.size - def layout(ncols: Int): Option[(Int, Seq[Int], Seq[Seq[String]])] = { - val nrows = items.size /% ncols - val xwise = isAcross || ncols >= items.length - // max width item in each column - def maxima(rows: Seq[Seq[String]]) = - (0 until (ncols min items.size)) map { col => - val widths = for (r <- rows if r.size > col) yield r(col).length - widths.max - } - def resulting(rows: Seq[Seq[String]]) = { - val columnWidths = maxima(rows) map (_ + marginSize) - val linelen = columnWidths.sum - if (linelen <= width) Some((nrows, columnWidths, rows)) - else None - } - if (ncols == 1) resulting(columnize(items)) - else if (xwise) resulting((items grouped ncols).toSeq) - else { - val cols = (items grouped nrows).toList - val rows = - for (i <- 0 until nrows) yield - for (j <- 0 until ncols) yield - if (j < cols.size && i < cols(j).size) cols(j)(i) else "" - resulting(rows) - } - } - - if (fattest >= width) { - columnize(items) - } else { - // if every col is widest, we have at least this many cols - val mincols = 1 max (width / fattest) - // if every other col is skinniest, we have at most this many cols - val maxcols = 1 + ((width - fattest) / skinny) - val possibles = (mincols to maxcols).map(n => layout(n)).flatten - val minrows = (possibles map (_._1)).min - - // select the min ncols that results in minrows - val (_, columnWidths, sss) = (possibles find (_._1 == minrows)).get - - // format to column width - sss map (ss => ss.zipWithIndex map { - case (s, i) => s"%-${columnWidths(i)}s" format s - }) - } - } -} - -private[interpreter] object SimpleMath { - implicit class DivRem(private val i: Int) extends AnyVal { - /** i/n + if (i % n != 0) 1 else 0 */ - def /%(n: Int): Int = (i + n - 1) / n - } -} diff --git a/src/repl/scala/tools/nsc/interpreter/Delimited.scala b/src/repl/scala/tools/nsc/interpreter/Delimited.scala deleted file mode 100644 index b7f06f1d0a..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/Delimited.scala +++ /dev/null @@ -1,41 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc -package interpreter - -import jline.console.completer.ArgumentCompleter.{ ArgumentDelimiter, ArgumentList } - -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 = Parsed(buffer.toString, cursor) - toJLine(p.args, cursor) - } - def isDelimiter(buffer: CharSequence, cursor: Int) = Parsed(buffer.toString, cursor).isDelimiter -} - -trait Delimited { - self: Parsed => - - def delimited: Char => Boolean - def escapeChars: List[Char] = List('\\') - - /** Break String into args based on delimiting function. - */ - protected def toArgs(s: String): List[String] = - if (s == "") Nil - else (s indexWhere isDelimiterChar) match { - case -1 => List(s) - case idx => (s take idx) :: toArgs(s drop (idx + 1)) - } - - def isDelimiterChar(ch: Char) = delimited(ch) - def isEscapeChar(ch: Char): Boolean = escapeChars contains ch -} diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala index 4221126caa..3ce9668b97 100644 --- a/src/repl/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -503,10 +503,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) val errless = intp compileSources new BatchSourceFile("", s"object pastel {\n$code\n}") if (errless) echo("The compiler reports no errors.") } - def historicize(text: String) = history match { - case jlh: JLineHistory => text.lines foreach jlh.add ; jlh.moveToEnd() ; true - case _ => false - } + def edit(text: String): Result = editor match { case Some(ed) => val tmp = File.makeTemp() @@ -522,7 +519,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) val res = intp interpret edited if (res == IR.Incomplete) diagnose(edited) else { - historicize(edited) + history.historicize(edited) Result(lineToRecord = Some(edited), keepRunning = true) } case None => echo("Can't read edited text. Did you delete it?") @@ -533,7 +530,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) tmp.delete() } case None => - if (historicize(text)) echo("Placing text in recent history.") + if (history.historicize(text)) echo("Placing text in recent history.") else echo(f"No EDITOR defined and you can't change history, echoing your text:%n$text") } @@ -565,10 +562,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } import scala.collection.JavaConverters._ val index = (start - 1) max 0 - val text = history match { - case jlh: JLineHistory => jlh.entries(index).asScala.take(len) map (_.value) mkString "\n" - case _ => history.asStrings.slice(index, index + len) mkString "\n" - } + val text = history.asStrings(index, index + len) mkString "\n" edit(text) } catch { case _: NumberFormatException => echo(s"Bad range '$what'") @@ -866,16 +860,18 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) * with SimpleReader. */ def chooseReader(settings: Settings): InteractiveReader = { - if (settings.Xnojline || Properties.isEmacsShell) - SimpleReader() - else try new JLineReader( - if (settings.noCompletion) NoCompletion - else new JLineCompletion(intp) - ) - catch { - case ex @ (_: Exception | _: NoClassDefFoundError) => - echo(f"Failed to created JLineReader: ${ex}%nFalling back to SimpleReader.") - SimpleReader() + 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)) } } @@ -896,10 +892,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) asyncMessage(power.banner) } // SI-7418 Now, and only now, can we enable TAB completion. - in match { - case x: JLineReader => x.consoleReader.postInit - case _ => - } + in.postInit() } // start an interpreter with the given settings diff --git a/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala index ed69d449cb..71753a3e39 100644 --- a/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala +++ b/src/repl/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -13,6 +13,8 @@ import Properties.isMac /** Reads lines from an input stream */ trait InteractiveReader { + def postInit(): Unit = {} + val interactive: Boolean def reset(): Unit diff --git a/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala b/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala index c1122d4223..d878988e26 100644 --- a/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala +++ b/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala @@ -12,6 +12,7 @@ import scala.reflect.internal.util.StringOps.longestCommonPrefix // REPL completor - queries supplied interpreter for valid // completions based on current contents of buffer. +// TODO: change class name to reflect it's not specific to jline (nor does it depend on it) class JLineCompletion(val intp: IMain) extends Completion with CompletionOutput { val global: intp.global.type = intp.global import global._ diff --git a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala deleted file mode 100644 index b6e834a1ed..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala +++ /dev/null @@ -1,75 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Stepan Koltsov - */ - -package scala.tools.nsc -package interpreter - -import jline.console.ConsoleReader -import jline.console.completer._ -import session._ -import Completion._ - -/** - * Reads from the console using JLine. - */ -class JLineReader(_completion: => Completion) extends InteractiveReader { - val interactive = true - val consoleReader = new JLineConsoleReader() - - lazy val completion = _completion - lazy val history: JLineHistory = JLineHistory() - - private def term = consoleReader.getTerminal() - def reset() = term.reset() - - def scalaToJline(tc: ScalaCompleter): Completer = new 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 - } - } - - class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper with VariColumnTabulator { - val isAcross = interpreter.`package`.isAcross - - this setPaginationEnabled interpreter.`package`.isPaged - - // ASAP - this setExpandEvents false - - // working around protected/trait/java insufficiencies. - def goBack(num: Int): Unit = back(num) - if ((history: History) ne NoHistory) - this setHistory history - - def readOneKey(prompt: String) = { - this.print(prompt) - this.flush() - this.readCharacter() - } - def eraseLine() = consoleReader.resetPromptLine("", "", 0) - def redrawLineAndFlush(): Unit = { flush() ; drawLine() ; flush() } - - // A hook for running code after the repl is done initializing. - lazy val postInit: Unit = { - this setBellEnabled false - - if (completion ne NoCompletion) { - val argCompletor: ArgumentCompleter = - new ArgumentCompleter(new JLineDelimiter, scalaToJline(completion.completer())) - argCompletor setStrict false - - this addCompleter argCompletor - this setAutoprintThreshold 400 // max completion candidates without warning - } - } - } - - def redrawLine() = consoleReader.redrawLineAndFlush() - def readOneLine(prompt: String) = consoleReader readLine prompt - def readOneKey(prompt: String) = consoleReader readOneKey prompt -} diff --git a/src/repl/scala/tools/nsc/interpreter/Parsed.scala b/src/repl/scala/tools/nsc/interpreter/Parsed.scala index 672a6fd28f..5e58d3a2c4 100644 --- a/src/repl/scala/tools/nsc/interpreter/Parsed.scala +++ b/src/repl/scala/tools/nsc/interpreter/Parsed.scala @@ -8,6 +8,25 @@ package interpreter import util.returning +trait Delimited { + self: Parsed => + + def delimited: Char => Boolean + def escapeChars: List[Char] = List('\\') + + /** Break String into args based on delimiting function. + */ + protected def toArgs(s: String): List[String] = + if (s == "") Nil + else (s indexWhere isDelimiterChar) match { + case -1 => List(s) + case idx => (s take idx) :: toArgs(s drop (idx + 1)) + } + + def isDelimiterChar(ch: Char) = delimited(ch) + def isEscapeChar(ch: Char): Boolean = escapeChars contains ch +} + /** One instance of a command buffer. */ class Parsed private ( diff --git a/src/repl/scala/tools/nsc/interpreter/Tabulators.scala b/src/repl/scala/tools/nsc/interpreter/Tabulators.scala new file mode 100644 index 0000000000..75bec168eb --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/Tabulators.scala @@ -0,0 +1,112 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc.interpreter + +trait Tabulator { + def isAcross: Boolean + def width: Int + def marginSize: Int + + protected def fits(items: Seq[String], width: Int): Boolean = ( + (items map (_.length)).sum + (items.length - 1) * marginSize < width + ) + def tabulate(items: Seq[String]): Seq[Seq[String]] = ( + if (fits(items, width)) Seq(Seq(items mkString " " * marginSize)) + else printMultiLineColumns(items) + ) + protected def columnize(ss: Seq[String]): Seq[Seq[String]] = ss map (s => Seq(s)) + protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = { + import scala.tools.nsc.interpreter.SimpleMath._ + val longest = (items map (_.length)).max + val columnWidth = longest + marginSize + val maxcols = ( + if (columnWidth >= width) 1 + else 1 max (width / columnWidth) // make sure it doesn't divide to 0 + ) + val nrows = items.size /% maxcols + val ncols = items.size /% nrows + val groupSize = ncols + val padded = items map (s"%-${columnWidth}s" format _) + val xwise = isAcross || ncols >= items.length + val grouped: Seq[Seq[String]] = + if (groupSize == 1) columnize(items) + else if (xwise) (padded grouped groupSize).toSeq + else { + val h = 1 max padded.size /% groupSize + val cols = (padded grouped h).toList + for (i <- 0 until h) yield + for (j <- 0 until groupSize) yield + if (i < cols(j).size) cols(j)(i) else "" + } + grouped + } +} + +/** Adjust the column width and number of columns to minimize the row count. */ +trait VariColumnTabulator extends Tabulator { + override protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = { + import scala.tools.nsc.interpreter.SimpleMath._ + val longest = (items map (_.length)).max + val shortest = (items map (_.length)).min + val fattest = longest + marginSize + val skinny = shortest + marginSize + + // given ncols, calculate nrows and a list of column widths, or none if not possible + // if ncols > items.size, then columnWidths.size == items.size + def layout(ncols: Int): Option[(Int, Seq[Int], Seq[Seq[String]])] = { + val nrows = items.size /% ncols + val xwise = isAcross || ncols >= items.length + // max width item in each column + def maxima(rows: Seq[Seq[String]]) = + (0 until (ncols min items.size)) map { col => + val widths = for (r <- rows if r.size > col) yield r(col).length + widths.max + } + def resulting(rows: Seq[Seq[String]]) = { + val columnWidths = maxima(rows) map (_ + marginSize) + val linelen = columnWidths.sum + if (linelen <= width) Some((nrows, columnWidths, rows)) + else None + } + if (ncols == 1) resulting(columnize(items)) + else if (xwise) resulting((items grouped ncols).toSeq) + else { + val cols = (items grouped nrows).toList + val rows = + for (i <- 0 until nrows) yield + for (j <- 0 until ncols) yield + if (j < cols.size && i < cols(j).size) cols(j)(i) else "" + resulting(rows) + } + } + + if (fattest >= width) { + columnize(items) + } else { + // if every col is widest, we have at least this many cols + val mincols = 1 max (width / fattest) + // if every other col is skinniest, we have at most this many cols + val maxcols = 1 + ((width - fattest) / skinny) + val possibles = (mincols to maxcols).map(n => layout(n)).flatten + val minrows = (possibles map (_._1)).min + + // select the min ncols that results in minrows + val (_, columnWidths, sss) = (possibles find (_._1 == minrows)).get + + // format to column width + sss map (ss => ss.zipWithIndex map { + case (s, i) => s"%-${columnWidths(i)}s" format s + }) + } + } +} + +private[interpreter] object SimpleMath { + implicit class DivRem(private val i: Int) extends AnyVal { + /** i/n + if (i % n != 0) 1 else 0 */ + def /%(n: Int): Int = (i + n - 1) / n + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala b/src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala new file mode 100644 index 0000000000..b6c9792ec0 --- /dev/null +++ b/src/repl/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/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala b/src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala new file mode 100644 index 0000000000..c18a9809a0 --- /dev/null +++ b/src/repl/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/scala/tools/nsc/interpreter/jline/JLineHistory.scala b/src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala new file mode 100644 index 0000000000..1f6a1f7022 --- /dev/null +++ b/src/repl/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/scala/tools/nsc/interpreter/jline/JLineReader.scala b/src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala new file mode 100644 index 0000000000..414868a7e5 --- /dev/null +++ b/src/repl/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 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 + } + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala b/src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala deleted file mode 100644 index dddfb1b8f6..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/session/FileBackedHistory.scala +++ /dev/null @@ -1,84 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc -package interpreter -package session - -import scala.tools.nsc.io._ -import FileBackedHistory._ - -/** TODO: file locking. - */ -trait FileBackedHistory extends JLineHistory with JPersistentHistory { - def maxSize: Int - protected lazy val historyFile: File = 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() } - } - } - - 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) { - 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 Properties.userHome - - def defaultFileName = ".scala_history" - def defaultFile: File = File(Path(userHome) / defaultFileName) -} diff --git a/src/repl/scala/tools/nsc/interpreter/session/History.scala b/src/repl/scala/tools/nsc/interpreter/session/History.scala index 794d41adc7..2028a13dfd 100644 --- a/src/repl/scala/tools/nsc/interpreter/session/History.scala +++ b/src/repl/scala/tools/nsc/interpreter/session/History.scala @@ -11,7 +11,10 @@ package session * reference to the jline classes. Very sparse right now. */ trait History { + def historicize(text: String): Boolean = false + def asStrings: List[String] + def asStrings(from: Int, to: Int): List[String] = asStrings.slice(from, to) def index: Int def size: Int } diff --git a/src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala b/src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala deleted file mode 100644 index 18e0ee7c85..0000000000 --- a/src/repl/scala/tools/nsc/interpreter/session/JLineHistory.scala +++ /dev/null @@ -1,49 +0,0 @@ -/* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL - * @author Paul Phillips - */ - -package scala.tools.nsc -package interpreter -package session - -/** 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 -} - -object JLineHistory { - class JLineFileHistory extends SimpleHistory with FileBackedHistory { - override def add(item: CharSequence): Unit = { - if (!isEmpty && last == item) - repldbg("Ignoring duplicate entry '" + item + "'") - else { - super.add(item) - addLineToFile(item) - } - } - override def toString = "History(size = " + size + ", index = " + index + ")" - } - - def apply(): JLineHistory = try new JLineFileHistory catch { case x: Exception => new SimpleHistory() } -} diff --git a/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala b/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala index 7c49b91296..504d0d30ee 100644 --- a/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala +++ b/src/repl/scala/tools/nsc/interpreter/session/SimpleHistory.scala @@ -10,10 +10,9 @@ package session import scala.collection.mutable.{ Buffer, ListBuffer } import scala.collection.JavaConverters._ -class SimpleHistory extends JLineHistory { +class SimpleHistory extends History { private var _index: Int = 0 - private val buf: Buffer[String] = new ListBuffer[String] - private def toEntries(): Seq[JEntry] = buf.zipWithIndex map { case (x, i) => Entry(i, x) } + protected val buf: Buffer[String] = new ListBuffer[String] private def setTo(num: Int) = { _index = num ; true } private def minusOne = { _index -= 1 ; true } private def plusOne = { _index += 1 ; true } @@ -25,10 +24,6 @@ class SimpleHistory extends JLineHistory { "" } - case class Entry(index: Int, value: CharSequence) extends JEntry { - override def toString = value - } - def maxSize: Int = 2500 def last = if (isEmpty) fail("last") else buf.last @@ -42,9 +37,6 @@ class SimpleHistory extends JLineHistory { buf trimEnd 1 add(item) } - 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 remove(idx: Int): CharSequence = buf remove idx def removeFirst(): CharSequence = buf remove 0 diff --git a/src/repl/scala/tools/nsc/interpreter/session/package.scala b/src/repl/scala/tools/nsc/interpreter/session/package.scala index a3d7312c98..06e7f6207b 100644 --- a/src/repl/scala/tools/nsc/interpreter/session/package.scala +++ b/src/repl/scala/tools/nsc/interpreter/session/package.scala @@ -14,10 +14,5 @@ package object session { type JIterator[T] = java.util.Iterator[T] type JListIterator[T] = java.util.ListIterator[T] - type JEntry = jline.console.history.History.Entry - type JHistory = jline.console.history.History - type JMemoryHistory = jline.console.history.MemoryHistory - type JPersistentHistory = jline.console.history.PersistentHistory - private[interpreter] implicit def charSequenceFix(x: CharSequence): String = x.toString } -- cgit v1.2.3 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. --- build.sbt | 1 + build.xml | 40 +++++- .../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 +++++++++++++++++++++ src/repl/scala/tools/nsc/interpreter/ILoop.scala | 40 ++++-- .../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 --------------------- 11 files changed, 407 insertions(+), 350 deletions(-) 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 delete mode 100644 src/repl/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/jline/JLineHistory.scala delete mode 100644 src/repl/scala/tools/nsc/interpreter/jline/JLineReader.scala (limited to 'src') diff --git a/build.sbt b/build.sbt index 553c217d4a..76d66481d0 100644 --- a/build.sbt +++ b/build.sbt @@ -191,6 +191,7 @@ lazy val interactive = configureAsSubproject(project) .settings(disableDocsAndPublishingTasks: _*) .dependsOn(compiler) +// TODO: SI-9339 embed shaded copy of jline & its interface (see #4563) lazy val repl = configureAsSubproject(project) .settings(libraryDependencies += jlineDep) .settings(disableDocsAndPublishingTasks: _*) diff --git a/build.xml b/build.xml index 421646a2b0..589e1931b8 100755 --- a/build.xml +++ b/build.xml @@ -275,6 +275,10 @@ TODO: + + + + @@ -696,7 +700,7 @@ TODO: - + @@ -799,6 +803,11 @@ TODO: + + + + + @@ -873,6 +882,8 @@ TODO: + + @@ -1076,6 +1087,7 @@ TODO: + + + + + + + + + + + + + + + + + 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 + } + } +} 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 - } - } -} -- cgit v1.2.3