Scratch: implement output handlers for displaying execution results

This commit is contained in:
Natalia Selezneva
2018-01-17 13:26:38 +03:00
parent 01e62c9bdb
commit 1a84c05db2
8 changed files with 358 additions and 2 deletions
@@ -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
}
@@ -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<ScratchOutputHandler>()
fun addOutputHandler(outputHandler: ScratchOutputHandler) {
handlers.add(outputHandler)
}
}
@@ -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<ScratchFileLanguageProvider>("org.jetbrains.kotlin.scratchFileLanguageProvider")
@@ -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()
}
@@ -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<TextEditor>()
.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
)
}
}
}
@@ -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) {}
}
@@ -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<ScratchExpression>()
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
}
}
}
}
@@ -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)
}
}
}
}