Scratch: implement output handlers for displaying execution results
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+162
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user