[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.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.kotlin.cli.common.KotlinVersion;
|
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.cli.jvm.repl.messages.ReplSystemOutWrapper;
|
||||||
import org.jetbrains.kotlin.config.CompilerConfiguration;
|
import org.jetbrains.kotlin.config.CompilerConfiguration;
|
||||||
import org.jetbrains.kotlin.utils.UtilsPackage;
|
import org.jetbrains.kotlin.utils.UtilsPackage;
|
||||||
@@ -41,8 +42,8 @@ public class ReplFromTerminal {
|
|||||||
private final Object waitRepl = new Object();
|
private final Object waitRepl = new Object();
|
||||||
|
|
||||||
private final boolean ideMode;
|
private final boolean ideMode;
|
||||||
|
private ReplSystemInWrapper replReader;
|
||||||
private final ReplSystemOutWrapper replWriter;
|
private final ReplSystemOutWrapper replWriter;
|
||||||
private final IdeXmlMessagesParser ideXmlParser = new IdeXmlMessagesParser();
|
|
||||||
|
|
||||||
private final ConsoleReader consoleReader;
|
private final ConsoleReader consoleReader;
|
||||||
|
|
||||||
@@ -53,11 +54,18 @@ public class ReplFromTerminal {
|
|||||||
String replIdeMode = System.getProperty("repl.ideMode");
|
String replIdeMode = System.getProperty("repl.ideMode");
|
||||||
ideMode = replIdeMode != null && replIdeMode.equals("true");
|
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") {
|
new Thread("initialize-repl") {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
replInterpreter = new ReplInterpreter(disposable, compilerConfiguration, ideMode);
|
replInterpreter = new ReplInterpreter(disposable, compilerConfiguration, ideMode, replReader);
|
||||||
}
|
}
|
||||||
catch (Throwable e) {
|
catch (Throwable e) {
|
||||||
replInitializationFailed = e;
|
replInitializationFailed = e;
|
||||||
@@ -68,10 +76,10 @@ public class ReplFromTerminal {
|
|||||||
}
|
}
|
||||||
}.start();
|
}.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 [ideMode == false] then just redirects all input to [System.out]
|
||||||
// if user calls [System.setOut(...)] then undefined behaviour
|
// if user calls [System.setOut(...)] then undefined behaviour
|
||||||
replWriter = new ReplSystemOutWrapper(System.out, ideMode);
|
replWriter = new ReplSystemOutWrapper(ideMode, System.out);
|
||||||
System.setOut(replWriter);
|
System.setOut(replWriter);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -187,7 +195,7 @@ public class ReplFromTerminal {
|
|||||||
private String readLine(@Nullable String prompt) throws IOException {
|
private String readLine(@Nullable String prompt) throws IOException {
|
||||||
String line = consoleReader.readLine(prompt);
|
String line = consoleReader.readLine(prompt);
|
||||||
if (ideMode && line != null) {
|
if (ideMode && line != null) {
|
||||||
line = ideXmlParser.parse(line);
|
line = IdeLinebreaksUnescaper.unescapeFromDiez(line).trim();
|
||||||
}
|
}
|
||||||
return line;
|
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.di.DiPackage;
|
||||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.DiagnosticMessageHolder;
|
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.ReplIdeDiagnosticMessageHolder;
|
||||||
|
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplSystemInWrapper;
|
||||||
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplTerminalDiagnosticMessageHolder;
|
import org.jetbrains.kotlin.cli.jvm.repl.messages.ReplTerminalDiagnosticMessageHolder;
|
||||||
import org.jetbrains.kotlin.codegen.ClassBuilderFactories;
|
import org.jetbrains.kotlin.codegen.ClassBuilderFactories;
|
||||||
import org.jetbrains.kotlin.codegen.CompilationErrorHandler;
|
import org.jetbrains.kotlin.codegen.CompilationErrorHandler;
|
||||||
@@ -104,8 +105,14 @@ public class ReplInterpreter {
|
|||||||
private final ScriptMutableDeclarationProviderFactory scriptDeclarationFactory;
|
private final ScriptMutableDeclarationProviderFactory scriptDeclarationFactory;
|
||||||
|
|
||||||
private final boolean ideMode;
|
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 environment =
|
||||||
KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES);
|
KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES);
|
||||||
Project project = environment.getProject();
|
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.classLoader = new ReplClassLoader(new URLClassLoader(classpath.toArray(new URL[classpath.size()]), null));
|
||||||
|
|
||||||
this.ideMode = ideMode;
|
this.ideMode = ideMode;
|
||||||
|
this.replReader = replReader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void prepareForTheNextReplLine(@NotNull TopDownAnalysisContext c) {
|
private static void prepareForTheNextReplLine(@NotNull TopDownAnalysisContext c) {
|
||||||
@@ -329,11 +337,15 @@ public class ReplInterpreter {
|
|||||||
Constructor<?> scriptInstanceConstructor = scriptClass.getConstructor(constructorParams);
|
Constructor<?> scriptInstanceConstructor = scriptClass.getConstructor(constructorParams);
|
||||||
Object scriptInstance;
|
Object scriptInstance;
|
||||||
try {
|
try {
|
||||||
|
setReplScriptExecuting(true);
|
||||||
scriptInstance = scriptInstanceConstructor.newInstance(constructorArgs);
|
scriptInstance = scriptInstanceConstructor.newInstance(constructorArgs);
|
||||||
}
|
}
|
||||||
catch (Throwable e) {
|
catch (Throwable e) {
|
||||||
|
setReplScriptExecuting(false);
|
||||||
return LineResult.runtimeError(renderStackTrace(e.getCause()));
|
return LineResult.runtimeError(renderStackTrace(e.getCause()));
|
||||||
}
|
}
|
||||||
|
setReplScriptExecuting(false);
|
||||||
|
|
||||||
Field rvField = scriptClass.getDeclaredField("rv");
|
Field rvField = scriptClass.getDeclaredField("rv");
|
||||||
rvField.setAccessible(true);
|
rvField.setAccessible(true);
|
||||||
Object rv = rvField.get(scriptInstance);
|
Object rv = rvField.get(scriptInstance);
|
||||||
@@ -352,6 +364,12 @@ public class ReplInterpreter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setReplScriptExecuting(boolean isExecuting) {
|
||||||
|
if (replReader != null) {
|
||||||
|
replReader.setIsReplScriptExecuting(isExecuting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static String renderStackTrace(@NotNull Throwable cause) {
|
private static String renderStackTrace(@NotNull Throwable cause) {
|
||||||
StackTraceElement[] oldTrace = cause.getStackTrace();
|
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
|
package org.jetbrains.kotlin.cli.jvm.repl.messages
|
||||||
|
|
||||||
import com.intellij.openapi.util.text.StringUtil
|
import com.intellij.openapi.util.text.StringUtil
|
||||||
import com.intellij.util.LineSeparator
|
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
|
|
||||||
public class ReplSystemOutWrapper(private val standardOut: PrintStream, private val ideMode: Boolean) : PrintStream(standardOut, true) {
|
public val END_LINE: String = "\n"
|
||||||
private val END_LINE = LineSeparator.getSystemLineSeparator().separatorString
|
|
||||||
|
|
||||||
|
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 {
|
private enum class EscapeType {
|
||||||
INITIAL_PROMPT,
|
INITIAL_PROMPT,
|
||||||
HELP_PROMPT,
|
HELP_PROMPT,
|
||||||
@@ -53,8 +54,8 @@ public class ReplSystemOutWrapper(private val standardOut: PrintStream, private
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun xmlEscape(s: String, escapeType: EscapeType): String {
|
private fun xmlEscape(s: String, escapeType: EscapeType): String {
|
||||||
val singleLine = StringUtil.replace(s, EscapeConstants.SOURCE_CHARS, EscapeConstants.XML_REPLACEMENTS)
|
val singleLine = StringUtil.replace(s, SOURCE_CHARS, XML_REPLACEMENTS)
|
||||||
return "${EscapeConstants.XML_PREAMBLE}<output type=\"$escapeType\">${StringUtil.escapeXml(singleLine)}</output>"
|
return "$XML_PREAMBLE<output type=\"$escapeType\">${StringUtil.escapeXml(singleLine)}</output>"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun printlnInit(x: String) = printlnWithEscaping(x, EscapeType.INITIAL_PROMPT)
|
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 java.io.ByteArrayInputStream
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
public class IdeXmlMessagesParser {
|
// using '#' to avoid collisions with xml escaping
|
||||||
private fun strToSource(s: String) = InputSource(ByteArrayInputStream(s.toByteArray()))
|
public val SOURCE_CHARS: Array<String> = arrayOf("\n", "#")
|
||||||
|
public val XML_REPLACEMENTS: Array<String> = arrayOf("#n", "#diez")
|
||||||
|
|
||||||
fun parse(inputMessage: String): String {
|
fun parseXml(inputMessage: String): String {
|
||||||
val docFactory = DocumentBuilderFactory.newInstance()
|
fun strToSource(s: String) = InputSource(ByteArrayInputStream(s.toByteArray()))
|
||||||
val docBuilder = docFactory.newDocumentBuilder()
|
|
||||||
val input = docBuilder.parse(strToSource(inputMessage))
|
|
||||||
|
|
||||||
val root = input.firstChild as Element
|
val docFactory = DocumentBuilderFactory.newInstance()
|
||||||
val inputContent = StringUtil.replace(root.textContent, EscapeConstants.XML_REPLACEMENTS, EscapeConstants.SOURCE_CHARS).trim()
|
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) {
|
protected fun doTest(path: String) {
|
||||||
val configuration = JetTestUtils.compilerConfigurationForTests(ConfigurationKind.ALL, TestJdkKind.MOCK_JDK)
|
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))) {
|
for ((code, expected) in loadLines(File(path))) {
|
||||||
val lineResult = repl.eval(code)
|
val lineResult = repl.eval(code)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class KotlinConsoleExecutor(
|
|||||||
val xmlRes = "$XML_PREAMBLE" +
|
val xmlRes = "$XML_PREAMBLE" +
|
||||||
"<input>" +
|
"<input>" +
|
||||||
"${StringUtil.escapeXml(
|
"${StringUtil.escapeXml(
|
||||||
StringUtil.replace(command.trim(), SOURCE_CHARS, XML_REPLACEMENTS)
|
StringUtil.replace("${command.trim()}\n", SOURCE_CHARS, XML_REPLACEMENTS)
|
||||||
)}" +
|
)}" +
|
||||||
"</input>"
|
"</input>"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user