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]]) { 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.typeTag.tpe}]
    """
  }.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
  )
  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