Implement REPL Mode for scratches

^KT-27963 Fixed
This commit is contained in:
Natalia Selezneva
2019-06-03 11:40:43 +03:00
parent 8bc03e54f7
commit 414f98aa29
37 changed files with 739 additions and 98 deletions
@@ -123,6 +123,7 @@ import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractMultiModuleSafeD
import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractSafeDeleteTest
import org.jetbrains.kotlin.idea.repl.AbstractIdeReplCompletionTest
import org.jetbrains.kotlin.idea.resolve.*
import org.jetbrains.kotlin.idea.scratch.AbstractScratchLineMarkersTest
import org.jetbrains.kotlin.idea.scratch.AbstractScratchRunActionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationCompletionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationHighlightingTest
@@ -808,6 +809,10 @@ fun main(args: Array<String>) {
model("scratch/multiFile", extension = null, testMethod = "doMultiFileTest", recursive = false)
}
testClass<AbstractScratchLineMarkersTest> {
model("scratch/lineMarker", testMethod = "doScratchTest", pattern = KT_OR_KTS)
}
testClass<AbstractFirMultiModuleResolveTest> {
model("fir/multiModule", recursive = false, extension = null)
}
@@ -129,6 +129,7 @@ import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractMultiModuleSafeD
import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractSafeDeleteTest
import org.jetbrains.kotlin.idea.repl.AbstractIdeReplCompletionTest
import org.jetbrains.kotlin.idea.resolve.*
import org.jetbrains.kotlin.idea.scratch.AbstractScratchLineMarkersTest
import org.jetbrains.kotlin.idea.scratch.AbstractScratchRunActionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationCompletionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationHighlightingTest
@@ -818,6 +819,10 @@ fun main(args: Array<String>) {
model("scratch/multiFile", extension = null, testMethod = "doMultiFileTest", recursive = false)
}
testClass<AbstractScratchLineMarkersTest> {
model("scratch/lineMarker", testMethod = "doScratchTest", pattern = KT_OR_KTS)
}
testClass<AbstractFirMultiModuleResolveTest> {
model("fir/multiModule", recursive = false, extension = null)
}
@@ -117,6 +117,7 @@ import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractMultiModuleSafeD
import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractSafeDeleteTest
import org.jetbrains.kotlin.idea.repl.AbstractIdeReplCompletionTest
import org.jetbrains.kotlin.idea.resolve.*
import org.jetbrains.kotlin.idea.scratch.AbstractScratchLineMarkersTest
import org.jetbrains.kotlin.idea.scratch.AbstractScratchRunActionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationCompletionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationHighlightingTest
@@ -782,6 +783,10 @@ fun main(args: Array<String>) {
model("scratch/multiFile", extension = null, testMethod = "doMultiFileTest", recursive = false)
}
testClass<AbstractScratchLineMarkersTest> {
model("scratch/lineMarker", testMethod = "doScratchTest", pattern = KT_OR_KTS)
}
testClass<AbstractFirMultiModuleResolveTest> {
model("fir/multiModule", recursive = false, extension = null)
}
@@ -117,6 +117,7 @@ import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractMultiModuleSafeD
import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractSafeDeleteTest
import org.jetbrains.kotlin.idea.repl.AbstractIdeReplCompletionTest
import org.jetbrains.kotlin.idea.resolve.*
import org.jetbrains.kotlin.idea.scratch.AbstractScratchLineMarkersTest
import org.jetbrains.kotlin.idea.scratch.AbstractScratchRunActionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationCompletionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationHighlightingTest
@@ -786,6 +787,10 @@ fun main(args: Array<String>) {
model("scratch/multiFile", extension = null, testMethod = "doMultiFileTest", recursive = false)
}
testClass<AbstractScratchLineMarkersTest> {
model("scratch/lineMarker", testMethod = "doScratchTest", pattern = KT_OR_KTS)
}
testClass<AbstractFirMultiModuleResolveTest> {
model("fir/multiModule", recursive = false, extension = null)
}
@@ -117,6 +117,7 @@ import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractMultiModuleSafeD
import org.jetbrains.kotlin.idea.refactoring.safeDelete.AbstractSafeDeleteTest
import org.jetbrains.kotlin.idea.repl.AbstractIdeReplCompletionTest
import org.jetbrains.kotlin.idea.resolve.*
import org.jetbrains.kotlin.idea.scratch.AbstractScratchLineMarkersTest
import org.jetbrains.kotlin.idea.scratch.AbstractScratchRunActionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationCompletionTest
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationHighlightingTest
@@ -784,6 +785,10 @@ fun main(args: Array<String>) {
model("scratch/multiFile", extension = null, testMethod = "doMultiFileTest", recursive = false)
}
testClass<AbstractScratchLineMarkersTest> {
model("scratch/lineMarker", testMethod = "doScratchTest", pattern = KT_OR_KTS)
}
testClass<AbstractFirMultiModuleResolveTest> {
model("fir/multiModule", recursive = false, extension = null)
}
@@ -251,6 +251,7 @@ android.klint.inspections.group.name=Android Lint for Kotlin
# Scratch Files
scratch.run.button=Run Scratch File
scratch.run.from.here.button=Run Scratch From Here
scratch.stop.button=Stop scratch execution
scratch.clear.button=Clear results
@@ -19,18 +19,19 @@ package org.jetbrains.kotlin.idea.scratch
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiUtil
import org.jetbrains.kotlin.diagnostics.Severity
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.util.getLineNumber
import org.jetbrains.kotlin.idea.core.util.getLineStartOffset
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.AnalyzingUtils
class KtScratchFile(project: Project, editor: TextEditor) : ScratchFile(project, editor) {
@@ -65,6 +66,11 @@ class KtScratchFile(project: Project, editor: TextEditor) : ScratchFile(project,
return result
}
fun PsiElement.getLineNumber(start: Boolean = true): Int {
val document = PsiDocumentManager.getInstance(project).getLastCommittedDocument(containingFile)
return document?.getLineNumber(if (start) startOffset else endOffset) ?: 0
}
override fun hasErrors(): Boolean {
val psiFile = getPsiFile() as? KtFile ?: return false
try {
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
* Copyrig()ht 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.
@@ -16,6 +16,13 @@
package org.jetbrains.kotlin.idea.scratch
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutput
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandler
@@ -39,13 +46,17 @@ abstract class ScratchExecutor(protected val file: ScratchFile) {
if (e != null) LOG.error(e)
}
protected class CompositeOutputHandler : ScratchOutputHandler {
private val handlers = mutableListOf<ScratchOutputHandler>()
class CompositeOutputHandler : ScratchOutputHandler {
private val handlers = mutableSetOf<ScratchOutputHandler>()
fun add(handler: ScratchOutputHandler) {
handlers.add(handler)
}
fun remove(handler: ScratchOutputHandler) {
handlers.remove(handler)
}
override fun onStart(file: ScratchFile) {
handlers.forEach { it.onStart(file) }
}
@@ -66,4 +77,90 @@ abstract class ScratchExecutor(protected val file: ScratchFile) {
handlers.forEach { it.clear(file) }
}
}
}
abstract class SequentialScratchExecutor(file: ScratchFile) : ScratchExecutor(file) {
abstract fun executeStatement(expression: ScratchExpression)
protected abstract fun startExecution()
protected abstract fun stopExecution(callback: (() -> Unit)? = null)
protected abstract fun needProcessToStart(): Boolean
private var lastExecuted = 0
fun start() {
EditorFactory.getInstance().eventMulticaster.addDocumentListener(listener, file.project.messageBus.connect())
startExecution()
}
override fun stop() {
EditorFactory.getInstance().eventMulticaster.removeDocumentListener(listener)
stopExecution()
}
fun executeNew() {
handler.onStart(file)
for ((index, expression) in file.getExpressions().withIndex()) {
if (index + 1 <= lastExecuted) continue
executeStatement(expression)
lastExecuted = index + 1
}
}
override fun execute() {
if (needToRestartProcess()) {
lastExecuted = 0
handler.clear(file)
handler.onStart(file)
stopExecution {
ApplicationManager.getApplication().invokeLater {
executeNew()
}
}
} else {
executeNew()
}
}
fun getFirstNewExpression(): ScratchExpression? {
val expressions = runReadAction { file.getExpressions() }
if (lastExecuted in expressions.indices) {
return expressions[lastExecuted]
}
return null
}
private fun needToRestartProcess(): Boolean {
return lastExecuted > 0
}
private val listener = object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
if (event.newFragment.isBlank() && event.oldFragment.isBlank()) return
if (!needToRestartProcess()) return
val document = event.document
val virtualFile = FileDocumentManager.getInstance().getFile(document)?.takeIf { it.isInLocalFileSystem } ?: return
if (!virtualFile.isValid) {
return
}
if (PsiManager.getInstance(file.project).findFile(virtualFile) != file.getPsiFile()) return
val changedLine = document.getLineNumber(event.offset)
val changedExpression = file.getExpressionAtLine(changedLine) ?: return
val changedExpressionIndex = file.getExpressions().indexOf(changedExpression) + 1
if (changedExpressionIndex <= lastExecuted) {
lastExecuted = 0
handler.clear(file)
stopExecution()
}
}
}
}
@@ -26,6 +26,9 @@ import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.scratch.ui.scratchFileOptions
abstract class ScratchFile(val project: Project, val editor: TextEditor) {
var replScratchExecutor: SequentialScratchExecutor? = null
var compilingScratchExecutor: ScratchExecutor? = null
fun getExpressions(): List<ScratchExpression> = runReadAction {
getPsiFile()?.let { getExpressions(it) } ?: emptyList()
}
@@ -49,6 +52,10 @@ abstract class ScratchFile(val project: Project, val editor: TextEditor) {
}
}
fun getExpressionAtLine(line: Int): ScratchExpression? {
return getExpressions().find { line in it.lineStart..it.lineEnd }
}
abstract fun getExpressions(psiFile: PsiFile): List<ScratchExpression>
abstract fun hasErrors(): Boolean
}
@@ -17,6 +17,7 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Alarm
import org.jetbrains.kotlin.idea.scratch.actions.RunScratchAction
import org.jetbrains.kotlin.idea.scratch.actions.RunScratchFromHereAction
import org.jetbrains.kotlin.idea.scratch.actions.ScratchCompilationSupport
import org.jetbrains.kotlin.idea.scratch.ui.ScratchTopPanel
@@ -46,22 +47,26 @@ class ScratchFileAutoRunner(private val project: Project) : DocumentListener {
if (!panel.scratchFile.options.isInteractiveMode) return
if (!event.newFragment.isBlank()) {
runScratch(panel)
runScratch(panel.scratchFile)
}
}
private fun runScratch(panel: ScratchTopPanel) {
private fun runScratch(scratchFile: ScratchFile) {
myAlarm.cancelAllRequests()
if (ScratchCompilationSupport.isInProgress(panel.scratchFile)) {
if (ScratchCompilationSupport.isInProgress(scratchFile) && !scratchFile.options.isRepl) {
ScratchCompilationSupport.forceStop()
}
myAlarm.addRequest(
{
val psiFile = panel.scratchFile.getPsiFile()
if (psiFile != null && psiFile.isValid && !panel.scratchFile.hasErrors()) {
RunScratchAction.doAction(panel, true)
val psiFile = scratchFile.getPsiFile()
if (psiFile != null && psiFile.isValid && !scratchFile.hasErrors()) {
if (scratchFile.options.isRepl) {
RunScratchFromHereAction.doAction(scratchFile)
} else {
RunScratchAction.doAction(scratchFile, true)
}
}
}, auto_run_delay, true
)
@@ -22,12 +22,39 @@ import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.scratch.actions.ScratchCompilationSupport
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandler
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandlerAdapter
abstract class ScratchFileLanguageProvider {
abstract fun createFile(project: Project, editor: TextEditor): ScratchFile?
abstract fun createReplExecutor(file: ScratchFile): ScratchExecutor?
abstract fun createCompilingExecutor(file: ScratchFile): ScratchExecutor?
fun newScratchFile(project: Project, editor: TextEditor): ScratchFile? {
val scratchFile = createFile(project, editor) ?: return null
scratchFile.replScratchExecutor = createReplExecutor(scratchFile)
scratchFile.compilingScratchExecutor = createCompilingExecutor(scratchFile)
scratchFile.replScratchExecutor?.addOutputHandlers()
scratchFile.compilingScratchExecutor?.addOutputHandlers()
return scratchFile
}
private fun ScratchExecutor.addOutputHandlers() {
addOutputHandler(getOutputHandler())
addOutputHandler(object : ScratchOutputHandlerAdapter() {
override fun onStart(file: ScratchFile) {
ScratchCompilationSupport.start(file, this@addOutputHandlers)
}
override fun onFinish(file: ScratchFile) {
ScratchCompilationSupport.stop()
}
})
}
protected abstract fun createFile(project: Project, editor: TextEditor): ScratchFile?
protected abstract fun createReplExecutor(file: ScratchFile): SequentialScratchExecutor?
protected abstract fun createCompilingExecutor(file: ScratchFile): ScratchExecutor?
abstract fun getOutputHandler(): ScratchOutputHandler
@@ -23,9 +23,10 @@ import com.intellij.openapi.keymap.KeymapUtil
import com.intellij.openapi.project.DumbService
import com.intellij.task.ProjectTaskManager
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.scratch.*
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandlerAdapter
import org.jetbrains.kotlin.idea.scratch.ui.ScratchTopPanel
import org.jetbrains.kotlin.idea.scratch.ScratchFile
import org.jetbrains.kotlin.idea.scratch.SequentialScratchExecutor
import org.jetbrains.kotlin.idea.scratch.getScratchPanelFromSelectedEditor
import org.jetbrains.kotlin.idea.scratch.printDebugMessage
import org.jetbrains.kotlin.idea.scratch.LOG as log
class RunScratchAction : ScratchAction(
@@ -43,56 +44,36 @@ class RunScratchAction : ScratchAction(
val project = e.project ?: return
val scratchPanel = getScratchPanelFromSelectedEditor(project) ?: return
doAction(scratchPanel, false)
doAction(scratchPanel.scratchFile, false)
}
companion object {
fun doAction(scratchPanel: ScratchTopPanel, isAutoRun: Boolean) {
val scratchFile = scratchPanel.scratchFile
val psiFile = scratchFile.getPsiFile() ?: return
val isMakeBeforeRun = scratchFile.options.isMakeBeforeRun
fun doAction(scratchFile: ScratchFile, isAutoRun: Boolean) {
val isRepl = scratchFile.options.isRepl
val executor = (if (isRepl) scratchFile.replScratchExecutor else scratchFile.compilingScratchExecutor) ?: return
val provider = ScratchFileLanguageProvider.get(psiFile.language) ?: return
log.printDebugMessage("Run Action: isMakeBeforeRun = $isMakeBeforeRun, isRepl = $isRepl")
val defaultOutputHandler = provider.getOutputHandler()
val module = scratchPanel.getModule()
val executor = if (isRepl) provider.createReplExecutor(scratchFile) else provider.createCompilingExecutor(scratchFile)
@Suppress("FoldInitializerAndIfToElvis")
if (executor == null) {
return defaultOutputHandler.error(scratchFile, "Couldn't run ${psiFile.name}")
}
executor.addOutputHandler(defaultOutputHandler)
executor.addOutputHandler(object : ScratchOutputHandlerAdapter() {
override fun onStart(file: ScratchFile) {
ScratchCompilationSupport.start(file, executor)
scratchPanel.updateToolbar()
}
override fun onFinish(file: ScratchFile) {
ScratchCompilationSupport.stop()
scratchPanel.updateToolbar()
}
})
log.printDebugMessage("Run Action: isRepl = $isRepl")
fun executeScratch() {
try {
executor.execute()
if (isAutoRun && executor is SequentialScratchExecutor) {
executor.executeNew()
} else {
executor.execute()
}
} catch (ex: Throwable) {
executor.errorOccurs("Exception occurs during Run Scratch Action", ex, true)
}
}
val isMakeBeforeRun = scratchFile.options.isMakeBeforeRun
log.printDebugMessage("Run Action: isMakeBeforeRun = $isMakeBeforeRun")
val module = scratchFile.getModule()
log.printDebugMessage("Run Action: module = ${module?.name}")
if (!isAutoRun && module != null && isMakeBeforeRun) {
val project = scratchPanel.scratchFile.project
val project = scratchFile.project
ProjectTaskManager.getInstance(project).build(arrayOf(module)) { result ->
if (result.isAborted || result.errors > 0) {
executor.errorOccurs("There were compilation errors in module ${module.name}")
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scratch.actions
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.scratch.ScratchFile
import org.jetbrains.kotlin.idea.scratch.getScratchPanelFromSelectedEditor
class RunScratchFromHereAction : ScratchAction(
KotlinBundle.message("scratch.run.from.here.button"),
AllIcons.Diff.ArrowRight
) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val scratchPanel = getScratchPanelFromSelectedEditor(project) ?: return
doAction(scratchPanel.scratchFile)
}
companion object {
fun doAction(scratchFile: ScratchFile) {
val executor = scratchFile.replScratchExecutor ?: return
try {
executor.executeNew()
} catch (ex: Throwable) {
executor.errorOccurs("Exception occurs during Run Scratch Action", ex, true)
}
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scratch.actions
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.ide.scratch.ScratchFileService
import com.intellij.ide.scratch.ScratchRootType
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.idea.core.util.getLineCount
import org.jetbrains.kotlin.idea.refactoring.getLineNumber
import org.jetbrains.kotlin.idea.scratch.ScratchExpression
import org.jetbrains.kotlin.idea.scratch.getEditorWithScratchPanel
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
class ScratchRunLineMarkerContributor : RunLineMarkerContributor() {
override fun getInfo(element: PsiElement): Info? {
val ktFile = element.containingFile as? KtFile
if (ktFile?.isScript() != true) return null
val file = ktFile.virtualFile
if (ScratchFileService.getInstance().getRootType(file) !is ScratchRootType) return null
val declaration = element.getStrictParentOfType<KtNamedDeclaration>()
if (declaration != null && declaration !is KtParameter && declaration.nameIdentifier == element) {
return isLastExecutedExpression(element)
}
val scriptInitializer = element.getParentOfType<KtScriptInitializer>(true)?.body
if (scriptInitializer != null) {
if (scriptInitializer.findDescendantOfType<LeafPsiElement>() == element) {
return isLastExecutedExpression(element)
}
return null
}
// Show arrow for last added empty line
if (declaration is KtScript && element is PsiWhiteSpace) {
val expression = getLastExecutedExpression(element)
if (expression == null) {
if (element.getLineNumber() == element.containingFile.getLineCount()
|| element.getLineNumber(false) == element.containingFile.getLineCount()) {
return Info(RunScratchFromHereAction())
}
}
}
return null
}
private fun isLastExecutedExpression(element: PsiElement): Info? {
val expression = getLastExecutedExpression(element) ?: return null
if (element.getLineNumber(true) != expression.lineStart) {
return null
}
if (PsiTreeUtil.isAncestor(expression.element, element, false)) {
return Info(RunScratchFromHereAction())
}
return null
}
private fun getLastExecutedExpression(element: PsiElement): ScratchExpression? {
val (_, panel) = getEditorWithScratchPanel(
FileEditorManager.getInstance(element.project),
element.containingFile.virtualFile
) ?: return null
val scratchFile = panel.scratchFile
if (!scratchFile.options.isRepl) return null
val replExecutor = scratchFile.replScratchExecutor ?: return null
return replExecutor.getFirstNewExpression()
}
}
@@ -18,8 +18,9 @@ package org.jetbrains.kotlin.idea.scratch.repl
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.OSProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.util.Key
import org.jetbrains.kotlin.cli.common.repl.replInputAsXml
import org.jetbrains.kotlin.cli.common.repl.replNormalizeLineBreaks
@@ -36,42 +37,68 @@ import java.io.ByteArrayInputStream
import java.nio.charset.Charset
import javax.xml.parsers.DocumentBuilderFactory
class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) {
class KtScratchReplExecutor(file: ScratchFile) : SequentialScratchExecutor(file) {
private val history: ReplHistory = ReplHistory()
private lateinit var osProcessHandler: OSProcessHandler
override fun execute() {
handler.onStart(file)
private var osProcessHandler: OSProcessHandler? = null
override fun startExecution() {
val module = file.getModule()
val cmdLine = KotlinConsoleKeeper.createReplCommandLine(file.project, module)
LOG.printDebugMessage("Execute REPL: ${cmdLine.commandLineString}")
osProcessHandler = ReplOSProcessHandler(cmdLine)
osProcessHandler.startNotify()
file.getExpressions().forEach { expression ->
history.addEntry(expression)
sendCommandToProcess(runReadAction { expression.element.text })
}
sendCommandToProcess(":quit")
osProcessHandler?.startNotify()
}
override fun stop() {
override fun stopExecution(callback: (() -> Unit)?) {
val osProcessHandler = osProcessHandler ?: return
try {
osProcessHandler.process.destroy()
} finally {
handler.onFinish(file)
if (callback != null) {
osProcessHandler.addProcessListener(object : ProcessAdapter() {
override fun processTerminated(event: ProcessEvent) {
callback()
}
})
}
sendCommandToProcess(":quit")
} catch (e: Exception) {
errorOccurs("Couldn't stop REPL process", e, false)
osProcessHandler.destroyProcess()
clearState()
}
}
private fun clearState() {
history.clear()
osProcessHandler = null
handler.onFinish(file)
}
override fun executeStatement(expression: ScratchExpression) {
if (needProcessToStart()) {
startExecution()
}
history.addEntry(expression)
try {
sendCommandToProcess(expression.element.text)
} catch (e: Throwable) {
errorOccurs("Couldn't execute statement: ${expression.element.text}", e, true)
}
}
override fun needProcessToStart(): Boolean {
return osProcessHandler == null
}
private fun sendCommandToProcess(command: String) {
LOG.printDebugMessage("Send to REPL: ${command}")
val processInputOS = osProcessHandler.processInput ?: return logError(this::class.java, "<p>Broken execute stream</p>")
val charset = osProcessHandler.charset ?: Charsets.UTF_8
val processInputOS = osProcessHandler?.processInput ?: return logError(this::class.java, "<p>Broken execute stream</p>")
val charset = osProcessHandler?.charset ?: Charsets.UTF_8
val xmlRes = command.replInputAsXml()
@@ -93,6 +120,8 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) {
}
fun lastProcessedEntry(): ScratchExpression? {
if (processedEntriesCount < 1) return null
val lastProcessedEntryIndex = processedEntriesCount - 1
return entries.takeIf { lastProcessedEntryIndex < entries.size }?.get(lastProcessedEntryIndex)
}
@@ -100,6 +129,13 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) {
fun entryProcessed() {
processedEntriesCount++
}
fun clear() {
entries = arrayListOf()
processedEntriesCount = 0
}
fun isAllProcessed() = entries.size == processedEntriesCount
}
private inner class ReplOSProcessHandler(cmd: GeneralCommandLine) : OSProcessHandler(cmd) {
@@ -115,7 +151,9 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) {
}
override fun notifyProcessTerminated(exitCode: Int) {
handler.onFinish(file)
super.notifyProcessTerminated(exitCode)
clearState()
}
private fun strToSource(s: String, encoding: Charset = Charsets.UTF_8) = InputSource(ByteArrayInputStream(s.toByteArray(encoding)))
@@ -136,6 +174,9 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) {
if (outputType in setOf("SUCCESS", "COMPILE_ERROR", "INTERNAL_ERROR", "RUNTIME_ERROR", "READLINE_END")) {
history.entryProcessed()
if (history.isAllProcessed()) {
handler.onFinish(file)
}
}
val result = parseReplOutput(content, outputType)
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.scratch.ui
import com.intellij.application.options.ModulesComboBox
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.execution.ui.ConfigurationModuleSelector
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
@@ -43,19 +44,50 @@ import org.jetbrains.kotlin.idea.scratch.actions.ClearScratchAction
import org.jetbrains.kotlin.idea.scratch.actions.RunScratchAction
import org.jetbrains.kotlin.idea.scratch.actions.StopScratchAction
import org.jetbrains.kotlin.idea.scratch.addScratchPanel
import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandlerAdapter
import org.jetbrains.kotlin.idea.scratch.removeScratchPanel
import javax.swing.*
class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel(HorizontalLayout(5)), Disposable {
override fun dispose() {
scratchFile.replScratchExecutor?.stop()
scratchFile.compilingScratchExecutor?.stop()
scratchFile.editor.removeScratchPanel()
}
companion object {
fun createPanel(project: Project, virtualFile: VirtualFile, editor: TextEditor) {
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return
val scratchFile = ScratchFileLanguageProvider.get(psiFile.language)?.createFile(project, editor) ?: return
editor.addScratchPanel(ScratchTopPanel(scratchFile))
val scratchFile = ScratchFileLanguageProvider.get(psiFile.language)?.newScratchFile(project, editor) ?: return
val panel = ScratchTopPanel(scratchFile)
val toolbarHandler = createUpdateToolbarHandler(panel)
scratchFile.replScratchExecutor?.addOutputHandler(object : ScratchOutputHandlerAdapter() {
override fun onFinish(file: ScratchFile) {
ApplicationManager.getApplication().invokeLater {
if (!file.project.isDisposed) {
val scratch = file.getPsiFile()
if (scratch?.isValid == true) {
DaemonCodeAnalyzer.getInstance(project).restart(scratch)
}
}
}
}
})
scratchFile.replScratchExecutor?.addOutputHandler(toolbarHandler)
scratchFile.compilingScratchExecutor?.addOutputHandler(toolbarHandler)
editor.addScratchPanel(panel)
}
private fun createUpdateToolbarHandler(panel: ScratchTopPanel) = object : ScratchOutputHandlerAdapter() {
override fun onStart(file: ScratchFile) {
panel.updateToolbar()
}
override fun onFinish(file: ScratchFile) {
panel.updateToolbar()
}
}
}
@@ -104,6 +136,13 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel
scratchFile.saveOptions {
copy(isRepl = isReplCheckbox.isSelected)
}
if (isReplCheckbox.isSelected) {
// TODO start REPL process when checkbox is selected to speed up execution
// Now it is switched off due to KT-18355: REPL process is keep alive if no command is executed
//scratchFile.replScratchExecutor?.start()
} else {
scratchFile.replScratchExecutor?.stop()
}
}
add(JSeparator(SwingConstants.VERTICAL))
+2
View File
@@ -143,6 +143,8 @@
<scratch.creationHelper language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.scratch.KtScratchFileCreationHelper"/>
<runLineMarkerContributor language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.scratch.actions.ScratchRunLineMarkerContributor"/>
<localInspection
groupName="Plugin DevKit"
+2
View File
@@ -142,6 +142,8 @@
<scratch.creationHelper language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.scratch.KtScratchFileCreationHelper"/>
<runLineMarkerContributor language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.scratch.actions.ScratchRunLineMarkerContributor"/>
<localInspection
groupName="Plugin DevKit"
+2
View File
@@ -143,6 +143,8 @@
<scratch.creationHelper language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.scratch.KtScratchFileCreationHelper"/>
<runLineMarkerContributor language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.scratch.actions.ScratchRunLineMarkerContributor"/>
<localInspection
groupName="Plugin DevKit"
@@ -311,7 +311,9 @@ class SelectionAwareScopeHighlighter(val editor: Editor) {
fun PsiElement.getLineNumber(start: Boolean = true): Int {
val document = containingFile.viewProvider.document ?: PsiDocumentManager.getInstance(project).getDocument(containingFile)
return document?.getLineNumber(if (start) this.startOffset else this.endOffset) ?: 0
val index = if (start) this.startOffset else this.endOffset
if (index > document?.textLength ?: 0) return 0
return document?.getLineNumber(index) ?: 0
}
class SeparateFileWrapper(manager: PsiManager) : LightElement(manager, KotlinLanguage.INSTANCE) {
+3
View File
@@ -0,0 +1,3 @@
<lineMarker>1</lineMarker> + 1
// REPL_MODE: true
@@ -0,0 +1,3 @@
<lineMarker>1</lineMarker> + 2 + 3
// REPL_MODE: true
@@ -0,0 +1,3 @@
<lineMarker>1</lineMarker>
// REPL_MODE: true
+5
View File
@@ -0,0 +1,5 @@
<lineMarker>for</lineMarker> (i in 0..4) {
println(i)
}
// REPL_MODE: true
+5
View File
@@ -0,0 +1,5 @@
fun <lineMarker>foo</lineMarker>(): Int {
return 1
}
// REPL_MODE: true
+5
View File
@@ -0,0 +1,5 @@
<lineMarker>run</lineMarker> {
1
}
// REPL_MODE: true
+5
View File
@@ -0,0 +1,5 @@
<lineMarker>{</lineMarker>
1
}()
// REPL_MODE: true
+7
View File
@@ -0,0 +1,7 @@
<lineMarker>object</lineMarker>: Comparable<String> {
override fun compareTo(other: String): Int {
return 0
}
}
// REPL_MODE: true
+3
View File
@@ -0,0 +1,3 @@
<lineMarker>(</lineMarker>1 + 2)
// REPL_MODE: true
+3
View File
@@ -0,0 +1,3 @@
<lineMarker>kotlin</lineMarker>.io.println("Hi")
// REPL_MODE: true
@@ -0,0 +1,5 @@
<lineMarker>kotlin</lineMarker>
.io
.println("Hi")
// REPL_MODE: true
@@ -0,0 +1,3 @@
<lineMarker>println</lineMarker>("Hi")
// REPL_MODE: true
+3
View File
@@ -0,0 +1,3 @@
val <lineMarker>a</lineMarker> = 1
// REPL_MODE: true
@@ -140,27 +140,6 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
return inlineElementsInRange.map { it.renderer as InlayScratchFileRenderer }
}
private fun configureOptions(
scratchPanel: ScratchTopPanel,
fileText: String
) {
if (InTextDirectivesUtils.getPrefixedBoolean(fileText, "// INTERACTIVE_MODE: ") != true) {
scratchPanel.scratchFile.saveOptions {
copy(isInteractiveMode = false)
}
}
if (InTextDirectivesUtils.getPrefixedBoolean(fileText, "// REPL_MODE: ") == true) {
scratchPanel.scratchFile.saveOptions {
copy(isRepl = true)
}
}
if (!InTextDirectivesUtils.isDirectiveDefined(fileText, "// NO_MODULE")) {
scratchPanel.setModule(myFixture.module)
}
}
protected fun configureScratchByText(name: String, text: String): ScratchTopPanel {
val scratchFile = ScratchRootType.getInstance().createScratchFile(
project,
@@ -177,7 +156,7 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
val (_, scratchPanel) = getEditorWithScratchPanel(myManager, myFixture.file.virtualFile)
?: error("Couldn't find scratch panel")
configureOptions(scratchPanel, text)
configureOptions(scratchPanel, text, myFixture.module)
return scratchPanel
}
@@ -240,6 +219,12 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
}
override fun tearDown() {
if (myFixture.file != null) {
val (_, scratchPanel) = getEditorWithScratchPanel(myManager, myFixture.file.virtualFile)
?: error("Couldn't find scratch panel")
scratchPanel.scratchFile.replScratchExecutor?.stop()
}
super.tearDown()
VfsRootAccess.disallowRootAccess(KotlinTestUtils.getHomeDirectory())
@@ -262,5 +247,24 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
private val INSTANCE_WITHOUT_RUNTIME = object : KotlinLightProjectDescriptor() {
override fun getSdk() = PluginTestCaseBase.fullJdk()
}
fun configureOptions(
scratchPanel: ScratchTopPanel,
fileText: String,
module: Module?
) {
if (InTextDirectivesUtils.getPrefixedBoolean(fileText, "// INTERACTIVE_MODE: ") != true) {
scratchPanel.setInteractiveMode(false)
}
if (InTextDirectivesUtils.getPrefixedBoolean(fileText, "// REPL_MODE: ") == true) {
scratchPanel.setReplMode(true)
}
if (module != null && !InTextDirectivesUtils.isDirectiveDefined(fileText, "// NO_MODULE")) {
scratchPanel.setModule(module)
}
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scratch
import com.intellij.ide.scratch.ScratchFileService
import com.intellij.ide.scratch.ScratchRootType
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiDocumentManager
import com.intellij.testFramework.ExpectedHighlightingData
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.codeInsight.AbstractLineMarkersTest
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager
import org.jetbrains.kotlin.idea.scratch.AbstractScratchRunActionTest.Companion.configureOptions
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
abstract class AbstractScratchLineMarkersTest : AbstractLineMarkersTest() {
fun doScratchTest(path: String) {
val fileText = FileUtil.loadFile(File(path))
val scratchFile = ScratchRootType.getInstance().createScratchFile(
project,
"scratch.kts",
KotlinLanguage.INSTANCE,
fileText,
ScratchFileService.Option.create_if_missing
) ?: error("Couldn't create scratch file")
myFixture.openFileInEditor(scratchFile)
ScriptDependenciesManager.updateScriptDependenciesSynchronously(scratchFile, project)
val (_, scratchPanel) = getEditorWithScratchPanel(FileEditorManager.getInstance(project), myFixture.file.virtualFile)
?: error("Couldn't find scratch panel")
configureOptions(scratchPanel, fileText, null)
val project = myFixture.project
val document = myFixture.editor.document
val data = ExpectedHighlightingData(document, false, false, false, myFixture.file)
data.init()
PsiDocumentManager.getInstance(project).commitAllDocuments()
val markers = doAndCheckHighlighting(document, data, File(path))
assertNavigationElements(myFixture.project, myFixture.file as KtFile, markers)
}
override fun tearDown() {
super.tearDown()
ScratchFileService.getInstance().scratchesMapping.mappings.forEach { file, _ ->
runWriteAction { file.delete(this) }
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scratch;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
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/lineMarker")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class ScratchLineMarkersTestGenerated extends AbstractScratchLineMarkersTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doScratchTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInLineMarker() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/scratch/lineMarker"), Pattern.compile("^(.+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("binaryExpression.kts")
public void testBinaryExpression() throws Exception {
runTest("idea/testData/scratch/lineMarker/binaryExpression.kts");
}
@TestMetadata("binaryExpression2.kts")
public void testBinaryExpression2() throws Exception {
runTest("idea/testData/scratch/lineMarker/binaryExpression2.kts");
}
@TestMetadata("constantExpression.kts")
public void testConstantExpression() throws Exception {
runTest("idea/testData/scratch/lineMarker/constantExpression.kts");
}
@TestMetadata("for.kts")
public void testFor() throws Exception {
runTest("idea/testData/scratch/lineMarker/for.kts");
}
@TestMetadata("function.kts")
public void testFunction() throws Exception {
runTest("idea/testData/scratch/lineMarker/function.kts");
}
@TestMetadata("lambda.kts")
public void testLambda() throws Exception {
runTest("idea/testData/scratch/lineMarker/lambda.kts");
}
@TestMetadata("lambda2.kts")
public void testLambda2() throws Exception {
runTest("idea/testData/scratch/lineMarker/lambda2.kts");
}
@TestMetadata("object.kts")
public void testObject() throws Exception {
runTest("idea/testData/scratch/lineMarker/object.kts");
}
@TestMetadata("parentesized.kts")
public void testParentesized() throws Exception {
runTest("idea/testData/scratch/lineMarker/parentesized.kts");
}
@TestMetadata("qualified.kts")
public void testQualified() throws Exception {
runTest("idea/testData/scratch/lineMarker/qualified.kts");
}
@TestMetadata("qualifiedMultiLine.kts")
public void testQualifiedMultiLine() throws Exception {
runTest("idea/testData/scratch/lineMarker/qualifiedMultiLine.kts");
}
@TestMetadata("referenceExpression.kts")
public void testReferenceExpression() throws Exception {
runTest("idea/testData/scratch/lineMarker/referenceExpression.kts");
}
@TestMetadata("val.kts")
public void testVal() throws Exception {
runTest("idea/testData/scratch/lineMarker/val.kts");
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scratch
import org.jetbrains.kotlin.idea.scratch.actions.RunScratchFromHereAction
import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner
import org.junit.runner.RunWith
@RunWith(JUnit3WithIdeaConfigurationRunner::class)
class SequentialScratchExecutorTest : AbstractScratchRunActionTest() {
fun testSingleLine() {
doTest(
listOf(
"2 + 2\n" to "RESULT: res1: kotlin.Int = 4"
)
)
}
fun testMultipleLinesAtOnce() {
doTest(
listOf(
"2\n3\n4\n" to "RESULT: res1: kotlin.Int = 2, RESULT: res2: kotlin.Int = 3, RESULT: res3: kotlin.Int = 4"
)
)
}
fun testMultipleLinesAfterEach() {
doTest(
listOf(
"2\n" to "RESULT: res1: kotlin.Int = 2",
"3\n" to "RESULT: res2: kotlin.Int = 3",
"4\n" to "RESULT: res3: kotlin.Int = 4"
)
)
}
fun doTest(expression: List<Pair<String, String>>) {
val scratchPanel = configureScratchByText("scratch_1.kts", testScratchText().inlinePropertiesValues(isRepl = true))
myFixture.editor.caretModel.moveToOffset(myFixture.file.textLength)
try {
launchScratch()
waitUntilScratchFinishes()
for ((text, expected) in expression) {
typeAndCheckOutput(text, expected)
}
} finally {
scratchPanel.scratchFile.replScratchExecutor?.stop()
}
}
private fun typeAndCheckOutput(text: String, expected: String) {
val inlaysBefore = getInlays()
myFixture.type(text)
launchAction(RunScratchFromHereAction())
waitUntilScratchFinishes()
val inlayAfter = getInlays().filterNot { inlaysBefore.contains(it) }
assertEquals(expected, inlayAfter.joinToString { it.toString().trim() })
}
}