Scratch: implement interactive mode

^KT-27955 Fixed
This commit is contained in:
Natalia Selezneva
2019-01-17 10:49:26 +03:00
parent 9b0b3971e4
commit 290ae16235
14 changed files with 249 additions and 62 deletions
@@ -22,12 +22,16 @@ import com.intellij.psi.PsiDocumentManager
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.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.refactoring.getLineNumber
import org.jetbrains.kotlin.idea.refactoring.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.resolve.AnalyzingUtils
class KtScratchFile(project: Project, editor: TextEditor) : ScratchFile(project, editor) {
override fun getExpressions(psiFile: PsiFile): List<ScratchExpression> {
@@ -60,4 +64,14 @@ class KtScratchFile(project: Project, editor: TextEditor) : ScratchFile(project,
return result
}
override fun hasErrors(): Boolean {
val psiFile = getPsiFile() as? KtFile ?: return false
try {
AnalyzingUtils.checkForSyntacticErrors(psiFile)
} catch (e: IllegalArgumentException) {
return true
}
return psiFile.analyzeWithContent().diagnostics.any { it.severity == Severity.ERROR }
}
}
@@ -50,11 +50,13 @@ abstract class ScratchFile(val project: Project, val editor: TextEditor) {
}
abstract fun getExpressions(psiFile: PsiFile): List<ScratchExpression>
abstract fun hasErrors(): Boolean
}
data class ScratchExpression(val element: PsiElement, val lineStart: Int, val lineEnd: Int = lineStart)
data class ScratchFileOptions(
val isRepl: Boolean = false,
val isMakeBeforeRun: Boolean = false
val isMakeBeforeRun: Boolean = false,
val isInteractiveMode: Boolean = true
)
@@ -0,0 +1,73 @@
/*
* Copyright 2010-2018 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
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
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.ScratchCompilationSupport
import org.jetbrains.kotlin.idea.scratch.ui.ScratchTopPanel
class ScratchFileAutoRunner(private val project: Project) : DocumentListener {
companion object {
fun addListener(project: Project, editor: TextEditor) {
if (editor.getScratchPanel() != null) {
editor.editor.document.addDocumentListener(getInstance(project))
Disposer.register(editor, Disposable {
editor.editor.document.removeDocumentListener(getInstance(project))
})
}
}
private fun getInstance(project: Project) = ServiceManager.getService(project, ScratchFileAutoRunner::class.java)
private const val auto_run_delay = 2000
}
private val myAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
override fun documentChanged(event: DocumentEvent) {
val file = FileDocumentManager.getInstance().getFile(event.document) ?: return
if (project.isDisposed) return
val panel = getScratchPanel(file, project) ?: return
if (!panel.scratchFile.options.isInteractiveMode) return
if (isScratchChanged(project, file)) {
runScratch(panel)
}
}
private fun runScratch(panel: ScratchTopPanel) {
myAlarm.cancelAllRequests()
if (ScratchCompilationSupport.isInProgress(panel.scratchFile)) {
ScratchCompilationSupport.forceStop()
}
myAlarm.addRequest(
{
val psiFile = panel.scratchFile.getPsiFile()
if (psiFile != null && psiFile.isValid && !panel.scratchFile.hasErrors()) {
RunScratchAction.doAction(panel, true)
}
}, auto_run_delay, true
)
}
private fun getScratchPanel(file: VirtualFile, project: Project): ScratchTopPanel? {
return getEditorWithScratchPanel(FileEditorManager.getInstance(project), file)?.second
}
}
@@ -25,6 +25,7 @@ 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.LOG as log
class RunScratchAction : ScratchAction(
@@ -42,70 +43,77 @@ class RunScratchAction : ScratchAction(
val project = e.project ?: return
val scratchPanel = getScratchPanelFromSelectedEditor(project) ?: return
val scratchFile = scratchPanel.scratchFile
val psiFile = scratchFile.getPsiFile() ?: return
doAction(scratchPanel, false)
}
companion object {
fun doAction(scratchPanel: ScratchTopPanel, isAutoRun: Boolean) {
val scratchFile = scratchPanel.scratchFile
val psiFile = scratchFile.getPsiFile() ?: return
val isMakeBeforeRun = scratchFile.options.isMakeBeforeRun
val isRepl = scratchFile.options.isRepl
val provider = ScratchFileLanguageProvider.get(psiFile.language) ?: return
val provider = ScratchFileLanguageProvider.get(psiFile.language) ?: return
log.printDebugMessage("Run Action: isMakeBeforeRun = $isMakeBeforeRun, isRepl = $isRepl")
log.printDebugMessage("Run Action: isMakeBeforeRun = $isMakeBeforeRun, isRepl = $isRepl")
val defaultOutputHandler = provider.getOutputHandler()
val defaultOutputHandler = provider.getOutputHandler()
val module = scratchPanel.getModule()
val module = scratchPanel.getModule()
@Suppress("FoldInitializerAndIfToElvis")
if (module == null) {
return defaultOutputHandler.error(scratchFile, "Module should be selected")
}
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()
@Suppress("FoldInitializerAndIfToElvis")
if (module == null) {
return defaultOutputHandler.error(scratchFile, "Module should be selected")
}
override fun onFinish(file: ScratchFile) {
ScratchCompilationSupport.stop()
scratchPanel.updateToolbar()
}
})
val executor = if (isRepl) provider.createReplExecutor(scratchFile) else provider.createCompilingExecutor(scratchFile)
fun executeScratch() {
try {
executor.execute()
} catch (ex: Throwable) {
executor.errorOccurs("Exception occurs during Run Scratch Action", ex, true)
@Suppress("FoldInitializerAndIfToElvis")
if (executor == null) {
return defaultOutputHandler.error(scratchFile, "Couldn't run ${psiFile.name}")
}
}
if (isMakeBeforeRun) {
ProjectTaskManager.getInstance(project).build(arrayOf(module)) { result ->
if (result.isAborted || result.errors > 0) {
executor.errorOccurs("There were compilation errors in module ${module.name}")
executor.addOutputHandler(defaultOutputHandler)
executor.addOutputHandler(object : ScratchOutputHandlerAdapter() {
override fun onStart(file: ScratchFile) {
ScratchCompilationSupport.start(file, executor)
scratchPanel.updateToolbar()
}
if (DumbService.isDumb(project)) {
DumbService.getInstance(project).smartInvokeLater {
override fun onFinish(file: ScratchFile) {
ScratchCompilationSupport.stop()
scratchPanel.updateToolbar()
}
})
fun executeScratch() {
try {
executor.execute()
} catch (ex: Throwable) {
executor.errorOccurs("Exception occurs during Run Scratch Action", ex, true)
}
}
if (!isAutoRun && isMakeBeforeRun) {
val project = scratchPanel.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}")
}
if (DumbService.isDumb(project)) {
DumbService.getInstance(project).smartInvokeLater {
executeScratch()
}
} else {
executeScratch()
}
} else {
executeScratch()
}
} else {
executeScratch()
}
} else {
executeScratch()
}
}
@@ -18,8 +18,10 @@ package org.jetbrains.kotlin.idea.scratch.output
import com.intellij.execution.filters.OpenFileHyperlinkInfo
import com.intellij.execution.impl.ConsoleViewImpl
import com.intellij.execution.runners.ExecutionUtil
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.ide.scratch.ScratchFileType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.ex.EditorEx
@@ -47,7 +49,7 @@ fun getToolwindowHandler(): ScratchOutputHandler {
private object ToolWindowScratchOutputHandler : ScratchOutputHandlerAdapter() {
override fun handle(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) {
printToConsole(file.project) {
printToConsole(file) {
val psiFile = file.getPsiFile()
if (psiFile != null) {
printHyperlink(
@@ -65,14 +67,17 @@ private object ToolWindowScratchOutputHandler : ScratchOutputHandlerAdapter() {
}
override fun error(file: ScratchFile, message: String) {
printToConsole(file.project) {
printToConsole(file) {
print(message, ConsoleViewContentType.ERROR_OUTPUT)
}
}
private fun printToConsole(project: Project, print: ConsoleViewImpl.() -> Unit) {
private fun printToConsole(file: ScratchFile, print: ConsoleViewImpl.() -> Unit) {
ApplicationManager.getApplication().invokeLater {
val toolWindow = getToolWindow(project) ?: createToolWindow(project)
val project = file.project.takeIf { !it.isDisposed } ?: return@invokeLater
val toolWindow = getToolWindow(project) ?: createToolWindow(file)
val contents = toolWindow.contentManager.contents
for (content in contents) {
val component = content.component
@@ -81,8 +86,14 @@ private object ToolWindowScratchOutputHandler : ScratchOutputHandlerAdapter() {
component.print("\n", ConsoleViewContentType.NORMAL_OUTPUT)
}
}
toolWindow.setAvailable(true, null)
toolWindow.show(null)
if (!file.options.isInteractiveMode) {
toolWindow.show(null)
}
toolWindow.icon = ExecutionUtil.getLiveIndicator(ScratchFileType.INSTANCE.icon)
}
}
@@ -96,8 +107,12 @@ private object ToolWindowScratchOutputHandler : ScratchOutputHandlerAdapter() {
component.clear()
}
}
toolWindow.setAvailable(false, null)
toolWindow.hide(null)
if (!file.options.isInteractiveMode) {
toolWindow.hide(null)
}
toolWindow.icon = ScratchFileType.INSTANCE.icon
}
}
@@ -112,11 +127,17 @@ private object ToolWindowScratchOutputHandler : ScratchOutputHandlerAdapter() {
return toolWindowManager.getToolWindow(ScratchToolWindowFactory.ID)
}
private fun createToolWindow(project: Project): ToolWindow {
private fun createToolWindow(file: ScratchFile): ToolWindow {
val project = file.project
val toolWindowManager = ToolWindowManager.getInstance(project)
toolWindowManager.registerToolWindow(ScratchToolWindowFactory.ID, true, ToolWindowAnchor.BOTTOM)
val window = toolWindowManager.getToolWindow(ScratchToolWindowFactory.ID)
ScratchToolWindowFactory().createToolWindowContent(project, window)
Disposer.register(file.editor, Disposable {
window.setAvailable(false, null)
})
return window
}
}
@@ -133,6 +154,7 @@ private class ScratchToolWindowFactory : ToolWindowFactory {
val consoleView = ConsoleViewImpl(project, true)
toolWindow.isToHideOnEmptyContent = true
toolWindow.icon = ScratchFileType.INSTANCE.icon
toolWindow.hide(null)
val contentManager = toolWindow.contentManager
val content = contentManager.factory.createContent(consoleView.component, null, false)
@@ -0,0 +1,24 @@
/*
* 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
import com.intellij.openapi.externalSystem.service.project.autoimport.ConfigurationFileCrcFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.psi.UserDataProperty
fun isScratchChanged(project: Project, file: VirtualFile) : Boolean {
val beforeLastRun = file.crcWithoutSpaces
val newCrc = ConfigurationFileCrcFactory(project, file).create()
if (beforeLastRun != newCrc) {
file.crcWithoutSpaces = newCrc
return true
}
return false
}
private var VirtualFile.crcWithoutSpaces by UserDataProperty(Key.create<Long>("CRC_WITHOUT_SPACES"))
@@ -0,0 +1,13 @@
/*
* 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
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
fun isScratchChanged(project: Project, file: VirtualFile) : Boolean {
return true
}
@@ -10,11 +10,12 @@ import org.jetbrains.kotlin.idea.core.util.cachedFileAttribute
import org.jetbrains.kotlin.idea.scratch.ScratchFileOptions
var VirtualFile.scratchFileOptions: ScratchFileOptions? by cachedFileAttribute(
name = "kotlin-scratch-panel-config",
name = "kotlin-scratch-file-options",
version = 1,
read = { ScratchFileOptions(readBoolean(), readBoolean()) },
read = { ScratchFileOptions(readBoolean(), readBoolean(), readBoolean()) },
write = {
writeBoolean(it.isRepl)
writeBoolean(it.isMakeBeforeRun)
writeBoolean(it.isInteractiveMode)
}
)
@@ -24,10 +24,7 @@ import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.scratch.ScratchFileLanguageProvider
import org.jetbrains.kotlin.idea.scratch.getAllEditorsWithScratchPanel
import org.jetbrains.kotlin.idea.scratch.getEditorWithoutScratchPanel
import org.jetbrains.kotlin.idea.scratch.removeScratchPanel
import org.jetbrains.kotlin.idea.scratch.*
class ScratchFileHook(val project: Project) : ProjectComponent {
@@ -46,6 +43,8 @@ class ScratchFileHook(val project: Project) : ProjectComponent {
val editor = getEditorWithoutScratchPanel(source, file) ?: return
ScratchTopPanel.createPanel(project, file, editor)
ScratchFileAutoRunner.addListener(project, editor)
}
override fun fileClosed(source: FileEditorManager, file: VirtualFile) {}
@@ -60,6 +60,7 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel
private val moduleChooser: ModulesComboBox
private val isReplCheckbox: JCheckBox
private val isMakeBeforeRunCheckbox: JCheckBox
private val isInteractiveCheckbox: JCheckBox
private val actionsToolbar: ActionToolbar
@@ -81,6 +82,16 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel
add(JSeparator(SwingConstants.VERTICAL))
isInteractiveCheckbox = JCheckBox("Interactive mode")
add(isInteractiveCheckbox)
isInteractiveCheckbox.addItemListener {
scratchFile.saveOptions {
copy(isInteractiveMode = isInteractiveCheckbox.isSelected)
}
}
add(JSeparator(SwingConstants.VERTICAL))
isReplCheckbox = JCheckBox("Use REPL")
add(isReplCheckbox)
isReplCheckbox.addItemListener {
@@ -94,6 +105,7 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel
scratchFile.options.let {
isReplCheckbox.isSelected = it.isRepl
isMakeBeforeRunCheckbox.isSelected = it.isMakeBeforeRun
isInteractiveCheckbox.isSelected = it.isInteractiveMode
}
}
@@ -123,6 +135,11 @@ class ScratchTopPanel private constructor(val scratchFile: ScratchFile) : JPanel
isMakeBeforeRunCheckbox.isSelected = isSelected
}
@TestOnly
fun setInteractiveMode(isSelected: Boolean) {
isInteractiveCheckbox.isSelected = isSelected
}
fun updateToolbar() {
ApplicationManager.getApplication().invokeLater {
actionsToolbar.updateActionsImmediately()
+3
View File
@@ -72,6 +72,9 @@
<projectService serviceInterface="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches"
serviceImplementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.scratch.ScratchFileAutoRunner"
serviceImplementation="org.jetbrains.kotlin.idea.scratch.ScratchFileAutoRunner"/>
<projectService serviceImplementation="org.jetbrains.kotlin.idea.versions.SuppressNotificationState"/>
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSmartStepIntoHandler"/>
+3
View File
@@ -72,6 +72,9 @@
<projectService serviceInterface="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches"
serviceImplementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.scratch.ScratchFileAutoRunner"
serviceImplementation="org.jetbrains.kotlin.idea.scratch.ScratchFileAutoRunner"/>
<projectService serviceImplementation="org.jetbrains.kotlin.idea.versions.SuppressNotificationState"/>
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSmartStepIntoHandler"/>
@@ -113,7 +113,7 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
val (editor, scratchPanel) = getEditorWithScratchPanel(myManager, scratchFile)?: error("Couldn't find scratch panel")
scratchPanel.scratchFile.saveOptions {
copy(isRepl = isRepl)
copy(isRepl = isRepl, isInteractiveMode = false)
}
launchScratch(scratchFile)
@@ -33,14 +33,17 @@ class ScratchOptionsSaveTest : AbstractScratchRunActionTest() {
Assert.assertEquals(
"This test checks that checkbox options are restored after file closing. Not all checkboxes are checked in this test",
2,
3,
ScratchTopPanel::class.declaredMemberProperties.filter { it.returnType == JCheckBox::class.createType() }.size
)
val newIsReplValue = !scratchPanelBeforeClosingFile.scratchFile.options.isRepl
val newIsMakeBeforeRunValue = !scratchPanelBeforeClosingFile.scratchFile.options.isMakeBeforeRun
val newIsInteractiveModeValue = !scratchPanelBeforeClosingFile.scratchFile.options.isInteractiveMode
scratchPanelBeforeClosingFile.setReplMode(newIsReplValue)
scratchPanelBeforeClosingFile.setMakeBeforeRun(newIsMakeBeforeRunValue)
scratchPanelBeforeClosingFile.setInteractiveMode(newIsInteractiveModeValue)
myManager.closeFile(scratchFile)
myManager.openFile(scratchFile, true)
@@ -53,5 +56,10 @@ class ScratchOptionsSaveTest : AbstractScratchRunActionTest() {
newIsMakeBeforeRunValue,
scratchPanelAfterClosingFile.scratchFile.options.isMakeBeforeRun
)
Assert.assertEquals(
"Wrong value for isInteractiveMode checkbox",
newIsInteractiveModeValue,
scratchPanelAfterClosingFile.scratchFile.options.isInteractiveMode
)
}
}