[cli-repl] Support readLine() during repl session
This commit is contained in:
committed by
Pavel V. Talanov
parent
00a117f089
commit
42c0396308
@@ -25,7 +25,8 @@ import jline.console.history.FileHistory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.cli.common.KotlinVersion;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.IdeXmlMessagesParser;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.IdeLinebreaksUnescaper;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplSystemInWrapper;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplSystemOutWrapper;
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration;
|
||||
import org.jetbrains.kotlin.utils.UtilsPackage;
|
||||
@@ -41,8 +42,8 @@ public class ReplFromTerminal {
|
||||
private final Object waitRepl = new Object();
|
||||
|
||||
private final boolean ideMode;
|
||||
private ReplSystemInWrapper replReader;
|
||||
private final ReplSystemOutWrapper replWriter;
|
||||
private final IdeXmlMessagesParser ideXmlParser = new IdeXmlMessagesParser();
|
||||
|
||||
private final ConsoleReader consoleReader;
|
||||
|
||||
@@ -53,11 +54,18 @@ public class ReplFromTerminal {
|
||||
String replIdeMode = System.getProperty("repl.ideMode");
|
||||
ideMode = replIdeMode != null && replIdeMode.equals("true");
|
||||
|
||||
// wrapper for `in` is required to give user possibility of calling
|
||||
// [readLine] from ide-console repl
|
||||
if (ideMode) {
|
||||
replReader = new ReplSystemInWrapper(System.in);
|
||||
System.setIn(replReader);
|
||||
}
|
||||
|
||||
new Thread("initialize-repl") {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
replInterpreter = new ReplInterpreter(disposable, compilerConfiguration, ideMode);
|
||||
replInterpreter = new ReplInterpreter(disposable, compilerConfiguration, ideMode, replReader);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
replInitializationFailed = e;
|
||||
@@ -68,10 +76,10 @@ public class ReplFromTerminal {
|
||||
}
|
||||
}.start();
|
||||
|
||||
// wrapper is required to escape every input in [ideMode];
|
||||
// wrapper for `out` is required to escape every input in [ideMode];
|
||||
// if [ideMode == false] then just redirects all input to [System.out]
|
||||
// if user calls [System.setOut(...)] then undefined behaviour
|
||||
replWriter = new ReplSystemOutWrapper(System.out, ideMode);
|
||||
replWriter = new ReplSystemOutWrapper(ideMode, System.out);
|
||||
System.setOut(replWriter);
|
||||
|
||||
try {
|
||||
@@ -187,7 +195,7 @@ public class ReplFromTerminal {
|
||||
private String readLine(@Nullable String prompt) throws IOException {
|
||||
String line = consoleReader.readLine(prompt);
|
||||
if (ideMode && line != null) {
|
||||
line = ideXmlParser.parse(line);
|
||||
line = IdeLinebreaksUnescaper.unescapeFromDiez(line).trim();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.jetbrains.kotlin.cli.jvm.repl.di.ContainerForReplWithJava;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.di.DiPackage;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.DiagnosticMessageHolder;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplIdeDiagnosticMessageHolder;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplSystemInWrapper;
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplTerminalDiagnosticMessageHolder;
|
||||
import org.jetbrains.kotlin.codegen.ClassBuilderFactories;
|
||||
import org.jetbrains.kotlin.codegen.CompilationErrorHandler;
|
||||
@@ -104,8 +105,14 @@ public class ReplInterpreter {
|
||||
private final ScriptMutableDeclarationProviderFactory scriptDeclarationFactory;
|
||||
|
||||
private final boolean ideMode;
|
||||
private final ReplSystemInWrapper replReader;
|
||||
|
||||
public ReplInterpreter(@NotNull Disposable disposable, @NotNull CompilerConfiguration configuration, boolean ideMode) {
|
||||
public ReplInterpreter(
|
||||
@NotNull Disposable disposable,
|
||||
@NotNull CompilerConfiguration configuration,
|
||||
boolean ideMode,
|
||||
@Nullable ReplSystemInWrapper replReader
|
||||
) {
|
||||
KotlinCoreEnvironment environment =
|
||||
KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES);
|
||||
Project project = environment.getProject();
|
||||
@@ -158,6 +165,7 @@ public class ReplInterpreter {
|
||||
this.classLoader = new ReplClassLoader(new URLClassLoader(classpath.toArray(new URL[classpath.size()]), null));
|
||||
|
||||
this.ideMode = ideMode;
|
||||
this.replReader = replReader;
|
||||
}
|
||||
|
||||
private static void prepareForTheNextReplLine(@NotNull TopDownAnalysisContext c) {
|
||||
@@ -329,11 +337,15 @@ public class ReplInterpreter {
|
||||
Constructor<?> scriptInstanceConstructor = scriptClass.getConstructor(constructorParams);
|
||||
Object scriptInstance;
|
||||
try {
|
||||
setReplScriptExecuting(true);
|
||||
scriptInstance = scriptInstanceConstructor.newInstance(constructorArgs);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
setReplScriptExecuting(false);
|
||||
return LineResult.runtimeError(renderStackTrace(e.getCause()));
|
||||
}
|
||||
setReplScriptExecuting(false);
|
||||
|
||||
Field rvField = scriptClass.getDeclaredField("rv");
|
||||
rvField.setAccessible(true);
|
||||
Object rv = rvField.get(scriptInstance);
|
||||
@@ -352,6 +364,12 @@ public class ReplInterpreter {
|
||||
}
|
||||
}
|
||||
|
||||
private void setReplScriptExecuting(boolean isExecuting) {
|
||||
if (replReader != null) {
|
||||
replReader.setIsReplScriptExecuting(isExecuting);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String renderStackTrace(@NotNull Throwable cause) {
|
||||
StackTraceElement[] oldTrace = cause.getStackTrace();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2015 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.cli.jvm.repl.messages
|
||||
|
||||
public object EscapeConstants {
|
||||
public val XML_PREAMBLE: String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||
|
||||
// using '#' to avoid collisions with xml escaping
|
||||
public val SOURCE_CHARS: Array<String> = arrayOf("\n", "#")
|
||||
public val XML_REPLACEMENTS: Array<String> = arrayOf("#n", "#diez")
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2010-2015 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.cli.jvm.repl.messages
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
public class ReplSystemInWrapper(private val stdin: InputStream) : InputStream() {
|
||||
private var isXmlIncomplete = true
|
||||
private var isLastScriptByteProcessed = false
|
||||
private var byteBuilder = ByteArrayOutputStream()
|
||||
private var curBytePos = 0
|
||||
private var inputByteArray = byteArrayOf()
|
||||
|
||||
private val isReachBufferEnd: Boolean
|
||||
get() = curBytePos == inputByteArray.size()
|
||||
|
||||
var isReplScriptExecuting = false
|
||||
|
||||
override synchronized fun read(): Int {
|
||||
if (isLastScriptByteProcessed && isReplScriptExecuting) {
|
||||
isLastScriptByteProcessed = false
|
||||
return -1
|
||||
}
|
||||
|
||||
while (isXmlIncomplete) {
|
||||
byteBuilder.write(stdin.read())
|
||||
|
||||
if (byteBuilder.toString().endsWith(END_LINE)) {
|
||||
isXmlIncomplete = false
|
||||
isLastScriptByteProcessed = false
|
||||
|
||||
inputByteArray = unescapedInput().toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
val nextByte = inputByteArray[curBytePos++].toInt()
|
||||
resetBufferIfNeeded()
|
||||
return nextByte
|
||||
}
|
||||
|
||||
private fun unescapedInput(): String {
|
||||
val xmlInput = byteBuilder.toString()
|
||||
val unescapedXml = parseXml(xmlInput)
|
||||
|
||||
val inputMessage = if (isReplScriptExecuting)
|
||||
IdeLinebreaksUnescaper.unescapeFromDiez(unescapedXml)
|
||||
else
|
||||
"$unescapedXml$END_LINE"
|
||||
|
||||
return inputMessage
|
||||
}
|
||||
|
||||
private fun resetBufferIfNeeded() {
|
||||
if (isReachBufferEnd) {
|
||||
isXmlIncomplete = true
|
||||
byteBuilder = ByteArrayOutputStream()
|
||||
curBytePos = 0
|
||||
|
||||
if (isReplScriptExecuting) isLastScriptByteProcessed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,13 @@
|
||||
package org.jetbrains.kotlin.cli.jvm.repl.messages
|
||||
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.util.LineSeparator
|
||||
import java.io.PrintStream
|
||||
|
||||
public class ReplSystemOutWrapper(private val standardOut: PrintStream, private val ideMode: Boolean) : PrintStream(standardOut, true) {
|
||||
private val END_LINE = LineSeparator.getSystemLineSeparator().separatorString
|
||||
public val END_LINE: String = "\n"
|
||||
|
||||
private val XML_PREAMBLE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
||||
|
||||
public class ReplSystemOutWrapper(private val ideMode: Boolean, standardOut: PrintStream) : PrintStream(standardOut, true) {
|
||||
private enum class EscapeType {
|
||||
INITIAL_PROMPT,
|
||||
HELP_PROMPT,
|
||||
@@ -53,8 +54,8 @@ public class ReplSystemOutWrapper(private val standardOut: PrintStream, private
|
||||
}
|
||||
|
||||
private fun xmlEscape(s: String, escapeType: EscapeType): String {
|
||||
val singleLine = StringUtil.replace(s, EscapeConstants.SOURCE_CHARS, EscapeConstants.XML_REPLACEMENTS)
|
||||
return "${EscapeConstants.XML_PREAMBLE}<output type=\"$escapeType\">${StringUtil.escapeXml(singleLine)}</output>"
|
||||
val singleLine = StringUtil.replace(s, SOURCE_CHARS, XML_REPLACEMENTS)
|
||||
return "$XML_PREAMBLE<output type=\"$escapeType\">${StringUtil.escapeXml(singleLine)}</output>"
|
||||
}
|
||||
|
||||
fun printlnInit(x: String) = printlnWithEscaping(x, EscapeType.INITIAL_PROMPT)
|
||||
|
||||
+14
-10
@@ -22,17 +22,21 @@ import org.xml.sax.InputSource
|
||||
import java.io.ByteArrayInputStream
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
public class IdeXmlMessagesParser {
|
||||
private fun strToSource(s: String) = InputSource(ByteArrayInputStream(s.toByteArray()))
|
||||
// using '#' to avoid collisions with xml escaping
|
||||
public val SOURCE_CHARS: Array<String> = arrayOf("\n", "#")
|
||||
public val XML_REPLACEMENTS: Array<String> = arrayOf("#n", "#diez")
|
||||
|
||||
fun parse(inputMessage: String): String {
|
||||
val docFactory = DocumentBuilderFactory.newInstance()
|
||||
val docBuilder = docFactory.newDocumentBuilder()
|
||||
val input = docBuilder.parse(strToSource(inputMessage))
|
||||
fun parseXml(inputMessage: String): String {
|
||||
fun strToSource(s: String) = InputSource(ByteArrayInputStream(s.toByteArray()))
|
||||
|
||||
val root = input.firstChild as Element
|
||||
val inputContent = StringUtil.replace(root.textContent, EscapeConstants.XML_REPLACEMENTS, EscapeConstants.SOURCE_CHARS).trim()
|
||||
val docFactory = DocumentBuilderFactory.newInstance()
|
||||
val docBuilder = docFactory.newDocumentBuilder()
|
||||
val input = docBuilder.parse(strToSource(inputMessage))
|
||||
|
||||
return inputContent
|
||||
}
|
||||
val root = input.firstChild as Element
|
||||
return root.textContent
|
||||
}
|
||||
|
||||
public object IdeLinebreaksUnescaper {
|
||||
jvmStatic fun unescapeFromDiez(inputMessage: String) = StringUtil.replace(inputMessage, XML_REPLACEMENTS, SOURCE_CHARS)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public abstract class AbstractReplInterpreterTest : UsefulTestCase() {
|
||||
|
||||
protected fun doTest(path: String) {
|
||||
val configuration = JetTestUtils.compilerConfigurationForTests(ConfigurationKind.ALL, TestJdkKind.MOCK_JDK)
|
||||
val repl = ReplInterpreter(getTestRootDisposable()!!, configuration, false)
|
||||
val repl = ReplInterpreter(getTestRootDisposable()!!, configuration, false, null)
|
||||
|
||||
for ((code, expected) in loadLines(File(path))) {
|
||||
val lineResult = repl.eval(code)
|
||||
|
||||
@@ -51,7 +51,7 @@ public class KotlinConsoleExecutor(
|
||||
val xmlRes = "$XML_PREAMBLE" +
|
||||
"<input>" +
|
||||
"${StringUtil.escapeXml(
|
||||
StringUtil.replace(command.trim(), SOURCE_CHARS, XML_REPLACEMENTS)
|
||||
StringUtil.replace("${command.trim()}\n", SOURCE_CHARS, XML_REPLACEMENTS)
|
||||
)}" +
|
||||
"</input>"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user