Skip to content

Commit 32e2120

Browse files
committed
Fix REPL REPL command completion (:help, :quit, etc.)
1 parent ed4ae72 commit 32e2120

File tree

2 files changed

+114
-223
lines changed

2 files changed

+114
-223
lines changed

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

Lines changed: 63 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -18,76 +18,61 @@ import org.jline.terminal.TerminalBuilder
1818
import org.jline.utils.AttributedString
1919

2020
class JLineTerminal extends java.io.Closeable {
21-
// import java.util.logging.{Logger, Level}
22-
// Logger.getLogger("org.jline").setLevel(Level.FINEST)
2321

2422
private val terminal =
2523
var builder = TerminalBuilder.builder()
2624
if System.getenv("TERM") == "dumb" then
27-
// Force dumb terminal if `TERM` is `"dumb"`.
28-
// Note: the default value for the `dumb` option is `null`, which allows
29-
// JLine to fall back to a dumb terminal. This is different than `true` or
30-
// `false` and can't be set using the `dumb` setter.
31-
// This option is used at https://github.com/jline/jline3/blob/894b5e72cde28a551079402add4caea7f5527806/terminal/src/main/java/org/jline/terminal/TerminalBuilder.java#L528.
3225
builder.dumb(true)
3326
builder.build()
27+
3428
private val history = new DefaultHistory
3529

3630
private def blue(str: String)(using Context) =
3731
if (ctx.settings.color.value != "never") Console.BLUE + str + Console.RESET
3832
else str
33+
3934
protected def promptStr = "scala"
4035
private def prompt(using Context) = blue(s"\n$promptStr> ")
4136
private def newLinePrompt(using Context) = " "
4237

43-
/** Blockingly read line from `System.in`
44-
*
45-
* This entry point into JLine handles everything to do with terminal
46-
* emulation. This includes:
47-
*
48-
* - Multi-line support
49-
* - Copy-pasting
50-
* - History
51-
* - Syntax highlighting
52-
* - Auto-completions
53-
*
54-
* @throws EndOfFileException This exception is thrown when the user types Ctrl-D.
55-
*/
5638
def readLine(
57-
completer: Completer // provide auto-completions
39+
completer: Completer
5840
)(using Context): String = {
5941
import LineReader.Option.*
6042
import LineReader.*
6143
val userHome = System.getProperty("user.home")
44+
6245
val lineReader = LineReaderBuilder
6346
.builder()
6447
.terminal(terminal)
6548
.history(history)
6649
.completer(completer)
6750
.highlighter(new Highlighter)
6851
.parser(new Parser)
69-
.variable(HISTORY_FILE, s"$userHome/.dotty_history") // Save history to file
70-
.variable(SECONDARY_PROMPT_PATTERN, "%M") // A short word explaining what is "missing",
71-
// this is supplied from the EOFError.getMissing() method
72-
.variable(LIST_MAX, 400) // Ask user when number of completions exceed this limit (default is 100).
73-
.variable(BLINK_MATCHING_PAREN, 0L) // Don't blink the opening paren after typing a closing paren.
74-
.variable(WORDCHARS,
75-
LineReaderImpl.DEFAULT_WORDCHARS.filterNot("*?.[]~=/&;!#%^(){}<>".toSet)) // Finer grained word boundaries
76-
.option(INSERT_TAB, true) // At the beginning of the line, insert tab instead of completing.
77-
.option(AUTO_FRESH_LINE, true) // if not at start of line before prompt, move to new line.
78-
.option(DISABLE_EVENT_EXPANSION, true) // don't process escape sequences in input
52+
.variable(HISTORY_FILE, s"$userHome/.dotty_history")
53+
.variable(SECONDARY_PROMPT_PATTERN, "%M")
54+
.variable(LIST_MAX, 400)
55+
.variable(BLINK_MATCHING_PAREN, 0L)
56+
.variable(
57+
WORDCHARS,
58+
LineReaderImpl.DEFAULT_WORDCHARS.filterNot("*?.[]~=/&;!#%^(){}<>".toSet)
59+
)
60+
.option(INSERT_TAB, true)
61+
.option(AUTO_FRESH_LINE, true)
62+
.option(DISABLE_EVENT_EXPANSION, true)
7963
.build()
8064

8165
lineReader.readLine(prompt)
8266
}
8367

8468
def close(): Unit = terminal.close()
8569

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 =
70+
def handle(
71+
signal: org.jline.terminal.Terminal.Signal,
72+
handler: org.jline.terminal.Terminal.SignalHandler
73+
): org.jline.terminal.Terminal.SignalHandler =
8874
terminal.handle(signal, handler)
8975

90-
/** Provide syntax highlighting */
9176
private class Highlighter(using Context) extends reader.Highlighter {
9277
def highlight(reader: LineReader, buffer: String): AttributedString = {
9378
val highlighted = SyntaxHighlighting.highlight(buffer)
@@ -97,39 +82,35 @@ class JLineTerminal extends java.io.Closeable {
9782
def setErrorIndex(errorIndex: Int): Unit = {}
9883
}
9984

100-
/** Provide multi-line editing support */
10185
private class Parser(using Context) extends reader.Parser {
10286

103-
/**
104-
* @param cursor The cursor position within the line
105-
* @param line The unparsed line
106-
* @param word The current word being completed
107-
* @param wordCursor The cursor position within the current word
108-
*/
10987
private class ParsedLine(
11088
val cursor: Int, val line: String, val word: String, val wordCursor: Int
11189
) extends reader.ParsedLine {
112-
// Using dummy values, not sure what they are used for
11390
def wordIndex = -1
114-
def words: java.util.List[String] = java.util.Collections.emptyList[String]
91+
def words: java.util.List[String] =
92+
java.util.Collections.emptyList[String]
11593
}
11694

117-
def parse(input: String, cursor: Int, context: ParseContext): reader.ParsedLine = {
95+
def parse(
96+
input: String,
97+
cursor: Int,
98+
context: ParseContext
99+
): reader.ParsedLine = {
100+
118101
def parsedLine(word: String, wordCursor: Int) =
119102
new ParsedLine(cursor, input, word, wordCursor)
120-
// Used when no word is being completed
103+
121104
def defaultParsedLine = parsedLine("", 0)
122105

123106
def incomplete(): Nothing = throw new EOFError(
124-
// Using dummy values, not sure what they are used for
125-
/* line = */ -1,
126-
/* column = */ -1,
127-
/* message = */ "",
128-
/* missing = */ newLinePrompt)
107+
-1, -1, "", newLinePrompt
108+
)
129109

130110
case class TokenData(token: Token, start: Int, end: Int)
131-
def currentToken: TokenData /* | Null */ = {
132-
val source = SourceFile.virtual("<completions>", input)
111+
112+
def currentToken: TokenData | Null = {
113+
val source = SourceFile.virtual("<completions>", input)
133114
val scanner = new Scanner(source)(using ctx.fresh.setReporter(Reporter.NoReporter))
134115
var lastBacktickErrorStart: Option[Int] = None
135116

@@ -139,12 +120,12 @@ class JLineTerminal extends java.io.Closeable {
139120
scanner.nextToken()
140121
val end = scanner.lastOffset
141122

142-
val isCurrentToken = cursor >= start && cursor <= end
123+
val isCurrentToken =
124+
cursor >= start && cursor <= end
125+
143126
if (isCurrentToken)
144127
return TokenData(token, lastBacktickErrorStart.getOrElse(start), end)
145128

146-
147-
// we need to enclose the last backtick, which unclosed produces ERROR token
148129
if (token == ERROR && input(start) == '`') then
149130
lastBacktickErrorStart = Some(start)
150131
else
@@ -153,39 +134,35 @@ class JLineTerminal extends java.io.Closeable {
153134
null
154135
}
155136

156-
def acceptLine = {
157-
val onLastLine = !input.substring(cursor).contains(System.lineSeparator)
158-
onLastLine && !ParseResult.isIncomplete(input)
159-
}
137+
def acceptLine =
138+
!input.substring(cursor).contains(System.lineSeparator) &&
139+
!ParseResult.isIncomplete(input)
160140

161141
context match {
162-
case ParseContext.ACCEPT_LINE if acceptLine =>
163-
// using dummy values, resulting parsed input is probably unused
164-
defaultParsedLine
165-
166-
// In the situation where we have a partial command that we want to
167-
// complete we need to ensure that the :<partial-word> isn't split into
168-
// 2 tokens, but rather the entire thing is treated as the "word", in
169-
// order to insure the : is replaced in the completion.
170-
case ParseContext.COMPLETE if
171-
ParseResult.commands.exists(command => command._1.startsWith(input)) =>
172-
parsedLine(input, cursor)
173-
174-
case ParseContext.COMPLETE =>
175-
// Parse to find completions (typically after a Tab).
176-
def isCompletable(token: Token) = isIdentifier(token) || isKeyword(token)
177-
currentToken match {
178-
case TokenData(token, start, end) if isCompletable(token) =>
179-
val word = input.substring(start, end)
180-
val wordCursor = cursor - start
181-
parsedLine(word, wordCursor)
182-
case _ =>
183-
defaultParsedLine
184-
}
185-
186-
case _ =>
187-
incomplete()
188-
}
142+
143+
case ParseContext.ACCEPT_LINE if acceptLine =>
144+
defaultParsedLine
145+
146+
// FIX: REPL commands starting with ":" must be treated as one word
147+
case ParseContext.COMPLETE if input.startsWith(":") =>
148+
parsedLine(input, cursor)
149+
150+
case ParseContext.COMPLETE =>
151+
def isCompletable(token: Token) =
152+
isIdentifier(token) || isKeyword(token)
153+
154+
currentToken match
155+
case TokenData(token, start, end) if isCompletable(token) =>
156+
val word = input.substring(start, end)
157+
val wordCursor = cursor - start
158+
parsedLine(word, wordCursor)
159+
case _ =>
160+
defaultParsedLine
161+
162+
case _ =>
163+
incomplete()
164+
}
165+
189166
}
190167
}
191168
}

0 commit comments

Comments
 (0)