Skip to content

Commit

Permalink
Added the ability to run a custom logic upon quitting the REPL
Browse files Browse the repository at this point in the history
  • Loading branch information
John Johnson II committed May 31, 2022
1 parent 962aa3f commit 266616e
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.sbt
Expand Up @@ -21,6 +21,7 @@ lazy val rebel = (project in file("."))
.settings(
name := "rebel",
libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value,
libraryDependencies += "dev.zio" %% "zio" % "2.0.0-RC6",
libraryDependencies += scalaTest % Test,
)

Expand Down
61 changes: 60 additions & 1 deletion src/main/scala/com/potenciasoftware/rebel/BaseRepl.scala
@@ -1,12 +1,16 @@
package com.potenciasoftware.rebel

import zio._

import java.lang.reflect.Field
import java.net.URLClassLoader
import scala.reflect.ClassTag
import scala.reflect.runtime.universe.TypeTag
import scala.tools.nsc.Settings
import scala.tools.nsc.interpreter.Repl
import scala.tools.nsc.interpreter.shell.Completion
import scala.tools.nsc.interpreter.shell.ILoop
import scala.tools.nsc.interpreter.shell.NoCompletion
import scala.tools.nsc.interpreter.shell.ShellConfig
import scala.tools.nsc.typechecker.TypeStrings

Expand Down Expand Up @@ -54,7 +58,7 @@ class BaseRepl {
* Override to provide bound values.
* These will be available from within the REPL.
*/
protected def boundValues: Seq[Parameter] = Seq()
protected def boundValues: Seq[Parameter] = Seq.empty

// Because ILoop declares 'prompt' to be a lazy val,
// we can't just override it or set it directly.
Expand All @@ -76,6 +80,12 @@ class BaseRepl {
*/
protected def startupScript: String = ""

/** Override to provide additional colon commands to the REPL. */
protected def customCommands: Seq[LoopCommand] = Seq.empty

/** Override to provide logic to execute when quitting the REPL. */
def onQuit(): Unit = ()

/** Read the current text of the REPL prompt. */
def prompt: String = repl.prompt

Expand Down Expand Up @@ -103,6 +113,26 @@ class BaseRepl {
intp.interpret(startupScript)
}
}

private def customQuit(q: LoopCommand): Seq[LoopCommand] =
Seq(LoopCommand.cmd(
name = q.name,
usage = q.usage,
help = q.help,
f = { line =>
delayedAction(5.seconds) { sys.exit() }
onQuit()
q(line)
},
completion = q.completion))

override def commands: List[LoopCommand] = {
val (Seq(quitCommand), cmds) =
super.commands.partition(_.name == "quit")
(cmds ++ customCommands
.map(_.convert(LoopCommand.cmd _, Result.apply)))
.sortBy(_.name) ++ customQuit(quitCommand)
}
}

def run(): Unit = {
Expand All @@ -111,8 +141,37 @@ class BaseRepl {
}

object BaseRepl {

private val WelcomePlaceholder = "%%%%welcome%%%%"

private def delayedAction(after: Duration)(action: => Unit): Unit =
Runtime.default.unsafeRunAsync {
ZIO.attempt(action)
.delay(after)
.sandbox
.catchAll(_ => ZIO.unit)
}

case class LoopCommand(
name: String,
usage: String,
help: String,
f: String => LoopCommand.Result,
completion: Completion = NoCompletion
) {
private[BaseRepl] def convert[A, B](
toCommand: (String, String, String, String => B, Completion) => A,
toResult: (Boolean, Option[String]) => B
): A =
toCommand(name, usage, help,
{ s => val r = f(s); toResult(r.keepRunning, r.lineToRecord) },
completion)
}

object LoopCommand {
case class Result(keepRunning: Boolean, lineToRecord: Option[String])
}

class Parameter private (
name: String,
`type`: String,
Expand Down
40 changes: 39 additions & 1 deletion src/test/scala/com/potenciasoftware/rebel/BaseReplTest.scala
Expand Up @@ -8,7 +8,7 @@ import scala.tools.nsc.Settings
import scala.tools.nsc.interpreter.shell.ILoop
import scala.tools.nsc.interpreter.shell.ShellConfig

import BaseRepl.Parameter
import BaseRepl._
import BaseReplTest._
import TestUtils._

Expand Down Expand Up @@ -117,6 +117,44 @@ class BaseReplTest extends AnyFlatSpec with Matchers {
"""
|scala> printAnswer()The answer is: 42""".stripMargin
}

def customCommand() = new TestRepl { repl =>
override protected def customCommands: Seq[LoopCommand] =
Seq(LoopCommand(
"ps1", "<promptText>", "Change the prompt text",
{ text =>
repl.prompt = "\n" + text
LoopCommand.Result(true, None)
}))
}

it should "allow custom commands" in {
replTest[BaseReplTest]("customCommand",
":help ps1", ":ps1 $", "1+1"
).out.map(_.trim).asBlock shouldBe
"""
|scala>
|Change the prompt text
|
|scala>
|$val res0: Int = 2
|
|$""".stripMargin
}

def customQuit() = new TestRepl {
override def onQuit(): Unit = {
import zio._
print("Quitting")
// The quit command will only wait 5 seconds before issuing a sys.exit()
Thread.sleep(1.minute.toMillis)
println("...")
}
}

it should "allow custom logic during quit (up to 5 seconds)" in {
replTest[BaseReplTest]("customQuit").out(1) shouldBe "scala> Quitting"
}
}

object BaseReplTest {
Expand Down

0 comments on commit 266616e

Please sign in to comment.