PY-73345 Optimize cell bounds calculations

GitOrigin-RevId: a65412190555d20e08a54d6717e7224912611230
This commit is contained in:
Anton Efimchuk
2024-06-18 17:10:27 +02:00
committed by intellij-monorepo-bot
parent 141a46b4ec
commit d738bf447f
12 changed files with 256 additions and 295 deletions

View File

@@ -1,11 +1,8 @@
package org.jetbrains.plugins.notebooks.visualization
import com.intellij.codeInsight.codeVision.ui.popup.layouter.bottom
import com.intellij.codeInsight.codeVision.ui.popup.layouter.right
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.FoldRegion
@@ -36,16 +33,14 @@ import org.jetbrains.plugins.notebooks.visualization.ui.EditorCellEventListener.
import org.jetbrains.plugins.notebooks.visualization.ui.EditorCellView
import org.jetbrains.plugins.notebooks.visualization.ui.keepScrollingPositionWhile
import java.awt.Graphics
import java.awt.Point
import java.util.*
import kotlin.math.max
import kotlin.math.min
class NotebookCellInlayManager private constructor(
val editor: EditorImpl
val editor: EditorImpl,
) : Disposable, NotebookIntervalPointerFactory.ChangeListener {
private val notebookCellLines = NotebookCellLines.get(editor)
private val viewportQueue = MergingUpdateQueue("NotebookCellInlayManager Viewport Update", 100, true, null, editor.disposable, null, true)
/** 20 is 1000 / 50, two times faster than the eye refresh rate. Actually, the value has been chosen randomly, without experiments. */
private val updateQueue = MergingUpdateQueue("NotebookCellInlayManager Interval Update", 20, true, null, editor.disposable, null, true)
@@ -62,6 +57,8 @@ class NotebookCellInlayManager private constructor(
private val cellEventListeners = EventDispatcher.create(EditorCellEventListener::class.java)
private var valid = false
override fun dispose() {}
fun getCellForInterval(interval: NotebookCellLines.Interval): EditorCell =
@@ -100,18 +97,9 @@ class NotebookCellInlayManager private constructor(
private fun addViewportChangeListener() {
editor.scrollPane.viewport.addChangeListener {
val rect = editor.scrollPane.viewport.viewRect
val top = editor.xyToLogicalPosition(rect.location)
val bottom = editor.xyToLogicalPosition(Point(rect.right, rect.bottom))
scheduleUpdatePositions(top.line, bottom.line)
viewportQueue.queue(object : Update("Viewport change") {
override fun run() {
if (editor.isDisposed) return
_cells.forEach {
it.onViewportChange()
}
}
})
_cells.forEach {
it.onViewportChange()
}
}
}
@@ -136,7 +124,7 @@ class NotebookCellInlayManager private constructor(
editor.foldingModel.addListener(object : FoldingListener {
override fun onFoldProcessingEnd() {
scheduleUpdatePositions()
invalidateCells()
}
}, editor.disposable)
@@ -215,24 +203,6 @@ class NotebookCellInlayManager private constructor(
startOffset >= region.startOffset && endOffset <= region.endOffset
}
private fun scheduleUpdatePositions(from: Int = 0, to: Int = 1000_000_000) {
runInEdt {
val fromIndex = _cells.indexOfFirst { cell ->
val lines = cell.intervalPointer.get()?.lines
lines != null && lines.hasIntersectionWith(from..to + 1)
}
if (fromIndex == -1) return@runInEdt
val toIndex = _cells.subList(fromIndex, _cells.size).indexOfFirst { cell ->
val lines = cell.intervalPointer.get()?.lines
lines != null && !lines.hasIntersectionWith(from..to + 1)
}
val finalIndex = if (toIndex == -1) _cells.size - 1 else fromIndex + toIndex
for (i in max(fromIndex - 1, 0)..min(finalIndex + 1, _cells.size - 1)) {
_cells[i].updatePositions()
}
}
}
private fun refreshHighlightersLookAndFeel() {
for (highlighter in editor.markupModel.allHighlighters) {
if (highlighter.customRenderer === NotebookCellHighlighterRenderer) {
@@ -257,7 +227,7 @@ class NotebookCellInlayManager private constructor(
}
private fun createCell(interval: NotebookIntervalPointer) = EditorCell(editor, interval) { cell ->
EditorCellView(editor, notebookCellLines, cell).also { Disposer.register(cell, it) }
EditorCellView(editor, notebookCellLines, cell, this).also { Disposer.register(cell, it) }
}.also { Disposer.register(this, it) }
private fun ensureInlaysAndHighlightersExist(matchingCellsBeforeChange: List<NotebookCellLines.Interval>, logicalLines: IntRange) {
@@ -413,7 +383,7 @@ class NotebookCellInlayManager private constructor(
}
}
if (needUpdatePositions) {
scheduleUpdatePositions()
invalidateCells()
}
cellEventListeners.multicaster.onEditorCellEvents(events)
updateConsequentInlays(start..end)
@@ -426,6 +396,22 @@ class NotebookCellInlayManager private constructor(
fun getCell(index: Int): EditorCell {
return cells[index]
}
fun invalidateCells() {
valid = false
}
fun validateCells() {
if (!valid) {
_cells.forEach {
it.view?.also { view ->
view.bounds = view.calculateBounds()
view.validate()
}
}
valid = true
}
}
}
/**
@@ -455,10 +441,12 @@ private object NotebookCellHighlighterRenderer : CustomHighlighterRenderer {
}
}
private class UpdateInlaysTask(private val manager: NotebookCellInlayManager,
pointers: Collection<NotebookIntervalPointer>? = null,
private var updateAll: Boolean = false,
private var forceUpdate: Boolean = false) : Update(Any()) {
private class UpdateInlaysTask(
private val manager: NotebookCellInlayManager,
pointers: Collection<NotebookIntervalPointer>? = null,
private var updateAll: Boolean = false,
private var forceUpdate: Boolean = false,
) : Update(Any()) {
private val pointerSet = pointers?.let { SmartHashSet(pointers) } ?: SmartHashSet()
override fun run() {

View File

@@ -2,36 +2,21 @@ package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.util.Disposer
import com.intellij.util.EventDispatcher
import com.intellij.util.asSafely
import org.jetbrains.plugins.notebooks.visualization.NotebookCellInlayController
import java.awt.Dimension
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JComponent
class ControllerEditorCellViewComponent(
internal val controller: NotebookCellInlayController
) : EditorCellViewComponent {
private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java)
override val location: Point
get() {
val component = controller.inlay.renderer as JComponent
return component.location
}
override val size: Dimension
get() {
val component = controller.inlay.renderer as JComponent
return component.size
}
internal val controller: NotebookCellInlayController,
private val parent: EditorCellInput,
) : EditorCellViewComponent(), HasGutterIcon {
private val listener = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
cellEventListeners.multicaster.componentBoundaryChanged(e.component.location, e.component.size)
parent.invalidate()
}
}
@@ -45,21 +30,17 @@ class ControllerEditorCellViewComponent(
inlay.update()
}
override fun dispose() {
override fun doDispose() {
controller.inlay.renderer.asSafely<JComponent>()?.removeComponentListener(listener)
controller.let { controller -> Disposer.dispose(controller.inlay) }
}
override fun onViewportChange() {
override fun doViewportChange() {
controller.onViewportChange()
}
override fun addViewComponentListener(listener: EditorCellViewComponentListener) {
cellEventListeners.addListener(listener)
}
override fun updatePositions() {
cellEventListeners.multicaster.componentBoundaryChanged(location, size)
override fun calculateBounds(): Rectangle {
val component = controller.inlay.renderer as JComponent
return component.bounds
}
}

View File

@@ -22,7 +22,7 @@ import javax.swing.JLayer
import javax.swing.SwingUtilities
import javax.swing.plaf.LayerUI
private class DecoratedEditor(private val original: TextEditor) : TextEditor by original {
private class DecoratedEditor(private val original: TextEditor, private val manager: NotebookCellInlayManager) : TextEditor by original {
private var mouseOverCell: EditorCellView? = null
@@ -64,6 +64,7 @@ private class DecoratedEditor(private val original: TextEditor) : TextEditor by
override fun validateTree() {
keepScrollingPositionWhile(editor) {
manager.validateCells()
super.validateTree()
}
}
@@ -118,12 +119,12 @@ private class DecoratedEditor(private val original: TextEditor) : TextEditor by
})
private fun updateMouseOverCell(component: JComponent, point: Point) {
val cells = NotebookCellInlayManager.get(editor)!!.cells
val cells = manager.cells
val currentOverCell = cells.filter { it.visible }.mapNotNull { it.view }.firstOrNull {
val viewLeft = 0
val viewTop = it.location.y
val viewTop = it.bounds.y
val viewRight = component.size.width
val viewBottom = viewTop + it.size.height
val viewBottom = viewTop + it.bounds.height
viewLeft <= point.x && viewTop <= point.y && viewRight >= point.x && viewBottom >= point.y
}
if (mouseOverCell != currentOverCell) {
@@ -135,8 +136,8 @@ private class DecoratedEditor(private val original: TextEditor) : TextEditor by
}
fun decorateTextEditor(textEditor: TextEditor): TextEditor {
return DecoratedEditor(textEditor)
fun decorateTextEditor(textEditor: TextEditor, manager: NotebookCellInlayManager): TextEditor {
return DecoratedEditor(textEditor, manager)
}
internal fun keepScrollingPositionWhile(editor: Editor, task: Runnable) {

View File

@@ -68,10 +68,6 @@ class EditorCell(
}
}
fun updatePositions() {
view?.updatePositions()
}
override fun dispose() {
view?.let { Disposer.dispose(it) }
}

View File

@@ -4,20 +4,15 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.editor.FoldRegion
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.util.TextRange
import com.intellij.util.EventDispatcher
import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance
import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines
import java.awt.Dimension
import java.awt.Point
import java.awt.Rectangle
class EditorCellInput(
private val editor: EditorEx,
private val componentFactory: (EditorCellViewComponent?) -> EditorCellViewComponent,
private val cell: EditorCell
) {
private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java)
private val componentFactory: (EditorCellInput, EditorCellViewComponent?) -> EditorCellViewComponent,
private val cell: EditorCell,
): EditorCellViewComponent() {
val interval: NotebookCellLines.Interval
get() = cell.intervalPointer.get() ?: error("Invalid interval")
@@ -33,60 +28,31 @@ class EditorCellInput(
else -> editor.notebookAppearance.cellBorderHeight / 2
}
val bounds: Rectangle
get() {
val linesRange = interval.lines
val startOffset = editor.document.getLineStartOffset(linesRange.first)
val endOffset = editor.document.getLineEndOffset(linesRange.last)
val bounds = editor.inlayModel.getBlockElementsInRange(startOffset, endOffset)
.asSequence()
.filter { it.properties.priority > editor.notebookAppearance.NOTEBOOK_OUTPUT_INLAY_PRIORITY }
.mapNotNull { it.bounds }
.fold(Rectangle(_component.location, _component.size)) { b, i ->
b.union(i)
}
return bounds
}
private var _component: EditorCellViewComponent = componentFactory(null).also { bind(it) }
private var _component: EditorCellViewComponent = componentFactory(this, null)
set(value) {
if (value != field) {
field.dispose()
remove(field)
field = value
bind(value)
add(value)
}
}
private fun bind(value: EditorCellViewComponent) {
value.addViewComponentListener(object : EditorCellViewComponentListener {
override fun componentBoundaryChanged(location: Point, size: Dimension) {
cellEventListeners.multicaster.componentBoundaryChanged(bounds.location, bounds.size)
}
})
}
val component: EditorCellViewComponent
get() = _component
private val folding: EditorCellFolding = EditorCellFolding(editor) {
toggleFolding(componentFactory)
}.also {
cellEventListeners.addListener(object : EditorCellViewComponentListener {
override fun componentBoundaryChanged(location: Point, size: Dimension) {
it.updatePosition(location.y + delimiterPanelSize, size.height - delimiterPanelSize)
}
})
}
private fun toggleFolding(inputComponentFactory: (EditorCellViewComponent) -> EditorCellViewComponent) {
private fun toggleFolding(inputComponentFactory: (EditorCellInput, EditorCellViewComponent) -> EditorCellViewComponent) {
_component = if (_component is ControllerEditorCellViewComponent) {
_component.dispose()
toggleTextFolding()
TextEditorCellViewComponent(editor, cell)
}
else {
toggleTextFolding()
inputComponentFactory(_component)
inputComponentFactory(this, _component)
}
}
@@ -116,7 +82,7 @@ class EditorCellInput(
private var gutterAction: AnAction? = null
fun dispose() {
override fun doDispose() {
folding.dispose()
runCellButton?.dispose()
_component.dispose()
@@ -124,20 +90,16 @@ class EditorCellInput(
fun update(force: Boolean = false) {
val oldComponent = if (force) null else _component
_component = componentFactory(oldComponent)
_component = componentFactory(this, oldComponent)
updateGutterIcons()
}
private fun updateGutterIcons() {
_component.updateGutterIcons(gutterAction)
(_component as? HasGutterIcon)?.updateGutterIcons(gutterAction)
}
fun updatePositions() {
_component.updatePositions()
}
fun onViewportChange() {
_component.onViewportChange()
override fun doLayout() {
folding.updatePosition(bounds.y + delimiterPanelSize, bounds.height - delimiterPanelSize)
}
fun setGutterAction(action: AnAction) {
@@ -156,32 +118,44 @@ class EditorCellInput(
fun showRunButton() {
try {
runCellButton?.showRunButton(interval)
} catch (e: IllegalStateException) { return }
}
catch (e: IllegalStateException) {
return
}
}
fun hideRunButton() {
runCellButton?.hideRunButton()
}
fun addViewComponentListener(listener: EditorCellViewComponentListener) {
cellEventListeners.addListener(listener)
}
fun updatePresentation(view: EditorCellViewComponent) {
_component.dispose()
_component = view
}
fun updateSelection(value: Boolean) {
folding.updateSelection(value)
}
override fun calculateBounds(): Rectangle {
val linesRange = interval.lines
val startOffset = editor.document.getLineStartOffset(linesRange.first)
val endOffset = editor.document.getLineEndOffset(linesRange.last)
val bounds = editor.inlayModel.getBlockElementsInRange(startOffset, endOffset)
.asSequence()
.filter { it.properties.priority > editor.notebookAppearance.NOTEBOOK_OUTPUT_INLAY_PRIORITY }
.mapNotNull { it.bounds }
.fold(_component.calculateBounds()) { b, i ->
b.union(i)
}
return bounds
}
}
private fun String.ellipsis(length: Int): String {
return if (this.length > length) {
substring(0, length - 1)
}
else {
else {
this
} + "\u2026"
}

View File

@@ -19,12 +19,12 @@ import javax.swing.SwingUtilities
val NOTEBOOK_CELL_OUTPUT_DATA_KEY = DataKey.create<EditorCellOutput>("NOTEBOOK_CELL_OUTPUT")
class EditorCellOutput internal constructor(private val editor: EditorEx, private val component: CollapsingComponent, private val disposable: Disposable?) {
class EditorCellOutput internal constructor(
private val editor: EditorEx,
private val component: CollapsingComponent,
private val disposable: Disposable?,
) : EditorCellViewComponent() {
val location: Point
get() = SwingUtilities.convertPoint(component.parent, component.location, editor.contentComponent)
val size: Dimension
get() = component.size
var collapsed: Boolean
get() = !component.isSeen
set(value) {
@@ -38,11 +38,11 @@ class EditorCellOutput internal constructor(private val editor: EditorEx, privat
.also {
component.addComponentListener(object : ComponentAdapter() {
override fun componentMoved(e: ComponentEvent) {
updatePositions()
invalidate()
}
override fun componentResized(e: ComponentEvent) {
updatePositions()
invalidate()
}
})
}
@@ -58,21 +58,17 @@ class EditorCellOutput internal constructor(private val editor: EditorEx, privat
}
}
fun updatePositions() {
folding.updatePosition(location.y, size.height)
}
fun dispose() {
override fun doDispose() {
folding.dispose()
disposable?.let { Disposer.dispose(it) }
}
fun onViewportChange() {
val component = component.mainComponent as? NotebookOutputInlayShowable ?: return
if (component !is JComponent) return
validateComponent(component)
val componentRect = SwingUtilities.convertRectangle(component, component.bounds, editor.scrollPane.viewport.view)
component.shown = editor.scrollPane.viewport.viewRect.intersects(componentRect)
override fun doViewportChange() {
val component = component.mainComponent as? NotebookOutputInlayShowable ?: return
if (component !is JComponent) return
validateComponent(component)
val componentRect = SwingUtilities.convertRectangle(component, component.bounds, editor.scrollPane.viewport.view)
component.shown = editor.scrollPane.viewport.viewRect.intersects(componentRect)
}
fun hideFolding() {
@@ -100,4 +96,14 @@ class EditorCellOutput internal constructor(private val editor: EditorEx, privat
}
}
}
override fun calculateBounds(): Rectangle {
val location = SwingUtilities.convertPoint(component.parent, component.location, editor.contentComponent)
val size = component.size
return Rectangle(location, size)
}
override fun doLayout() {
folding.updatePosition(bounds.y, bounds.height)
}
}

View File

@@ -8,7 +8,6 @@ import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.util.Disposer
import com.intellij.util.EventDispatcher
import com.intellij.util.asSafely
import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance
import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines
@@ -30,12 +29,8 @@ import javax.swing.JComponent
class EditorCellOutputs(
private val editor: EditorImpl,
private val interval: () -> NotebookCellLines.Interval,
private val onInlayDisposed: (EditorCellOutputs) -> Unit = {}
) : Disposable {
private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java)
private val onInlayDisposed: (EditorCellOutputs) -> Unit = {},
) : EditorCellViewComponent(), Disposable {
private val _outputs = mutableListOf<EditorCellOutput>()
val outputs
@@ -55,30 +50,17 @@ class EditorCellOutputs(
}
private var inlay: Inlay<*>? = null
val bounds: Rectangle?
get() {
return inlay?.bounds
}
init {
update()
}
override fun dispose() {
override fun doDispose() {
outputs.forEach { it.dispose() }
inlay?.let { Disposer.dispose(it) }
}
fun updatePositions() {
val b = bounds
if (b != null) {
cellEventListeners.multicaster.componentBoundaryChanged(b.location, b.size)
outputs.forEach { it.updatePositions() }
}
}
fun onViewportChange() {
outputs.forEach { it.onViewportChange() }
override fun calculateBounds(): Rectangle {
return inlay?.bounds ?: Rectangle(0, 0, 0, 0)
}
fun updateSelection(selected: Boolean) {
@@ -172,7 +154,9 @@ class EditorCellOutputs(
private fun removeOutput(idx: Int) {
innerComponent.remove(idx)
_outputs.removeAt(idx).dispose()
val outputComponent = _outputs.removeAt(idx)
outputComponent.dispose()
remove(outputComponent)
}
private fun <K : NotebookOutputDataKey> createOutputGuessingFactory(outputDataKey: K): NotebookOutputComponentFactory.CreatedComponent<*>? =
@@ -185,8 +169,10 @@ class EditorCellOutputs(
}
.firstOrNull()
private fun <K : NotebookOutputDataKey> createOutput(factory: NotebookOutputComponentFactory<*, K>,
outputDataKey: K): NotebookOutputComponentFactory.CreatedComponent<*>? {
private fun <K : NotebookOutputDataKey> createOutput(
factory: NotebookOutputComponentFactory<*, K>,
outputDataKey: K,
): NotebookOutputComponentFactory.CreatedComponent<*>? {
val lines = interval().lines
ApplicationManager.getApplication().messageBus.syncPublisher(OUTPUT_LISTENER).beforeOutputCreated(editor, lines.last)
val result = try {
@@ -238,7 +224,7 @@ class EditorCellOutputs(
).also {
it.renderer.asSafely<JComponent>()?.addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
cellEventListeners.multicaster.componentBoundaryChanged(e.component.location, e.component.size)
invalidate()
}
})
Disposer.register(it) {
@@ -263,7 +249,9 @@ class EditorCellOutputs(
pos,
)
_outputs.add(if (pos == -1) _outputs.size else pos, EditorCellOutput(editor, collapsingComponent, newComponent.disposable))
val outputComponent = EditorCellOutput(editor, collapsingComponent, newComponent.disposable)
_outputs.add(if (pos == -1) _outputs.size else pos, outputComponent)
add(outputComponent)
// DS-1972 Without revalidation, the component would be just invalidated, and would be rendered only after anything else requests
// for repainting the editor.
@@ -275,9 +263,11 @@ class EditorCellOutputs(
override fun next(): Pair<A, B> = this@zip.next() to other.next()
}
fun paintGutter(editor: EditorImpl,
g: Graphics,
r: Rectangle) {
fun paintGutter(
editor: EditorImpl,
g: Graphics,
r: Rectangle,
) {
val yOffset = innerComponent.yOffsetFromEditor(editor) ?: return
val oldClip = g.clipBounds

View File

@@ -36,8 +36,9 @@ import kotlin.reflect.KClass
class EditorCellView(
private val editor: EditorImpl,
private val intervals: NotebookCellLines,
internal var cell: EditorCell
) : Disposable {
internal var cell: EditorCell,
private val cellInlayManager: NotebookCellInlayManager,
) : EditorCellViewComponent(), Disposable {
private var _controllers: List<NotebookCellInlayController> = emptyList()
private val controllers: List<NotebookCellInlayController>
@@ -54,7 +55,7 @@ class EditorCellView(
val input: EditorCellInput = EditorCellInput(
editor,
{ currentComponent: EditorCellViewComponent? ->
{ parent: EditorCellInput, currentComponent: EditorCellViewComponent? ->
val currentController = (currentComponent as? ControllerEditorCellViewComponent)?.controller
val controller = getInputFactories().firstNotNullOfOrNull { factory ->
failSafeCompute(factory, editor, currentController?.let { listOf(it) }
@@ -65,27 +66,16 @@ class EditorCellView(
currentComponent
}
else {
ControllerEditorCellViewComponent(controller)
ControllerEditorCellViewComponent(controller, parent)
}
}
else {
TextEditorCellViewComponent(editor, cell)
}
}, cell).also {
it.addViewComponentListener(object : EditorCellViewComponentListener {
override fun componentBoundaryChanged(location: Point, size: Dimension) {
updateBoundaries()
}
})
}
private var _location: Point = Point(0, 0)
val location: Point get() = _location
private var _size: Dimension = Dimension(0, 0)
val size: Dimension get() = _size
}, cell)
.also {
add(it)
}
private var _outputs: EditorCellOutputs? = null
@@ -101,23 +91,12 @@ class EditorCellView(
updateSelection(false)
}
private fun updateBoundaries() {
val inputBounds = input.bounds
val y = inputBounds.y
_location = Point(0, y)
val currentOutputs = outputs
_size = Dimension(
editor.contentSize.width,
currentOutputs?.bounds?.let { it.height + it.y - y } ?: inputBounds.height
)
}
override fun dispose() {
override fun doDispose() {
_controllers.forEach { controller ->
disposeController(controller)
}
input.dispose()
outputs?.dispose()
removeCellHighlight()
}
@@ -128,13 +107,8 @@ class EditorCellView(
}
fun update(force: Boolean = false) {
extracted(force)
}
private fun extracted(force: Boolean) {
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)) }
@@ -157,14 +131,18 @@ class EditorCellView(
}
input.update(force)
updateOutputs()
updateBoundaries()
updateCellHighlight()
invalidate()
}
private fun updateOutputs() {
if (hasOutputs()) {
if (_outputs == null) {
_outputs = EditorCellOutputs(editor, { interval }).also { Disposer.register(this, it) }
_outputs = EditorCellOutputs(editor, { interval })
.also {
Disposer.register(this, it)
add(it)
}
updateCellHighlight()
updateFolding()
}
@@ -173,7 +151,10 @@ class EditorCellView(
}
}
else {
outputs?.let { Disposer.dispose(it) }
outputs?.let {
Disposer.dispose(it)
remove(it)
}
_outputs = null
}
}
@@ -186,10 +167,12 @@ class EditorCellView(
.filter { it is NotebookCellInlayController.InputFactory }
}
private fun failSafeCompute(factory: NotebookCellInlayController.Factory,
editor: Editor,
controllers: Collection<NotebookCellInlayController>,
intervalIterator: ListIterator<NotebookCellLines.Interval>): NotebookCellInlayController? {
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)
}
@@ -199,11 +182,6 @@ class EditorCellView(
}
}
fun updatePositions() {
input.updatePositions()
outputs?.updatePositions()
}
fun onViewportChanges() {
input.onViewportChange()
outputs?.onViewportChange()
@@ -362,6 +340,24 @@ class EditorCellView(
}
}
override fun doInvalidate() {
cellInlayManager.invalidateCells()
}
override fun calculateBounds(): Rectangle {
val inputBounds = input.calculateBounds()
val currentOutputs = outputs
return Rectangle(
0,
inputBounds.y,
editor.contentSize.width,
currentOutputs?.calculateBounds()
?.takeIf { !it.isEmpty }
?.let { it.height + it.y - inputBounds.y }
?: inputBounds.height
)
}
inner class NotebookGutterLineMarkerRenderer(private val interval: NotebookCellLines.Interval) : NotebookLineMarkerRenderer() {
override fun paint(editor: Editor, g: Graphics, r: Rectangle) {
editor as EditorImpl
@@ -381,10 +377,12 @@ class EditorCellView(
}
}
private fun paintBackground(editor: EditorImpl,
g: Graphics,
r: Rectangle,
interval: NotebookCellLines.Interval) {
private fun paintBackground(
editor: EditorImpl,
g: Graphics,
r: Rectangle,
interval: NotebookCellLines.Interval,
) {
for (controller: NotebookCellInlayController in controllers) {
controller.paintGutter(editor, g, r, interval)
}

View File

@@ -1,16 +1,64 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.actionSystem.AnAction
import java.awt.Dimension
import java.awt.Point
import java.awt.Rectangle
interface EditorCellViewComponent {
val size: Dimension
val location: Point
abstract class EditorCellViewComponent {
fun updateGutterIcons(gutterAction: AnAction?)
fun dispose()
fun onViewportChange()
fun addViewComponentListener(listener: EditorCellViewComponentListener)
fun updatePositions()
var bounds: Rectangle = Rectangle(0, 0, 0, 0)
private var parent: EditorCellViewComponent? = null
private val children = mutableListOf<EditorCellViewComponent>()
private var valid = false
fun add(child: EditorCellViewComponent) {
children.add(child)
child.parent = this
}
fun remove(child: EditorCellViewComponent) {
children.remove(child)
child.parent = null
}
fun dispose() {
children.forEach { it.dispose() }
doDispose()
}
open fun doDispose() {}
fun onViewportChange() {
children.forEach { it.onViewportChange() }
doViewportChange()
}
open fun doViewportChange() {}
abstract fun calculateBounds(): Rectangle
fun validate() {
if (!valid) {
doLayout()
children.forEach { it.validate() }
valid = true
}
}
open fun doLayout() {
children.forEach {
it.bounds = it.calculateBounds()
}
}
fun invalidate() {
if (valid) {
doInvalidate()
valid = false
parent?.invalidate()
}
}
open fun doInvalidate() {}
}

View File

@@ -1,11 +0,0 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import java.awt.Dimension
import java.awt.Point
import java.util.EventListener
interface EditorCellViewComponentListener : EventListener {
fun componentBoundaryChanged(location: Point, size: Dimension) {}
}

View File

@@ -0,0 +1,8 @@
package org.jetbrains.plugins.notebooks.visualization.ui
import com.intellij.openapi.actionSystem.AnAction
interface HasGutterIcon {
fun updateGutterIcons(gutterAction: AnAction?)
}

View File

@@ -6,39 +6,20 @@ import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.EventDispatcher
import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines
import java.awt.Dimension
import java.awt.Point
import java.awt.Rectangle
class TextEditorCellViewComponent(
private val editor: EditorEx,
private val cell: EditorCell
) : EditorCellViewComponent {
private val cell: EditorCell,
) : EditorCellViewComponent(), HasGutterIcon {
private var highlighters: List<RangeHighlighter>? = null
private val interval: NotebookCellLines.Interval
get() = cell.intervalPointer.get() ?: error("Invalid interval")
private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java)
override val location: Point
get() {
val startOffset = editor.document.getLineStartOffset(interval.lines.first)
return editor.offsetToXY(startOffset)
}
override val size: Dimension
get() {
val interval = interval
val startOffset = editor.document.getLineStartOffset(interval.lines.first)
val endOffset = editor.document.getLineEndOffset(interval.lines.last)
val location = editor.offsetToXY(startOffset)
val height = editor.offsetToXY(endOffset).y + editor.lineHeight - location.y
val width = editor.offsetToXY(endOffset).x - location.x
return Dimension(width, height)
}
override fun updateGutterIcons(gutterAction: AnAction?) {
disposeExistingHighlighter()
val action = gutterAction
@@ -59,13 +40,10 @@ class TextEditorCellViewComponent(
}
}
override fun dispose() {
override fun doDispose() {
disposeExistingHighlighter()
}
override fun onViewportChange() {
}
private fun disposeExistingHighlighter() {
if (highlighters != null) {
highlighters?.forEach {
@@ -75,11 +53,15 @@ class TextEditorCellViewComponent(
}
}
override fun addViewComponentListener(listener: EditorCellViewComponentListener) {
cellEventListeners.addListener(listener)
override fun calculateBounds(): Rectangle {
val startOffset = editor.document.getLineStartOffset(interval.lines.first)
val location = editor.offsetToXY(startOffset)
val interval = interval
val endOffset = editor.document.getLineEndOffset(interval.lines.last)
val height = editor.offsetToXY(endOffset).y + editor.lineHeight - location.y
val width = editor.offsetToXY(endOffset).x - location.x
val dimension = Dimension(width, height)
return Rectangle(location, dimension)
}
override fun updatePositions() {
cellEventListeners.multicaster.componentBoundaryChanged(location, size)
}
}