PY-65441 Expand/Collapse jupyter code/markdown cells

Merge-request: IJ-MR-126097
Merged-by: Anton Efimchuk <Anton.Efimchuk@jetbrains.com>

GitOrigin-RevId: 04b7b1b9745fbe7da922cda2060416e3d457be1c
This commit is contained in:
Anton Efimchuk
2024-02-21 21:43:15 +00:00
committed by intellij-monorepo-bot
parent 26cc0286b4
commit b30ac18caa
18 changed files with 626 additions and 302 deletions

View File

@@ -1,5 +1,6 @@
package org.jetbrains.plugins.notebooks.ui
import com.intellij.openapi.util.Key
import javax.swing.JPanel
import javax.swing.plaf.PanelUI
@@ -21,3 +22,5 @@ open class SteadyUIPanel(private val steadyUi: PanelUI) : JPanel() {
setUI(steadyUi)
}
}
val isFoldingEnabledKey = Key.create<Boolean>("jupyter.editor.folding.cells")

View File

@@ -6,12 +6,15 @@ import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.ui.JBColor
import com.intellij.ui.NewUiValue
import com.intellij.util.ui.JBUI
import org.jetbrains.plugins.notebooks.ui.editor.actions.command.mode.NotebookEditorMode
import org.jetbrains.plugins.notebooks.ui.editor.actions.command.mode.currentMode
import org.jetbrains.plugins.notebooks.ui.visualization.DefaultNotebookEditorAppearanceSizes
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearance
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearance.Companion.CELL_STRIPE_COLOR
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearance.Companion.CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearanceSizes
import java.awt.Color
@@ -56,7 +59,6 @@ object DefaultNotebookEditorAppearance : NotebookEditorAppearance,
val CELL_UNDER_CARET_COMMAND_MODE_STRIPE_COLOR = ColorKey.createColorKey("JUPYTER.CELL_UNDER_CARET_COMMAND_MODE_STRIPE_COLOR")
val CELL_UNDER_CARET_EDITOR_MODE_STRIPE_COLOR = ColorKey.createColorKey("JUPYTER.CELL_UNDER_CARET_EDITOR_MODE_STRIPE_COLOR")
private val CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR = ColorKey.createColorKey("JUPYTER.CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR")
// see org.jetbrains.plugins.notebooks.visualization.CaretBasedCellSelectionModelKt.getSelectionLines
private fun isCellSelected(editor: Editor, lines: IntRange): Boolean {
@@ -93,6 +95,14 @@ object DefaultNotebookEditorAppearance : NotebookEditorAppearance,
return null
}
override fun getCellStripeHoverColor(editor: Editor): Color {
return editor.colorsScheme.getColor(CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR) ?: JBColor.BLUE
}
override fun getCellStripeColor(editor: Editor): Color {
return editor.colorsScheme.getColor(CELL_STRIPE_COLOR) ?: JBColor.GRAY
}
override fun getCellLeftLineWidth(): Int =
when (currentMode()) {
NotebookEditorMode.EDIT -> EDIT_MODE_CELL_LEFT_LINE_WIDTH

View File

@@ -1,7 +1,9 @@
package org.jetbrains.plugins.notebooks.ui.editor
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.ColorKey
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.ui.JBColor
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearance
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearanceSizes
import java.awt.Color
@@ -18,6 +20,12 @@ object NewUINotebookDiffEditorAppearance: NotebookEditorAppearance,
override fun shouldShowCellLineNumbers(): Boolean = false
override fun shouldShowExecutionCounts(): Boolean = false // not needed for DIFF -> execution does not reach it
override fun shouldShowOutExecutionCounts(): Boolean = false
override fun getCellStripeHoverColor(editor: Editor): Color {
return editor.colorsScheme.getColor(NotebookEditorAppearance.CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR) ?: JBColor.BLUE
}
override fun getCellStripeColor(editor: Editor): Color {
return editor.colorsScheme.getColor(NotebookEditorAppearance.CELL_STRIPE_COLOR) ?: JBColor.GRAY
}
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.notebooks.ui.visualization
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.ColorKey
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.impl.EditorImpl
@@ -18,6 +19,8 @@ interface NotebookEditorAppearance: NotebookEditorAppearanceColors, NotebookEdit
val NOTEBOOK_APPEARANCE_KEY = Key.create<NotebookEditorAppearance>(NotebookEditorAppearance::class.java.name)
val CODE_CELL_BACKGROUND = ColorKey.createColorKey("JUPYTER.CODE_CELL_BACKGROUND")
internal val CODE_CELL_BACKGROUND_NEW_UI = ColorKey.createColorKey("JUPYTER.CODE_CELL_BACKGROUND_NEW_UI")
internal val CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR = ColorKey.createColorKey("JUPYTER.CELL_UNDER_CURSOR_STRIPE_HOVER_COLOR")
internal val CELL_STRIPE_COLOR = ColorKey.createColorKey("JUPYTER.CELL_STRIPE_COLOR")
}
fun getCaretRowColor(scheme: EditorColorsScheme): Color?
@@ -85,6 +88,9 @@ interface NotebookEditorAppearanceColors {
*/
fun getCellStripeColor(editor: EditorImpl, lines: IntRange): Color? = null
fun getCellStripeHoverColor(editor: EditorImpl, lines: IntRange): Color? = null
fun getCellStripeColor(editor: Editor): Color
fun getCellStripeHoverColor(editor: Editor): Color
}
interface NotebookEditorAppearanceFlags {

View File

@@ -12,6 +12,7 @@ import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.impl.view.FontLayoutService
import com.intellij.openapi.editor.markup.LineMarkerRendererEx
import com.intellij.openapi.editor.markup.RangeHighlighter
import org.jetbrains.plugins.notebooks.ui.isFoldingEnabledKey
import java.awt.Graphics
import java.awt.Point
import java.awt.Rectangle
@@ -23,7 +24,7 @@ abstract class NotebookLineMarkerRenderer(private val inlayId: Long? = null) : L
override fun getPosition(): LineMarkerRendererEx.Position = LineMarkerRendererEx.Position.CUSTOM
protected fun getInlayBounds(editor: EditorEx, linesRange: IntRange) : Rectangle? {
protected fun getInlayBounds(editor: EditorEx, linesRange: IntRange): Rectangle? {
val startOffset = editor.document.getLineStartOffset(linesRange.first)
val endOffset = editor.document.getLineEndOffset(linesRange.last)
val inlays = editor.inlayModel.getBlockElementsInRange(startOffset, endOffset)
@@ -56,10 +57,12 @@ class NotebookBelowCellCellGutterLineMarkerRenderer(private val highlighter: Ran
class MarkdownCellGutterLineMarkerRenderer(private val highlighter: RangeHighlighter, inlayId: Long) : NotebookLineMarkerRenderer(inlayId) {
override fun paint(editor: Editor, g: Graphics, r: Rectangle) {
editor as EditorImpl
val lines = IntRange(editor.document.getLineNumber(highlighter.startOffset), editor.document.getLineNumber(highlighter.endOffset))
val inlayBounds = getInlayBounds(editor, lines) ?: return
paintCellGutter(inlayBounds, lines, editor, g, r)
if (editor.getUserData(isFoldingEnabledKey) != true) {
editor as EditorImpl
val lines = IntRange(editor.document.getLineNumber(highlighter.startOffset), editor.document.getLineNumber(highlighter.endOffset))
val inlayBounds = getInlayBounds(editor, lines) ?: return
paintCellGutter(inlayBounds, lines, editor, g, r)
}
}
}
@@ -127,9 +130,11 @@ class NotebookTextCellBackgroundLineMarkerRenderer(private val highlighter: Rang
val height = editor.offsetToXY(editor.document.getLineEndOffset(lines.last)).y + editor.lineHeight - top
paintCaretRow(editor, g, lines)
val appearance = editor.notebookAppearance
appearance.getCellStripeColor(editor, lines)?.let {
paintCellStripe(appearance, g, r, it, top, height)
if (editor.getUserData(isFoldingEnabledKey) != true) {
val appearance = editor.notebookAppearance
appearance.getCellStripeColor(editor, lines)?.let {
paintCellStripe(appearance, g, r, it, top, height)
}
}
}
}

View File

@@ -7,6 +7,7 @@ import com.intellij.openapi.editor.LineNumberConverter
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorImpl
import org.jetbrains.plugins.notebooks.ui.isFoldingEnabledKey
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookEditorAppearance.Companion.NOTEBOOK_APPEARANCE_KEY
import java.awt.Color
import java.awt.Graphics
@@ -42,13 +43,15 @@ inline fun paintNotebookCellBackgroundGutter(
}
actionBetweenBackgroundAndStripe()
if (editor.editorKind == EditorKind.DIFF) return
if (stripe != null) {
paintCellStripe(appearance, g, r, stripe, top, height)
}
if (stripeHover != null) {
g.color = stripeHover
g.fillRect(r.width - appearance.getLeftBorderWidth(), top, appearance.getCellLeftLineHoverWidth(), height)
if (editor.getUserData(isFoldingEnabledKey) != true) {
if (editor.editorKind == EditorKind.DIFF) return
if (stripe != null) {
paintCellStripe(appearance, g, r, stripe, top, height)
}
if (stripeHover != null) {
g.color = stripeHover
g.fillRect(r.width - appearance.getLeftBorderWidth(), top, appearance.getCellLeftLineHoverWidth(), height)
}
}
}

View File

@@ -3,7 +3,8 @@ package org.jetbrains.plugins.notebooks.visualization
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.ex.PrioritizedDocumentListener
import com.intellij.openapi.editor.impl.EditorDocumentPriorities
import com.intellij.openapi.util.TextRange
import com.intellij.util.EventDispatcher
import com.intellij.util.concurrency.ThreadingAssertions
@@ -35,11 +36,11 @@ class NonIncrementalCellLines private constructor(private val document: Document
return intervals.listIterator(ordinal)
}
private fun notifyChanged(oldCells: List<NotebookCellLines.Interval>,
oldAffectedCells: List<NotebookCellLines.Interval>,
newCells: List<NotebookCellLines.Interval>,
newAffectedCells: List<NotebookCellLines.Interval>,
documentEvent: DocumentEvent) {
private fun createEvent(oldCells: List<NotebookCellLines.Interval>,
newCells: List<NotebookCellLines.Interval>,
oldAffectedCells: List<NotebookCellLines.Interval>,
newAffectedCells: List<NotebookCellLines.Interval>,
documentEvent: DocumentEvent): NotebookCellLinesEvent {
val (trimmedOldCells, trimmedNewCells) =
if (oldCells == newCells) {
Pair(emptyList(), emptyList())
@@ -70,15 +71,22 @@ class NonIncrementalCellLines private constructor(private val document: Document
newAffectedIntervals = newAffectedCells,
modificationStamp = modificationStamp,
)
return event
}
private fun notify(event: NotebookCellLinesEvent) {
catchThrowableAndLog {
intervalListeners.multicaster.documentChanged(event)
}
}
private fun createDocumentListener() = object : DocumentListener {
private fun createDocumentListener() = object : PrioritizedDocumentListener {
private var oldAffectedCells: List<NotebookCellLines.Interval> = emptyList()
private val postponedEvents = mutableListOf<NotebookCellLinesEvent>()
override fun getPriority(): Int = EditorDocumentPriorities.INLAY_MODEL + 1
override fun beforeDocumentChange(event: DocumentEvent) {
oldAffectedCells = getAffectedCells(intervals, document, TextRange(event.offset, event.offset + event.oldLength))
@@ -99,7 +107,21 @@ class NonIncrementalCellLines private constructor(private val document: Document
intervals = intervalsGenerator.makeIntervals(document)
val newAffectedCells = getAffectedCells(intervals, document, TextRange(event.offset, event.offset + event.newLength))
notifyChanged(oldIntervals, oldAffectedCells, intervals, newAffectedCells, event)
val newEvent = createEvent(oldIntervals, intervals, oldAffectedCells, newAffectedCells, event)
if (event.document.isInBulkUpdate) {
postponedEvents.add(newEvent)
} else {
notify(newEvent)
}
}
override fun bulkUpdateFinished(document: Document) {
if (postponedEvents.isNotEmpty()) {
postponedEvents.forEach {
notify(it)
}
}
postponedEvents.clear()
}
}

View File

@@ -34,6 +34,11 @@ interface NotebookCellInlayController {
}
}
/**
* Marker interface for factories producing custom editors for cells
*/
interface InputFactory
val inlay: Inlay<*>
val factory: Factory

View File

@@ -1,27 +1,19 @@
package org.jetbrains.plugins.notebooks.visualization
import com.intellij.ide.DataManager
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Document
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.colors.EditorColorsListener
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.ex.FoldingListener
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.*
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.Processor
import com.intellij.util.SmartList
import com.intellij.util.asSafely
@@ -31,14 +23,14 @@ import com.intellij.util.ui.update.MergingUpdateQueue
import com.intellij.util.ui.update.Update
import org.jetbrains.annotations.TestOnly
import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance
import org.jetbrains.plugins.notebooks.visualization.UpdateInlaysTask.Companion.CELL_MARKER
import org.jetbrains.plugins.notebooks.visualization.outputs.NotebookOutputInlayController
import org.jetbrains.plugins.notebooks.visualization.ui.EditorCell
import org.jetbrains.plugins.notebooks.ui.isFoldingEnabledKey
import java.awt.Graphics
import javax.swing.JComponent
import kotlin.math.max
import kotlin.math.min
class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
class NotebookCellInlayManager private constructor(val editor: EditorImpl) : NotebookIntervalPointerFactory.ChangeListener {
private val inlays: MutableMap<Inlay<*>, NotebookCellInlayController> = HashMap()
private val notebookCellLines = NotebookCellLines.get(editor)
private val viewportQueue = MergingUpdateQueue("NotebookCellInlayManager Viewport Update", 100, true, null, editor.disposable, null, true)
@@ -47,13 +39,17 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
private val updateQueue = MergingUpdateQueue("NotebookCellInlayManager Interval Update", 20, true, null, editor.disposable, null, true)
private var initialized = false
private var _cells = mutableListOf<EditorCell>()
val cells: List<EditorCell> get() = _cells.toList()
/**
* Listens for inlay changes (called after all inlays are updated). Feel free to convert it to the EP if you need another listener
*/
var changedListener: InlaysChangedListener? = null
fun inlaysForInterval(interval: NotebookCellLines.Interval): Iterable<NotebookCellInlayController> =
getMatchingInlaysForLines(interval.lines)
_cells[interval.ordinal].controllers
/** It's public, but think twice before using it. Called many times in a row, it can freeze UI. Consider using [update] instead. */
fun updateImmediately(lines: IntRange) {
@@ -84,6 +80,7 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
private fun addViewportChangeListener() {
editor.scrollPane.viewport.addChangeListener {
scheduleUpdatePositions()
viewportQueue.queue(object : Update("Viewport change") {
override fun run() {
if (editor.isDisposed) return
@@ -106,8 +103,6 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
handleRefreshedDocument()
addDocumentListener()
val connection = ApplicationManager.getApplication().messageBus.connect(editor.disposable)
connection.subscribe(EditorColorsManager.TOPIC, EditorColorsListener {
updateAll()
@@ -120,9 +115,21 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
addViewportChangeListener()
editor.foldingModel.addListener(object : FoldingListener {
override fun onFoldProcessingEnd() {
scheduleUpdatePositions()
}
}, editor.disposable)
initialized = true
}
private fun scheduleUpdatePositions() {
runInEdt {
_cells.forEach { cell -> cell.updatePositions() }
}
}
private fun refreshHighlightersLookAndFeel() {
for (highlighter in editor.markupModel.allHighlighters) {
if (highlighter.customRenderer === NotebookCellHighlighterRenderer) {
@@ -133,78 +140,22 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
private fun handleRefreshedDocument() {
ThreadingAssertions.softAssertReadAccess()
val factories = NotebookCellInlayController.Factory.EP_NAME.extensionList
for (interval in notebookCellLines.intervals) {
for (factory in factories) {
val controller = failSafeCompute(factory, editor, emptyList(), notebookCellLines.intervals.listIterator(interval.ordinal))
if (controller != null) {
rememberController(controller, interval)
}
}
_cells.forEach {
it.dispose()
}
val pointerFactory = NotebookIntervalPointerFactory.get(editor)
_cells = notebookCellLines.intervals.map { interval ->
createCell(pointerFactory.create(interval))
}.toMutableList()
addHighlighters(notebookCellLines.intervals)
inlaysChanged()
}
private fun addDocumentListener() {
val documentListener = object : DocumentListener {
private var matchingCellsBeforeChange: List<NotebookCellLines.Interval> = emptyList()
private var isBulkModeEnabled = false
private fun interestingLogicalLines(document: Document, startOffset: Int, length: Int): IntRange {
// Adding one additional line is needed to handle deletions at the end of the document.
val end =
if (startOffset + length <= document.textLength) document.getLineNumber(startOffset + length)
else document.lineCount + 1
return document.getLineNumber(startOffset)..end
}
override fun bulkUpdateStarting(document: Document) {
isBulkModeEnabled = true
matchingCellsBeforeChange = notebookCellLines.getMatchingCells(0 until document.lineCount)
}
override fun beforeDocumentChange(event: DocumentEvent) {
if (isBulkModeEnabled) return
val document = event.document
val logicalLines = interestingLogicalLines(document, event.offset, event.oldLength)
matchingCellsBeforeChange = notebookCellLines.getMatchingCells(logicalLines)
}
override fun documentChanged(event: DocumentEvent) {
if (isBulkModeEnabled) return
if (event.oldLength == 0 && event.newFragment.contains(CELL_MARKER)) {
refreshInlays()
}
val logicalLines = interestingLogicalLines(event.document, event.offset, event.newLength)
ensureInlaysAndHighlightersExist(matchingCellsBeforeChange, logicalLines)
}
override fun bulkUpdateFinished(document: Document) {
isBulkModeEnabled = false
// bulk mode is over, now we could access inlays, let's update them all
refreshInlays()
ensureInlaysAndHighlightersExist(matchingCellsBeforeChange, 0 until document.lineCount)
}
}
editor.document.addDocumentListener(documentListener, editor.disposable)
}
/**
* Hack. When we are adding cell in notebook, previous cell changes their range and we need to update it.
*/
private fun refreshInlays() {
val outputInlays = inlays.values.filterIsInstance<NotebookOutputInlayController>()
for (outputInlay in outputInlays) {
val oldInlay = outputInlay.checkAndUpdateInlayPosition() ?: continue
inlays.remove(oldInlay)
inlays[outputInlay.inlay] = outputInlay
}
}
private fun createCell(interval: NotebookIntervalPointer) = EditorCell(
editor,
notebookCellLines,
interval
)
private fun ensureInlaysAndHighlightersExist(matchingCellsBeforeChange: List<NotebookCellLines.Interval>, logicalLines: IntRange) {
val interestingRange =
@@ -238,86 +189,15 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
}
addHighlighters(intervalsToAddHighlightersFor.values)
val allMatchingInlays: MutableList<Pair<Int, NotebookCellInlayController>> = getMatchingInlaysForLines(fullInterestingRange)
.mapTo(mutableListOf()) {
editor.document.getLineNumber(it.inlay.offset) to it
}
val allFactories = NotebookCellInlayController.Factory.EP_NAME.extensionList
for (interval in matchingIntervals) {
val seenControllersByFactory: Map<NotebookCellInlayController.Factory, MutableList<NotebookCellInlayController>> =
allFactories.associateWith { SmartList() }
allMatchingInlays.removeIf { (inlayLine, controller) ->
if (inlayLine in interval.lines) {
seenControllersByFactory[controller.factory]?.add(controller)
true
}
else false
}
for ((factory, controllers) in seenControllersByFactory) {
val actualController = if (!editor.isDisposed) {
failSafeCompute(factory, editor, controllers, notebookCellLines.intervals.listIterator(interval.ordinal))
}
else {
null
}
if (actualController != null) {
rememberController(actualController, interval)
}
for (oldController in controllers) {
if (oldController != actualController) {
Disposer.dispose(oldController.inlay, false)
}
}
}
_cells[interval.ordinal].update()
}
NotebookGutterLineMarkerManager().putHighlighters(editor)
for ((_, controller) in allMatchingInlays) {
Disposer.dispose(controller.inlay, false)
}
inlaysChanged()
}
private data class NotebookCellDataProvider(
val editor: EditorImpl,
val component: JComponent,
val interval: NotebookCellLines.Interval,
) : DataProvider {
override fun getData(key: String): Any? =
when (key) {
NOTEBOOK_CELL_LINES_INTERVAL_DATA_KEY.name -> interval
PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> component
PlatformDataKeys.EDITOR.name -> editor
else -> null
}
}
private fun rememberController(controller: NotebookCellInlayController, interval: NotebookCellLines.Interval) {
val inlay = controller.inlay
inlay.renderer.asSafely<JComponent>()?.let { component ->
val oldProvider = DataManager.getDataProvider(component)
if (oldProvider != null && oldProvider !is NotebookCellDataProvider) {
LOG.error("Overwriting an existing CLIENT_PROPERTY_DATA_PROVIDER. Old provider: $oldProvider")
}
DataManager.removeDataProvider(component)
DataManager.registerDataProvider(component, NotebookCellDataProvider(editor, component, interval))
}
if (inlays.put(inlay, controller) !== controller) {
val disposable = Disposable {
inlay.renderer.asSafely<JComponent>()?.let { DataManager.removeDataProvider(it) }
inlays.remove(inlay)
}
if (Disposer.isDisposed(inlay)) {
disposable.dispose()
}
else {
Disposer.register(inlay, disposable)
}
}
}
private fun getMatchingHighlightersForLines(lines: IntRange): List<RangeHighlighterEx> =
mutableListOf<RangeHighlighterEx>()
.also { list ->
@@ -331,19 +211,9 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
})
}
private fun getMatchingInlaysForLines(lines: IntRange): List<NotebookCellInlayController> =
getMatchingInlaysForOffsets(
editor.document.getLineStartOffset(saturateLine(lines.first)),
editor.document.getLineEndOffset(saturateLine(lines.last)))
private fun saturateLine(line: Int): Int =
line.coerceAtMost(editor.document.lineCount - 1).coerceAtLeast(0)
private fun getMatchingInlaysForOffsets(startOffset: Int, endOffset: Int): List<NotebookCellInlayController> =
editor.inlayModel
.getBlockElementsInRange(startOffset, endOffset)
.mapNotNull(inlays::get)
private val NotebookCellLines.Interval.shouldHaveHighlighter: Boolean
get() = type == NotebookCellLines.CellType.CODE
@@ -379,19 +249,6 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
}
}
private fun failSafeCompute(factory: NotebookCellInlayController.Factory,
editor: EditorImpl,
controllers: Collection<NotebookCellInlayController>,
intervalIterator: ListIterator<NotebookCellLines.Interval>): NotebookCellInlayController? {
try {
return factory.compute(editor, controllers, intervalIterator)
}
catch (t: Throwable) {
thisLogger().error("${factory.javaClass.name} shouldn't throw exceptions at NotebookCellInlayController.Factory.compute(...)", t)
return null
}
}
@TestOnly
fun getInlays(): MutableMap<Inlay<*>, NotebookCellInlayController> = inlays
@@ -401,11 +258,12 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
}
companion object {
private val LOG = logger<NotebookCellInlayManager>()
@JvmStatic
fun install(editor: EditorImpl) {
NotebookCellInlayManager(editor).initialize()
val notebookCellInlayManager = NotebookCellInlayManager(editor)
editor.putUserData(isFoldingEnabledKey, Registry.`is`("jupyter.editor.folding.cells"))
NotebookIntervalPointerFactory.get(editor).changeListeners.addListener(notebookCellInlayManager, editor.disposable)
notebookCellInlayManager.initialize()
}
@JvmStatic
@@ -413,6 +271,44 @@ class NotebookCellInlayManager private constructor(val editor: EditorImpl) {
private val key = Key.create<NotebookCellInlayManager>(NotebookCellInlayManager::class.java.name)
}
override fun onUpdated(event: NotebookIntervalPointersEvent) {
var start = Int.MAX_VALUE
var end = Int.MIN_VALUE
for (change in event.changes) {
when (change) {
is NotebookIntervalPointersEvent.OnEdited -> {
start = minOf(start, change.intervalBefore.lines.first, change.intervalAfter.lines.first)
end = maxOf(end, change.intervalBefore.lines.last, change.intervalAfter.lines.last)
}
is NotebookIntervalPointersEvent.OnInserted -> {
change.subsequentPointers.forEach {
_cells.add(it.interval.ordinal, createCell(it.pointer))
}
start = minOf(start, change.subsequentPointers.first().interval.lines.first)
end = maxOf(end, change.subsequentPointers.last().interval.lines.last)
scheduleUpdatePositions()
}
is NotebookIntervalPointersEvent.OnRemoved -> {
change.subsequentPointers.reversed().forEach {
val removed = _cells.removeAt(it.interval.ordinal)
removed.dispose()
}
start = minOf(start, change.subsequentPointers.first().interval.lines.first)
end = maxOf(end, change.subsequentPointers.last().interval.lines.last)
scheduleUpdatePositions()
}
is NotebookIntervalPointersEvent.OnSwapped -> {
val first = _cells[change.firstOrdinal].intervalPointer
_cells[change.firstOrdinal].intervalPointer = _cells[change.secondOrdinal].intervalPointer
_cells[change.secondOrdinal].intervalPointer = first
start = minOf(start, change.first.interval.lines.first)
end = maxOf(end, change.second.interval.lines.last)
}
}
}
updateConsequentInlays(start..end)
}
}
/**
@@ -473,8 +369,4 @@ private class UpdateInlaysTask(private val manager: NotebookCellInlayManager,
pointerSet.addAll(update.pointerSet)
return true
}
companion object {
const val CELL_MARKER = "#%%"
}
}
}

View File

@@ -3,8 +3,6 @@ package org.jetbrains.plugins.notebooks.visualization
import com.intellij.openapi.editor.*
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.EditorImpl
@@ -21,10 +19,11 @@ import java.awt.Rectangle
class NotebookGutterLineMarkerManager {
fun attachHighlighters(editor: EditorEx) {
editor.addEditorDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) = putHighlighters(editor)
override fun bulkUpdateFinished(document: Document) = putHighlighters(editor)
})
NotebookIntervalPointerFactory.get(editor).changeListeners.addListener(object: NotebookIntervalPointerFactory.ChangeListener {
override fun onUpdated(event: NotebookIntervalPointersEvent) {
putHighlighters(editor)
}
}, (editor as EditorImpl).disposable)
editor.caretModel.addCaretListener(object : CaretListener {
override fun caretPositionChanged(event: CaretEvent) {

View File

@@ -42,13 +42,6 @@ private class NotebookIntervalPointerImpl(@Volatile var interval: NotebookCellLi
private typealias NotebookIntervalPointersEventChanges = ArrayList<Change>
private sealed interface ChangesContext
private data class DocumentChangedContext(var redoContext: RedoContext? = null) : ChangesContext
private data class UndoContext(val changes: List<Change>) : ChangesContext
private data class RedoContext(val changes: List<Change>) : ChangesContext
/**
* One unique NotebookIntervalPointer exists for each current interval. You can use NotebookIntervalPointer as map key.
* [NotebookIntervalPointerFactoryImpl] automatically supports undo/redo for [documentChanged] and [modifyPointers] calls.
@@ -62,7 +55,7 @@ class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: Notebook
undoManager: UndoManager?,
private val project: Project) : NotebookIntervalPointerFactory, NotebookCellLines.IntervalListener {
private val pointers = ArrayList<NotebookIntervalPointerImpl>()
private var changesContext: ChangesContext? = null
private var postponedEvent: NotebookIntervalPointersEvent? = null
override val changeListeners: EventDispatcher<NotebookIntervalPointerFactory.ChangeListener> =
EventDispatcher.create(NotebookIntervalPointerFactory.ChangeListener::class.java)
@@ -109,77 +102,44 @@ class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: Notebook
override fun documentChanged(event: NotebookCellLinesEvent) {
ThreadingAssertions.assertWriteAccess()
try {
val pointersEvent = when (val context = changesContext) {
is DocumentChangedContext -> documentChangedByAction(event, context)
is UndoContext -> documentChangedByUndo(event, context)
is RedoContext -> documentChangedByRedo(event, context)
null -> documentChangedByAction(event, null) // changesContext is null if undo manager is unavailable
if (validUndoManager?.isUndoOrRedoInProgress != true) {
documentChangedByAction(event)
} else {
val e = postponedEvent
if (e != null) {
onUpdated(e)
}
}
onUpdated(pointersEvent)
}
catch (ex: Exception) {
thisLogger().error(ex)
// DS-3893 consume exception and log it, actions changing document should work as usual
}
finally {
changesContext = null
} finally {
postponedEvent = null
}
}
override fun beforeDocumentChange(event: NotebookCellLinesEventBeforeChange) {
ThreadingAssertions.assertWriteAccess()
val undoManager = validUndoManager
if (undoManager == null || undoManager.isUndoOrRedoInProgress) return
val context = DocumentChangedContext()
try {
undoManager.undoableActionPerformed(object : BasicUndoableAction() {
override fun undo() {}
override fun redo() {
changesContext = context.redoContext
}
})
changesContext = context
}
catch (ex: Exception) {
thisLogger().error(ex)
// DS-3893 consume exception, don't prevent document updating
}
}
private fun documentChangedByAction(event: NotebookCellLinesEvent,
documentChangedContext: DocumentChangedContext?): NotebookIntervalPointersEvent {
val eventChanges = NotebookIntervalPointersEventChanges()
updateChangedIntervals(event, eventChanges)
updateShiftedIntervals(event)
private fun documentChangedByAction(event: NotebookCellLinesEvent) {
val eventChanges = updateChangedIntervals(event)
val shiftChanges = updateShiftedIntervals(event)
validUndoManager?.undoableActionPerformed(object : BasicUndoableAction(documentReference) {
override fun undo() {
changesContext = UndoContext(eventChanges)
ThreadingAssertions.assertWriteAccess()
updatePointersByChanges(invertChanges(shiftChanges))
val invertChanges = invertChanges(eventChanges)
updatePointersByChanges(invertChanges)
onUpdated(NotebookIntervalPointersEvent(invertChanges, event, EventSource.UNDO_ACTION))
}
override fun redo() {}
override fun redo() {
ThreadingAssertions.assertWriteAccess()
updatePointersByChanges(eventChanges)
updatePointersByChanges(shiftChanges)
postponedEvent = NotebookIntervalPointersEvent(eventChanges, event, EventSource.REDO_ACTION)
}
})
documentChangedContext?.let {
it.redoContext = RedoContext(eventChanges)
}
return NotebookIntervalPointersEvent(eventChanges, event, EventSource.ACTION)
}
private fun documentChangedByUndo(event: NotebookCellLinesEvent, context: UndoContext): NotebookIntervalPointersEvent {
val invertedChanges = invertChanges(context.changes)
updatePointersByChanges(invertedChanges)
updateShiftedIntervals(event)
return NotebookIntervalPointersEvent(invertedChanges, event, EventSource.UNDO_ACTION)
}
private fun documentChangedByRedo(event: NotebookCellLinesEvent, context: RedoContext): NotebookIntervalPointersEvent {
updatePointersByChanges(context.changes)
updateShiftedIntervals(event)
return NotebookIntervalPointersEvent(context.changes, event, EventSource.REDO_ACTION)
onUpdated(NotebookIntervalPointersEvent(eventChanges, event, EventSource.ACTION))
}
private fun updatePointersByChanges(changes: List<Change>) {
@@ -215,7 +175,8 @@ class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: Notebook
return old.type == new.type && old.language == new.language
}
private fun updateChangedIntervals(e: NotebookCellLinesEvent, eventChanges: NotebookIntervalPointersEventChanges) {
private fun updateChangedIntervals(e: NotebookCellLinesEvent): NotebookIntervalPointersEventChanges {
val eventChanges = NotebookIntervalPointersEventChanges()
when {
!e.isIntervalsChanged() -> {
// content edited without affecting intervals values
@@ -257,17 +218,25 @@ class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: Notebook
}
}
}
return eventChanges
}
private fun updateShiftedIntervals(event: NotebookCellLinesEvent) {
private fun updateShiftedIntervals(event: NotebookCellLinesEvent): NotebookIntervalPointersEventChanges {
val invalidPointersStart =
event.newIntervals.firstOrNull()?.let { it.ordinal + event.newIntervals.size }
?: event.oldIntervals.firstOrNull()?.ordinal
?: pointers.size
val eventChanges = NotebookIntervalPointersEventChanges()
val intervals = notebookCellLines.intervals
for (i in invalidPointersStart until pointers.size) {
pointers[i].interval = notebookCellLines.intervals[i]
val ptr = pointers[i]
val intervalBefore = ptr.interval!!
val intervalAfter = intervals[i]
ptr.interval = intervals[i]
eventChanges.add(OnEdited(ptr, intervalBefore, intervalAfter))
}
return eventChanges
}
private fun applyChanges(changes: Iterable<NotebookIntervalPointerFactory.Change>, eventChanges: NotebookIntervalPointersEventChanges) {

View File

@@ -12,6 +12,7 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.asSafely
import com.intellij.util.messages.Topic
import org.jetbrains.plugins.notebooks.ui.isFoldingEnabledKey
import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance
import org.jetbrains.plugins.notebooks.visualization.*
import org.jetbrains.plugins.notebooks.visualization.outputs.NotebookOutputComponentFactory.Companion.gutterPainter
@@ -80,22 +81,6 @@ class NotebookOutputInlayController private constructor(
}
}
fun checkAndUpdateInlayPosition(): Inlay<*>? {
val lines = intervalPointer.get()?.lines ?: return null
val lineEnd = computeInlayOffset(editor.document, lines)
if (inlay.offset != lineEnd) {
val oldInlay = inlay
isInReplaceInlay = true
Disposer.dispose(inlay)
inlay = createInlay()
isInReplaceInlay = false
return oldInlay
}
else {
return null
}
}
private fun createInlay() = editor.addComponentInlay(
outerComponent,
isRelatedToPrecedingText = true,
@@ -124,7 +109,9 @@ class NotebookOutputInlayController private constructor(
for (collapsingComponent in innerComponent.components) {
val mainComponent = (collapsingComponent as CollapsingComponent).mainComponent
collapsingComponent.paintGutter(editor, yOffset, g)
if (editor.getUserData(isFoldingEnabledKey) != true) {
collapsingComponent.paintGutter(editor, yOffset, g)
}
mainComponent.gutterPainter?.let { painter ->
mainComponent.yOffsetFromEditor(editor)?.let { yOffset ->
@@ -278,6 +265,14 @@ class NotebookOutputInlayController private constructor(
return result
}
fun toggle() {
collapsingComponents.forEach { collapsingComponent ->
if (collapsingComponent.isWorthCollapsing) {
collapsingComponent.isSeen = !collapsingComponent.isSeen
}
}
}
class Factory : NotebookCellInlayController.Factory {
override fun compute(
editor: EditorImpl,

View File

@@ -0,0 +1,135 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.Disposer
import com.intellij.util.asSafely
import org.jetbrains.plugins.notebooks.visualization.NOTEBOOK_CELL_LINES_INTERVAL_DATA_KEY
import org.jetbrains.plugins.notebooks.visualization.NotebookCellInlayController
import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointer
import org.jetbrains.plugins.notebooks.visualization.outputs.NotebookOutputInlayController
import javax.swing.JComponent
class EditorCell(
private val editor: Editor,
private val intervals: NotebookCellLines,
internal var intervalPointer: NotebookIntervalPointer
) {
private var _controllers: List<NotebookCellInlayController> = emptyList()
val controllers: List<NotebookCellInlayController>
get() = _controllers + (input.inputController?.let { listOf(it) } ?: emptyList())
private val interval get() = intervalPointer.get() ?: error("Invalid interval")
private var input: EditorCellInput = EditorCellInput(
editor as EditorEx,
{ currentController: NotebookCellInlayController? ->
getInputFactories().firstNotNullOfOrNull {
failSafeCompute(it, editor, currentController?.let { listOf(it) }
?: emptyList(), intervals.intervals.listIterator(interval.ordinal))
}
}, intervalPointer)
private var output: EditorCellOutput? = null
init {
update()
}
fun dispose() {
controllers.forEach { controller ->
disposeController(controller)
}
input.dispose()
output?.dispose()
}
private fun disposeController(controller: NotebookCellInlayController) {
val inlay = controller.inlay
inlay.renderer.asSafely<JComponent>()?.let { DataManager.removeDataProvider(it) }
Disposer.dispose(inlay)
}
fun update() {
val otherFactories = NotebookCellInlayController.Factory.EP_NAME.extensionList
.filter { it !is NotebookCellInlayController.InputFactory }
val controllersToDispose = controllers.toMutableSet()
_controllers = if (!editor.isDisposed) {
otherFactories.mapNotNull { factory -> failSafeCompute(factory, editor, controllers, intervals.intervals.listIterator(interval.ordinal)) }
}
else {
emptyList()
}
controllersToDispose.removeAll(controllers.toSet())
controllersToDispose.forEach { disposeController(it) }
for (controller in controllers) {
val inlay = controller.inlay
inlay.renderer.asSafely<JComponent>()?.let { component ->
val oldProvider = DataManager.getDataProvider(component)
if (oldProvider != null && oldProvider !is NotebookCellDataProvider) {
LOG.error("Overwriting an existing CLIENT_PROPERTY_DATA_PROVIDER. Old provider: $oldProvider")
}
DataManager.removeDataProvider(component)
DataManager.registerDataProvider(component, NotebookCellDataProvider(editor, component) { interval })
}
}
input.update()
output?.dispose()
val outputController = controllers.filterIsInstance<NotebookOutputInlayController>().firstOrNull()
if (outputController != null) {
output = EditorCellOutput(editor as EditorEx, outputController)
}
}
private fun getInputFactories(): Sequence<NotebookCellInlayController.Factory> {
return NotebookCellInlayController.Factory.EP_NAME.extensionList.asSequence()
.filter { it is NotebookCellInlayController.InputFactory }
}
private fun failSafeCompute(factory: NotebookCellInlayController.Factory,
editor: Editor,
controllers: Collection<NotebookCellInlayController>,
intervalIterator: ListIterator<NotebookCellLines.Interval>): NotebookCellInlayController? {
try {
return factory.compute(editor as EditorImpl, controllers, intervalIterator)
}
catch (t: Throwable) {
thisLogger().error("${factory.javaClass.name} shouldn't throw exceptions at NotebookCellInlayController.Factory.compute(...)", t)
return null
}
}
fun updatePositions() {
input.updatePositions()
output?.updatePositions()
}
companion object {
private val LOG = logger<EditorCell>()
}
private data class NotebookCellDataProvider(
val editor: Editor,
val component: JComponent,
val intervalProvider: () -> NotebookCellLines.Interval,
) : DataProvider {
override fun getData(key: String): Any? =
when (key) {
NOTEBOOK_CELL_LINES_INTERVAL_DATA_KEY.name -> intervalProvider()
PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> component
PlatformDataKeys.EDITOR.name -> editor
else -> null
}
}
}

View File

@@ -0,0 +1,43 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.editor.ex.EditorEx
import org.jetbrains.plugins.notebooks.ui.isFoldingEnabledKey
import org.jetbrains.plugins.notebooks.visualization.NotebookCellInlayController
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointer
class EditorCellFolding(editor: EditorEx, toggleListener: () -> Unit) {
private val foldingBar: EditorCellFoldingBar?
private val binder: EditorCellFoldingBarLocationBinder?
init {
val isFoldingEnabled = editor.getUserData(isFoldingEnabledKey) ?: false
if (isFoldingEnabled) {
foldingBar = EditorCellFoldingBar(editor, toggleListener)
binder = EditorCellFoldingBarLocationBinder(editor, foldingBar)
}
else {
foldingBar = null
binder = null
}
}
fun bindTo(controller: NotebookCellInlayController?) {
binder?.bindTo(controller)
}
fun dispose() {
binder?.dispose()
foldingBar?.dispose()
}
fun bindTo(interval: NotebookIntervalPointer) {
binder?.bindTo(interval)
}
fun updatePosition() {
binder?.updatePositions()
}
}

View File

@@ -0,0 +1,50 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.editor.ex.EditorEx
import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance
import java.awt.Cursor
import java.awt.Dimension
import java.awt.Point
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JPanel
internal class EditorCellFoldingBar(
private val editor: EditorEx,
private val toggleListener: () -> Unit
) {
val panel = JPanel().also {
val appearance = editor.notebookAppearance
it.background = appearance.getCellStripeColor(editor)
it.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
it.addMouseListener(object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
it.background = appearance.getCellStripeHoverColor(editor)
}
override fun mouseExited(e: MouseEvent) {
it.background = appearance.getCellStripeColor(editor)
}
override fun mouseClicked(e: MouseEvent) {
toggleListener.invoke()
}
})
}
init {
editor.gutterComponentEx.add(panel)
}
fun dispose() {
editor.gutterComponentEx.remove(panel)
}
fun setLocation(y: Int, height: Int) {
panel.location = Point(editor.gutterComponentEx.extraLineMarkerFreePaintersAreaOffset, y)
panel.size = Dimension(8, height)
}
}

View File

@@ -0,0 +1,64 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.util.asSafely
import org.jetbrains.plugins.notebooks.visualization.NotebookCellInlayController
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointer
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JComponent
internal class EditorCellFoldingBarLocationBinder(private val editor: EditorEx, private val foldingBar: EditorCellFoldingBar) {
private val listener = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
foldingBar.setLocation(e.component.location.y, e.component.size.height)
}
}
private var currentController: NotebookCellInlayController? = null
private var currentInterval: NotebookIntervalPointer? = null
fun dispose() {
unbind()
}
private fun unbind() {
currentController?.let {
it.inlay.renderer.asSafely<JComponent>()?.removeComponentListener(listener)
}
currentController = null
}
fun bindTo(controller: NotebookCellInlayController?) {
if (controller != currentController) {
unbind()
if (controller != null) {
controller.inlay.renderer.asSafely<JComponent>()?.addComponentListener(listener)
currentController = controller
}
}
}
fun bindTo(interval: NotebookIntervalPointer) {
currentInterval = interval
}
fun updatePositions() {
val controller = currentController
if (controller != null) {
val component = controller.inlay.renderer as JComponent
foldingBar.setLocation(component.location.y, component.size.height)
}
else {
val interval = currentInterval?.get()
if (interval != null) {
val startOffset = editor.document.getLineStartOffset(interval.lines.first)
val endOffset = editor.document.getLineEndOffset(interval.lines.last)
val top = editor.offsetToXY(startOffset).y
val height = editor.offsetToXY(endOffset).y + editor.lineHeight - top
foldingBar.setLocation(top, height)
}
}
}
}

View File

@@ -0,0 +1,95 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.TextRange
import org.jetbrains.plugins.notebooks.visualization.NotebookCellInlayController
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointer
internal class EditorCellInput(
private val editor: EditorEx,
private val inputControllerFactory: ((NotebookCellInlayController?) -> NotebookCellInlayController?)?,
private val intervalPointer: NotebookIntervalPointer
) {
private val folding: EditorCellFolding = EditorCellFolding(editor) {
if (inputControllerFactory == null) {
toggleTextFolding()
}
else {
toggleFolding(inputControllerFactory)
}
}
private fun toggleFolding(inputControllerFactory: (NotebookCellInlayController?) -> NotebookCellInlayController?) {
val controller = inputController
inputController = if (controller != null) {
Disposer.dispose(controller.inlay)
toggleTextFolding()
null
}
else {
toggleTextFolding()
inputControllerFactory.invoke(inputController)
}
folding.bindTo(inputController)
}
private fun toggleTextFolding() {
val interval = intervalPointer.get() ?: error("Invalid interval")
val startOffset = editor.document.getLineStartOffset(interval.lines.first)
val endOffset = editor.document.getLineEndOffset(interval.lines.last)
val foldingModel = editor.foldingModel
val foldRegion = foldingModel.getFoldRegion(startOffset, endOffset)
if (foldRegion == null) {
foldingModel.runBatchFoldingOperation {
val text = editor.document.getText(TextRange(startOffset, endOffset))
val placeholder = text.lines().drop(1).firstOrNull { it.trim().isNotEmpty() }?.ellipsis(30) ?: "..."
foldingModel.createFoldRegion(startOffset, endOffset, placeholder, null, true)
}
}
else {
foldingModel.runBatchFoldingOperation {
foldingModel.removeFoldRegion(foldRegion)
}
}
}
internal var inputController: NotebookCellInlayController? = createOrUpdateController()
init {
folding.bindTo(inputController)
folding.bindTo(intervalPointer)
}
fun dispose() {
folding.dispose()
inputController?.let { controller -> Disposer.dispose(controller.inlay) }
}
fun update() {
inputController = createOrUpdateController()
}
private fun createOrUpdateController(): NotebookCellInlayController? {
val actualController = inputControllerFactory?.invoke(inputController)
if (actualController != inputController) {
inputController?.let { controller -> Disposer.dispose(controller.inlay) }
folding.bindTo(actualController)
}
return actualController
}
fun updatePositions() {
folding.updatePosition()
}
}
private fun String.ellipsis(length: Int): String {
return if (this.length > length) {
substring(0, length - 3) + "..."
}
else {
this
}
}

View File

@@ -0,0 +1,20 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.editor.ex.EditorEx
import org.jetbrains.plugins.notebooks.visualization.outputs.NotebookOutputInlayController
internal class EditorCellOutput(editor: EditorEx, private val outputController: NotebookOutputInlayController) {
private val folding = EditorCellFolding(editor) { outputController.toggle() }.also {
it.bindTo(outputController)
}
fun updatePositions() {
folding.updatePosition()
}
fun dispose() {
folding.dispose()
}
}