Skip to content

Commit d1e53c7

Browse files
committed
.
1 parent e73ff2c commit d1e53c7

File tree

2 files changed

+57
-24
lines changed

2 files changed

+57
-24
lines changed

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

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ import org.jline.reader.*
1515
import org.jline.reader.impl.LineReaderImpl
1616
import org.jline.reader.impl.history.DefaultHistory
1717
import org.jline.terminal.TerminalBuilder
18+
import org.jline.terminal.Attributes
19+
import org.jline.terminal.Attributes.ControlChar
1820
import org.jline.utils.AttributedString
1921

2022
class JLineTerminal extends java.io.Closeable {
2123
// import java.util.logging.{Logger, Level}
2224
// Logger.getLogger("org.jline").setLevel(Level.FINEST)
2325

2426
private val terminal =
25-
var builder = TerminalBuilder.builder()
27+
val builder = TerminalBuilder.builder()
2628
if System.getenv("TERM") == "dumb" then
2729
// Force dumb terminal if `TERM` is `"dumb"`.
2830
// Note: the default value for the `dumb` option is `null`, which allows
@@ -31,6 +33,16 @@ class JLineTerminal extends java.io.Closeable {
3133
// This option is used at https://github.com/jline/jline3/blob/894b5e72cde28a551079402add4caea7f5527806/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java#L528.
3234
builder.dumb(true)
3335
builder.build()
36+
37+
// Save original attributes before any modifications
38+
private val originalAttributes = terminal.getAttributes
39+
40+
// Disable VINTR so Ctrl-C is not converted to SIGINT by the tty driver, then enter raw mode
41+
val noIntr = new Attributes(originalAttributes)
42+
noIntr.setControlChar(ControlChar.VINTR, 0)
43+
terminal.setAttributes(noIntr)
44+
terminal.enterRawMode()
45+
3446
private val history = new DefaultHistory
3547

3648
private def magenta(str: String)(using Context) =
@@ -54,7 +66,7 @@ class JLineTerminal extends java.io.Closeable {
5466
* @throws EndOfFileException This exception is thrown when the user types Ctrl-D.
5567
*/
5668
def readLine(
57-
completer: Completer // provide auto-completions
69+
completer: Completer, // provide auto-completions
5870
)(using Context): String = {
5971
import LineReader.Option.*
6072
import LineReader.*
@@ -78,14 +90,43 @@ class JLineTerminal extends java.io.Closeable {
7890
.option(DISABLE_EVENT_EXPANSION, true) // don't process escape sequences in input
7991
.build()
8092

93+
lineReader.getKeyMaps.get(LineReader.MAIN).bind(
94+
new Widget { override def apply(): Boolean = throw new UserInterruptException("") },
95+
"\u0003"
96+
)
8197
lineReader.readLine(prompt)
8298
}
8399

84-
def close(): Unit = terminal.close()
100+
def close(): Unit =
101+
try terminal.setAttributes(originalAttributes)
102+
finally terminal.close()
103+
104+
/** Execute a block while monitoring for Ctrl-C keypresses.
105+
* Calls the handler when Ctrl-C is detected during block execution.
106+
*/
107+
def withMonitoringCtrlC[T](handler: () => Unit)(block: => T): T = {
108+
@volatile var monitoring = true
109+
val terminalReader = terminal.reader()
110+
111+
val monitorThread = new Thread(() => {
112+
while (monitoring) {
113+
val ch =
114+
try terminalReader.peek(1) // timeout after 1ms so the loop gets a chance to check `monitoring`
115+
catch { case _: Exception => -1 } // Ignore all read errors, just continue
85116

86-
/** Register a signal handler and return the previous handler */
87-
def handle(signal: org.jline.terminal.Terminal.Signal, handler: org.jline.terminal.Terminal.SignalHandler): org.jline.terminal.Terminal.SignalHandler =
88-
terminal.handle(signal, handler)
117+
if (ch == 3 /* Ctrl-C is ASCII 0x03 */ && monitoring) handler()
118+
}
119+
}, "REPL-CtrlC-Monitor")
120+
monitorThread.setDaemon(true)
121+
monitorThread.start()
122+
123+
try block
124+
finally {
125+
monitoring = false
126+
Thread.interrupted() // clear any interrupted flag so the `join` below doesn't explode
127+
monitorThread.join()
128+
}
129+
}
89130

90131
/** Provide syntax highlighting */
91132
private class Highlighter(using Context) extends reader.Highlighter {

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

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -214,48 +214,40 @@ class ReplDriver(settings: Array[String],
214214
val line = terminal.readLine(completer)
215215
ParseResult(line)
216216
} catch {
217-
case _: EndOfFileException => // Ctrl+D
218-
Quit
219-
case _: UserInterruptException => // Ctrl+C at prompt - clear and continue
220-
SigKill
217+
case _: EndOfFileException => Quit // Ctrl+D
218+
case _: UserInterruptException => SigKill // Ctrl+C at prompt
221219
}
222220
}
223221

224222
@tailrec def loop(using state: State)(): State = {
225223

226224
val res = readLine()
227225
if (res == Quit) state
228-
// Ctrl-C pressed at prompt - just continue with same state (line is cleared by JLine)
229226
else if (res == SigKill) loop(using state)()
230227
else {
231-
// Set up interrupt handler for command execution
228+
// Set up Ctrl-C monitoring for command execution
232229
var firstCtrlCEntered = false
233230
val thread = Thread.currentThread()
234231

235232
// Clear the stop flag before executing new code
236233
ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), false)
237234

238-
val previousSignalHandler = terminal.handle(
239-
org.jline.terminal.Terminal.Signal.INT,
240-
(sig: org.jline.terminal.Terminal.Signal) => {
235+
val newState = terminal.withMonitoringCtrlC(
236+
handler = () =>
241237
if (!firstCtrlCEntered) {
242238
firstCtrlCEntered = true
243239
// Set the stop flag to trigger throwIfReplStopped() in instrumented code
244240
ReplBytecodeInstrumentation.setStopFlag(rendering.classLoader()(using state.context), true)
245-
// Also interrupt the thread as a fallback for non-instrumented code
241+
// Also interrupt the thread as a fallback for non-instrumented code, e.g. IO/sleeps
246242
thread.interrupt()
247-
out.println("\nAttempting to interrupt running thread with `Thread.interrupt`")
243+
out.println("\nAttempting to interrupt running REPL command")
248244
} else {
249245
out.println("\nTerminating REPL Process...")
250246
System.exit(130) // Standard exit code for SIGINT
251247
}
252-
}
253-
)
254-
255-
val newState =
256-
try interpret(res)
257-
// Restore previous handler
258-
finally terminal.handle(org.jline.terminal.Terminal.Signal.INT, previousSignalHandler)
248+
) {
249+
interpret(res)
250+
}
259251

260252
loop(using newState)()
261253
}

0 commit comments

Comments
 (0)