All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ammonite.repl.Repl.scala Maven / Gradle / Ivy

package ammonite.repl

import java.io.{InputStream, InputStreamReader, OutputStream}

import ammonite.repl.api.{FrontEnd, History, ReplLoad}
import ammonite.runtime._
import ammonite.terminal.Filter
import ammonite.util.Util.{newLine, normalizeNewlines}
import ammonite.util._
import ammonite.compiler.iface.{CodeWrapper, CompilerBuilder, Parser}
import ammonite.interp.Interpreter
import coursierapi.Dependency

import scala.annotation.tailrec

class Repl(
    input: InputStream,
    output: OutputStream,
    error: OutputStream,
    storage: Storage,
    baseImports: Imports,
    basePredefs: Seq[PredefInfo],
    customPredefs: Seq[PredefInfo],
    wd: os.Path,
    welcomeBanner: Option[String],
    replArgs: IndexedSeq[Bind[_]] = Vector.empty,
    initialColors: Colors = Colors.Default,
    replCodeWrapper: CodeWrapper,
    scriptCodeWrapper: CodeWrapper,
    alreadyLoadedDependencies: Seq[Dependency],
    importHooks: Map[Seq[String], ImportHook],
    compilerBuilder: CompilerBuilder,
    parser: Parser,
    initialClassLoader: ClassLoader =
      classOf[ammonite.repl.api.ReplAPI].getClassLoader,
    classPathWhitelist: Set[Seq[String]],
    warnings: Boolean
) { repl =>

  val prompt = Ref("@ ")

  val frontEnd = Ref[FrontEnd](
    if (scala.util.Properties.isWin)
      new ammonite.repl.FrontEnds.JLineWindows(parser)
    else
      AmmoniteFrontEnd(parser, Filter.empty)
  )

  var lastException: Throwable = null

  var history = new History(Vector())

  val (colors, printer) =
    Interpreter.initPrinters(initialColors, output, error, true)

  val argString = replArgs.zipWithIndex.map { case (b, idx) =>
    s"""
    val ${b.name} =
      ammonite.repl.ReplBridge.value.Internal.replArgs($idx).value.asInstanceOf[${b.typeName.value}]
    """
  }.mkString(newLine)

  val frames = Ref(List(ammonite.runtime.Frame.createInitial(initialClassLoader)))

  /**
   * The current line number of the REPL, used to make sure every snippet
   * evaluated can have a distinct name that doesn't collide.
   */
  var currentLine = 0

  val sess0 = new SessionApiImpl(frames)

  def imports = frames().head.imports
  def fullImports = interp.predefImports ++ imports

  def usedEarlierDefinitions = frames().head.usedEarlierDefinitions

  val interpParams = Interpreter.Parameters(
    printer = printer,
    storage = storage,
    wd = wd,
    colors = colors,
    verboseOutput = true,
    initialClassLoader = initialClassLoader,
    importHooks = importHooks,
    classPathWhitelist = classPathWhitelist,
    alreadyLoadedDependencies = alreadyLoadedDependencies,
    warnings = warnings
  )
  val interp = new Interpreter(
    compilerBuilder,
    () => parser,
    getFrame = () => frames().head,
    createFrame = () => { val f = sess0.childFrame(frames().head); frames() = f :: frames(); f },
    replCodeWrapper = replCodeWrapper,
    scriptCodeWrapper = scriptCodeWrapper,
    parameters = interpParams
  )

  val bridges = Seq(
    (
      "ammonite.repl.ReplBridge",
      "repl",
      new ReplApiImpl {
        def replArgs0 = repl.replArgs
        def printer = repl.printer
        val colors = repl.colors
        def sess = repl.sess0
        val prompt = repl.prompt
        val frontEnd = repl.frontEnd

        def lastException = repl.lastException
        def fullHistory = storage.fullHistory()
        def history = repl.history
        def newCompiler() = interp.compilerManager.init(force = true)
        def fullImports = repl.fullImports
        def imports = repl.imports
        def usedEarlierDefinitions = repl.usedEarlierDefinitions
        def width = frontEnd().width
        def height = frontEnd().height

        object load extends ReplLoad with (String => Unit) {

          def apply(line: String) = {
            interp.processExec(line, currentLine, () => currentLine += 1) match {
              case Res.Failure(s) => throw new CompilationError(s)
              case Res.Exception(t, s) => throw t
              case _ =>
            }
          }

          def exec(file: os.Path): Unit = {
            interp.watch(file)
            apply(normalizeNewlines(os.read(file)))
          }
        }

        def _compilerManager = interp.compilerManager
      }
    ),
    (
      "ammonite.repl.api.FrontEndBridge",
      "frontEnd",
      new FrontEndAPIImpl {
        def parser = repl.parser
      }
    )
  )

  def initializePredef() = interp.initializePredef(basePredefs, customPredefs, bridges, baseImports)

  def warmup() = {
    // An arbitrary input, randomized to make sure it doesn't get cached or
    // anything anywhere (though it shouldn't since it's processed as a line).
    //
    // Should exercise the main code paths that the Ammonite REPL uses, and
    // can be run asynchronously while the user is typing their first command
    // to make sure their command reaches an already-warm command when submitted.
    //
    // Otherwise, this isn't a particularly complex chunk of code and shouldn't
    // make the minimum first-compilation time significantly longer than just
    // running the user code directly. Could be made longer to better warm more
    // code paths, but then the fixed overhead gets larger so not really worth it
    val code = s"""val array = Seq.tabulate(10)(_*2).toArray.max"""
    val stmts = parser.split(code).get.toOption.get
    interp.processLine(code, stmts, 9999999, silent = true, () => () /*donothing*/ )
  }

  sess0.save()
  interp.createFrame()

  val reader = new InputStreamReader(input)

  def action() = for {
    _ <- Catching {
      case Ex(e: ThreadDeath) =>
        Thread.interrupted()
        Res.Failure("Interrupted!")

      case ex => Res.Exception(ex, "")
    }
    // workaround to wildcard imports breaking code completion, see
    // https://github.com/com-lihaoyi/Ammonite/issues/1009
    importsForCompletion = Imports(fullImports.value.filter(_.fromName.raw != "package"))
    _ <- Signaller("INT") {
      // Put a fake `ThreadDeath` error in `lastException`, because `Thread#stop`
      // raises an error with the stack trace of *this interrupt thread*, rather
      // than the stack trace of *the mainThread*
      lastException = new ThreadDeath()
      lastException.setStackTrace(Repl.truncateStackTrace(interp.mainThread.getStackTrace))
      interp.mainThread.stop()
    }
    (code, stmts) <- frontEnd().action(
      input,
      reader,
      output,
      colors().prompt()(prompt()).render,
      colors(),
      interp.compilerManager.complete(_, importsForCompletion.toString, _),
      storage.fullHistory(),
      addHistory = (code) =>
        if (code != "") {
          storage.fullHistory() = storage.fullHistory() :+ code
          history = history :+ code
        }
    )
    out <- interp.processLine(code, stmts, currentLine, false, () => currentLine += 1)
  } yield {
    printer.outStream.println()
    out
  }

  def run(): Any = {
    welcomeBanner
      .map(_.replace("%SCALA_VERSION%", compilerBuilder.scalaVersion))
      .foreach(printer.outStream.println)
    @tailrec def loop(): Any = {
      val actionResult = action()
      Repl.handleOutput(interp, actionResult)
      Repl.handleRes(
        actionResult,
        printer.info,
        printer.error,
        lastException = _,
        colors()
      ) match {
        case None =>
          printer.outStream.println()
          loop()
        case Some(value) => value
      }
    }
    loop()
  }

  def beforeExit(exitValue: Any): Any = {
    Function.chain(interp.beforeExitHooks)(exitValue)
  }
}

object Repl {
  def handleOutput(interp: Interpreter, res: Res[Evaluated]): Unit = {
    res match {
      case Res.Skip => // do nothing
      case Res.Exit(value) => interp.compilerManager.shutdownPressy()
      case Res.Success(ev) =>
        interp.handleImports(ev.imports)
        if (interp.headFrame.frozen)
          interp.createFrame()
      case _ => ()
    }
  }
  def handleRes(
      res: Res[Any],
      printInfo: String => Unit,
      printError: String => Unit,
      setLastException: Throwable => Unit,
      colors: Colors
  ): Option[Any] = {
    res match {
      case Res.Exit(value) =>
        printInfo("Bye!")
        Some(value)
      case Res.Failure(msg) =>
        printError(msg)
        None
      case Res.Exception(ex, msg) =>
        setLastException(ex)
        printError(
          Repl.showException(ex, colors.error(), fansi.Attr.Reset, colors.literal())
        )
        printError(msg)
        None
      case _ =>
        None
    }
  }
  def highlightFrame(
      f: StackTraceElement,
      error: fansi.Attrs,
      highlightError: fansi.Attrs,
      source: fansi.Attrs
  ) = {
    val src =
      if (f.isNativeMethod) source("Native Method")
      else if (f.getFileName == null) source("Unknown Source")
      else {
        val lineSuffix =
          if (f.getLineNumber == -1) fansi.Str("")
          else error(":") ++ source(f.getLineNumber.toString)

        source(f.getFileName) ++ lineSuffix
      }

    val prefix :+ clsName = f.getClassName.split('.').toSeq
    val prefixString = prefix.map(_ + '.').mkString("")
    val clsNameString = clsName // .replace("$", error("$"))
    val method =
      error(prefixString) ++ highlightError(clsNameString) ++ error(".") ++
        highlightError(f.getMethodName)

    fansi.Str(s"  ") ++ method ++ "(" ++ src ++ ")"
  }
  val cutoff = Set("$main", "evaluatorRunPrinter")
  def truncateStackTrace(x: Array[StackTraceElement]) = {
    x.takeWhile(x => !cutoff(x.getMethodName))
  }

  def showException(
      ex: Throwable,
      error: fansi.Attrs,
      highlightError: fansi.Attrs,
      source: fansi.Attrs
  ) = {

    val traces = Ex.unapplySeq(ex).get.map(exception =>
      error(exception.toString + newLine +
        truncateStackTrace(exception.getStackTrace)
          .map(highlightFrame(_, error, highlightError, source))
          .mkString(newLine))
    )
    traces.mkString(newLine)
  }

  def getClassPathWhitelist(thin: Boolean): Set[Seq[String]] = {
    if (!thin) Set.empty
    else {
      os.read
        .lines(os.resource / "ammonite-api-whitelist.txt")
        .map(_.split('/').toSeq)
        .toSet
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy