Skip to content

Commit

Permalink
Merge pull request scala#4564 from som-snytt/issue/prompt
Browse files Browse the repository at this point in the history
SI-9206 Fix REPL code indentation
  • Loading branch information
adriaanm committed Jun 22, 2015
2 parents c3b6cfa + 7968421 commit 1fbce46
Show file tree
Hide file tree
Showing 30 changed files with 315 additions and 268 deletions.
2 changes: 1 addition & 1 deletion src/compiler/scala/tools/nsc/Properties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ object Properties extends scala.util.PropertiesTrait {

// settings based on jar properties, falling back to System prefixed by "scala."
def residentPromptString = scalaPropOrElse("resident.prompt", "\nnsc> ")
def shellPromptString = scalaPropOrElse("shell.prompt", "\nscala> ")
def shellPromptString = scalaPropOrElse("shell.prompt", "%nscala> ")
// message to display at EOF (which by default ends with
// a newline so as not to break the user's terminal)
def shellInterruptedString = scalaPropOrElse("shell.interrupted", f":quit$lineSeparator")
Expand Down
7 changes: 4 additions & 3 deletions src/library/scala/sys/BooleanProp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ object BooleanProp {
def valueIsTrue[T](key: String): BooleanProp = new BooleanPropImpl(key, _.toLowerCase == "true")

/** As an alternative, this method creates a BooleanProp which is true
* if the key exists in the map. This way -Dfoo.bar is enough to be
* considered true.
* if the key exists in the map and is not assigned a value other than "true",
* compared case-insensitively, or the empty string. This way -Dmy.property
* results in a true-valued property, but -Dmy.property=false does not.
*
* @return A BooleanProp with a liberal truth policy
*/
def keyExists[T](key: String): BooleanProp = new BooleanPropImpl(key, _ => true)
def keyExists[T](key: String): BooleanProp = new BooleanPropImpl(key, s => s == "" || s.equalsIgnoreCase("true"))

/** A constant true or false property which ignores all method calls.
*/
Expand Down
12 changes: 7 additions & 5 deletions src/partest-extras/scala/tools/partest/ReplTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,20 @@ abstract class SessionTest extends ReplTest {
* Retain user input: prompt lines and continuations, without the prefix; or pasted text plus ctl-D.
*/
import SessionTest._
override final def code = input findAllMatchIn (expected mkString ("", "\n", "\n")) map {
case input(null, null, prompted) =>
lazy val pasted = input(prompt)
override final def code = pasted findAllMatchIn (expected mkString ("", "\n", "\n")) map {
case pasted(null, null, prompted) =>
def continued(m: Match): Option[String] = m match {
case margin(text) => Some(text)
case _ => None
}
margin.replaceSomeIn(prompted, continued)
case input(cmd, pasted, null) =>
case pasted(cmd, pasted, null) =>
cmd + pasted + "\u0004"
} mkString

final def prompt = "scala> "
// Just the last line of the interactive prompt
def prompt = "scala> "

/** Default test is to compare expected and actual output and emit the diff on a failed comparison. */
override def show() = {
Expand All @@ -98,7 +100,7 @@ abstract class SessionTest extends ReplTest {
}
object SessionTest {
// \R for line break is Java 8, \v for vertical space might suffice
val input = """(?m)^scala> (:pa.*\u000A)// Entering paste mode.*\u000A\u000A((?:.*\u000A)*)\u000A// Exiting paste mode.*\u000A|^scala> (.*\u000A(?:\s*\| .*\u000A)*)""".r
def input(prompt: String) = s"""(?m)^$prompt(:pa.*\u000A)// Entering paste mode.*\u000A\u000A((?:.*\u000A)*)\u000A// Exiting paste mode.*\u000A|^scala> (.*\u000A(?:\\s*\\| .*\u000A)*)""".r

val margin = """(?m)^\s*\| (.*)$""".r
}
27 changes: 12 additions & 15 deletions src/repl/scala/tools/nsc/interpreter/Formatting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,25 @@ package interpreter

import util.stringFromWriter

trait Formatting {
def prompt: String
class Formatting(indent: Int) {

def spaces(code: String): String = {
private val indentation = " " * indent

private def indenting(code: String): Boolean = {
/** Heuristic to avoid indenting and thereby corrupting """-strings and XML literals. */
val tokens = List("\"\"\"", "</", "/>")
val noIndent = (code contains "\n") && (tokens exists code.contains)

if (noIndent) ""
else prompt drop 1 map (_ => ' ')
!noIndent
}
/** Indent some code by the width of the scala> prompt.
* This way, compiler error messages read better.
*/
def indentCode(code: String) = {
val indent = spaces(code)
stringFromWriter(str =>
for (line <- code.lines) {
str print indent
str print (line + "\n")
str.flush()
}
)
}
def indentCode(code: String) = stringFromWriter(str =>
for (line <- code.lines) {
if (indenting(code)) str print indentation
str println line
str.flush()
}
)
}
110 changes: 51 additions & 59 deletions src/repl/scala/tools/nsc/interpreter/ILoop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,10 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
}

class ILoopInterpreter extends IMain(settings, out) {
outer =>

override lazy val formatting = new Formatting {
def prompt = ILoop.this.prompt
}
// the expanded prompt but without color escapes and without leading newline, for purposes of indenting
override lazy val formatting: Formatting = new Formatting(
(replProps.promptString format Properties.versionNumberString).lines.toList.last.length
)
override protected def parentClassLoader =
settings.explicitParentLoader.getOrElse( classOf[ILoop].getClassLoader )
}
Expand Down Expand Up @@ -199,10 +198,8 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
echo("%d %s".format(index + offset, line))
}

private val currentPrompt = Properties.shellPromptString

/** Prompt to print when awaiting input */
def prompt = currentPrompt
def prompt = replProps.prompt

import LoopCommand.{ cmd, nullary }

Expand Down Expand Up @@ -412,14 +409,8 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
}

private def readOneLine() = {
import scala.io.AnsiColor.{ MAGENTA, RESET }
out.flush()
in readLine (
if (replProps.colorOk)
MAGENTA + prompt + RESET
else
prompt
)
in readLine prompt
}

/** The main read-eval-print loop for the repl. It calls
Expand Down Expand Up @@ -770,8 +761,13 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
}

private object paste extends Pasted {
import scala.util.matching.Regex.quote
val ContinueString = " | "
val PromptString = "scala> "
val PromptString = prompt.lines.toList.last
val anyPrompt = s"""\\s*(?:${quote(PromptString.trim)}|${quote(AltPromptString.trim)})\\s*""".r

def isPrompted(line: String) = matchesPrompt(line)
def isPromptOnly(line: String) = line match { case anyPrompt() => true ; case _ => false }

def interpret(line: String): Unit = {
echo(line.trim)
Expand All @@ -781,10 +777,17 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)

def transcript(start: String) = {
echo("\n// Detected repl transcript paste: ctrl-D to finish.\n")
apply(Iterator(start) ++ readWhile(_.trim != PromptString.trim))
apply(Iterator(start) ++ readWhile(!isPromptOnly(_)))
}

def unapply(line: String): Boolean = isPrompted(line)
}

private object invocation {
def unapply(line: String): Boolean = Completion.looksLikeInvocation(line)
}
import paste.{ ContinueString, PromptString }

private val lineComment = """\s*//.*""".r // all comment

/** Interpret expressions starting with the first line.
* Read lines until a complete compilation unit is available
Expand All @@ -796,53 +799,42 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
// signal completion non-completion input has been received
in.completion.resetVerbosity()

def reallyInterpret = {
val reallyResult = intp.interpret(code)
(reallyResult, reallyResult match {
case IR.Error => None
case IR.Success => Some(code)
case IR.Incomplete =>
if (in.interactive && code.endsWith("\n\n")) {
echo("You typed two blank lines. Starting a new command.")
def reallyInterpret = intp.interpret(code) match {
case IR.Error => None
case IR.Success => Some(code)
case IR.Incomplete if in.interactive && code.endsWith("\n\n") =>
echo("You typed two blank lines. Starting a new command.")
None
case IR.Incomplete =>
in.readLine(paste.ContinueString) 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. So we send
// it straight to the compiler for the nice error message.
intp.compileString(code)
None
}
else in.readLine(ContinueString) 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. So we send
// it straight to the compiler for the nice error message.
intp.compileString(code)
None

case line => interpretStartingWith(code + "\n" + line)
}
})

case line => interpretStartingWith(code + "\n" + line)
}
}

/** Here we place ourselves between the user and the interpreter and examine
* the input they are ostensibly submitting. We intervene in several cases:
/* 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.
* 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.
*/
if (code == "") None
else if (!paste.running && code.trim.startsWith(PromptString)) {
paste.transcript(code)
None
}
else if (Completion.looksLikeInvocation(code) && intp.mostRecentVar != "") {
interpretStartingWith(intp.mostRecentVar + code)
code match {
case "" => None
case lineComment() => None // line comment, do nothing
case paste() if !paste.running => paste.transcript(code) ; None
case invocation() if intp.mostRecentVar != "" => interpretStartingWith(intp.mostRecentVar + code)
case _ => reallyInterpret
}
else if (code.trim startsWith "//") {
// line comment, do nothing
None
}
else
reallyInterpret._2
}

// runs :load `file` on any files passed via -i
Expand Down
19 changes: 10 additions & 9 deletions src/repl/scala/tools/nsc/interpreter/IMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,13 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
def this(factory: ScriptEngineFactory) = this(factory, new Settings())
def this() = this(new Settings())

lazy val formatting: Formatting = new Formatting {
val prompt = Properties.shellPromptString
}
// the expanded prompt but without color escapes and without leading newline, for purposes of indenting
lazy val formatting: Formatting = new Formatting(
(replProps.promptString format Properties.versionNumberString).lines.toList.last.length
)
lazy val reporter: ReplReporter = new ReplReporter(this)

import formatting._
import formatting.indentCode
import reporter.{ printMessage, printUntruncatedMessage }

// This exists mostly because using the reporter too early leads to deadlock.
Expand Down Expand Up @@ -468,7 +469,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
}

private def requestFromLine(line: String, synthetic: Boolean): Either[IR.Result, Request] = {
val content = indentCode(line)
val content = line //indentCode(line)
val trees = parse(content) match {
case parse.Incomplete => return Left(IR.Incomplete)
case parse.Error => return Left(IR.Error)
Expand Down Expand Up @@ -909,10 +910,10 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
else List("def %s = %s".format("$line", tquoted(originalLine)), "def %s = Nil".format("$trees"))
}
def preamble = s"""
|$preambleHeader
|%s%s%s
""".stripMargin.format(lineRep.readName, envLines.map(" " + _ + ";\n").mkString,
importsPreamble, indentCode(toCompute))
|${preambleHeader format lineRep.readName}
|${envLines mkString (" ", ";\n ", ";\n")}
|$importsPreamble
|${indentCode(toCompute)}""".stripMargin

val generate = (m: MemberHandler) => m extraCodeToEvaluate Request.this

Expand Down
22 changes: 14 additions & 8 deletions src/repl/scala/tools/nsc/interpreter/Pasted.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ package interpreter
* the same result.
*/
abstract class Pasted {
def interpret(line: String): Unit
def ContinueString: String
def PromptString: String
def interpret(line: String): Unit
def AltPromptString: String = "scala> "

private val testBoth = PromptString != AltPromptString
private val spacey = " \t".toSet

def matchesPrompt(line: String) = matchesString(line, PromptString)
def matchesPrompt(line: String) = matchesString(line, PromptString) || testBoth && matchesString(line, AltPromptString)
def matchesContinue(line: String) = matchesString(line, ContinueString)
def running = isRunning

private def matchesString(line: String, target: String): Boolean = (
(line startsWith target) ||
(line.nonEmpty && " \t".toSet(line.head) && matchesString(line.tail, target))
(line.nonEmpty && spacey(line.head) && matchesString(line.tail, target))
)
private def stripString(line: String, target: String) = line indexOf target match {
case -1 => line
Expand All @@ -39,7 +43,9 @@ abstract class Pasted {

private class PasteAnalyzer(val lines: List[String]) {
val referenced = lines flatMap (resReference findAllIn _.trim.stripPrefix("res")) toSet
val cmds = lines reduceLeft append split PromptString filterNot (_.trim == "") toList
val ActualPromptString = lines find matchesPrompt map (s =>
if (matchesString(s, PromptString)) PromptString else AltPromptString) getOrElse PromptString
val cmds = lines reduceLeft append split ActualPromptString filterNot (_.trim == "") toList

/** If it's a prompt or continuation line, strip the formatting bits and
* assemble the code. Otherwise ship it off to be analyzed for res references
Expand Down Expand Up @@ -67,10 +73,10 @@ abstract class Pasted {
*/
def fixResRefs(code: String, line: String) = line match {
case resCreation(resName) if referenced(resName) =>
code.lastIndexOf(PromptString) match {
code.lastIndexOf(ActualPromptString) match {
case -1 => code
case idx =>
val (str1, str2) = code splitAt (idx + PromptString.length)
val (str1, str2) = code splitAt (idx + ActualPromptString.length)
str2 match {
case resAssign(`resName`) => code
case _ => "%sval %s = { %s }".format(str1, resName, str2)
Expand All @@ -79,10 +85,10 @@ abstract class Pasted {
case _ => code
}

def run() {
def run(): Unit = {
println("// Replaying %d commands from transcript.\n" format cmds.size)
cmds foreach { cmd =>
print(PromptString)
print(ActualPromptString)
interpret(cmd)
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/repl/scala/tools/nsc/interpreter/ReplProps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
package scala.tools.nsc
package interpreter

import Properties.shellPromptString
import scala.sys._
import Prop._

class ReplProps {
private def bool(name: String) = BooleanProp.keyExists(name)
private def int(name: String) = IntProp(name)
private def int(name: String) = Prop[Int](name)

// This property is used in TypeDebugging. Let's recycle it.
val colorOk = bool("scala.color")
Expand All @@ -21,6 +22,14 @@ class ReplProps {
val trace = bool("scala.repl.trace")
val power = bool("scala.repl.power")

// Handy system prop for shell prompt, or else pick it up from compiler.properties
val promptString = Prop[String]("scala.repl.prompt").option getOrElse (if (info) "%nscala %s> " else shellPromptString)
val prompt = {
import scala.io.AnsiColor.{ MAGENTA, RESET }
val p = promptString format Properties.versionNumberString
if (colorOk) s"$MAGENTA$p$RESET" else p
}

/** CSV of paged,across to enable pagination or `-x` style
* columns, "across" instead of down the column. Since
* pagination turns off columnar output, these flags are
Expand Down
Loading

0 comments on commit 1fbce46

Please sign in to comment.