Skip to content

Commit

Permalink
Merge pull request #8848 from SethTisue/jline3-fixes
Browse files Browse the repository at this point in the history
REPL: JLine 3: fix various issues
  • Loading branch information
SethTisue committed Apr 6, 2020
2 parents ede7226 + dd3cee4 commit 018d78b
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 55 deletions.
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
}

0 comments on commit 018d78b

Please sign in to comment.