summaryrefslogtreecommitdiff
path: root/src/repl/scala/tools/nsc/interpreter/Pasted.scala
blob: 7ab5e5bb4207df64ae98f4b1bfa517f034779a04 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/* NSC -- new Scala compiler
 * Copyright 2005-2013 LAMP/EPFL
 * @author Paul Phillips
 */

package scala.tools.nsc
package interpreter

/** If it looks like they're pasting in a scala interpreter
 *  transcript, remove all the formatting we inserted so we
 *  can make some sense of it.
 *
 *  Most of the interesting code in here is due to my goal of
 *  "paste idempotence" i.e. the transcript resulting from pasting
 *  a transcript should itself be pasteable and should achieve
 *  the same result.
 */
abstract class Pasted(prompt: String) {
  def interpret(line: String): IR.Result
  def echo(message: String): Unit

  val PromptString    = prompt.lines.toList.last
  val AltPromptString = "scala> "
  val ContinuePrompt  = replProps.continuePrompt
  val ContinueString  = replProps.continueText     // "     | "
  val anyPrompt = {
    import scala.util.matching.Regex.quote
    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 }

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

  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 && spacey(line.head) && matchesString(line.tail, target))

  private def stripString(line: String, target: String) = line indexOf target match {
    case -1   => line
    case idx  => line drop (idx + target.length)
  }
  private var isRunning    = false
  private val resReference = """(?<!^)(res\d+)""".r
  private val resCreation  = """^\s*(res\d+):.*""".r
  private val resAssign    = """^val (res\d+).*""".r

  private class PasteAnalyzer(val lines: List[String]) {
    val referenced = lines flatMap (resReference findAllIn _.trim.stripPrefix("res")) toSet
    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
     *  and discarded.
     */
    def append(code: String, line: String): String =
      if (matchesPrompt(line)) code + "\n" + line
      else if (matchesContinue(line)) code + "\n" + stripString(line, ContinueString)
      else fixResRefs(code, line)

    /** If the line looks like
     *    res15: Int
     *
     *  and the additional conditions hold that:
     *    1) res15 is referenced from elsewhere in the transcript
     *    2) the preceding repl line is not "val res15 = ..." because that
     *    indicates it has already been "val-ified" on a previous paste
     *
     *  then we go back in time to the preceding scala> prompt and
     *  rewrite the line containing <expr> as
     *    val res15 = { <expr> }
     *  and the rest as they say is rewritten history.
     *
     *  In all other cases, discard the line.
     */
    def fixResRefs(code: String, line: String) = line match {
      case resCreation(resName) if referenced(resName) =>
        code.lastIndexOf(ActualPromptString) match {
          case -1   => code
          case idx  =>
            val (str1, str2) = code splitAt (idx + ActualPromptString.length)
            str2 match {
              case resAssign(`resName`) => code
              case _                    => "%sval %s = { %s }".format(str1, resName, str2)
            }
        }
      case _ => code
    }

    def interpreted(line: String) = {
      echo(line.trim)
      val res = interpret(line)
      if (res != IR.Incomplete) echo("")
      res
    }
    def incompletely(cmd: String) = {
      print(ActualPromptString)
      interpreted(cmd) == IR.Incomplete
    }
    def run(): Option[String] = {
      echo(s"// Replaying ${cmds.size} commands from transcript.\n")
      cmds find incompletely
    }
  }

  // Run transcript and return incomplete line if any.
  def transcript(lines: TraversableOnce[String]): Option[String] = {
    echo("\n// Detected repl transcript. Paste more, or ctrl-D to finish.\n")
    apply(lines)
  }

  /** Commands start on lines beginning with "scala>" and each successive
   *  line which begins with the continuation string is appended to that command.
   *  Everything else is discarded.  When the end of the transcript is spotted,
   *  all the commands are replayed.
   */
  def apply(lines: TraversableOnce[String]): Option[String] = {
    isRunning = true
    try new PasteAnalyzer(lines.toList).run()
    finally isRunning = false
  }
  def unapply(line: String): Boolean = isPrompted(line)
}