KT-32366: Add printing scratch output to preview window

- printing is done with `PreviewEditorScratchOutputHandler`
- `KtsScratchFileEditorWithPreview` redirects output depending on the layout selection
This commit is contained in:
Roman Golyshev
2019-08-19 18:35:44 +03:00
committed by Roman Golyshev
parent e321ee1396
commit bddf87337c
4 changed files with 287 additions and 11 deletions
@@ -0,0 +1,198 @@
/*
* 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.output
import com.intellij.diff.util.DiffUtil
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.FoldRegion
import com.intellij.openapi.editor.FoldingModel
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.MarkupModel
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.idea.scratch.ScratchExpression
import org.jetbrains.kotlin.idea.scratch.ScratchFile
import java.util.*
import kotlin.math.max
/**
* Output handler to print scratch output to separate [previewTextEditor] window.
*
* Multiline outputs from single expressions are folded.
*/
class PreviewEditorScratchOutputHandler(
private val previewTextEditor: TextEditor,
private val toolwindowHandler: ScratchOutputHandler
) : ScratchOutputHandler {
private val previewOutputBlocksManager: PreviewOutputBlocksManager = PreviewOutputBlocksManager(previewTextEditor.editor)
override fun onStart(file: ScratchFile) {
toolwindowHandler.onStart(file)
}
override fun handle(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) {
printToPreviewEditor(file, expression, output)
}
override fun error(file: ScratchFile, message: String) {
toolwindowHandler.error(file, message)
}
override fun onFinish(file: ScratchFile) {
toolwindowHandler.onFinish(file)
}
override fun clear(file: ScratchFile) {
toolwindowHandler.clear(file)
previewOutputBlocksManager.clear()
clearPreviewEditor()
}
private fun printToPreviewEditor(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) {
TransactionGuard.submitTransaction(previewTextEditor, Runnable {
val targetCell = previewOutputBlocksManager.getBlock(expression) ?: previewOutputBlocksManager.addBlockToTheEnd(expression)
targetCell.addOutput(output)
})
}
private fun clearPreviewEditor() {
TransactionGuard.submitTransaction(previewTextEditor, Runnable {
runWriteAction {
executeCommand {
previewTextEditor.editor.document.setText("")
}
}
})
}
}
private val ScratchExpression.height: Int get() = lineEnd - lineStart + 1
class PreviewOutputBlocksManager(editor: Editor) {
val targetDocument: Document = editor.document
val foldingModel: FoldingModel = editor.foldingModel
val markupModel: MarkupModel = editor.markupModel
val blocks: NavigableMap<ScratchExpression, OutputBlock> = TreeMap(Comparator.comparingInt { it.lineStart })
val alignments: List<Pair<Int, Int>> get() = blocks.values.map { it.sourceExpression.lineStart to it.lineStart }
fun getBlock(expression: ScratchExpression): OutputBlock? = blocks[expression]
fun addBlockToTheEnd(expression: ScratchExpression): OutputBlock = OutputBlock(expression).also {
if (blocks.putIfAbsent(expression, it) != null) {
error("There is already a cell for $expression!")
}
}
fun clear() {
blocks.clear()
}
inner class OutputBlock(val sourceExpression: ScratchExpression) {
private val outputs: MutableList<ScratchOutput> = mutableListOf()
var lineStart: Int = computeCellLineStart(sourceExpression)
private set
val lineEnd: Int get() = lineStart + countNewLines(outputs)
val height: Int get() = lineEnd - lineStart + 1
private var foldRegion: FoldRegion? = null
fun addOutput(output: ScratchOutput) {
printAndSaveOutput(output)
blocks.tailMap(sourceExpression).values.forEach {
it.recalculatePosition()
it.updateFolding()
}
}
private fun printAndSaveOutput(output: ScratchOutput) {
val beforeAdding = lineEnd
val currentOutputStartLine = if (outputs.isEmpty()) lineStart else beforeAdding + 1
outputs.add(output)
runWriteAction {
executeCommand {
targetDocument.insertStringAtLine(currentOutputStartLine, output.text)
}
}
val insertedTextStart = targetDocument.getLineStartOffset(currentOutputStartLine)
val insertedTextEnd = targetDocument.getLineEndOffset(lineEnd)
colorRange(insertedTextStart, insertedTextEnd, output.type)
}
private fun colorRange(startOffset: Int, endOffset: Int, outputType: ScratchOutputType) {
val textAttributes = getAttributesForOutputType(outputType)
markupModel.addRangeHighlighter(
startOffset,
endOffset,
HighlighterLayer.SYNTAX,
textAttributes,
HighlighterTargetArea.EXACT_RANGE
)
}
private fun recalculatePosition() {
lineStart = computeCellLineStart(sourceExpression)
}
private fun updateFolding() {
foldingModel.runBatchFoldingOperation {
foldRegion?.let(foldingModel::removeFoldRegion)
if (height <= sourceExpression.height) return@runBatchFoldingOperation
val firstFoldedLine = lineStart + (sourceExpression.height - 1)
val placeholderLine = "${targetDocument.getLineContent(firstFoldedLine)}..."
foldRegion = foldingModel.addFoldRegion(
targetDocument.getLineStartOffset(firstFoldedLine),
targetDocument.getLineEndOffset(lineEnd),
placeholderLine
)
foldRegion?.isExpanded = isLastCell && isOutputSmall
}
}
private val isLastCell: Boolean get() = false // blocks.higherEntry(sourceExpression) == null
private val isOutputSmall: Boolean get() = true
}
private fun computeCellLineStart(scratchExpression: ScratchExpression): Int {
val previous = blocks.lowerEntry(scratchExpression)?.value ?: return scratchExpression.lineStart
val distanceBetweenSources = scratchExpression.lineStart - previous.sourceExpression.lineEnd
val differenceBetweenSourceAndOutputHeight = previous.sourceExpression.height - previous.height
val compensation = max(differenceBetweenSourceAndOutputHeight, 0)
return previous.lineEnd + compensation + distanceBetweenSources
}
}
private fun countNewLines(list: List<ScratchOutput>) = list.sumBy { StringUtil.countNewLines(it.text) } + max(list.size - 1, 0)
private fun Document.getLineContent(lineNumber: Int) =
DiffUtil.getLinesContent(this, lineNumber, lineNumber + 1).toString()
fun Document.insertStringAtLine(lineNumber: Int, text: String) {
while (DiffUtil.getLineCount(this) <= lineNumber) {
insertString(textLength, "\n")
}
insertString(getLineStartOffset(lineNumber), text)
}
@@ -0,0 +1,8 @@
/*
* 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.output
inline fun executeCommand(crossinline command: () -> Unit) = com.intellij.openapi.command.executeCommand(command = command)
@@ -0,0 +1,12 @@
/*
* 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.output
import com.intellij.openapi.command.CommandProcessor
fun executeCommand(command: () -> Unit) {
CommandProcessor.getInstance().executeCommand(null, command, null, null)
}
@@ -51,21 +51,29 @@ class KtScratchFileEditorProvider : FileEditorProvider, DumbAware {
class KtScratchFileEditorWithPreview private constructor(
val scratchFile: ScratchFile,
editor: TextEditor,
preview: TextEditor
) : TextEditorWithPreview(editor, preview), TextEditor {
sourceTextEditor: TextEditor,
private val previewTextEditor: TextEditor
) : TextEditorWithPreview(sourceTextEditor, previewTextEditor), TextEditor {
private val toolWindowHandler: ScratchOutputHandler = requestToolWindowHandler()
private val inlayOutputHandler = InlayScratchOutputHandler(editor, toolWindowHandler)
private val inlayScratchOutputHandler = InlayScratchOutputHandler(sourceTextEditor, toolWindowHandler)
private val previewEditorScratchOutputHandler = PreviewEditorScratchOutputHandler(previewTextEditor, toolWindowHandler)
private val commonPreviewOutputHandler = LayoutDependantOutputHandler(
inlayScratchOutputHandler,
previewEditorScratchOutputHandler,
::getLayout
)
private val scratchTopPanel = ScratchTopPanel(scratchFile)
init {
editor.parentScratchEditorWithPreview = this
sourceTextEditor.parentScratchEditorWithPreview = this
scratchFile.compilingScratchExecutor?.addOutputHandler(inlayOutputHandler)
scratchFile.replScratchExecutor?.addOutputHandler(inlayOutputHandler)
scratchFile.compilingScratchExecutor?.addOutputHandler(commonPreviewOutputHandler)
scratchFile.replScratchExecutor?.addOutputHandler(commonPreviewOutputHandler)
ScratchFileAutoRunner.addListener(scratchFile.project, editor)
ScratchFileAutoRunner.addListener(scratchFile.project, sourceTextEditor)
}
override fun dispose() {
@@ -87,12 +95,23 @@ class KtScratchFileEditorWithPreview private constructor(
return myEditor.editor
}
override fun createToolbar(): ActionToolbar? {
override fun createToolbar(): ActionToolbar {
return scratchTopPanel.actionsToolbar
}
fun clearOutputHandlers() {
inlayOutputHandler.clear(scratchFile)
commonPreviewOutputHandler.clear(scratchFile)
}
override fun setLayout(newLayout: Layout) {
val previous = layout
super.setLayout(newLayout)
val current = layout
when {
previous == Layout.SHOW_EDITOR && current != Layout.SHOW_EDITOR -> clearOutputHandlers()
previous != Layout.SHOW_EDITOR && current == Layout.SHOW_EDITOR -> clearOutputHandlers()
}
}
@TestOnly
@@ -144,4 +163,43 @@ private fun setupCodeAnalyzerRestarterOutputHandler(project: Project, scratchFil
}
}
})
}
}
/**
* Redirects output to [noPreviewOutputHandler] or [previewOutputHandler] depending on the result of [layoutProvider] call.
*
* However, clears both handlers to simplify clearing when switching between layouts.
*/
private class LayoutDependantOutputHandler(
private val noPreviewOutputHandler: ScratchOutputHandler,
private val previewOutputHandler: ScratchOutputHandler,
private val layoutProvider: () -> TextEditorWithPreview.Layout
) : ScratchOutputHandler {
override fun onStart(file: ScratchFile) {
targetHandler.onStart(file)
}
override fun handle(file: ScratchFile, expression: ScratchExpression, output: ScratchOutput) {
targetHandler.handle(file, expression, output)
}
override fun error(file: ScratchFile, message: String) {
targetHandler.error(file, message)
}
override fun onFinish(file: ScratchFile) {
targetHandler.onFinish(file)
}
override fun clear(file: ScratchFile) {
noPreviewOutputHandler.clear(file)
previewOutputHandler.clear(file)
}
private val targetHandler
get() = when (layoutProvider()) {
TextEditorWithPreview.Layout.SHOW_EDITOR -> noPreviewOutputHandler
else -> previewOutputHandler
}
}