From 654890fac30ad8887120a2a115e334118737306a Mon Sep 17 00:00:00 2001 From: Natalia Selezneva Date: Fri, 19 Jan 2018 11:54:31 +0300 Subject: [PATCH] Scratch: implement compiling executor --- .../kotlin/generators/tests/GenerateTests.kt | 3 +- .../scratch/KtScratchFileLanguageProvider.kt | 3 + .../scratch/ScratchFileLanguageProvider.kt | 1 + .../idea/scratch/actions/RunScratchAction.kt | 7 +- .../scratch/compile/KtCompilingExecutor.kt | 268 ++++++++++++++++++ .../compile/KtScratchSourceFileProcessor.kt | 117 ++++++++ .../kotlin/idea/scratch/ui/ScratchTopPanel.kt | 17 +- .../kotlin/console/KotlinConsoleKeeper.kt | 2 +- idea/testData/scratch/custom/MyJavaClass.java | 7 + idea/testData/scratch/for.comp.after | 3 + idea/testData/scratch/simple.comp.after | 2 + idea/testData/scratch/simpleFun.comp.after | 5 + idea/testData/scratch/unresolved.comp.after | 1 + idea/testData/scratch/userOutput.comp.after | 1 + .../scratch/AbstractScratchRunActionTest.kt | 25 +- .../ScratchRunActionTestGenerated.java | 114 +++++--- 16 files changed, 529 insertions(+), 47 deletions(-) create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtCompilingExecutor.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtScratchSourceFileProcessor.kt create mode 100644 idea/testData/scratch/custom/MyJavaClass.java create mode 100644 idea/testData/scratch/for.comp.after create mode 100644 idea/testData/scratch/simple.comp.after create mode 100644 idea/testData/scratch/simpleFun.comp.after create mode 100644 idea/testData/scratch/unresolved.comp.after create mode 100644 idea/testData/scratch/userOutput.comp.after diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index 344b0161608..bfeb71f6b3e 100755 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -735,7 +735,8 @@ fun main(args: Array) { } testClass { - model("scratch", extension = "kts") + model("scratch", extension = "kts", testMethod = "doCompilingTest", testClassName = "Compiling") + model("scratch", extension = "kts", testMethod = "doReplTest", testClassName = "Repl") } } 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 97e6687e1fa..c13fc74992a 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 @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.idea.scratch import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.idea.scratch.compile.KtCompilingExecutor import org.jetbrains.kotlin.idea.scratch.output.InlayScratchOutputHandler import org.jetbrains.kotlin.idea.scratch.repl.KtScratchReplExecutor import org.jetbrains.kotlin.psi.KtFile @@ -27,5 +28,7 @@ class KtScratchFileLanguageProvider : ScratchFileLanguageProvider() { } override fun createReplExecutor(file: ScratchFile) = KtScratchReplExecutor(file) + override fun createCompilingExecutor(file: ScratchFile) = KtCompilingExecutor(file) + override fun getOutputHandler() = InlayScratchOutputHandler } \ 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 0bea857e1ef..d1e04f9b5dd 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 @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandler abstract class ScratchFileLanguageProvider { abstract fun createFile(psiFile: PsiFile): ScratchFile? abstract fun createReplExecutor(file: ScratchFile): ScratchExecutor? + abstract fun createCompilingExecutor(file: ScratchFile): ScratchExecutor? abstract fun getOutputHandler(): ScratchOutputHandler 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 22640735744..84daac2c67d 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 @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.idea.scratch.ScratchFile import org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider import org.jetbrains.kotlin.idea.scratch.getScratchPanelFromSelectedEditor import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandlerAdapter +import org.jetbrains.kotlin.idea.scratch.ui.scratchTopPanel class RunScratchAction : AnAction( KotlinBundle.message("scratch.run.button"), @@ -38,7 +39,7 @@ class RunScratchAction : AnAction( val scratchFile = scratchTopPanel.scratchFile val isMakeBeforeRun = false // todo use property from panel - val isRepl = true //todo use property from panel + val isRepl = scratchTopPanel.isRepl() val provider = ScratchFileLanguageProvider.get(scratchFile.psiFile.language) ?: return @@ -52,9 +53,9 @@ class RunScratchAction : AnAction( } val runnable = r@ { - val executor = provider.createReplExecutor(scratchFile) + val executor = if (isRepl) provider.createReplExecutor(scratchFile) else provider.createCompilingExecutor(scratchFile) if (executor == null) { - handler.error(scratchFile, "Couldn't run file using REPL") + handler.error(scratchFile, "Couldn't run ${scratchFile.psiFile.name}") handler.onFinish(scratchFile) return@r } diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtCompilingExecutor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtCompilingExecutor.kt new file mode 100644 index 00000000000..7e11a8c1e16 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtCompilingExecutor.kt @@ -0,0 +1,268 @@ +/* + * 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.compile + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.ProcessOutput +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.compiler.ex.CompilerPathsEx +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.OrderEnumerator +import com.intellij.openapi.util.io.FileUtil +import org.jetbrains.kotlin.codegen.ClassBuilderFactories +import org.jetbrains.kotlin.codegen.CompilationErrorHandler +import org.jetbrains.kotlin.codegen.KotlinCodegenFacade +import org.jetbrains.kotlin.codegen.filterClassFiles +import org.jetbrains.kotlin.codegen.state.GenerationState +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.console.KotlinConsoleKeeper +import org.jetbrains.kotlin.diagnostics.Severity +import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages +import org.jetbrains.kotlin.idea.caches.resolve.analyzeFullyAndGetResult +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.debugger.DebuggerUtils +import org.jetbrains.kotlin.idea.refactoring.getLineNumber +import org.jetbrains.kotlin.idea.scratch.ScratchExecutor +import org.jetbrains.kotlin.idea.scratch.ScratchExpression +import org.jetbrains.kotlin.idea.scratch.ScratchFile +import org.jetbrains.kotlin.idea.scratch.output.ScratchOutput +import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputType +import org.jetbrains.kotlin.idea.scratch.ui.scratchTopPanel +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtScript +import org.jetbrains.kotlin.resolve.AnalyzingUtils +import java.io.File + +class KtCompilingExecutor(file: ScratchFile) : ScratchExecutor(file) { + private val log = Logger.getInstance(this.javaClass) + + override fun execute() { + handlers.forEach { it.onStart(file) } + + val module = file.scratchTopPanel?.getModule() ?: return error("Module should be selected") + + if (!checkForErrors(file.psiFile as KtFile)) { + return error("Compilation Error") + } + + val result = KtScratchSourceFileProcessor().process(file) + when (result) { + is KtScratchSourceFileProcessor.Result.Error -> return error(result.message) + is KtScratchSourceFileProcessor.Result.OK -> { + ApplicationManager.getApplication().invokeLater { + val modifiedScratchSourceFile = + KtPsiFactory(file.psiFile.project).createFileWithLightClassSupport("tmp.kt", result.code, file.psiFile) + + try { + val tempDir = compileFileToTempDir(modifiedScratchSourceFile) ?: return@invokeLater + + try { + val handler = CapturingProcessHandler(createCommandLine(module, result.mainClassName, tempDir.path)) + ProcessOutputParser().parse(handler.runProcess()) + } finally { + tempDir.delete() + } + } catch (e: Throwable) { + log.info(result.code, e) + handlers.forEach { it.error(file, e.message ?: "Couldn't compile ${file.psiFile.name}") } + } finally { + handlers.forEach { it.onFinish(file) } + } + } + } + } + } + + private fun compileFileToTempDir(psiFile: KtFile): File? { + if (!checkForErrors(psiFile)) return null + + val resolutionFacade = psiFile.getResolutionFacade() + val (bindingContext, files) = DebuggerUtils.analyzeInlinedFunctions(resolutionFacade, psiFile, false) + + val generateClassFilter = object : GenerationState.GenerateClassFilter() { + override fun shouldGeneratePackagePart(ktFile: KtFile) = ktFile == psiFile + override fun shouldAnnotateClass(processingClassOrObject: KtClassOrObject) = true + override fun shouldGenerateClass(processingClassOrObject: KtClassOrObject) = processingClassOrObject.containingKtFile == psiFile + override fun shouldGenerateScript(script: KtScript) = false + } + + val state = GenerationState.Builder( + file.psiFile.project, + ClassBuilderFactories.binaries(false), + resolutionFacade.moduleDescriptor, + bindingContext, + files, + CompilerConfiguration.EMPTY + ).generateDeclaredClassFilter(generateClassFilter).build() + + KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION) + + return writeClassFilesToTempDir(state) + } + + private fun writeClassFilesToTempDir(state: GenerationState): File { + val classFiles = state.factory.asList().filterClassFiles() + + val dir = FileUtil.createTempDirectory("compile", "scratch") + for (classFile in classFiles) { + val tmpOutFile = File(dir, classFile.relativePath) + tmpOutFile.parentFile.mkdirs() + tmpOutFile.createNewFile() + tmpOutFile.writeBytes(classFile.asByteArray()) + } + return dir + } + + private fun createCommandLine(module: Module, mainClassName: String, tempOutDir: String): GeneralCommandLine { + val javaParameters = KotlinConsoleKeeper.createJavaParametersWithSdk(module) + javaParameters.mainClass = mainClassName + + val compiledModulePath = CompilerPathsEx.getOutputPaths(arrayOf(module)).toList() + val moduleDependencies = OrderEnumerator.orderEntries(module).recursively().pathsList.pathList + + javaParameters.classPath.add(tempOutDir) + javaParameters.classPath.addAll(compiledModulePath) + javaParameters.classPath.addAll(moduleDependencies) + + return javaParameters.toCommandLine() + } + + private fun checkForErrors(psiFile: KtFile): Boolean { + return runReadAction { + try { + AnalyzingUtils.checkForSyntacticErrors(psiFile) + } catch (e: IllegalArgumentException) { + handlers.forEach { it.error(file, e.message ?: "Couldn't compile ${psiFile.name}") } + return@runReadAction false + } + + val analysisResult = psiFile.analyzeFullyAndGetResult() + + if (analysisResult.isError()) { + handlers.forEach { it.error(file, analysisResult.error.message ?: "Couldn't compile ${psiFile.name}") } + return@runReadAction false + } + + val bindingContext = analysisResult.bindingContext + val diagnostics = bindingContext.diagnostics.filter { it.severity == Severity.ERROR } + if (diagnostics.isNotEmpty()) { + diagnostics.forEach { diagnostic -> + if (diagnostic.psiElement.containingFile == psiFile) { + val scratchExpression = file.findExpression( + diagnostic.psiElement.getLineNumber(true), + diagnostic.psiElement.getLineNumber(false) + ) + handlers.forEach { + it.handle( + file, + scratchExpression, + ScratchOutput(DefaultErrorMessages.render(diagnostic), ScratchOutputType.ERROR) + ) + } + } + } + return@runReadAction false + } + return@runReadAction true + } + } + + private fun error(message: String) { + handlers.forEach { it.error(file, message) } + handlers.forEach { it.onFinish(file) } + } + + private fun ScratchFile.findExpression(lineStart: Int, lineEnd: Int): ScratchExpression { + return getExpressions().first { it.lineStart == lineStart && it.lineEnd == lineEnd } + } + + private inner class ProcessOutputParser { + fun parse(processOutput: ProcessOutput) { + val out = processOutput.stdout + val err = processOutput.stderr + if (err.isNotBlank()) { + handlers.forEach { it.error(file, err) } + } + if (out.isNotBlank()) { + parseStdOut(out) + } + } + + private fun parseStdOut(out: String) { + var results = arrayListOf() + var userOutput = arrayListOf() + for (line in out.split("\n")) { + if (isOutputEnd(line)) { + return + } + + if (isGeneratedOutput(line)) { + val lineWoPrefix = line.removePrefix(KtScratchSourceFileProcessor.GENERATED_OUTPUT_PREFIX) + if (isResultEnd(lineWoPrefix)) { + val (startLine, endLine) = extractLineInfoFrom(lineWoPrefix) + ?: return error("Couldn't extract line info from line: $lineWoPrefix") + val scratchExpression = file.findExpression(startLine, endLine) + userOutput.forEach { output -> + handlers.forEach { + it.handle(file, scratchExpression, ScratchOutput(output, ScratchOutputType.OUTPUT)) + } + } + + results.forEach { result -> + handlers.forEach { + it.handle(file, scratchExpression, ScratchOutput(result, ScratchOutputType.RESULT)) + } + } + + results = arrayListOf() + userOutput = arrayListOf() + } else if (lineWoPrefix != Unit.toString()) { + results.add(lineWoPrefix) + } + } else { + userOutput.add(line) + } + } + } + + private fun isOutputEnd(line: String) = line.removeSuffix("\n") == KtScratchSourceFileProcessor.END_OUTPUT_MARKER + private fun isResultEnd(line: String) = line.startsWith(KtScratchSourceFileProcessor.LINES_INFO_MARKER) + private fun isGeneratedOutput(line: String) = line.startsWith(KtScratchSourceFileProcessor.GENERATED_OUTPUT_PREFIX) + + private fun extractLineInfoFrom(encoded: String): Pair? { + val lineInfo = encoded + .removePrefix(KtScratchSourceFileProcessor.LINES_INFO_MARKER) + .removeSuffix("\n") + .split('|') + if (lineInfo.size == 2) { + try { + val (a, b) = lineInfo[0].toInt() to lineInfo[1].toInt() + if (a > -1 && b > -1) { + return a to b + } + } catch (e: NumberFormatException) { + } + } + return null + } + } +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtScratchSourceFileProcessor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtScratchSourceFileProcessor.kt new file mode 100644 index 00000000000..645704fa6f8 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/compile/KtScratchSourceFileProcessor.kt @@ -0,0 +1,117 @@ +/* + * 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.compile + +import org.jetbrains.kotlin.diagnostics.rendering.Renderers +import org.jetbrains.kotlin.diagnostics.rendering.RenderingContext +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny +import org.jetbrains.kotlin.idea.scratch.ScratchExpression +import org.jetbrains.kotlin.idea.scratch.ScratchFile +import org.jetbrains.kotlin.psi.* + +class KtScratchSourceFileProcessor { + companion object { + const val GENERATED_OUTPUT_PREFIX = "##scratch##generated##" + const val LINES_INFO_MARKER = "end##" + const val END_OUTPUT_MARKER = "end##!@#%^&*" + + const val OBJECT_NAME = "ScratchFileRunnerGenerated" + const val INSTANCE_NAME = "instanceScratchFileRunner" + const val PACKAGE_NAME = "org.jetbrains.kotlin.idea.scratch.generated" + const val GET_RES_FUN_NAME_PREFIX = "generated_get_instance_res" + } + + fun process(file: ScratchFile): Result { + val sourceProcessor = KtSourceProcessor() + file.getExpressions().forEach { + sourceProcessor.process(it) + } + + val codeResult = + """ + package $PACKAGE_NAME + + ${sourceProcessor.imports.joinToString("\n") { it.text }} + + object $OBJECT_NAME { + class $OBJECT_NAME { + ${sourceProcessor.classBuilder} + } + + @JvmStatic fun main(args: Array) { + val $INSTANCE_NAME = $OBJECT_NAME() + ${sourceProcessor.objectBuilder} + println("$END_OUTPUT_MARKER") + } + } + """ + return Result.OK("$PACKAGE_NAME.$OBJECT_NAME", codeResult) + } + + class KtSourceProcessor { + val classBuilder = StringBuilder() + val objectBuilder = StringBuilder() + val imports = arrayListOf() + + private var resCount = 0 + + fun process(expression: ScratchExpression) { + val psiElement = expression.element + when (psiElement) { + is KtVariableDeclaration -> processDeclaration(expression, psiElement) + is KtFunction -> processDeclaration(expression, psiElement) + is KtClassOrObject -> processDeclaration(expression, psiElement) + is KtImportDirective -> imports.add(psiElement) + is KtExpression -> processExpression(expression, psiElement) + } + } + + private fun processDeclaration(e: ScratchExpression, c: KtDeclaration) { + classBuilder.append(c.text).newLine() + + val descriptor = c.resolveToDescriptorIfAny() ?: return + + val context = RenderingContext.of(descriptor) + objectBuilder.println(Renderers.COMPACT.render(descriptor, context)) + objectBuilder.appendLineInfo(e) + } + + private fun processExpression(e: ScratchExpression, expr: KtExpression) { + val resName = "$GET_RES_FUN_NAME_PREFIX$resCount" + + classBuilder.append("fun $resName() = run { ${expr.text} }").newLine() + + objectBuilder.printlnObj("$INSTANCE_NAME.$resName()") + objectBuilder.appendLineInfo(e) + + resCount += 1 + } + + private fun StringBuilder.appendLineInfo(e: ScratchExpression) { + println("$LINES_INFO_MARKER${e.lineStart}|${e.lineEnd}") + } + + private fun StringBuilder.println(str: String) = append("println(\"$GENERATED_OUTPUT_PREFIX$str\")").newLine() + private fun StringBuilder.printlnObj(str: String) = append("println(\"$GENERATED_OUTPUT_PREFIX\${$str}\")").newLine() + private fun StringBuilder.newLine() = append("\n") + } + + sealed class Result { + class Error(val message: String) : Result() + class OK(val mainClassName: String, val code: String) : Result() + } +} \ 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 76f569336c5..6abfd4aa5ac 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 @@ -27,15 +27,13 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiManager import com.intellij.ui.components.panels.HorizontalLayout import com.intellij.uiDesigner.core.Spacer +import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.idea.scratch.ScratchFile import org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider import org.jetbrains.kotlin.idea.scratch.actions.ClearScratchAction import org.jetbrains.kotlin.idea.scratch.actions.RunScratchAction import org.jetbrains.kotlin.idea.scratch.getScratchPanel -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JSeparator +import javax.swing.* val ScratchFile.scratchTopPanel: ScratchTopPanel? get() = getScratchPanel(psiFile)?.takeIf { it.scratchFile == this@scratchTopPanel } @@ -50,10 +48,14 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel } private val moduleChooser: ModulesComboBox + private val isReplCheckbox: JCheckBox init { add(createActionsToolbar()) add(JSeparator()) + isReplCheckbox = JCheckBox("Use Repl", false) + add(isReplCheckbox) + add(JSeparator()) add(JLabel("Use classpath of module: ")) moduleChooser = createModuleChooser(scratchFile.psiFile.project) add(moduleChooser) @@ -68,6 +70,13 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel } } + fun isRepl() = isReplCheckbox.isEnabled + + @TestOnly + fun setReplMode(isEnabled: Boolean) { + isReplCheckbox.isEnabled = isEnabled + } + private fun createActionsToolbar(): JComponent { val toolbarGroup = DefaultActionGroup().apply { add(RunScratchAction()) 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 419a3f3738d..2c83e880b42 100644 --- a/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt +++ b/idea/idea-repl/src/org/jetbrains/kotlin/console/KotlinConsoleKeeper.kt @@ -85,7 +85,7 @@ class KotlinConsoleKeeper(val project: Project) { return commandLine } - private fun createJavaParametersWithSdk(module: Module): JavaParameters { + fun createJavaParametersWithSdk(module: Module): JavaParameters { val params = JavaParameters() params.charset = null diff --git a/idea/testData/scratch/custom/MyJavaClass.java b/idea/testData/scratch/custom/MyJavaClass.java new file mode 100644 index 00000000000..551fa1f7ff4 --- /dev/null +++ b/idea/testData/scratch/custom/MyJavaClass.java @@ -0,0 +1,7 @@ +package myTest; + +public class MyJavaClass { + public int test() { + return 1; + } +} \ No newline at end of file diff --git a/idea/testData/scratch/for.comp.after b/idea/testData/scratch/for.comp.after new file mode 100644 index 00000000000..0a7a6928b84 --- /dev/null +++ b/idea/testData/scratch/for.comp.after @@ -0,0 +1,3 @@ +for (i in 0..5) { // OUTPUT: 0; 1; 2; 3; 4; 5 + println(i) +} diff --git a/idea/testData/scratch/simple.comp.after b/idea/testData/scratch/simple.comp.after new file mode 100644 index 00000000000..fbd154b1c57 --- /dev/null +++ b/idea/testData/scratch/simple.comp.after @@ -0,0 +1,2 @@ +val a = 1 // RESULT: val a: Int +a // RESULT: 1 diff --git a/idea/testData/scratch/simpleFun.comp.after b/idea/testData/scratch/simpleFun.comp.after new file mode 100644 index 00000000000..2aee84a673d --- /dev/null +++ b/idea/testData/scratch/simpleFun.comp.after @@ -0,0 +1,5 @@ +fun foo(): Int { // RESULT: fun foo(): Int + return 1 +} + +foo() // RESULT: 1 diff --git a/idea/testData/scratch/unresolved.comp.after b/idea/testData/scratch/unresolved.comp.after new file mode 100644 index 00000000000..4a1578f75c2 --- /dev/null +++ b/idea/testData/scratch/unresolved.comp.after @@ -0,0 +1 @@ +foo() // ERROR: Unresolved reference: foo \ No newline at end of file diff --git a/idea/testData/scratch/userOutput.comp.after b/idea/testData/scratch/userOutput.comp.after new file mode 100644 index 00000000000..b6091122aec --- /dev/null +++ b/idea/testData/scratch/userOutput.comp.after @@ -0,0 +1 @@ +println("hello") // OUTPUT: hello \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/scratch/AbstractScratchRunActionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/scratch/AbstractScratchRunActionTest.kt index a02d8bbde66..33ceb53ccea 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/scratch/AbstractScratchRunActionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/scratch/AbstractScratchRunActionTest.kt @@ -32,12 +32,22 @@ import com.intellij.util.ui.UIUtil import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.scratch.actions.RunScratchAction import org.jetbrains.kotlin.idea.scratch.output.InlayScratchOutputHandler +import org.jetbrains.kotlin.idea.scratch.ui.scratchTopPanel +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor import org.jetbrains.kotlin.test.KotlinTestUtils import org.junit.Assert import java.io.File abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() { - fun doTest(fileName: String) { + fun doReplTest(fileName: String) { + doTest(fileName, true) + } + + fun doCompilingTest(fileName: String) { + doTest(fileName, false) + } + + fun doTest(fileName: String, isRepl: Boolean) { val sourceFile = File(testDataPath, fileName) val fileText = sourceFile.readText() @@ -51,9 +61,13 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() { myFixture.openFileInEditor(scratchFile) + ScratchFileLanguageProvider.createFile(myFixture.file)?.scratchTopPanel?.setReplMode(isRepl) + val event = getActionEvent(myFixture.file.virtualFile, RunScratchAction()) launchAction(event, RunScratchAction()) + UIUtil.dispatchAllInvocationEvents() + val start = System.currentTimeMillis() // wait until output is displayed in editor or for 1 minute while (!event.presentation.isEnabled && (System.currentTimeMillis() - start) < 60000) { @@ -62,9 +76,8 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() { UIUtil.dispatchAllInvocationEvents() - val editors = FileEditorManager.getInstance(project).getEditors(myFixture.file.virtualFile).filterIsInstance() - val doc = - PsiDocumentManager.getInstance(project).getDocument(myFixture.file) ?: error("Document for ${myFixture.file.name} is null") + val editors = FileEditorManager.getInstance(project).getEditors(scratchFile).filterIsInstance() + val doc = PsiDocumentManager.getInstance(project).getDocument(myFixture.file) ?: error("Document for ${myFixture.file.name} is null") val actualOutput = StringBuilder(myFixture.file.text) for (line in doc.lineCount - 1 downTo 0) { @@ -79,7 +92,7 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() { } } - val expectedFileName = fileName.replace(".kts", ".repl.after") + val expectedFileName = if (isRepl) fileName.replace(".kts", ".repl.after") else fileName.replace(".kts", ".comp.after") val expectedFile = File("$testDataPath/$expectedFileName") KotlinTestUtils.assertEqualsToFile(expectedFile, actualOutput.toString()) } @@ -98,4 +111,6 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() { } override fun getTestDataPath() = KotlinTestUtils.getHomeDirectory() + + override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE_FULL_JDK } \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/scratch/ScratchRunActionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/scratch/ScratchRunActionTestGenerated.java index d2db2dc1f35..c4fa01943ac 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/scratch/ScratchRunActionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/scratch/ScratchRunActionTestGenerated.java @@ -17,47 +17,95 @@ import java.util.regex.Pattern; /** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ @SuppressWarnings("all") -@TestMetadata("idea/testData/scratch") -@TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) public class ScratchRunActionTestGenerated extends AbstractScratchRunActionTest { - public void testAllFilesPresentInScratch() throws Exception { - KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/scratch"), Pattern.compile("^(.+)\\.kts$"), TargetBackend.ANY, true); + @TestMetadata("idea/testData/scratch") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Compiling extends AbstractScratchRunActionTest { + public void testAllFilesPresentInCompiling() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/scratch"), Pattern.compile("^(.+)\\.kts$"), TargetBackend.ANY, true); + } + + @TestMetadata("for.kts") + public void testFor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/for.kts"); + doCompilingTest(fileName); + } + + @TestMetadata("simple.kts") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/simple.kts"); + doCompilingTest(fileName); + } + + @TestMetadata("simpleFun.kts") + public void testSimpleFun() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/simpleFun.kts"); + doCompilingTest(fileName); + } + + @TestMetadata("stdlibFun.kts") + public void testStdlibFun() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/stdlibFun.kts"); + doCompilingTest(fileName); + } + + @TestMetadata("unresolved.kts") + public void testUnresolved() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/unresolved.kts"); + doCompilingTest(fileName); + } + + @TestMetadata("userOutput.kts") + public void testUserOutput() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/userOutput.kts"); + doCompilingTest(fileName); + } } - @TestMetadata("for.kts") - public void testFor() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/for.kts"); - doTest(fileName); - } + @TestMetadata("idea/testData/scratch") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Repl extends AbstractScratchRunActionTest { + public void testAllFilesPresentInRepl() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/scratch"), Pattern.compile("^(.+)\\.kts$"), TargetBackend.ANY, true); + } - @TestMetadata("simple.kts") - public void testSimple() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/simple.kts"); - doTest(fileName); - } + @TestMetadata("for.kts") + public void testFor() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/for.kts"); + doReplTest(fileName); + } - @TestMetadata("simpleFun.kts") - public void testSimpleFun() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/simpleFun.kts"); - doTest(fileName); - } + @TestMetadata("simple.kts") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/simple.kts"); + doReplTest(fileName); + } - @TestMetadata("stdlibFun.kts") - public void testStdlibFun() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/stdlibFun.kts"); - doTest(fileName); - } + @TestMetadata("simpleFun.kts") + public void testSimpleFun() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/simpleFun.kts"); + doReplTest(fileName); + } - @TestMetadata("unresolved.kts") - public void testUnresolved() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/unresolved.kts"); - doTest(fileName); - } + @TestMetadata("stdlibFun.kts") + public void testStdlibFun() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/stdlibFun.kts"); + doReplTest(fileName); + } - @TestMetadata("userOutput.kts") - public void testUserOutput() throws Exception { - String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/userOutput.kts"); - doTest(fileName); + @TestMetadata("unresolved.kts") + public void testUnresolved() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/unresolved.kts"); + doReplTest(fileName); + } + + @TestMetadata("userOutput.kts") + public void testUserOutput() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/scratch/userOutput.kts"); + doReplTest(fileName); + } } }