Skip to content
This repository was archived by the owner on Sep 1, 2020. It is now read-only.

Commit 1fbce46

Browse files
committed
Merge pull request scala#4564 from som-snytt/issue/prompt
SI-9206 Fix REPL code indentation
2 parents c3b6cfa + 7968421 commit 1fbce46

30 files changed

+315
-268
lines changed

src/compiler/scala/tools/nsc/Properties.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ object Properties extends scala.util.PropertiesTrait {
1313

1414
// settings based on jar properties, falling back to System prefixed by "scala."
1515
def residentPromptString = scalaPropOrElse("resident.prompt", "\nnsc> ")
16-
def shellPromptString = scalaPropOrElse("shell.prompt", "\nscala> ")
16+
def shellPromptString = scalaPropOrElse("shell.prompt", "%nscala> ")
1717
// message to display at EOF (which by default ends with
1818
// a newline so as not to break the user's terminal)
1919
def shellInterruptedString = scalaPropOrElse("shell.interrupted", f":quit$lineSeparator")

src/library/scala/sys/BooleanProp.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,13 @@ object BooleanProp {
6363
def valueIsTrue[T](key: String): BooleanProp = new BooleanPropImpl(key, _.toLowerCase == "true")
6464

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

7374
/** A constant true or false property which ignores all method calls.
7475
*/

src/partest-extras/scala/tools/partest/ReplTest.scala

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,20 @@ abstract class SessionTest extends ReplTest {
7575
* Retain user input: prompt lines and continuations, without the prefix; or pasted text plus ctl-D.
7676
*/
7777
import SessionTest._
78-
override final def code = input findAllMatchIn (expected mkString ("", "\n", "\n")) map {
79-
case input(null, null, prompted) =>
78+
lazy val pasted = input(prompt)
79+
override final def code = pasted findAllMatchIn (expected mkString ("", "\n", "\n")) map {
80+
case pasted(null, null, prompted) =>
8081
def continued(m: Match): Option[String] = m match {
8182
case margin(text) => Some(text)
8283
case _ => None
8384
}
8485
margin.replaceSomeIn(prompted, continued)
85-
case input(cmd, pasted, null) =>
86+
case pasted(cmd, pasted, null) =>
8687
cmd + pasted + "\u0004"
8788
} mkString
8889

89-
final def prompt = "scala> "
90+
// Just the last line of the interactive prompt
91+
def prompt = "scala> "
9092

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

103105
val margin = """(?m)^\s*\| (.*)$""".r
104106
}

src/repl/scala/tools/nsc/interpreter/Formatting.scala

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,25 @@ package interpreter
88

99
import util.stringFromWriter
1010

11-
trait Formatting {
12-
def prompt: String
11+
class Formatting(indent: Int) {
1312

14-
def spaces(code: String): String = {
13+
private val indentation = " " * indent
14+
15+
private def indenting(code: String): Boolean = {
1516
/** Heuristic to avoid indenting and thereby corrupting """-strings and XML literals. */
1617
val tokens = List("\"\"\"", "</", "/>")
1718
val noIndent = (code contains "\n") && (tokens exists code.contains)
1819

19-
if (noIndent) ""
20-
else prompt drop 1 map (_ => ' ')
20+
!noIndent
2121
}
2222
/** Indent some code by the width of the scala> prompt.
2323
* This way, compiler error messages read better.
2424
*/
25-
def indentCode(code: String) = {
26-
val indent = spaces(code)
27-
stringFromWriter(str =>
28-
for (line <- code.lines) {
29-
str print indent
30-
str print (line + "\n")
31-
str.flush()
32-
}
33-
)
34-
}
25+
def indentCode(code: String) = stringFromWriter(str =>
26+
for (line <- code.lines) {
27+
if (indenting(code)) str print indentation
28+
str println line
29+
str.flush()
30+
}
31+
)
3532
}

src/repl/scala/tools/nsc/interpreter/ILoop.scala

Lines changed: 51 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,10 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
111111
}
112112

113113
class ILoopInterpreter extends IMain(settings, out) {
114-
outer =>
115-
116-
override lazy val formatting = new Formatting {
117-
def prompt = ILoop.this.prompt
118-
}
114+
// the expanded prompt but without color escapes and without leading newline, for purposes of indenting
115+
override lazy val formatting: Formatting = new Formatting(
116+
(replProps.promptString format Properties.versionNumberString).lines.toList.last.length
117+
)
119118
override protected def parentClassLoader =
120119
settings.explicitParentLoader.getOrElse( classOf[ILoop].getClassLoader )
121120
}
@@ -199,10 +198,8 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter)
199198
echo("%d %s".format(index + offset, line))
200199
}
201200

202-
private val currentPrompt = Properties.shellPromptString
203-
204201
/** Prompt to print when awaiting input */
205-
def prompt = currentPrompt
202+
def prompt = replProps.prompt
206203

207204
import LoopCommand.{ cmd, nullary }
208205

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

414411
private def readOneLine() = {
415-
import scala.io.AnsiColor.{ MAGENTA, RESET }
416412
out.flush()
417-
in readLine (
418-
if (replProps.colorOk)
419-
MAGENTA + prompt + RESET
420-
else
421-
prompt
422-
)
413+
in readLine prompt
423414
}
424415

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

772763
private object paste extends Pasted {
764+
import scala.util.matching.Regex.quote
773765
val ContinueString = " | "
774-
val PromptString = "scala> "
766+
val PromptString = prompt.lines.toList.last
767+
val anyPrompt = s"""\\s*(?:${quote(PromptString.trim)}|${quote(AltPromptString.trim)})\\s*""".r
768+
769+
def isPrompted(line: String) = matchesPrompt(line)
770+
def isPromptOnly(line: String) = line match { case anyPrompt() => true ; case _ => false }
775771

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

782778
def transcript(start: String) = {
783779
echo("\n// Detected repl transcript paste: ctrl-D to finish.\n")
784-
apply(Iterator(start) ++ readWhile(_.trim != PromptString.trim))
780+
apply(Iterator(start) ++ readWhile(!isPromptOnly(_)))
785781
}
782+
783+
def unapply(line: String): Boolean = isPrompted(line)
784+
}
785+
786+
private object invocation {
787+
def unapply(line: String): Boolean = Completion.looksLikeInvocation(line)
786788
}
787-
import paste.{ ContinueString, PromptString }
789+
790+
private val lineComment = """\s*//.*""".r // all comment
788791

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

799-
def reallyInterpret = {
800-
val reallyResult = intp.interpret(code)
801-
(reallyResult, reallyResult match {
802-
case IR.Error => None
803-
case IR.Success => Some(code)
804-
case IR.Incomplete =>
805-
if (in.interactive && code.endsWith("\n\n")) {
806-
echo("You typed two blank lines. Starting a new command.")
802+
def reallyInterpret = intp.interpret(code) match {
803+
case IR.Error => None
804+
case IR.Success => Some(code)
805+
case IR.Incomplete if in.interactive && code.endsWith("\n\n") =>
806+
echo("You typed two blank lines. Starting a new command.")
807+
None
808+
case IR.Incomplete =>
809+
in.readLine(paste.ContinueString) match {
810+
case null =>
811+
// we know compilation is going to fail since we're at EOF and the
812+
// parser thinks the input is still incomplete, but since this is
813+
// a file being read non-interactively we want to fail. So we send
814+
// it straight to the compiler for the nice error message.
815+
intp.compileString(code)
807816
None
808-
}
809-
else in.readLine(ContinueString) match {
810-
case null =>
811-
// we know compilation is going to fail since we're at EOF and the
812-
// parser thinks the input is still incomplete, but since this is
813-
// a file being read non-interactively we want to fail. So we send
814-
// it straight to the compiler for the nice error message.
815-
intp.compileString(code)
816-
None
817-
818-
case line => interpretStartingWith(code + "\n" + line)
819-
}
820-
})
817+
818+
case line => interpretStartingWith(code + "\n" + line)
819+
}
821820
}
822821

823-
/** Here we place ourselves between the user and the interpreter and examine
824-
* the input they are ostensibly submitting. We intervene in several cases:
822+
/* Here we place ourselves between the user and the interpreter and examine
823+
* the input they are ostensibly submitting. We intervene in several cases:
825824
*
826-
* 1) If the line starts with "scala> " it is assumed to be an interpreter paste.
827-
* 2) If the line starts with "." (but not ".." or "./") it is treated as an invocation
828-
* on the previous result.
829-
* 3) If the Completion object's execute returns Some(_), we inject that value
830-
* and avoid the interpreter, as it's likely not valid scala code.
825+
* 1) If the line starts with "scala> " it is assumed to be an interpreter paste.
826+
* 2) If the line starts with "." (but not ".." or "./") it is treated as an invocation
827+
* on the previous result.
828+
* 3) If the Completion object's execute returns Some(_), we inject that value
829+
* and avoid the interpreter, as it's likely not valid scala code.
831830
*/
832-
if (code == "") None
833-
else if (!paste.running && code.trim.startsWith(PromptString)) {
834-
paste.transcript(code)
835-
None
836-
}
837-
else if (Completion.looksLikeInvocation(code) && intp.mostRecentVar != "") {
838-
interpretStartingWith(intp.mostRecentVar + code)
831+
code match {
832+
case "" => None
833+
case lineComment() => None // line comment, do nothing
834+
case paste() if !paste.running => paste.transcript(code) ; None
835+
case invocation() if intp.mostRecentVar != "" => interpretStartingWith(intp.mostRecentVar + code)
836+
case _ => reallyInterpret
839837
}
840-
else if (code.trim startsWith "//") {
841-
// line comment, do nothing
842-
None
843-
}
844-
else
845-
reallyInterpret._2
846838
}
847839

848840
// runs :load `file` on any files passed via -i

src/repl/scala/tools/nsc/interpreter/IMain.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,13 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set
112112
def this(factory: ScriptEngineFactory) = this(factory, new Settings())
113113
def this() = this(new Settings())
114114

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

120-
import formatting._
121+
import formatting.indentCode
121122
import reporter.{ printMessage, printUntruncatedMessage }
122123

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

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

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

src/repl/scala/tools/nsc/interpreter/Pasted.scala

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ package interpreter
1616
* the same result.
1717
*/
1818
abstract class Pasted {
19+
def interpret(line: String): Unit
1920
def ContinueString: String
2021
def PromptString: String
21-
def interpret(line: String): Unit
22+
def AltPromptString: String = "scala> "
23+
24+
private val testBoth = PromptString != AltPromptString
25+
private val spacey = " \t".toSet
2226

23-
def matchesPrompt(line: String) = matchesString(line, PromptString)
27+
def matchesPrompt(line: String) = matchesString(line, PromptString) || testBoth && matchesString(line, AltPromptString)
2428
def matchesContinue(line: String) = matchesString(line, ContinueString)
2529
def running = isRunning
2630

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

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

4450
/** If it's a prompt or continuation line, strip the formatting bits and
4551
* assemble the code. Otherwise ship it off to be analyzed for res references
@@ -67,10 +73,10 @@ abstract class Pasted {
6773
*/
6874
def fixResRefs(code: String, line: String) = line match {
6975
case resCreation(resName) if referenced(resName) =>
70-
code.lastIndexOf(PromptString) match {
76+
code.lastIndexOf(ActualPromptString) match {
7177
case -1 => code
7278
case idx =>
73-
val (str1, str2) = code splitAt (idx + PromptString.length)
79+
val (str1, str2) = code splitAt (idx + ActualPromptString.length)
7480
str2 match {
7581
case resAssign(`resName`) => code
7682
case _ => "%sval %s = { %s }".format(str1, resName, str2)
@@ -79,10 +85,10 @@ abstract class Pasted {
7985
case _ => code
8086
}
8187

82-
def run() {
88+
def run(): Unit = {
8389
println("// Replaying %d commands from transcript.\n" format cmds.size)
8490
cmds foreach { cmd =>
85-
print(PromptString)
91+
print(ActualPromptString)
8692
interpret(cmd)
8793
}
8894
}

src/repl/scala/tools/nsc/interpreter/ReplProps.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
package scala.tools.nsc
77
package interpreter
88

9+
import Properties.shellPromptString
910
import scala.sys._
1011
import Prop._
1112

1213
class ReplProps {
1314
private def bool(name: String) = BooleanProp.keyExists(name)
14-
private def int(name: String) = IntProp(name)
15+
private def int(name: String) = Prop[Int](name)
1516

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

25+
// Handy system prop for shell prompt, or else pick it up from compiler.properties
26+
val promptString = Prop[String]("scala.repl.prompt").option getOrElse (if (info) "%nscala %s> " else shellPromptString)
27+
val prompt = {
28+
import scala.io.AnsiColor.{ MAGENTA, RESET }
29+
val p = promptString format Properties.versionNumberString
30+
if (colorOk) s"$MAGENTA$p$RESET" else p
31+
}
32+
2433
/** CSV of paged,across to enable pagination or `-x` style
2534
* columns, "across" instead of down the column. Since
2635
* pagination turns off columnar output, these flags are

0 commit comments

Comments
 (0)