From 8e15296edd43fb45b182b35b8abb79c2d9fc10ed Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Tue, 14 Aug 2018 07:23:38 -0700 Subject: [PATCH] Use JLine3 Minimal changes to support new API, with basic completion and history. JLine handles continuation line edit. --- build.sbt | 34 ++- .../tools/nsc/fsc/ResidentScriptRunner.scala | 1 - .../tools/nsc/settings/ScalaSettings.scala | 11 +- .../scala/tools/partest/ReplTest.scala | 2 +- .../interpreter/jline/FileBackedHistory.scala | 127 ----------- .../interpreter/jline/JLineDelimiter.scala | 32 --- .../nsc/interpreter/jline/JLineHistory.scala | 81 ------- .../nsc/interpreter/jline/JLineReader.scala | 149 ------------- .../tools/nsc/interpreter/jline/Reader.scala | 211 ++++++++++++++++++ .../nsc/interpreter/shell/Completion.scala | 22 +- .../tools/nsc/interpreter/shell/History.scala | 3 +- .../tools/nsc/interpreter/shell/ILoop.scala | 205 +++++++---------- .../interpreter/shell/InteractiveReader.scala | 93 +------- .../nsc/interpreter/shell/LoopCommands.scala | 4 +- .../interpreter/shell/ReplCompletion.scala | 23 +- .../nsc/interpreter/shell/ShellConfig.scala | 27 ++- .../nsc/interpreter/shell/SimpleReader.scala | 10 +- .../scala/tools/nsc/interpreter/IMain.scala | 21 ++ .../tools/nsc/interpreter/Interface.scala | 8 + .../interpreter/PresentationCompilation.scala | 12 +- test/files/run/repl-paste-parse.check | 8 +- .../nsc/interpreter/CompletionTest.scala | 24 +- versions.properties | 4 +- 23 files changed, 440 insertions(+), 672 deletions(-) delete mode 100644 src/repl-frontend/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala delete mode 100644 src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala delete mode 100644 src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineHistory.scala delete mode 100644 src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineReader.scala create mode 100644 src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala diff --git a/build.sbt b/build.sbt index 7e329b2e396e..17989adac1c5 100644 --- a/build.sbt +++ b/build.sbt @@ -44,7 +44,9 @@ val junitDep = "junit" % "junit" val junitInterfaceDep = "com.novocode" % "junit-interface" % "0.11" % "test" val jolDep = "org.openjdk.jol" % "jol-core" % "0.9" val asmDep = "org.scala-lang.modules" % "scala-asm" % versionProps("scala-asm.version") -val jlineDep = "jline" % "jline" % versionProps("jline.version") +val jlineDep = "org.jline" % "jline" % versionProps("jline.version") +val jansiDep = "org.fusesource.jansi" % "jansi" % versionProps("jansi.version") +val jnaDep = "net.java.dev.jna" % "jna" % versionProps("jna.version") val testInterfaceDep = "org.scala-sbt" % "test-interface" % "1.0" val diffUtilsDep = "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0" @@ -195,6 +197,7 @@ lazy val commonSettings = instanceSettings ++ clearSourceAndResourceDirectories "-doc-source-url", s"https://github.com/scala/scala/tree/${versionProperties.value.githubTree}€{FILE_PATH_EXT}#L€{FILE_LINE}" ), //maxErrors := 10, + maxErrors := 10, setIncOptions, apiURL := Some(url("https://www.scala-lang.org/api/" + versionProperties.value.mavenVersion + "/")), pomIncludeRepository := { _ => false }, @@ -453,7 +456,7 @@ lazy val compiler = configureAsSubproject(project) libraryDependencies += asmDep, // These are only needed for the POM: // TODO: jline dependency is only needed for the REPL shell, which should move to its own jar - libraryDependencies ++= Seq(jlineDep), + libraryDependencies ++= Seq(jlineDep, jansiDep, jnaDep), buildCharacterPropertiesFile := (resourceManaged in Compile).value / "scala-buildcharacter.properties", resourceGenerators in Compile += generateBuildCharacterPropertiesFile.map(file => Seq(file)).taskValue, // this a way to make sure that classes from interactive and scaladoc projects @@ -491,9 +494,16 @@ lazy val compiler = configureAsSubproject(project) "-doc-root-content", (sourceDirectory in Compile).value + "/rootdoc.txt" ), Osgi.headers ++= Seq( - "Import-Package" -> ("jline.*;resolution:=optional," + - raw"""scala.*;version="$${range;[==,=+);$${ver}}",""" + - "*"), + "Import-Package" -> raw"""org.jline.keymap.*;resolution:=optional + |org.jline.reader.*;resolution:=optional + |org.jline.style.*;resolution:=optional + |org.jline.terminal;resolution:=optional + |org.jline.terminal.impl;resolution:=optional + |org.jline.terminal.impl.jansi.*;resolution:=optional + |org.jline.terminal.spi;resolution:=optional + |org.jline.utils;resolution:=optional + |scala.*;version="$${range;[==,=+);$${ver}}" + |*""".stripMargin.linesIterator.mkString(","), "Class-Path" -> "scala-reflect.jar scala-library.jar" ), // Generate the ScriptEngineFactory service definition. The old Ant build did this when building @@ -528,7 +538,7 @@ lazy val replFrontend = configureAsSubproject(Project("repl-frontend", file(".") .settings(disableDocs) .settings(skip in publish := true) .settings( - libraryDependencies += jlineDep, + libraryDependencies ++= Seq(jlineDep, jansiDep, jnaDep), name := "scala-repl-frontend" ) .settings( @@ -881,7 +891,7 @@ lazy val scalaDist = Project("scala-dist", file(".") / "target" / "scala-dist-di (htmlOut ** "*.html").get ++ (fixedManOut ** "*.1").get }.taskValue, managedResourceDirectories in Compile := Seq((resourceManaged in Compile).value), - libraryDependencies += jlineDep, + libraryDependencies ++= Seq(jlineDep, jansiDep, jnaDep), apiURL := None, fixPom( "/project/name" -> Scala Distribution Artifacts, @@ -1020,7 +1030,7 @@ lazy val distDependencies = Seq(replFrontend, compiler, library, reflect, scalap lazy val dist = (project in file("dist")) .settings(commonSettings) .settings( - libraryDependencies ++= Seq(jlineDep), + libraryDependencies ++= Seq(jlineDep, jansiDep, jnaDep), mkBin := mkBinImpl.value, mkQuick := Def.task { val cp = (fullClasspath in IntegrationTest in LocalProject("test")).value @@ -1035,7 +1045,13 @@ lazy val dist = (project in file("dist")) packageBin in Compile := { val targetDir = (buildDirectory in ThisBuild).value / "pack" / "lib" val jlineJAR = findJar((dependencyClasspath in Compile).value, jlineDep).get.data - val mappings = Seq((jlineJAR, targetDir / "jline.jar")) + val jansiJAR = findJar((dependencyClasspath in Compile).value, jansiDep).get.data + val jnaJAR = findJar((dependencyClasspath in Compile).value, jnaDep).get.data + val mappings = Seq( + (jlineJAR, targetDir / "jline.jar"), + (jansiJAR, targetDir / "jansi.jar"), + (jnaJAR, targetDir / "jna.jar"), + ) IO.copy(mappings, CopyOptions() withOverwrite true) targetDir }, diff --git a/src/compiler/scala/tools/nsc/fsc/ResidentScriptRunner.scala b/src/compiler/scala/tools/nsc/fsc/ResidentScriptRunner.scala index 739af7ff27ec..03b715002196 100644 --- a/src/compiler/scala/tools/nsc/fsc/ResidentScriptRunner.scala +++ b/src/compiler/scala/tools/nsc/fsc/ResidentScriptRunner.scala @@ -13,7 +13,6 @@ package scala.tools.nsc package fsc -//import scala.tools.nsc.{AbstractScriptRunner, ScriptRunner, GenericRunnerSettings, Settings} import scala.reflect.io.Path import scala.util.control.NonFatal diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 8b8b589021ac..681939e12fb7 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -121,7 +121,16 @@ trait ScalaSettings extends AbsScalaSettings val maxwarns = IntSetting ("-Xmaxwarns", "Maximum warnings to print", 100, None, _ => None) val Xmigration = ScalaVersionSetting ("-Xmigration", "version", "Warn about constructs whose behavior may have changed since version.", initial = NoScalaVersion, default = Some(AnyScalaVersion)) val nouescape = BooleanSetting ("-Xno-uescape", "Disable handling of \\u unicode escapes.") - val Xnojline = BooleanSetting ("-Xnojline", "Do not use JLine for editing.") + val Xjline = ChoiceSetting ( + name = "-Xjline", + helpArg = "mode", + descr = "Select JLine mode.", + choices = List("emacs", "vi", "off"), + default = "emacs", + choicesHelp = List( + "emacs key bindings.", + "vi key bindings", + "No JLine editing.")) val Xverify = BooleanSetting ("-Xverify", "Verify generic signatures in generated bytecode.") val plugin = MultiStringSetting ("-Xplugin", "paths", "Load a plugin from each classpath.") val disable = MultiStringSetting ("-Xplugin-disable", "plugin", "Disable plugins by name.") diff --git a/src/partest/scala/tools/partest/ReplTest.scala b/src/partest/scala/tools/partest/ReplTest.scala index 93cca4a20e20..54d65dc983e4 100644 --- a/src/partest/scala/tools/partest/ReplTest.scala +++ b/src/partest/scala/tools/partest/ReplTest.scala @@ -26,7 +26,7 @@ abstract class ReplTest extends DirectTest { // final because we need to enforce the existence of a couple settings. final override def settings: Settings = { val s = super.settings - s.Xnojline.value = true + s.Xjline.value = "off" if (getClass.getClassLoader.getParent != null) { s.classpath.value = s.classpath.value match { case "" => testOutput.toString diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala b/src/repl-frontend/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala deleted file mode 100644 index fcefd6bf8fc3..000000000000 --- a/src/repl-frontend/scala/tools/nsc/interpreter/jline/FileBackedHistory.scala +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Scala (https://www.scala-lang.org) - * - * Copyright EPFL and Lightbend, Inc. - * - * Licensed under Apache License 2.0 - * (http://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -package scala.tools.nsc.interpreter.jline - -import java.io.IOException -import java.nio.charset.Charset -import java.nio.file.{FileSystems, Files, Path} - -import _root_.jline.console.history.PersistentHistory - -import scala.collection.JavaConverters._ -import scala.io.Codec -import scala.reflect.internal.util.OwnerOnlyChmod -import scala.util.control.NonFatal - - -/** TODO: file locking. - */ -trait FileBackedHistory extends JLineHistory with PersistentHistory { - def maxSize: Int - import java.nio.file.StandardOpenOption.{APPEND, TRUNCATE_EXISTING} - - val charSet: Charset = implicitly[Codec].charSet - - // For a history file in the standard location, always try to restrict permission, - // creating an empty file if none exists. - // For a user-specified location, only lock down permissions if we're the ones - // creating it, otherwise responsibility for permissions is up to the caller. - private lazy val historyPath = { - val fs = FileSystems.getDefault - - // This would really have been sufficient for our property getting infrastructure - def prop(p: String) = Option(System.getProperty(p)) - - (prop("scala.shell.histfile").map(fs.getPath(_)).map{ p => if (!Files.exists(p)) secure(p); p } orElse - prop("user.home").map(n => fs.getPath(n + s"${fs.getSeparator}${FileBackedHistory.defaultFileName}")).map(secure) - ).getOrElse(throw new IllegalStateException("Cannot determine path for history file.")) - } - - private def secure(p: Path): Path = { - try OwnerOnlyChmod.chmodFileOrCreateEmpty(p) - catch { case NonFatal(e) => - e.printStackTrace(Console.err) - Console.err.println(s"Warning: history file ${p}'s permissions could not be restricted to owner-only.") - } - - p - } - - - protected lazy val lines: List[String] = { - try Files.readAllLines(historyPath, charSet).asScala.toList - 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 _: IOException => - try Files.readAllLines(historyPath, Codec.ISO8859.charSet).asScala.toList - catch { - case _: IOException => Nil - } - } - } - - 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(s"$item\n") - } - - /** Overwrites the history file with the current memory. */ - protected def sync(): Unit = - Files.write(historyPath, asStrings.asJava, charSet, TRUNCATE_EXISTING) - - /** Append one or more lines to the history file. */ - protected def append(newLines: String*): Unit = - Files.write(historyPath, newLines.asJava, charSet, APPEND) - - def load(): Unit = try { - // 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) { - sync() - } - - moveToEnd() - } catch { - case _: IOException | _: IllegalStateException => - Console.err.println("Could not load history.") - isPersistent = false - } - - def flush(): Unit = () - - def purge(): Unit = Files.write(historyPath, Array.emptyByteArray) -} - -object FileBackedHistory { - // val ContinuationChar = '\003' - // val ContinuationNL: String = Array('\003', '\n').mkString - - final val defaultFileName = ".scala_history" -} diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala b/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala deleted file mode 100644 index d344ac3a0942..000000000000 --- a/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineDelimiter.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Scala (https://www.scala-lang.org) - * - * Copyright EPFL and Lightbend, Inc. - * - * Licensed under Apache License 2.0 - * (http://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -package scala.tools.nsc.interpreter.jline - -import scala.tools.nsc.interpreter.shell.Parsed - -import _root_.jline.console.completer.ArgumentCompleter.{ ArgumentDelimiter, ArgumentList } - -// implements a jline interface -class JLineDelimiter extends ArgumentDelimiter { - def toJLine(args: List[String], cursor: Int): ArgumentList = 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 -} diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineHistory.scala b/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineHistory.scala deleted file mode 100644 index eec2025e6262..000000000000 --- a/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineHistory.scala +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Scala (https://www.scala-lang.org) - * - * Copyright EPFL and Lightbend, Inc. - * - * Licensed under Apache License 2.0 - * (http://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -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.shell.{History, SimpleHistory} - - -/** A straight scalafication 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.linesIterator foreach add - moveToEnd() - true - } -} - -object JLineHistory { - class JLineFileHistory extends SimpleHistory with FileBackedHistory { - override def add(item: CharSequence): Unit = - if (isEmpty || last != item) { - super.add(item) - addLineToFile(item) - } // else interpreter.repldbg("Ignoring duplicate entry '" + 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(): scala.collection.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-frontend/scala/tools/nsc/interpreter/jline/JLineReader.scala b/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineReader.scala deleted file mode 100644 index 431696c7c0cc..000000000000 --- a/src/repl-frontend/scala/tools/nsc/interpreter/jline/JLineReader.scala +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Scala (https://www.scala-lang.org) - * - * Copyright EPFL and Lightbend, Inc. - * - * Licensed under Apache License 2.0 - * (http://www.apache.org/licenses/LICENSE-2.0). - * - * See the NOTICE file distributed with this work for - * additional information regarding copyright ownership. - */ - -package scala.tools.nsc.interpreter.jline - -import java.{util => ju} - -import _root_.jline.{console => jconsole} -import jline.console.completer.{CandidateListCompletionHandler, Completer} -import jconsole.history.{History => JHistory} - -import scala.tools.nsc.interpreter.shell._ - -trait JLineCompletion extends Completion with Completer { - final def complete(buf: String, cursor: Int, candidates: ju.List[CharSequence]): Int = - complete(if (buf == null) "" else buf, cursor) match { - case CompletionResult(newCursor, newCandidates) => - newCandidates foreach candidates.add - newCursor - } -} - -/** - * 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(isAcross: Boolean, isPaged: Boolean) extends InteractiveReader { - def interactive = true - - val history: History = new JLineHistory.JLineFileHistory() - - private val consoleReader = { - val reader = new JLineConsoleReader(isAcross) - - reader setPaginationEnabled isPaged - - // turn off magic ! - reader setExpandEvents false - - // enable detecting pasted tab char (when next char is immediately available) which is taken raw, not completion - reader setCopyPasteDetection true - - reader setHistory history.asInstanceOf[JHistory] - - reader - } - - private[this] var _completion: Completion = NoCompletion - def completion: Completion = _completion - - override def initCompletion(completion: Completion) = { - _completion = completion - completion match { - case NoCompletion => // ignore - case jlineCompleter: Completer => consoleReader.initCompletion(jlineCompleter) - case _ => // should not happen, but hey - } - } - - 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(val isAcross: Boolean) extends jconsole.ConsoleReader with VariColumnTabulator { - 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: ju.Collection[_ <: CharSequence]): Unit = { - import scala.collection.JavaConverters._ - - printColumns_(items.asScala.toList map (_.toString)) - } - - 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(completer: Completer): Unit = { - this setBellEnabled false - - getCompletionHandler match { - case clch: CandidateListCompletionHandler => - clch.setPrintSpaceAfterFullCompletion(false) - } - - this addCompleter completer - - setAutoprintThreshold(400) // max completion candidates without warning - } -} diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala b/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala new file mode 100644 index 000000000000..b4fb842d6efc --- /dev/null +++ b/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala @@ -0,0 +1,211 @@ + +package scala.tools.nsc.interpreter +package jline + +import shell.{Accumulator, ShellConfig} + +import org.jline.reader.{ + Candidate, Completer, CompletingParsedLine, + EndOfFileException, UserInterruptException, EOFError, + History, LineReader, ParsedLine, Parser}, Parser.ParseContext +import org.jline.reader.impl.DefaultParser + +import java.util.{List => JList} + +/** A Reader that delegates to JLine3. + */ +class Reader private (config: ShellConfig, reader: LineReader, val accumulator: Accumulator, val completion: shell.Completion) extends shell.InteractiveReader { + val history: shell.History = new HistoryAdaptor(reader.getHistory) + def interactive: Boolean = true + protected def readOneKey(prompt: String): Int = ??? + protected def readOneLine(prompt: String): String = { + try { + reader.readLine(prompt) + } catch { + case _: EndOfFileException | _: UserInterruptException => reader.getBuffer.delete() ; null + } + } + def redrawLine(): Unit = ??? + def reset(): Unit = accumulator.reset() +} + +object Reader { + import org.jline.reader.LineReaderBuilder + import org.jline.reader.impl.history.DefaultHistory + import org.jline.terminal.TerminalBuilder + + /** Construct a Reader with various JLine3-specific set-up. + * The `shell.Completion` is wrapped in the `jline.Completion` bridge to enable completion from JLine3. + */ + def apply(config: ShellConfig, repl: Repl, completion: shell.Completion, accumulator: Accumulator): Reader = { + require(repl != null) + if (config.isReplDebug) initLogging() + + System.setProperty(LineReader.PROP_SUPPORT_PARSEDLINE, java.lang.Boolean.TRUE.toString()) + + //val terminal = TerminalBuilder.builder().build() + val terminal = TerminalBuilder.terminal() // defaults for now + val completer = new Completion(completion) + val parser = new ReplParser(repl) + val history = new DefaultHistory + + val builder = + LineReaderBuilder.builder() + .appName("scala") + .completer(completer) + .history(history) + .parser(parser) + .terminal(terminal) + + locally { + import LineReader._, Option._ + builder + .option(AUTO_GROUP, false) + .option(LIST_PACKED, true) // TODO + .option(INSERT_TAB, true) // At the beginning of the line, insert tab instead of completing + .variable(HISTORY_FILE, config.historyFile) // Save history to file + .variable(SECONDARY_PROMPT_PATTERN, config.encolor(config.continueText)) // Continue prompt + } + + val reader = builder.build() + locally { + import LineReader._, Option._ + // VIINS, VICMD, EMACS + val keymap = if (config.viMode) VIINS else EMACS + reader.getKeyMaps.put(MAIN, reader.getKeyMaps.get(keymap)); + } + def backupHistory(): Unit = { + import java.nio.file.{Files, Paths, StandardCopyOption}, StandardCopyOption.REPLACE_EXISTING + val hf = Paths.get(config.historyFile) + val bk = Paths.get(config.historyFile + ".bk") + Files.move(/*source =*/ hf, /*target =*/ bk, REPLACE_EXISTING) + } + try history.attach(reader) + catch { + case e: IllegalArgumentException if e.getMessage.contains("Bad history file syntax") => + backupHistory() + history.attach(reader) + case _: NumberFormatException => + backupHistory() + history.attach(reader) + } + new Reader(config, reader, accumulator, completer) + } + + class ReplParser(repl: Repl) extends Parser { + val scalaParser = new ScalaParser(repl) + val commandParser = new CommandParser(repl) + def parse(line: String, cursor: Int, context: ParseContext): ParsedLine = + if (line.startsWith(":")) commandParser.parse(line, cursor, context) + else scalaParser.parse(line, cursor, context) + } + class ScalaParser(repl: Repl) extends Parser { + import scala.util.{Left, Right} + import Results._ + + def parse(line: String, cursor: Int, context: ParseContext): ParsedLine = { + import ParseContext._ + context match { + case ACCEPT_LINE => + if (repl.parseString(line) == Incomplete) throw new EOFError(0, 0, "incomplete") + tokenize(line, cursor) // Try a real "final" parse. + case COMPLETE => tokenize(line, cursor) // Parse to find completions (typically after a Tab). + case SECONDARY_PROMPT => + tokenize(line, cursor) // Called when we need to update the secondary prompts. + case UNSPECIFIED => ScalaParsedLine(line, cursor, 0, 0, Nil) + } + } + def tokenize(line: String, cursor: Int): ScalaParsedLine = { + val tokens = repl.tokenize(line) + //println(s"Got ${tokens.size} tokens") + if (tokens.isEmpty) ScalaParsedLine(line, cursor, 0, 0, List(TokenData(0,0,0))) + else { + val current = tokens.find(t => t.start <= cursor && cursor <= t.end) + val (wordCursor, wordIndex) = current match { + case Some(t) => (cursor - t.start, tokens.indexOf(t)) + case _ => (tokens.last.end - tokens.last.start, tokens.size - 1) + } + ScalaParsedLine(line, cursor, wordCursor, wordIndex, tokens) + } + } + } + class CommandParser(repl: Repl) extends Parser { + val defaultParser = new DefaultParser() + def parse(line: String, cursor: Int, context: ParseContext): ParsedLine = + defaultParser.parse(line, cursor, context) + } + + /** + * Lines of Scala are opaque to JLine. + * + * @param line the line + */ + case class ScalaParsedLine(line: String, cursor: Int, wordCursor: Int, wordIndex: Int, tokens: List[TokenData]) extends CompletingParsedLine { + require(wordIndex < tokens.size, s"wordIndex $wordIndex out of range ${tokens.size}") + require(wordCursor <= tokens(wordIndex).end - tokens(wordIndex).start, s"wordCursor $wordCursor should be in range ${tokens(wordIndex)}") + // Members declared in org.jline.reader.CompletingParsedLine. + // This is where backticks could be added, for example. + def escape(candidate: CharSequence, complete: Boolean): CharSequence = candidate + def rawWordCursor: Int = wordCursor + def rawWordLength: Int = word.length + + // Members declared in org.jline.reader.ParsedLine + //def cursor(): Int = ??? + //def line(): String = ??? + def word: String = { + val t = tokens(wordIndex) + line.substring(t.start, t.end) + } + //def wordCursor: Int = 0 // offset in current word + //def wordIndex: Int = 0 // index of current word in tokens + import collection.JavaConverters._ + def words: JList[String] = tokens.map(t => line.substring(t.start, t.end)).asJava + } + + private def initLogging(): Unit = { + import java.util.logging._ + val logger = Logger.getLogger("org.jline") + val handler = new ConsoleHandler() + logger.setLevel(Level.ALL) + handler.setLevel(Level.ALL) + logger.addHandler(handler) + } +} + +/** A Completion bridge to JLine3. + * It delegates both interfaces to an underlying `Completion`. + */ +class Completion(delegate: shell.Completion) extends shell.Completion with Completer { + require(delegate != null) + // REPL Completion + def complete(buffer: String, cursor: Int): shell.CompletionResult = delegate.complete(buffer, cursor) + + def reset(): Unit = delegate.reset() + + // JLine Completer + def complete(lineReader: LineReader, parsedLine: ParsedLine, newCandidates: JList[Candidate]): Unit = { + def candidateForResult(s: String): Candidate = { + val value = s + val displayed = s + val group = null // results may be grouped + val descr = null // "Scala member", displayed alongside `displayed` + val suffix = null // such as slash after directory name + val key = null // same key implies mergeable result + val complete = false // more to complete? + new Candidate(value, displayed, group, descr, suffix, key, complete) + } + val result = complete(parsedLine.line, parsedLine.cursor) + //Console.err.println(s"completing $parsedLine to ${result.candidates}") + for (s <- result.candidates) newCandidates.add(candidateForResult(s)) + } +} + +// TODO +class HistoryAdaptor(history: History) extends shell.History { + //def historicize(text: String): Boolean = false + + def asStrings: List[String] = Nil + //def asStrings(from: Int, to: Int): List[String] = asStrings.slice(from, to) + def index: Int = 0 + def size: Int = 0 +} diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala index 4c7ec2e1753e..9ad1a80b2e1b 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala @@ -13,18 +13,26 @@ package scala.tools.nsc.interpreter.shell trait Completion { - def resetVerbosity(): Unit + def reset(): Unit def complete(buffer: String, cursor: Int): CompletionResult - - // Code accumulated in multi-line REPL input - def partialInput: String = "" - def withPartialInput[T](code: String)(body: => T): T = body } object NoCompletion extends Completion { - def resetVerbosity() = () + def reset() = () def complete(buffer: String, cursor: Int) = NoCompletions } -case class CompletionResult(cursor: Int, candidates: List[String]) +case class CompletionResult(cursor: Int, candidates: List[String]) { + final def orElse(other: => CompletionResult): CompletionResult = + if (candidates.nonEmpty) this else other +} +object CompletionResult { + val empty: CompletionResult = NoCompletions +} object NoCompletions extends CompletionResult(-1, Nil) + +case class MultiCompletion(underlying: Completion*) extends Completion { + override def reset() = underlying.foreach(_.reset()) + override def complete(buffer: String, cursor: Int) = + underlying.foldLeft(CompletionResult.empty)((r,c) => r.orElse(c.complete(buffer, cursor))) +} diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/History.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/History.scala index 16003cc29ce9..8de352cf9185 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/History.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/History.scala @@ -12,8 +12,7 @@ package scala.tools.nsc.interpreter.shell -/** An implementation-agnostic history interface which makes no - * reference to the jline classes. Very sparse right now. +/** Support for adding to history and retrieving it. */ trait History { def historicize(text: String): Boolean = false diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala index b16b710477ae..e5d83cbbb644 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala @@ -38,8 +38,6 @@ import scala.tools.nsc.util.Exceptional.rootCause import scala.util.control.ControlThrowable import scala.collection.JavaConverters._ - - /** The Scala interactive shell. This part provides the user interface, * with evaluation and auto-complete handled by IMain. * @@ -67,14 +65,15 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, // so that this can be a lazy val private lazy val defaultIn: InteractiveReader = if (batchMode) SimpleReader(batchText) - else if (inOverride != null) SimpleReader(inOverride, out, interactive = true) - else if (haveInteractiveConsole) new jline.JlineReader(isAcross = isAcross, isPaged = isPaged) + else if (inOverride != null) SimpleReader(inOverride, out, completion(new Accumulator), interactive = true) + else if (haveInteractiveConsole) { + val accumulator = new Accumulator + jline.Reader(config, intp, completion(accumulator), accumulator) + } else SimpleReader() - private val interpreterInitialized = new java.util.concurrent.CountDownLatch(1) - // TODO: move echo and friends to ReplReporterImpl // When you know you are most likely breaking into the middle // of a line being typed. This softens the blow. @@ -94,9 +93,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, try op finally mum = saved } - private def printShellInterrupt(): Unit = { - out print ShellConfig.InterruptedString - } + private def printShellInterrupt() = out.print(ShellConfig.InterruptedString) protected def asyncMessage(msg: String): Unit = { if (isReplInfo || isReplPower) @@ -221,7 +218,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, // complete filename val fileCompletion: Completion = new Completion { - def resetVerbosity(): Unit = () + def reset(): Unit = () val emptyWord = """(\s+)$""".r.unanchored val directorily = """(\S*/)$""".r.unanchored val trailingWord = """(\S+)$""".r.unanchored @@ -246,7 +243,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, // complete settings name val settingsCompletion: Completion = new Completion { - def resetVerbosity(): Unit = () + def reset(): Unit = () val trailingWord = """(\S+)$""".r.unanchored def complete(buffer: String, cursor: Int): CompletionResult = { buffer.substring(0, cursor) match { @@ -454,37 +451,38 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, } import LineResults.LineResult + // Notice failure to create compiler + def command(line: String): Result = + if (line startsWith ":") colonCommand(line) + else if (!intp.initializeCompiler()) Result(keepRunning = false, None) + else Result(keepRunning = true, interpretStartingWith(line)) + // return false if repl should exit def processLine(line: String): Boolean = { // Long timeout here to avoid test failures under heavy load. interpreterInitialized.await(10, TimeUnit.MINUTES) - command(line) match { - case Result(false, _) => false - case Result(_, Some(line)) => addReplay(line) ; true - case _ => true - } + val res = command(line) + res.lineToRecord.foreach(addReplay) + res.keepRunning } lazy val prompt = encolor(promptText) - private def readOneLine() = { + // R as in REPL + def readOneLine(): String = { out.flush() - in readLine prompt + in.reset() + in.readLine(prompt) } - /** The main read-eval-print loop for the repl. It calls - * command() for each line of input, and stops when - * command() returns false. - */ - final def loop(): LineResult = loop(readOneLine()) - - @tailrec final def loop(line: String): LineResult = { - import LineResults._ - if (line == null) EOF - else if (try processLine(line) catch crashRecovery) loop(readOneLine()) - else ERR - } + // L as in REPL + @tailrec final def loop(): LineResult = + readOneLine() match { + case null => LineResults.EOF + case s if (try processLine(s) catch crashRecovery) => loop() + case _ => LineResults.ERR + } /** interpret all lines from a specified file */ def interpretAllFrom(file: File, verbose: Boolean = false): Unit = { @@ -562,26 +560,21 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, def lineCommand(what: String): Result = editCommand(what, None) - def newCompleter(): ReplCompletion = - new ReplCompletion(intp) { - override def shellCompletion(buffer: String, cursor: Int): Option[CompletionResult] = - if (buffer.startsWith(":")) Some(colonCompletion(buffer, cursor).complete(buffer, cursor)) - else None - } + def completion(accumulator: Accumulator = new Accumulator) = { + val rc = new ReplCompletion(intp, accumulator) + MultiCompletion(shellCompletion, rc) + } + val shellCompletion = new Completion { + override def reset() = () + override def complete(buffer: String, cursor: Int) = + if (buffer.startsWith(":")) colonCompletion(buffer, cursor).complete(buffer, cursor) + else NoCompletions + } def completionsCommand(what: String): Result = { - val completions = newCompleter().complete(what, what.length) + val completions = in.completion.complete(what, what.length) val prefix = if (completions == NoCompletions) "" else what.substring(0, completions.cursor) - - val completionLines = - completions.candidates.map { c => - s"[completions] $prefix$c" - } - - if (completionLines.nonEmpty) { - echo(completionLines.mkString("\n")) - } - + completions.candidates.map(c => s"[completions] $prefix$c").foreach(echo) Result.default // never record completions } @@ -777,17 +770,6 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, replinfo(s"Result printing is ${ if (intp.reporter.printResults) "on" else "off" }.") } - /** Run one command submitted by the user. Two values are returned: - * (1) whether to keep running, (2) the line to record for replay, if any. - */ - def command(line: String): Result = { - if (line startsWith ":") colonCommand(line) - else { - if (!intp.initializeCompiler()) Result(keepRunning = false, None) // Notice failure to create compiler - else Result(keepRunning = true, interpretStartingWith(line)) - } - } - private def readWhile(cond: String => Boolean) = { Iterator continually in.readLine("") takeWhile (x => x != null && cond(x)) } @@ -859,12 +841,6 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, result } - private val continueText = { - val text = enversion(continueString) - val margin = promptText.linesIterator.toList.last.length - text.length - if (margin > 0) " " * margin + text else text - } - private object paste extends Pasted(config.promptText, encolor(continueText), continueText) { def interpret(line: String) = intp interpret line def echo(message: String) = ILoop.this echo message @@ -905,53 +881,37 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, * read, go ahead and interpret it. Return the full string * to be recorded for replay, if any. */ - final def interpretStartingWith(code: String): Option[String] = { - // signal completion non-completion input has been received - in.completion.resetVerbosity() - - /* Here we place ourselves between the user and the interpreter and examine - * the input they are ostensibly submitting. We intervene in several cases: - * - * 1) If the line starts with "scala> " it is assumed to be an interpreter paste. - * 2) If the line starts with "." (but not ".." or "./") it is treated as an invocation - * on the previous result. - * 3) If the Completion object's execute returns Some(_), we inject that value - * and avoid the interpreter, as it's likely not valid scala code. - */ - code match { + final def interpretStartingWith(start: String): Option[String] = { + def loop(): Option[String] = { + val code = in.accumulator.toString + intp.interpret(code) match { + case Error => None + case Success => Some(code) + case Incomplete if in.interactive && code.endsWith("\n\n") => + echo("You typed two blank lines. Starting a new command.") + None + case Incomplete => + in.completion.reset() + in.readLine(paste.ContinuePrompt) match { + case null => intp.compileString(code) ; None // EOF, report error + case line => in.accumulator += line ; loop() + } + } + } + + // signal completion that non-completion input has been received + in.completion.reset() + + start match { case "" | lineComment() => None // empty or line comment, do nothing case paste() => - paste.transcript(Iterator(code) ++ readWhile(!paste.isPromptOnly(_))) match { + val pasted = Iterator(start) ++ readWhile(!paste.isPromptOnly(_)) + paste.transcript(pasted) match { case Some(s) => interpretStartingWith(s) case _ => None } - case invocation() => interpretStartingWith(intp.mostRecentVar + code) - case _ => - intp.interpret(code) match { - case Error => None - case Success => Some(code) - case Incomplete => - if (in.interactive && code.endsWith("\n\n")) { - echo("You typed two blank lines. Starting a new command.") - None - } else { - val prefix = code + "\n" - in.completion.withPartialInput(prefix) { - in.readLine(paste.ContinuePrompt) match { - case null => - // we know compilation is going to fail since we're at EOF and the - // parser thinks the input is still incomplete, but since this is - // a file being read non-interactively we want to fail. - // TODO: is this true ^^^^^^^^^^^^^^^^? - // So we send it straight to the compiler for the nice error message. - intp.compileString(code) - None - case line => - interpretStartingWith(prefix + line) // not in tailpos! - } - } - } - } + case invocation() => interpretStartingWith(intp.mostRecentVar + start) + case _ => in.accumulator += start ; loop() } } @@ -988,35 +948,26 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, def run(interpreterSettings: Settings): Boolean = { if (!batchMode) printWelcome() + createInterpreter(interpreterSettings) in = defaultIn - // let them start typing, using the splash reader (which avoids tab completion) - val firstLine = - SplashLoop.readLine(in, prompt) { - if (intp eq null) createInterpreter(interpreterSettings) - intp.reporter.withoutPrintingResults(intp.withSuppressedSettings { - intp.initializeCompiler() - interpreterInitialized.countDown() // TODO: move to reporter.compilerInitialized ? - - if (intp.reporter.hasErrors) { - echo("Interpreter encountered errors during initialization!") - throw new InterruptedException - } + intp.reporter.withoutPrintingResults(intp.withSuppressedSettings { + intp.initializeCompiler() + interpreterInitialized.countDown() // TODO: move to reporter.compilerInitialized ? - echoOff { interpretPreamble } + if (intp.reporter.hasErrors) { + echo("Interpreter encountered errors during initialization!") + throw new InterruptedException + } - // scala/bug#7418 Now that the interpreter is initialized, and `interpretPreamble` has populated the symbol table, - // enable TAB completion (we do this before blocking on input from the splash loop, - // so that it can offer tab completion as soon as we're ready). - if (doCompletion) - in.initCompletion(newCompleter()) + echoOff { interpretPreamble } - }) - }.orNull // null is used by readLine to signal EOF (`loop` will exit) + //if (doCompletion) in.initCompletion(newCompleter()) + }) // start full loop (if initialization was successful) try - loop(firstLine) match { + loop() match { case LineResults.EOF if in.interactive => printShellInterrupt(); true case LineResults.ERR => false case _ => true diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/InteractiveReader.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/InteractiveReader.scala index 3a52a7b67221..0d2a0efa5920 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/InteractiveReader.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/InteractiveReader.scala @@ -20,6 +20,8 @@ import InteractiveReader._ trait InteractiveReader { def interactive: Boolean + def accumulator: Accumulator + def reset(): Unit def history: History def completion: Completion @@ -35,12 +37,14 @@ trait InteractiveReader { protected def readOneLine(prompt: String): String protected def readOneKey(prompt: String): Int - def readLine(prompt: String): String = + def readLine(prompt: String): String = readOneLine(prompt) + /* // hack necessary for OSX jvm suspension because read calls are not restarted after SIGTSTP if (scala.util.Properties.isMac) restartSysCalls(readOneLine(prompt), reset()) else readOneLine(prompt) + */ - def initCompletion(completion: Completion): Unit = {} + def initCompletion(completion: Completion): Unit = () } object InteractiveReader { @@ -53,83 +57,10 @@ object InteractiveReader { def apply(): InteractiveReader = SimpleReader() } -/** Collect one line of user input from the supplied reader. - * Runs on a new thread while the REPL is initializing on the main thread. - * - * The user can enter text or a `:paste` command. - * - * TODO: obsolete the whole splash loop by making interpreter always run in separate thread from the UI, - * and communicating with it like we do in the presentation compiler - */ -class SplashLoop(in: InteractiveReader, prompt: String) extends Runnable { - import java.lang.System.{lineSeparator => EOL} - import java.util.concurrent.SynchronousQueue - - private val result = new SynchronousQueue[Option[String]] - @volatile private var running: Boolean = _ - private var thread: Thread = _ - - /** Read one line of input which can be retrieved with `line`. */ - def run(): Unit = { - var input = "" - try - while (input != null && input.isEmpty && running) { - input = in.readLine(prompt) - if (input != null) { - val trimmed = input.trim - if (trimmed.length >= 3 && ":paste".startsWith(trimmed)) - input = readPastedLines - } - } - finally { - try result.put(Option(input)) - catch { case ie: InterruptedException => } // we may have been interrupted because the interpreter reported an error - } - } - - /** Process `:paste`d input. */ - private def readPastedLines: String = { - // while collecting lines, check running flag - var help = f"// Entering paste mode (ctrl-D to finish)%n%n" - - val text = - try - Iterator.continually(in.readLine(help)).takeWhile { x => - help = "" - x != null && running - }.mkString(EOL).trim - catch { case ie: InterruptedException => "" } // TODO let the exception bubble up, or at least signal the interrupt happened? - - val next = - if (text.isEmpty) "// Nothing pasted, nothing gained." - else "// Exiting paste mode, now interpreting." - Console println f"%n${next}%n" - text - } - - def start(): Unit = result.synchronized { - require(thread == null, "Already started") - thread = new Thread(this) - thread.setDaemon(true) - running = true - thread.start() - } - - def stop(): Unit = result.synchronized { - running = false - if (thread != null) thread.interrupt() - thread = null - } - - /** Blocking. Returns Some(input) when received during splash loop, or None if interrupted (e.g., ctrl-D). */ - def line: Option[String] = result.take -} - -object SplashLoop { - def readLine(in: InteractiveReader, prompt: String)(body: => Unit): Option[String] = { - val splash = new SplashLoop(in, prompt) - try { splash.start; body ; splash.line } - catch { case ie: InterruptedException => Some(null) } - finally splash.stop() - } +/** Accumulate multi-line input. Shared by Reader and Completer, which must parse accumulated result. */ +class Accumulator { + var text: List[String] = Nil + def reset(): Unit = text = Nil + def +=(s: String): Unit = text :+= s + override def toString = text.mkString("\n") } diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala index b97dd580aad3..f38342439885 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala @@ -136,12 +136,12 @@ trait LoopCommands { case cmd :: Nil => val completion = if (cmd.isInstanceOf[NullaryCmd] || cursor < line.length) cmd.name else cmd.name + " " new Completion { - def resetVerbosity(): Unit = () + def reset(): Unit = () def complete(buffer: String, cursor: Int) = CompletionResult(cursor = 1, List(completion)) } case cmd :: rest => new Completion { - def resetVerbosity(): Unit = () + def reset(): Unit = () def complete(buffer: String, cursor: Int) = CompletionResult(cursor = 1, cmds.map(_.name)) } } diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala index ae7b1729ed99..0c5b4e391051 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala @@ -14,24 +14,14 @@ package scala.tools.nsc.interpreter.shell import scala.util.control.NonFatal import scala.tools.nsc.interpreter.Repl -import scala.tools.nsc.interpreter.jline import scala.tools.nsc.interpreter.Naming -class ReplCompletion(intp: Repl) extends jline.JLineCompletion { +/** Completion for the REPL. + */ +class ReplCompletion(intp: Repl, val accumulator: Accumulator) extends Completion { import ReplCompletion._ - private[this] var _partialInput: String = "" - override def partialInput: String = _partialInput - override def withPartialInput[T](code: String)(body: => T): T = { - val saved = partialInput - _partialInput = code - try body finally _partialInput = saved - } - - def shellCompletion(buffer: String, cursor: Int): Option[CompletionResult] = None - def complete(buffer: String, cursor: Int): CompletionResult = { - shellCompletion(buffer, cursor) getOrElse { // special case for: // // scala> 1 @@ -40,20 +30,19 @@ class ReplCompletion(intp: Repl) extends jline.JLineCompletion { if (Parsed.looksLikeInvocation(buffer)) intp.mostRecentVar + buffer else buffer - // prepend `partialInput` for multi-line input. - val bufferWithMultiLine = partialInput + bufferWithVar + val bufferWithMultiLine = accumulator.toString + bufferWithVar val cursor1 = cursor + (bufferWithMultiLine.length - buffer.length) codeCompletion(bufferWithMultiLine, cursor1) - } } private var lastRequest = NoRequest private var tabCount = 0 - def resetVerbosity(): Unit = { tabCount = 0 ; lastRequest = NoRequest } + def reset(): Unit = { tabCount = 0 ; lastRequest = NoRequest } // A convenience for testing def complete(before: String, after: String = ""): CompletionResult = complete(before + after, before.length) + private def codeCompletion(buf: String, cursor: Int): CompletionResult = { require(cursor >= 0 && cursor <= buf.length) diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ShellConfig.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ShellConfig.scala index acd1e5c3f846..0b46b953291a 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ShellConfig.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ShellConfig.scala @@ -35,7 +35,8 @@ object ShellConfig { val batchText: String = if (settings.execute.isSetByUser) settings.execute.value else "" val batchMode: Boolean = batchText.nonEmpty val doCompletion: Boolean = !(settings.noCompletion || batchMode) - val haveInteractiveConsole: Boolean = !settings.Xnojline + val haveInteractiveConsole: Boolean = settings.Xjline.value != "off" + override val viMode = super.viMode || settings.Xjline.value == "vi" } case _ => new ShellConfig { val filesToPaste: List[String] = Nil @@ -43,7 +44,8 @@ object ShellConfig { val batchText: String = "" val batchMode: Boolean = false val doCompletion: Boolean = !settings.noCompletion - val haveInteractiveConsole: Boolean = !settings.Xnojline + val haveInteractiveConsole: Boolean = settings.Xjline.value != "off" + override val viMode = super.viMode || settings.Xjline.value == "vi" } } } @@ -55,6 +57,7 @@ trait ShellConfig { def batchMode: Boolean def doCompletion: Boolean def haveInteractiveConsole: Boolean + def viMode: Boolean = envOrNone("SHELLOPTS").map(_.split(":").contains("vi")).getOrElse(false) private def bool(name: String) = BooleanProp.keyExists(name) private def int(name: String) = Prop[Int](name) @@ -62,6 +65,8 @@ trait ShellConfig { // This property is used in TypeDebugging. Let's recycle it. val colorOk = Properties.coloredOutputEnabled + val historyFile = s"$userHome/.scala_history" + private val info = bool("scala.repl.info") private val debug = bool("scala.repl.debug") private val trace = bool("scala.repl.trace") @@ -93,6 +98,11 @@ trait ShellConfig { // Prompt for continued input, will be right-adjusted to width of the primary prompt val continueString = Prop[String]("scala.repl.continue").option getOrElse "| " + val continueText = { + val text = enversion(continueString) + val margin = promptText.linesIterator.toList.last.length - text.length + if (margin > 0) " " * margin + text else text + } // What to display at REPL startup. val welcomeString = Prop[String]("scala.repl.welcome").option match { @@ -109,6 +119,9 @@ trait ShellConfig { * currently mutually exclusive. */ val format = Prop[String]("scala.repl.format") + val isPaged: Boolean = format.isSet && csv(format.get, "paged") + val isAcross: Boolean = format.isSet && csv(format.get, "across") + private def csv(p: String, v: String) = p.split(",").contains(v) val replAutorunCode = Prop[File]("scala.repl.autoruncode") val powerInitCode = Prop[File]("scala.repl.power.initcode") @@ -124,11 +137,11 @@ trait ShellConfig { def repltrace(msg: => String) = if (isReplTrace) echo(msg) def isReplPower: Boolean = power - def isPaged: Boolean = format.isSet && csv(format.get, "paged") - def isAcross: Boolean = format.isSet && csv(format.get, "across") - private def csv(p: String, v: String) = p split "," contains v private def echo(msg: => String) = - try Console println msg - catch { case x: AssertionError => Console.println("Assertion error printing debugging output: " + x) } + try Console.println(msg) + catch { + case e: AssertionError => + Console.println(s"Assertion error printing debugging output: $e") + } } diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/SimpleReader.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/SimpleReader.scala index 71de68a87e56..9a17fe3aa084 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/SimpleReader.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/SimpleReader.scala @@ -15,11 +15,11 @@ package scala.tools.nsc.interpreter.shell import java.io.{BufferedReader, StringReader, PrintWriter => JPrintWriter} /** Reads using standard JDK API. */ -class SimpleReader(in: BufferedReader, out: JPrintWriter, val interactive: Boolean, val verbose: Boolean) extends InteractiveReader { +class SimpleReader(in: BufferedReader, out: JPrintWriter, val completion: Completion, val interactive: Boolean, val verbose: Boolean) extends InteractiveReader { val history = NoHistory - val completion = NoCompletion + val accumulator = new Accumulator - def reset() = () + override def reset() = accumulator.reset() def redrawLine() = () // InteractiveReader internals @@ -48,8 +48,8 @@ object SimpleReader { def defaultIn = Console.in def defaultOut = new JPrintWriter(Console.out) - def apply(in: BufferedReader = defaultIn, out: JPrintWriter = defaultOut, interactive: Boolean = true, verbose: Boolean = false): SimpleReader = - new SimpleReader(in, out, interactive, verbose) + def apply(in: BufferedReader = defaultIn, out: JPrintWriter = defaultOut, completion: Completion = NoCompletion, interactive: Boolean = true, verbose: Boolean = false): SimpleReader = + new SimpleReader(in, out, completion, interactive, verbose) // a non-interactive SimpleReader that returns the given text def apply(text: String): SimpleReader = apply( diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala index 5dd2b7dcd51e..8152636f611d 100644 --- a/src/repl/scala/tools/nsc/interpreter/IMain.scala +++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala @@ -459,6 +459,27 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade case _ => tp } + // parseStats, returning status but no trees + def parseString(line: String): Result = parse(line).fold(e => e, _ => Success) + + def tokenize(line: String): List[TokenData] = { + import collection.mutable.ListBuffer + val u = newUnitScanner(newCompilationUnit(line)) + u.init() + val b = ListBuffer.empty[Int] + while (u.token != 0) { + b += u.lastOffset + b += u.token + b += u.offset + u.nextToken() + } + b += u.lastOffset + b.drop(1).grouped(3).flatMap(triple => triple.toList match { + case List(token, start, end) => Some(TokenData(token, start, end)) + case _ => println(s"Skipping token ${scala.runtime.ScalaRunTime.stringOf(triple)}") ; None + }).toList + } + /** * Interpret one line of input. All feedback, including parse errors * and evaluation results, are printed via the supplied compiler's diff --git a/src/repl/scala/tools/nsc/interpreter/Interface.scala b/src/repl/scala/tools/nsc/interpreter/Interface.scala index 6c87b8271f56..3a7ebf07c0aa 100644 --- a/src/repl/scala/tools/nsc/interpreter/Interface.scala +++ b/src/repl/scala/tools/nsc/interpreter/Interface.scala @@ -135,6 +135,11 @@ trait Repl extends ReplCore { def interpret(line: String, synthetic: Boolean): Result + def tokenize(line: String): List[TokenData] + + /** TODO resolve scan, parse, compile, interpret, which just indicate how much work to do. */ + def parseString(line: String): Result + final def beQuietDuring(body: => Unit): Unit = reporter.withoutPrintingResults(body) @@ -310,3 +315,6 @@ trait PresentationCompilationResult { def candidates(tabCount: Int): (Int, List[String]) } + +case class TokenData(token: Int, start: Int, end: Int) + diff --git a/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala b/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala index a1b8a5274842..435bd419df06 100644 --- a/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala +++ b/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala @@ -31,22 +31,22 @@ trait PresentationCompilation { self: IMain => * * The caller is responsible for calling [[PresentationCompileResult#cleanup]] to dispose of the compiler instance. */ - def presentationCompile(cursor: Int, buf: String): Either[Result, PresentationCompileResult] = { + def presentationCompile(cursor: Int, buf: String): Either[Result, PresentationCompilationResult] = { if (global == null) Left(Error) else { val pc = newPresentationCompiler() val line1 = buf.patch(cursor, Cursor, 0) val trees = pc.newUnitParser(line1).parseStats() val importer = global.mkImporter(pc) + //println(s"pc: [[$line1]], <<${trees.size}>>") val request = new Request(line1, trees map (t => importer.importTree(t)), generousImports = true) val origUnit = request.mkUnit val unit = new pc.CompilationUnit(origUnit.source) unit.body = pc.mkImporter(global).importTree(origUnit.body) - import pc._ - val richUnit = new RichCompilationUnit(unit.source) - unitOfFile(richUnit.source.file) = richUnit + val richUnit = new pc.RichCompilationUnit(unit.source) + pc.unitOfFile(richUnit.source.file) = richUnit richUnit.body = unit.body - enteringTyper(typeCheck(richUnit)) + pc.enteringTyper(pc.typeCheck(richUnit)) val inputRange = pc.wrappingPos(trees) // too bad dependent method types don't work for constructors val result = new PresentationCompileResult(pc, inputRange, cursor, buf) { val unit = richUnit ; override val compiler: pc.type = pc } @@ -89,7 +89,7 @@ trait PresentationCompilation { self: IMain => private var lastCommonPrefixCompletion: Option[String] = None - abstract class PresentationCompileResult(val compiler: interactive.Global, val inputRange: Position, val cursor: Int, val buf: String) extends PresentationCompilationResult { + private abstract class PresentationCompileResult(val compiler: interactive.Global, val inputRange: Position, val cursor: Int, val buf: String) extends PresentationCompilationResult { val unit: compiler.RichCompilationUnit // depmet broken for constructors, can't be ctor arg override def cleanup(): Unit = { diff --git a/test/files/run/repl-paste-parse.check b/test/files/run/repl-paste-parse.check index ea167c305fb1..02b42a4da518 100755 --- a/test/files/run/repl-paste-parse.check +++ b/test/files/run/repl-paste-parse.check @@ -1,8 +1,8 @@ -scala> - -scala> val case = 9 ^ repl-paste-parse.script:1: error: illegal start of simple pattern -:quit + +scala> + +scala> :quit diff --git a/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala b/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala index cac3ebea7170..95296f61dcb5 100644 --- a/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala +++ b/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala @@ -23,15 +23,16 @@ class CompletionTest { private def setup(sources: SourceFile*): Completion = { val intp = newIMain() intp.compileSources(sources: _*) - val completer = new ReplCompletion(intp) + val completer = new ReplCompletion(intp, new Accumulator) completer } - private def interpretLines(lines: String*): (Completion, Repl) = { + private def interpretLines(lines: String*): (Completion, Repl, Accumulator) = { val intp = newIMain() - lines foreach intp.interpret - val completer = new ReplCompletion(intp) - (completer, intp) + lines.foreach(intp.interpret) + val acc = new Accumulator + val completer = new ReplCompletion(intp, acc) + (completer, intp, acc) } implicit class BeforeAfterCompletion(completion: Completion) { @@ -130,7 +131,7 @@ class CompletionTest { @Test def previousLineCompletions(): Unit = { - val (completer, intp) = interpretLines( + val (completer, intp, _) = interpretLines( "class C { val x_y_z = 42 }", "object O { type T = Int }") @@ -138,7 +139,7 @@ class CompletionTest { checkExact(completer, "(1 : O.T).toCha")("toChar") intp.interpret("case class X_y_z()") - val completer1 = new ReplCompletion(intp) + val completer1 = new ReplCompletion(intp, new Accumulator) checkExact(completer1, "new X_y_")("X_y_z") checkExact(completer1, "X_y_")("X_y_z") checkExact(completer1, "X_y_z.app")("apply") @@ -146,17 +147,16 @@ class CompletionTest { @Test def previousResultInvocation(): Unit = { - val (completer, _) = interpretLines("1 + 1") + val (completer, _, _) = interpretLines("1 + 1") checkExact(completer, ".toCha")("toChar") } @Test def multiLineInvocation(): Unit = { - val (completer, _) = interpretLines() - completer.withPartialInput("class C {") { - checkExact(completer, "1 + 1.toCha")("toChar") - } + val (completer, _, accumulator) = interpretLines() + accumulator += "class C {" + checkExact(completer, "1 + 1.toCha")("toChar") } @Test diff --git a/versions.properties b/versions.properties index ad7729775e8a..4a448c840bde 100644 --- a/versions.properties +++ b/versions.properties @@ -8,4 +8,6 @@ starr.version=2.13.0-RC1 # - scala-asm: jar content included in scala-compiler # - jline: shaded with JarJar and included in scala-compiler scala-asm.version=7.0.0-scala-1 -jline.version=2.14.6 +jline.version=3.9.0 +jansi.version=1.17.1 +jna.version=4.2.2