From bddf87337c109e8eae99bf00178799284df73510 Mon Sep 17 00:00:00 2001 From: Roman Golyshev Date: Mon, 19 Aug 2019 18:35:44 +0300 Subject: [PATCH] KT-32366: Add printing scratch output to preview window - printing is done with `PreviewEditorScratchOutputHandler` - `KtsScratchFileEditorWithPreview` redirects output depending on the layout selection --- .../PreviewEditorScratchOutputHandler.kt | 198 ++++++++++++++++++ .../idea/scratch/output/commandHelper.kt | 8 + .../idea/scratch/output/commandHelper.kt.183 | 12 ++ .../scratch/ui/KtScratchFileEditorProvider.kt | 80 ++++++- 4 files changed, 287 insertions(+), 11 deletions(-) create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/PreviewEditorScratchOutputHandler.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt create mode 100644 idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt.183 diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/PreviewEditorScratchOutputHandler.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/PreviewEditorScratchOutputHandler.kt new file mode 100644 index 00000000000..7351e64348e --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/PreviewEditorScratchOutputHandler.kt @@ -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 = TreeMap(Comparator.comparingInt { it.lineStart }) + + val alignments: List> 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 = 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) = 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) +} diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt new file mode 100644 index 00000000000..9a0f89e78af --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt @@ -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) diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt.183 b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt.183 new file mode 100644 index 00000000000..c36caaedf37 --- /dev/null +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/output/commandHelper.kt.183 @@ -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) +} diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/KtScratchFileEditorProvider.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/KtScratchFileEditorProvider.kt index 4beb1558d81..9962a749d4d 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/KtScratchFileEditorProvider.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/scratch/ui/KtScratchFileEditorProvider.kt @@ -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 } } }) -} \ No newline at end of file +} + +/** + * 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 + } +}