diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java index e45c8474cbc..a7f80735c2b 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.java @@ -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; } diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.java index c977bdf3cfc..a8696c2ef6b 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.java @@ -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(); diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/EscapeConstants.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/EscapeConstants.kt deleted file mode 100644 index c9298e18542..00000000000 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/EscapeConstants.kt +++ /dev/null @@ -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 = "" - - // using '#' to avoid collisions with xml escaping - public val SOURCE_CHARS: Array = arrayOf("\n", "#") - public val XML_REPLACEMENTS: Array = arrayOf("#n", "#diez") -} \ No newline at end of file diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemInWrapper.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemInWrapper.kt new file mode 100644 index 00000000000..bb1215809c1 --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemInWrapper.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt index 442348c0366..122babc8650 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/ReplSystemOutWrapper.kt @@ -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 = "" + +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}${StringUtil.escapeXml(singleLine)}" + val singleLine = StringUtil.replace(s, SOURCE_CHARS, XML_REPLACEMENTS) + return "$XML_PREAMBLE${StringUtil.escapeXml(singleLine)}" } fun printlnInit(x: String) = printlnWithEscaping(x, EscapeType.INITIAL_PROMPT) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/IdeXmlMessagesParser.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/UnescapeUtils.kt similarity index 54% rename from compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/IdeXmlMessagesParser.kt rename to compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/UnescapeUtils.kt index 4e60a973a3d..99156b240b0 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/IdeXmlMessagesParser.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/messages/UnescapeUtils.kt @@ -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 = arrayOf("\n", "#") +public val XML_REPLACEMENTS: Array = 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) } \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt b/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt index 3e0dba1109f..1a8bff8bb68 100644 --- a/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt @@ -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) diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt index c7d1e6cccbe..d76b89e1de6 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleExecutor.kt @@ -51,7 +51,7 @@ public class KotlinConsoleExecutor( val xmlRes = "$XML_PREAMBLE" + "" + "${StringUtil.escapeXml( - StringUtil.replace(command.trim(), SOURCE_CHARS, XML_REPLACEMENTS) + StringUtil.replace("${command.trim()}\n", SOURCE_CHARS, XML_REPLACEMENTS) )}" + ""