Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPL: JLine 3: fix various issues #8848

Merged
merged 7 commits into from
Apr 6, 2020
Merged
68 changes: 46 additions & 22 deletions src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,23 @@ class Reader private (
terminal: Terminal) extends shell.InteractiveReader {
override val history: shell.History = new HistoryAdaptor(reader.getHistory)
override 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
}
}
override def redrawLine(): Unit = ???
override def reset(): Unit = accumulator.reset()
def redrawLine(): Unit = ???
def reset(): Unit = accumulator.reset()
override def close(): Unit = terminal.close()

override def withSecondaryPrompt[T](prompt: String)(body: => T): T = {
val oldPrompt = reader.getVariable(LineReader.SECONDARY_PROMPT_PATTERN)
reader.setVariable(LineReader.SECONDARY_PROMPT_PATTERN, prompt)
try body
finally reader.setVariable(LineReader.SECONDARY_PROMPT_PATTERN, oldPrompt)
}
}

object Reader {
Expand Down Expand Up @@ -148,13 +154,14 @@ object Reader {
}
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)))
if (tokens.isEmpty) ScalaParsedLine(line, cursor, 0, 0, Nil)
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)
case Some(t) if t.isIdentifier =>
(cursor - t.start, tokens.indexOf(t))
case _ =>
(0, -1)
}
ScalaParsedLine(line, cursor, wordCursor, wordIndex, tokens)
}
Expand All @@ -172,25 +179,26 @@ object Reader {
* @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)}")
require(wordIndex <= tokens.size,
s"wordIndex $wordIndex out of range ${tokens.size}")
require(wordIndex == -1 || wordCursor == 0 || 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 word: String =
if (wordIndex == -1 || wordIndex == tokens.size)
""
else {
val t = tokens(wordIndex)
line.substring(t.start, t.end)
}
def words: JList[String] = {
import scala.jdk.CollectionConverters._
tokens.map(t => line.substring(t.start, t.end)).asJava
}
//def wordCursor: Int = 0 // offset in current word
//def wordIndex: Int = 0 // index of current word in tokens
import scala.jdk.CollectionConverters._
def words: JList[String] = tokens.map(t => line.substring(t.start, t.end)).asJava
}

private def initLogging(): Unit = {
Expand Down Expand Up @@ -226,8 +234,24 @@ class Completion(delegate: shell.Completion) extends shell.Completion with Compl
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))
result.candidates match {
// the presence of the empty string here is a signal that the symbol
// is already complete and so instead of completing, we want to show
// the user the method signature. there are various JLine 3 features
// one might use to do this instead; sticking to basics for now
case "" :: defStrings if defStrings.nonEmpty =>
// specifics here are cargo-culted from Ammonite
lineReader.getTerminal.writer.println()
for (s <- defStrings)
lineReader.getTerminal.writer.println(s)
lineReader.callWidget(LineReader.REDRAW_LINE)
lineReader.callWidget(LineReader.REDISPLAY)
lineReader.getTerminal.flush()
// normal completion
case cs =>
for (s <- result.candidates)
newCandidates.add(candidateForResult(s))
}
}
}

Expand Down
24 changes: 13 additions & 11 deletions src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -801,17 +801,19 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
} getOrElse ""
case (eof, _) =>
echo(s"// Entering paste mode (${ eof getOrElse "ctrl-D" } to finish)\n")
val delimiter = eof orElse config.pasteDelimiter.option
val input = readWhile(s => delimiter.isEmpty || delimiter.get != s) mkString "\n"
val text = (
margin filter (_.nonEmpty) map {
case "-" => input.linesIterator map (_.trim) mkString "\n"
case m => input stripMargin m.head // ignore excess chars in "<<||"
} getOrElse input
).trim
if (text.isEmpty) echo("\n// Nothing pasted, nothing gained.\n")
else echo("\n// Exiting paste mode, now interpreting.\n")
text
in.withSecondaryPrompt("") {
val delimiter = eof orElse config.pasteDelimiter.option
val input = readWhile(s => delimiter.isEmpty || delimiter.get != s) mkString "\n"
val text = (
margin filter (_.nonEmpty) map {
case "-" => input.linesIterator map (_.trim) mkString "\n"
case m => input stripMargin m.head // ignore excess chars in "<<||"
} getOrElse input
).trim
if (text.isEmpty) echo("\n// Nothing pasted, nothing gained.\n")
else echo("\n// Exiting paste mode, now interpreting.\n")
text
}
}
def interpretCode() = {
if (intp.withLabel(label)(intp interpret code) == Incomplete)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ trait InteractiveReader {
def history: History
def completion: Completion
def redrawLine(): Unit
def withSecondaryPrompt[T](prompt: String)(body: => T): T = body

def readYesOrNo(prompt: String, alt: => Boolean): Boolean = readOneKey(prompt) match {
case 'y' => true
case 'n' => false
case -1 => false // EOF
case _ => alt
}
def readYesOrNo(prompt: String, alt: => Boolean): Boolean =
readOneLine(prompt).trim.toUpperCase.headOption match {
case Some('Y') => true
case Some('N') => false
case _ => alt
}

protected def readOneLine(prompt: String): String
protected def readOneKey(prompt: String): Int

def readLine(prompt: String): String = readOneLine(prompt)
/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ class ReplCompletion(intp: Repl, val accumulator: Accumulator = new Accumulator)
import ReplCompletion._

def complete(buffer: String, cursor: Int): CompletionResult = {
// special case for:
//
// scala> 1
// scala> .toInt
val bufferWithVar =
if (Parsed.looksLikeInvocation(buffer)) intp.mostRecentVar + buffer
else buffer
// special case for:
//
// scala> 1
// scala> .toInt
val bufferWithVar =
if (Parsed.looksLikeInvocation(buffer)) intp.mostRecentVar + buffer
else buffer

val bufferWithMultiLine = accumulator.toString + bufferWithVar
val cursor1 = cursor + (bufferWithMultiLine.length - buffer.length)
codeCompletion(bufferWithMultiLine, cursor1)
val bufferWithMultiLine = accumulator.toString + bufferWithVar
val cursor1 = cursor + (bufferWithMultiLine.length - buffer.length)
codeCompletion(bufferWithMultiLine, cursor1)
}

private var lastRequest = NoRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ class SimpleReader(in: BufferedReader, out: JPrintWriter, val completion: Comple
input
}

protected def readOneKey(prompt: String) = throw new IllegalStateException("No char-based input in SimpleReader")

protected def readOneLine(): String = in.readLine()
protected def echo(s: String): Unit = if (interactive) {
out.print(s)
Expand Down
8 changes: 6 additions & 2 deletions src/repl/scala/tools/nsc/interpreter/IMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,10 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade
}

// parseStats, returning status but no trees
def parseString(line: String): Result = parse(line).fold(e => e, _ => Success)
def parseString(line: String): Result =
reporter.suppressOutput {
parse(line).fold(e => e, _ => Success)
}

def tokenize(line: String): List[TokenData] = {
import collection.mutable.ListBuffer
Expand All @@ -479,8 +482,9 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade
u.nextToken()
}
b += u.lastOffset
import scala.tools.nsc.ast.parser.Tokens.isIdentifier
b.drop(1).grouped(3).flatMap(triple => triple.toList match {
case List(token, start, end) => Some(TokenData(token, start, end))
case List(token, start, end) => Some(TokenData(token, start, end, isIdentifier(token)))
case _ => println(s"Skipping token ${scala.runtime.ScalaRunTime.stringOf(triple)}") ; None
}).toList
}
Expand Down
2 changes: 1 addition & 1 deletion src/repl/scala/tools/nsc/interpreter/Interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,4 @@ trait PresentationCompilationResult {
def candidates(tabCount: Int): (Int, List[String])
}

case class TokenData(token: Int, start: Int, end: Int)
case class TokenData(token: Int, start: Int, end: Int, isIdentifier: Boolean)
8 changes: 8 additions & 0 deletions test/files/run/repl-previous-result.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

scala> "foobar"
val res0: String = foobar

scala> .size
val res1: Int = 6

scala> :quit
8 changes: 8 additions & 0 deletions test/files/run/repl-previous-result.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.tools.partest.ReplTest

object Test extends ReplTest {
override def code = """
|"foobar"
|.size
""".stripMargin.trim
}