From 01e62c9bdbc65b3b88349412b05e64e7e357158e Mon Sep 17 00:00:00 2001 From: Natalia Selezneva Date: Wed, 27 Dec 2017 11:57:14 +0300 Subject: [PATCH] Scratch: implement run action with repl --- .../scratch/KtScratchFileLanguageProvider.kt | 6 +- .../kotlin/idea/scratch/ScratchExecutor.kt | 21 +++ .../scratch/ScratchFileLanguageProvider.kt | 1 + .../idea/scratch/actions/RunScratchAction.kt | 9 +- .../scratch/repl/KtScratchReplExecutor.kt | 140 ++++++++++++++++++ .../kotlin/idea/scratch/ui/ScratchTopPanel.kt | 2 +- .../kotlin/console/KotlinConsoleKeeper.kt | 111 +++++++------- 7 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/KtScratchFileLanguageProvider.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/KtScratchFileLanguageProvider.kt index fe3dac673cd..9959781ccbc 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/KtScratchFileLanguageProvider.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/KtScratchFileLanguageProvider.kt @@ -20,8 +20,10 @@ import com.intellij.psi.PsiFile import org.jetbrains.kotlin.idea.scratch.repl.KtScratchReplExecutor import org.jetbrains.kotlin.psi.KtFile -class KtScratchFileLanguageProvider: ScratchFileLanguageProvider() { +class KtScratchFileLanguageProvider : ScratchFileLanguageProvider() { override fun createFile(psiFile: PsiFile): ScratchFile? { - return (psiFile as? KtFile)?.let { KtScratchFile(psiFile)} + return (psiFile as? KtFile)?.let { KtScratchFile(psiFile) } } + + override fun createReplExecutor(file: ScratchFile) = KtScratchReplExecutor(file) } \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt new file mode 100644 index 00000000000..6e4a6c03d81 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2017 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.idea.scratch + +abstract class ScratchExecutor(protected val file: ScratchFile) { + abstract fun execute() +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchFileLanguageProvider.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchFileLanguageProvider.kt index bc2a0a40f12..c46556ef1e0 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchFileLanguageProvider.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchFileLanguageProvider.kt @@ -24,6 +24,7 @@ import com.intellij.psi.PsiFile abstract class ScratchFileLanguageProvider { abstract fun createFile(psiFile: PsiFile): ScratchFile? + abstract fun createReplExecutor(file: ScratchFile): ScratchExecutor? companion object { private val EXTENSION = LanguageExtension("org.jetbrains.kotlin.scratchFileLanguageProvider") diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/actions/RunScratchAction.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/actions/RunScratchAction.kt index 6dc9e29c21d..934a39ebca4 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/actions/RunScratchAction.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/actions/RunScratchAction.kt @@ -46,7 +46,14 @@ class RunScratchAction : AnAction( } val runnable = r@ { - // todo + val executor = provider.createReplExecutor(scratchFile) + if (executor == null) { + return@r + } + + e.presentation.isEnabled = false + + executor.execute() } if (isMakeBeforeRun) { diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt new file mode 100644 index 00000000000..a9ba436d35c --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2010-2017 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.idea.scratch.repl + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.ProcessOutputTypes +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.kotlin.console.KotlinConsoleKeeper +import org.jetbrains.kotlin.console.SOURCE_CHARS +import org.jetbrains.kotlin.console.XML_REPLACEMENTS +import org.jetbrains.kotlin.console.actions.logError +import org.jetbrains.kotlin.idea.scratch.ScratchExecutor +import org.jetbrains.kotlin.idea.scratch.ScratchExpression +import org.jetbrains.kotlin.idea.scratch.ScratchFile +import org.w3c.dom.Element +import org.xml.sax.InputSource +import java.io.ByteArrayInputStream +import java.nio.charset.Charset +import javax.xml.parsers.DocumentBuilderFactory + +private val XML_PREAMBLE = "" + +class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { + private val history: ReplHistory = ReplHistory() + private lateinit var osProcessHandler: OSProcessHandler + + override fun execute() { + val module = file.module ?: return + val cmdLine = KotlinConsoleKeeper.createCommandLine(module) + + osProcessHandler = ReplOSProcessHandler(cmdLine) + osProcessHandler.startNotify() + + file.getExpressions().forEach { expression -> + history.addEntry(expression) + sendCommandToProcess(expression.element.text) + } + + sendCommandToProcess(":quit") + } + + private fun sendCommandToProcess(command: String) { + val processInputOS = osProcessHandler.processInput ?: return logError(this::class.java, "

Broken execute stream

") + val charset = osProcessHandler.charset ?: Charsets.UTF_8 + + val xmlRes = XML_PREAMBLE + + "" + + StringUtil.escapeXml( + StringUtil.replace(command, SOURCE_CHARS, XML_REPLACEMENTS) + ) + + "" + + val bytes = ("$xmlRes\n").toByteArray(charset) + processInputOS.write(bytes) + processInputOS.flush() + } + + private class ReplHistory { + private var entries = arrayListOf() + private var processedEntriesCount: Int = 0 + + fun addEntry(entry: ScratchExpression) { + entries.add(entry) + } + + fun lastUnprocessedEntry(): ScratchExpression? { + return entries.takeIf { processedEntriesCount < entries.size }?.get(processedEntriesCount) + } + + fun lastProcessedEntry(): ScratchExpression? { + val lastProcessedEntryIndex = processedEntriesCount - 1 + return entries.takeIf { lastProcessedEntryIndex < entries.size }?.get(lastProcessedEntryIndex) + } + + fun entryProcessed() { + processedEntriesCount++ + } + } + + private inner class ReplOSProcessHandler(cmd: GeneralCommandLine) : OSProcessHandler(cmd) { + private val factory = DocumentBuilderFactory.newInstance() + + override fun notifyTextAvailable(text: String, outputType: Key<*>) { + if (text.startsWith("warning: classpath entry points to a non-existent location")) return + + if (outputType == ProcessOutputTypes.STDOUT) { + handleReplMessage(text) + } + } + + private fun strToSource(s: String, encoding: Charset = Charsets.UTF_8) = InputSource(ByteArrayInputStream(s.toByteArray(encoding))) + + private fun handleReplMessage(text: String) { + if (text.isBlank()) return + val output = try { + factory.newDocumentBuilder().parse(strToSource(text)) + } catch (e: Exception) { + return + } + + val root = output.firstChild as Element + val outputType = root.getAttribute("type") + val content = StringUtil.replace(root.textContent, XML_REPLACEMENTS, SOURCE_CHARS) + + if (outputType in setOf("SUCCESS", "COMPILE_ERROR", "INTERNAL_ERROR", "RUNTIME_ERROR", "READLINE_END")) { + history.entryProcessed() + } + + val result = content + if (result != null) { + val lastExpression = if (outputType == "USER_OUTPUT") { + // success command is printed after user output + history.lastUnprocessedEntry() + } else { + history.lastProcessedEntry() + } + + if (lastExpression != null) { + } + } + + } + } +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchTopPanel.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchTopPanel.kt index 4f276f66120..76f569336c5 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchTopPanel.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchTopPanel.kt @@ -38,7 +38,7 @@ import javax.swing.JPanel import javax.swing.JSeparator val ScratchFile.scratchTopPanel: ScratchTopPanel? - get() = getScratchPanel(psiFile).firstOrNull { it.scratchFile == this@scratchTopPanel } + get() = getScratchPanel(psiFile)?.takeIf { it.scratchFile == this@scratchTopPanel } class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel(HorizontalLayout(5)) { companion object { diff --git a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt index fa61393051d..419a3f3738d 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt @@ -29,7 +29,6 @@ import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.OrderEnumerator import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.SystemProperties -import org.jetbrains.kotlin.console.actions.errorNotification import org.jetbrains.kotlin.utils.PathUtil import java.io.File import java.util.concurrent.ConcurrentHashMap @@ -45,7 +44,7 @@ class KotlinConsoleKeeper(val project: Project) { fun run(module: Module, previousCompilationFailed: Boolean = false): KotlinConsoleRunner? { val path = module.moduleFilePath - val cmdLine = createCommandLine(module) ?: return run { errorNotification(project, "Module SDK not found"); null } + val cmdLine = createCommandLine(module) val consoleRunner = KotlinConsoleRunner(module, cmdLine, previousCompilationFailed, project, REPL_TITLE, path) consoleRunner.initAndRun() @@ -54,61 +53,61 @@ class KotlinConsoleKeeper(val project: Project) { return consoleRunner } - private fun createCommandLine(module: Module): GeneralCommandLine? { - val javaParameters = createJavaParametersWithSdk(module) - - javaParameters.mainClass = "dummy" - - val commandLine = javaParameters.toCommandLine() - - val paramList = commandLine.parametersList - paramList.clearAll() - - // use to debug repl process - //paramList.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") - - val kotlinPaths = PathUtil.kotlinPathsForIdeaPlugin - val replClassPath = listOf(kotlinPaths.compilerPath, kotlinPaths.reflectPath, kotlinPaths.stdlibPath, kotlinPaths.scriptRuntimePath) - .joinToString(File.pathSeparator) { it.absolutePath } - - paramList.add("-cp") - paramList.add(replClassPath) - - paramList.add("-Dkotlin.repl.ideMode=true") - - paramList.add("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler") - - addPathToCompiledOutput(paramList, module) - - return commandLine - } - - private fun createJavaParametersWithSdk(module: Module): JavaParameters { - val params = JavaParameters() - params.charset = null - - val sdk = ModuleRootManager.getInstance(module).sdk - if (sdk != null && sdk.sdkType is JavaSdkType && File(sdk.homePath).exists()) { - params.jdk = sdk - } - - if (params.jdk == null) { - params.jdk = SimpleJavaSdkType().createJdk("tmp", SystemProperties.getJavaHome()) - } - - return params - } - - private fun addPathToCompiledOutput(paramList: ParametersList, module: Module) { - val compiledModulePath = CompilerPathsEx.getOutputPaths(arrayOf(module)).joinToString(File.pathSeparator) - val moduleDependencies = OrderEnumerator.orderEntries(module).recursively().pathsList.pathsString - val compiledOutputClasspath = "$compiledModulePath${File.pathSeparator}$moduleDependencies" - - paramList.add("-cp") - paramList.add(compiledOutputClasspath) - } - companion object { @JvmStatic fun getInstance(project: Project) = ServiceManager.getService(project, KotlinConsoleKeeper::class.java) + + fun createCommandLine(module: Module): GeneralCommandLine { + val javaParameters = createJavaParametersWithSdk(module) + + javaParameters.mainClass = "dummy" + + val commandLine = javaParameters.toCommandLine() + + val paramList = commandLine.parametersList + paramList.clearAll() + + // use to debug repl process + //paramList.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") + + val kotlinPaths = PathUtil.kotlinPathsForIdeaPlugin + val replClassPath = listOf(kotlinPaths.compilerPath, kotlinPaths.reflectPath, kotlinPaths.stdlibPath, kotlinPaths.scriptRuntimePath) + .joinToString(File.pathSeparator) { it.absolutePath } + + paramList.add("-cp") + paramList.add(replClassPath) + + paramList.add("-Dkotlin.repl.ideMode=true") + + paramList.add("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler") + + addPathToCompiledOutput(paramList, module) + + return commandLine + } + + private fun createJavaParametersWithSdk(module: Module): JavaParameters { + val params = JavaParameters() + params.charset = null + + val sdk = ModuleRootManager.getInstance(module).sdk + if (sdk != null && sdk.sdkType is JavaSdkType && File(sdk.homePath).exists()) { + params.jdk = sdk + } + + if (params.jdk == null) { + params.jdk = SimpleJavaSdkType().createJdk("tmp", SystemProperties.getJavaHome()) + } + + return params + } + + private fun addPathToCompiledOutput(paramList: ParametersList, module: Module) { + val compiledModulePath = CompilerPathsEx.getOutputPaths(arrayOf(module)).joinToString(File.pathSeparator) + val moduleDependencies = OrderEnumerator.orderEntries(module).recursively().pathsList.pathsString + val compiledOutputClasspath = "$compiledModulePath${File.pathSeparator}$moduleDependencies" + + paramList.add("-cp") + paramList.add(compiledOutputClasspath) + } } }