From 1a84c05db2a4cc1f49459b946cc9c600c3e08a97 Mon Sep 17 00:00:00 2001 From: Natalia Selezneva Date: Wed, 17 Jan 2018 13:26:38 +0300 Subject: [PATCH] Scratch: implement output handlers for displaying execution results --- .../scratch/KtScratchFileLanguageProvider.kt | 2 + .../kotlin/idea/scratch/ScratchExecutor.kt | 8 + .../scratch/ScratchFileLanguageProvider.kt | 3 + .../idea/scratch/actions/RunScratchAction.kt | 16 ++ .../output/InlayScratchOutputHandler.kt | 162 ++++++++++++++++++ .../scratch/output/ScratchOutputHandler.kt | 44 +++++ .../scratch/repl/KtScratchReplExecutor.kt | 30 +++- .../scratch/ui/ScratchToolWindowFactory.kt | 95 ++++++++++ 8 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/InlayScratchOutputHandler.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/ScratchOutputHandler.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchToolWindowFactory.kt diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/KtScratchFileLanguageProvider.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/KtScratchFileLanguageProvider.kt index 9959781ccbc..97e6687e1fa 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.output.InlayScratchOutputHandler import org.jetbrains.kotlin.idea.scratch.repl.KtScratchReplExecutor import org.jetbrains.kotlin.psi.KtFile @@ -26,4 +27,5 @@ class KtScratchFileLanguageProvider : ScratchFileLanguageProvider() { } override fun createReplExecutor(file: ScratchFile) = KtScratchReplExecutor(file) + override fun getOutputHandler() = InlayScratchOutputHandler } \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt index 6e4a6c03d81..f48bca60ec7 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ScratchExecutor.kt @@ -16,6 +16,14 @@ package org.jetbrains.kotlin.idea.scratch +import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputHandler + abstract class ScratchExecutor(protected val file: ScratchFile) { abstract fun execute() + + protected val handlers = mutableListOf() + + fun addOutputHandler(outputHandler: ScratchOutputHandler) { + handlers.add(outputHandler) + } } \ 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 c46556ef1e0..0bea857e1ef 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 @@ -21,11 +21,14 @@ import com.intellij.lang.LanguageExtension import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.fileTypes.LanguageFileType import com.intellij.psi.PsiFile +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 getOutputHandler(): ScratchOutputHandler + companion object { private val EXTENSION = LanguageExtension("org.jetbrains.kotlin.scratchFileLanguageProvider") diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/actions/RunScratchAction.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/actions/RunScratchAction.kt index 934a39ebca4..22640735744 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 @@ -21,8 +21,10 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.compiler.CompilerManager import org.jetbrains.kotlin.idea.KotlinBundle +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 class RunScratchAction : AnAction( KotlinBundle.message("scratch.run.button"), @@ -40,19 +42,33 @@ class RunScratchAction : AnAction( val provider = ScratchFileLanguageProvider.get(scratchFile.psiFile.language) ?: return + val handler = provider.getOutputHandler() + val module = scratchTopPanel.getModule() if (module == null) { + handler.error(scratchFile, "Module should be selected") + handler.onFinish(scratchFile) return } val runnable = r@ { val executor = provider.createReplExecutor(scratchFile) if (executor == null) { + handler.error(scratchFile, "Couldn't run file using REPL") + handler.onFinish(scratchFile) return@r } e.presentation.isEnabled = false + executor.addOutputHandler(handler) + + executor.addOutputHandler(object : ScratchOutputHandlerAdapter() { + override fun onFinish(file: ScratchFile) { + e.presentation.isEnabled = true + } + }) + executor.execute() } diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/InlayScratchOutputHandler.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/InlayScratchOutputHandler.kt new file mode 100644 index 00000000000..8cdef11516e --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/InlayScratchOutputHandler.kt @@ -0,0 +1,162 @@ +/* + * 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.output + +import com.intellij.execution.ui.ConsoleViewContentType +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorCustomElementRenderer +import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry +import com.intellij.openapi.editor.impl.FontInfo +import com.intellij.openapi.editor.markup.TextAttributes +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.psi.PsiDocumentManager +import com.intellij.ui.Gray +import com.intellij.ui.JBColor +import com.intellij.util.ui.UIUtil +import org.jetbrains.kotlin.idea.scratch.ScratchExpression +import org.jetbrains.kotlin.idea.scratch.ScratchFile +import org.jetbrains.kotlin.idea.scratch.ui.showToolWindow +import java.awt.Color +import java.awt.Font +import java.awt.Graphics +import java.awt.Rectangle + +object InlayScratchOutputHandler : ScratchOutputHandler { + override fun onStart(file: ScratchFile) { + clearInlays(file.psiFile.project, file.psiFile.virtualFile) + } + + override fun handle(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) { + val document = PsiDocumentManager.getInstance(file.psiFile.project).getDocument(file.psiFile) ?: return + val lineEndOffset = document.getLineEndOffset(expression.lineStart) + createInlay(file.psiFile.project, file.psiFile.virtualFile, lineEndOffset, output.text.substringBefore("\n"), output.type) + + if (output.type == ScratchOutputType.ERROR) { + error(file, output.text) + } + } + + override fun error(file: ScratchFile, message: String) { + // todo multiple errors, clear, close + showToolWindow(file.psiFile.project, message, ConsoleViewContentType.ERROR_OUTPUT) + } + + override fun onFinish(file: ScratchFile) { + + } + + override fun clear(file: ScratchFile) { + clearInlays(file.psiFile.project, file.psiFile.virtualFile) + } + + private fun createInlay(project: Project, file: VirtualFile, offset: Int, inlayText: String, outputType: ScratchOutputType) { + UIUtil.invokeLaterIfNeeded { + val textEditor = FileEditorManager.getInstance(project).getSelectedEditor(file) as? TextEditor ?: return@invokeLaterIfNeeded + val editor = textEditor.editor + + val text = editor.document.immutableCharSequence + var insertOffset = offset + while (insertOffset < text.length && Character.isJavaIdentifierPart(text[insertOffset])) insertOffset++ + + val existing = editor.inlayModel + .getInlineElementsInRange(insertOffset, insertOffset) + .singleOrNull { it.renderer is ScratchFileRenderer } + if (existing != null) { + existing.dispose() + editor.inlayModel.addInlineElement( + insertOffset, + ScratchFileRenderer((existing.renderer as ScratchFileRenderer).text + "; " + inlayText, outputType) + ) + } else { + editor.inlayModel.addInlineElement(insertOffset, ScratchFileRenderer(" $inlayText", outputType)) + } + } + } + + private fun clearInlays(project: Project, file: VirtualFile) { + UIUtil.invokeLaterIfNeeded { + val editors = FileEditorManager.getInstance(project).getEditors(file) + editors.filterIsInstance() + .flatMap { it.editor.inlayModel.getInlineElementsInRange(0, it.editor.document.textLength) } + .filter { it.renderer is ScratchFileRenderer } + .forEach { Disposer.dispose(it) } + } + } + + class ScratchFileRenderer(val text: String, val outputType: ScratchOutputType) : EditorCustomElementRenderer { + private fun getFontInfo(editor: Editor): FontInfo { + val colorsScheme = editor.colorsScheme + val fontPreferences = colorsScheme.fontPreferences + val attributes = getAttributes() + val fontStyle = attributes.fontType + return ComplementaryFontsRegistry.getFontAbleToDisplay( + 'a'.toInt(), fontStyle, fontPreferences, FontInfo.getFontRenderContext(editor.contentComponent) + ) + } + + override fun calcWidthInPixels(editor: Editor): Int { + val fontInfo = getFontInfo(editor) + return fontInfo.fontMetrics().stringWidth(text) + } + + override fun paint(editor: Editor, g: Graphics, r: Rectangle, textAttributes: TextAttributes) { + val attributes = getAttributes() + val fgColor = attributes.foregroundColor ?: return + g.color = fgColor + val fontInfo = getFontInfo(editor) + g.font = fontInfo.font + val metrics = fontInfo.fontMetrics() + g.drawString(text, r.x, r.y + metrics.ascent) + } + + private fun getAttributes(): TextAttributes { + return when (outputType) { + ScratchOutputType.OUTPUT -> userOutputAttributes + ScratchOutputType.RESULT -> normalAttributes + ScratchOutputType.ERROR -> errorAttributes + } + } + + override fun toString(): String { + return "${outputType.name}: ${text.trim()}" + } + + companion object { + private val normalAttributes = TextAttributes( + JBColor(Gray._135, Color(0x3d8065)), + null, null, null, + Font.ITALIC + ) + + private val errorAttributes = TextAttributes( + JBColor(Color.RED, Color.RED), + null, null, null, + Font.ITALIC + ) + + private val userOutputAttributes = TextAttributes( + JBColor(Color.GREEN, Color.GREEN), + null, null, null, + Font.ITALIC + ) + } + } +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/ScratchOutputHandler.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/ScratchOutputHandler.kt new file mode 100644 index 00000000000..35aabef18df --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/ScratchOutputHandler.kt @@ -0,0 +1,44 @@ +/* + * 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.output + +import org.jetbrains.kotlin.idea.scratch.ScratchExpression +import org.jetbrains.kotlin.idea.scratch.ScratchFile + +interface ScratchOutputHandler { + fun onStart(file: ScratchFile) + fun handle(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) + fun error(file: ScratchFile, message: String) + fun onFinish(file: ScratchFile) + fun clear(file: ScratchFile) +} + +data class ScratchOutput(val text: String, val type: ScratchOutputType) + +enum class ScratchOutputType { + RESULT, + OUTPUT, + ERROR +} + +open class ScratchOutputHandlerAdapter: ScratchOutputHandler { + override fun onStart(file: ScratchFile) {} + override fun handle(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) {} + override fun error(file: ScratchFile, message: String) {} + override fun onFinish(file: ScratchFile) {} + override fun clear(file: ScratchFile) {} +} \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt index a9ba436d35c..67aba0a74cc 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/repl/KtScratchReplExecutor.kt @@ -28,6 +28,8 @@ import org.jetbrains.kotlin.console.actions.logError import org.jetbrains.kotlin.idea.scratch.ScratchExecutor import org.jetbrains.kotlin.idea.scratch.ScratchExpression import org.jetbrains.kotlin.idea.scratch.ScratchFile +import org.jetbrains.kotlin.idea.scratch.output.ScratchOutput +import org.jetbrains.kotlin.idea.scratch.output.ScratchOutputType import org.w3c.dom.Element import org.xml.sax.InputSource import java.io.ByteArrayInputStream @@ -41,7 +43,9 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { private lateinit var osProcessHandler: OSProcessHandler override fun execute() { - val module = file.module ?: return + handlers.forEach { it.onStart(file) } + + val module = file.module ?: return error(file, "Module should be selected") val cmdLine = KotlinConsoleKeeper.createCommandLine(module) osProcessHandler = ReplOSProcessHandler(cmdLine) @@ -71,6 +75,11 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { processInputOS.flush() } + private fun error(file: ScratchFile, message: String) { + handlers.forEach { it.error(file, message) } + handlers.forEach { it.onFinish(file) } + } + private class ReplHistory { private var entries = arrayListOf() private var processedEntriesCount: Int = 0 @@ -104,6 +113,10 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { } } + override fun notifyProcessTerminated(exitCode: Int) { + handlers.forEach { it.onFinish(file) } + } + private fun strToSource(s: String, encoding: Charset = Charsets.UTF_8) = InputSource(ByteArrayInputStream(s.toByteArray(encoding))) private fun handleReplMessage(text: String) { @@ -111,6 +124,7 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { val output = try { factory.newDocumentBuilder().parse(strToSource(text)) } catch (e: Exception) { + handlers.forEach { it.error(file, "Couldn't parse REPL output: $text") } return } @@ -122,7 +136,7 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { history.entryProcessed() } - val result = content + val result = parseReplOutput(content, outputType) if (result != null) { val lastExpression = if (outputType == "USER_OUTPUT") { // success command is printed after user output @@ -132,9 +146,21 @@ class KtScratchReplExecutor(file: ScratchFile) : ScratchExecutor(file) { } if (lastExpression != null) { + handlers.forEach { it.handle(file, lastExpression, result) } } } + } + private fun parseReplOutput(text: String, outputType: String): ScratchOutput? { + return when (outputType) { + "USER_OUTPUT" -> ScratchOutput(text, ScratchOutputType.OUTPUT) + "REPL_RESULT" -> ScratchOutput(text, ScratchOutputType.RESULT) + "REPL_INCOMPLETE", + "INTERNAL_ERROR", + "COMPILE_ERROR", + "RUNTIME_ERROR" -> ScratchOutput(text, ScratchOutputType.ERROR) + else -> null + } } } } \ No newline at end of file diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchToolWindowFactory.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchToolWindowFactory.kt new file mode 100644 index 00000000000..9e3be09b440 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/ScratchToolWindowFactory.kt @@ -0,0 +1,95 @@ +/* + * 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.ui + +import com.intellij.execution.impl.ConsoleViewImpl +import com.intellij.execution.ui.ConsoleViewContentType +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowAnchor +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.openapi.wm.ToolWindowManager + +class ScratchToolWindowFactory : ToolWindowFactory { + companion object { + val ID = "Scratch error output" + } + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val consoleView = ConsoleViewImpl(project, true) + toolWindow.isToHideOnEmptyContent = true + + val contentManager = toolWindow.contentManager + val content = contentManager.factory.createContent(consoleView.component, null, false) + contentManager.addContent(content) + val editor = consoleView.editor + if (editor is EditorEx) { + editor.isRendererMode = true + } + + Disposer.register(project, consoleView) + project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, getFileEditorManagerListener(toolWindow)) + } + + private fun getFileEditorManagerListener(toolWindow: ToolWindow): FileEditorManagerListener { + return object : FileEditorManagerListener { + override fun fileClosed(source: FileEditorManager, file: VirtualFile) { + toolWindow.setAvailable(false, {}) + } + + override fun selectionChanged(event: FileEditorManagerEvent) { + toolWindow.setAvailable(false, {}) + } + } + } +} + +fun showToolWindow( + project: Project, + message: String, + type: ConsoleViewContentType +) { + if (ApplicationManager.getApplication().isUnitTestMode) return + + ApplicationManager.getApplication().invokeLater { + val toolWindowManager = ToolWindowManager.getInstance(project) + var window: ToolWindow? = toolWindowManager.getToolWindow(ScratchToolWindowFactory.ID) + if (window == null) { + toolWindowManager.registerToolWindow(ScratchToolWindowFactory.ID, true, ToolWindowAnchor.BOTTOM) + window = toolWindowManager.getToolWindow(ScratchToolWindowFactory.ID) + ScratchToolWindowFactory().createToolWindowContent(project, window!!) + } + + val contents = window.contentManager.contents + for (content in contents) { + val component = content.component + if (component is ConsoleViewImpl) { + component.clear() + component.print(message, type) + window.setAvailable(true, null) + window.show(null) + } + } + } +} \ No newline at end of file