[cli-repl] Support readLine() during repl session

This commit is contained in:
Dmitry Kovanikov
2015-08-31 20:18:57 +03:00
committed by Pavel V. Talanov
parent 00a117f089
commit 42c0396308
8 changed files with 132 additions and 49 deletions
@@ -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)
@@ -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>"