Skip to content

Commit 7b7f2f1

Browse files
authored
Trap Ctrl-C in the REPL: if no command is running clear the prompt, if some command is running ask for confirmation before exiting (#24127)
This ports over one of the other Ammonite features to the Scala 3 REPL, making it behave much more similarly to other prompt environment (Bash, Python, etc.): - If there is no code running, `Ctrl-C` just resets the prompt without exiting - If there is code running, `Ctrl-C` first interrupts the thread to try and force a gentle exit. A second `Ctrl-C` would then terminate the process. Unfortunately we will no longer be able to terminate the thread forcefully, as `Thread.stop` is busted Java >20 (see com-lihaoyi/Ammonite#1379), so using `Thread.interrupt` and `sys.exit` is the best we have Tested manually using `bin/scala`. Doesn't work in `sbt repl` due to SBT also intercepting the signal, but that's a separate issue we can follow up later
2 parents 0afa50d + f6654b7 commit 7b7f2f1

File tree

2 files changed

+35
-3
lines changed

2 files changed

+35
-3
lines changed

compiler/src/dotty/tools/repl/JLineTerminal.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ class JLineTerminal extends java.io.Closeable {
8282

8383
def close(): Unit = terminal.close()
8484

85+
/** Register a signal handler and return the previous handler */
86+
def handle(signal: org.jline.terminal.Terminal.Signal, handler: org.jline.terminal.Terminal.SignalHandler): org.jline.terminal.Terminal.SignalHandler =
87+
terminal.handle(signal, handler)
88+
8589
/** Provide syntax highlighting */
8690
private class Highlighter(using Context) extends reader.Highlighter {
8791
def highlight(reader: LineReader, buffer: String): AttributedString = {

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,44 @@ class ReplDriver(settings: Array[String],
211211
val line = terminal.readLine(completer)
212212
ParseResult(line)
213213
} catch {
214-
case _: EndOfFileException |
215-
_: UserInterruptException => // Ctrl+D or Ctrl+C
214+
case _: EndOfFileException => // Ctrl+D
216215
Quit
216+
case _: UserInterruptException => // Ctrl+C at prompt - clear and continue
217+
SigKill
217218
}
218219
}
219220

220221
@tailrec def loop(using state: State)(): State = {
222+
221223
val res = readLine()
222224
if (res == Quit) state
223-
else loop(using interpret(res))()
225+
// Ctrl-C pressed at prompt - just continue with same state (line is cleared by JLine)
226+
else if (res == SigKill) loop(using state)()
227+
else {
228+
// Set up interrupt handler for command execution
229+
var firstCtrlCEntered = false
230+
val thread = Thread.currentThread()
231+
val previousSignalHandler = terminal.handle(
232+
org.jline.terminal.Terminal.Signal.INT,
233+
(sig: org.jline.terminal.Terminal.Signal) => {
234+
if (!firstCtrlCEntered) {
235+
firstCtrlCEntered = true
236+
thread.interrupt()
237+
out.println("\nInterrupting running thread, Ctrl-C again to terminate the REPL Process")
238+
} else {
239+
out.println("\nTerminating REPL Process...")
240+
System.exit(130) // Standard exit code for SIGINT
241+
}
242+
}
243+
)
244+
245+
val newState =
246+
try interpret(res)
247+
// Restore previous handler
248+
finally terminal.handle(org.jline.terminal.Terminal.Signal.INT, previousSignalHandler)
249+
250+
loop(using newState)()
251+
}
224252
}
225253

226254
try runBody { loop() }

0 commit comments

Comments
 (0)