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:
committed by
Roman Golyshev
parent
e321ee1396
commit
bddf87337c
+198
@@ -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)
|
||||
}
|
||||
+69
-11
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user