[DS-3445] refactor NotebookIntervalPointerFactoryImpl and interface, add tests

Merge-request: IJ-MR-95153
Merged-by: Igor Slobodskov <Igor.Slobodskov@jetbrains.com>

GitOrigin-RevId: 278dd256b4c8cb0d205fb1527e676b0cd375df53
This commit is contained in:
Igor Slobodskov
2022-09-09 19:56:25 +00:00
committed by intellij-monorepo-bot
parent df9885fe22
commit 4aaad8916f
7 changed files with 298 additions and 407 deletions

View File

@@ -75,6 +75,14 @@ class NonIncrementalCellLines private constructor(private val document: Document
override fun beforeDocumentChange(event: DocumentEvent) {
oldAffectedCells = getAffectedCells(intervals, document, TextRange(event.offset, event.offset + event.oldLength))
intervalListeners.multicaster.beforeDocumentChange(
NotebookCellLinesEventBeforeChange(
documentEvent = event,
oldAffectedIntervals = oldAffectedCells,
modificationStamp = modificationStamp
)
)
}
override fun documentChanged(event: DocumentEvent) {

View File

@@ -55,6 +55,8 @@ interface NotebookCellLines {
* Components which work with intervals can simply listen for NotebookCellLinesEvent and don't subscribe for DocumentEvent.
*/
fun documentChanged(event: NotebookCellLinesEvent)
fun beforeDocumentChange(event: NotebookCellLinesEventBeforeChange) {}
}
fun intervalsIterator(startLine: Int = 0): ListIterator<Interval>

View File

@@ -47,4 +47,14 @@ data class NotebookCellLinesEvent(
fun isIntervalsChanged(): Boolean =
!(oldIntervals.isEmpty() && newIntervals.isEmpty())
}
}
/**
* Passed to [NotebookCellLines.IntervalListener] before document change.
* [modificationStamp] is old, version before change
*/
data class NotebookCellLinesEventBeforeChange(
val documentEvent: DocumentEvent,
val oldAffectedIntervals: List<NotebookCellLines.Interval>,
val modificationStamp: Long,
)

View File

@@ -6,54 +6,32 @@ import com.intellij.util.EventDispatcher
import java.util.*
/**
* Pointer becomes invalid when code cell is removed
* Invalid pointer returns null
* Pointer becomes invalid when code cell is removed.
* It may become valid again when action is undone or redone.
* Invalid pointer returns null.
*/
interface NotebookIntervalPointer {
/** should be called in read-action */
fun get(): NotebookCellLines.Interval?
}
private val key = Key.create<NotebookIntervalPointerFactory>(NotebookIntervalPointerFactory::class.java.name)
interface NotebookIntervalPointerFactory {
/** interval should be valid, return pointer to it */
/**
* Interval should be valid, return pointer to it.
* Should be called in read-action.
*/
fun create(interval: NotebookCellLines.Interval): NotebookIntervalPointer
/**
* if action changes document, hints applied instantly at document change
* if doesn't - hints applied after action
* hint should contain pointers created by this factory
* Can be called only inside write-action.
* Undo and redo will be added automatically.
*/
fun <T> modifyingPointers(changes: Iterable<Change>, modifyDocumentAction: () -> T): T
fun invalidateCell(cell: NotebookCellLines.Interval) {
modifyingPointers(listOf(Invalidate(create(cell)))) {}
}
fun swapCells(swapped: List<Swap>) {
modifyingPointers(swapped) {}
}
fun modifyPointers(changes: Iterable<Change>)
interface ChangeListener : EventListener {
fun onUpdated(event: NotebookIntervalPointersEvent) {
for(change in event.changes) {
when (change) {
is NotebookIntervalPointersEvent.OnEdited -> onEdited(change.ordinal)
is NotebookIntervalPointersEvent.OnInserted -> onInserted(change.ordinals)
is NotebookIntervalPointersEvent.OnRemoved -> onRemoved(change.ordinals)
is NotebookIntervalPointersEvent.OnSwapped -> onSwapped(change.firstOrdinal, change.secondOrdinal)
}
}
}
fun onInserted(ordinals: IntRange)
fun onEdited(ordinal: Int)
fun onRemoved(ordinals: IntRange)
/** [firstOrdinal] and [secondOrdinal] are never equal */
fun onSwapped(firstOrdinal: Int, secondOrdinal: Int)
fun onUpdated(event: NotebookIntervalPointersEvent)
}
val changeListeners: EventDispatcher<ChangeListener>
@@ -74,29 +52,9 @@ interface NotebookIntervalPointerFactory {
sealed interface Change
/** invalidate pointer, create new pointer for interval if necessary */
data class Invalidate(val ptr: NotebookIntervalPointer) : Change
/** invalidate pointer to interval, create new pointer */
data class Invalidate(val interval: NotebookCellLines.Interval) : Change
/** swap two pointers */
data class Swap(val firstOrdinal: Int, val secondOrdinal: Int) : Change
}
fun <T> NotebookIntervalPointerFactory?.invalidatingCell(cell: NotebookCellLines.Interval, action: () -> T): T =
if (this == null) {
action()
}
else {
modifyingPointers(listOf(NotebookIntervalPointerFactory.Invalidate(create(cell)))) {
action()
}
}
fun <T> NotebookIntervalPointerFactory?.swappingCells(swapedCells: List<NotebookIntervalPointerFactory.Swap>, action: () -> T): T =
if (this == null) {
action()
}
else {
modifyingPointers(swapedCells) {
action()
}
}

View File

@@ -1,30 +1,56 @@
package org.jetbrains.plugins.notebooks.visualization
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.undo.BasicUndoableAction
import com.intellij.openapi.command.undo.DocumentReference
import com.intellij.openapi.command.undo.DocumentReferenceManager
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Editor
import com.intellij.util.EventDispatcher
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointersEvent.OnEdited
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointersEvent.OnInserted
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointersEvent.OnRemoved
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointersEvent.OnSwapped
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointersEvent.Change
import org.jetbrains.annotations.TestOnly
import org.jetbrains.plugins.notebooks.visualization.NotebookIntervalPointersEvent.*
class NotebookIntervalPointerFactoryImplProvider : NotebookIntervalPointerFactoryProvider {
override fun create(editor: Editor): NotebookIntervalPointerFactory =
NotebookIntervalPointerFactoryImpl(NotebookCellLines.get(editor))
NotebookIntervalPointerFactoryImpl(NotebookCellLines.get(editor),
DocumentReferenceManager.getInstance().create(editor.document),
editor.project?.let(UndoManager::getInstance))
}
private class NotebookIntervalPointerImpl(var interval: NotebookCellLines.Interval?) : NotebookIntervalPointer {
override fun get(): NotebookCellLines.Interval? = interval
override fun get(): NotebookCellLines.Interval? {
ApplicationManager.getApplication().assertReadAccessAllowed()
return interval
}
override fun toString(): String = "NotebookIntervalPointerImpl($interval)"
}
class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: NotebookCellLines) : NotebookIntervalPointerFactory, NotebookCellLines.IntervalListener {
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.
*
* During undo or redo operations old intervals are restored.
* For example, you can save pointer anywhere, remove interval, undo removal and pointer instance will contain interval again.
* You can store interval-related data into WeakHashMap<NotebookIntervalPointer, Data> and this data will outlive undo/redo actions.
*/
class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: NotebookCellLines,
private val documentReference: DocumentReference,
private val undoManager: UndoManager?) : NotebookIntervalPointerFactory, NotebookCellLines.IntervalListener {
private val pointers = ArrayList<NotebookIntervalPointerImpl>()
private var mySavedChanges: Iterable<NotebookIntervalPointerFactory.Change>? = null
private var changesContext: ChangesContext? = null
override val changeListeners: EventDispatcher<NotebookIntervalPointerFactory.ChangeListener> =
EventDispatcher.create(NotebookIntervalPointerFactory.ChangeListener::class.java)
@@ -33,114 +59,221 @@ class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: Notebook
notebookCellLines.intervalListeners.addListener(this)
}
override fun create(interval: NotebookCellLines.Interval): NotebookIntervalPointer =
pointers[interval.ordinal].also {
override fun create(interval: NotebookCellLines.Interval): NotebookIntervalPointer {
ApplicationManager.getApplication().assertReadAccessAllowed()
return pointers[interval.ordinal].also {
require(it.interval == interval)
}
}
override fun <T> modifyingPointers(changes: Iterable<NotebookIntervalPointerFactory.Change>, modifyDocumentAction: () -> T): T {
try {
require(mySavedChanges == null) { "NotebookIntervalPointerFactory hints already added somewhere" }
mySavedChanges = changes
return modifyDocumentAction().also {
mySavedChanges?.let {
val eventBuilder = NotebookIntervalPointersEventBuilder()
applyChanges(it, eventBuilder)
eventBuilder.applyEvent(changeListeners, cellLinesEvent = null)
}
override fun modifyPointers(changes: Iterable<NotebookIntervalPointerFactory.Change>) {
ApplicationManager.getApplication()?.assertWriteAccessAllowed()
val eventChanges = NotebookIntervalPointersEventChanges()
applyChanges(changes, eventChanges)
val pointerEvent = NotebookIntervalPointersEvent(eventChanges, cellLinesEvent = null, EventSource.ACTION)
undoManager?.undoableActionPerformed(object : BasicUndoableAction(documentReference) {
override fun undo() {
val invertedChanges = invertChanges(eventChanges)
updatePointersByChanges(invertedChanges)
changeListeners.multicaster.onUpdated(
NotebookIntervalPointersEvent(invertedChanges, cellLinesEvent = null, EventSource.UNDO_ACTION))
}
}
finally {
mySavedChanges = null
}
override fun redo() {
updatePointersByChanges(eventChanges)
changeListeners.multicaster.onUpdated(
NotebookIntervalPointersEvent(eventChanges, cellLinesEvent = null, EventSource.REDO_ACTION))
}
})
changeListeners.multicaster.onUpdated(pointerEvent)
}
override fun documentChanged(event: NotebookCellLinesEvent) {
val eventBuilder = NotebookIntervalPointersEventBuilder()
updateIntervals(event, eventBuilder)
mySavedChanges?.let {
applyChanges(it, eventBuilder)
mySavedChanges = null
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
}
changeListeners.multicaster.onUpdated(pointersEvent)
}
finally {
changesContext = null
}
eventBuilder.applyEvent(changeListeners, event)
}
private fun updateIntervals(e: NotebookCellLinesEvent, eventBuilder: NotebookIntervalPointersEventBuilder) {
override fun beforeDocumentChange(event: NotebookCellLinesEventBeforeChange) {
if (undoManager == null || undoManager.isUndoOrRedoInProgress) return
val context = DocumentChangedContext()
changesContext = context
undoManager.undoableActionPerformed(object : BasicUndoableAction() {
override fun undo() {}
override fun redo() {
changesContext = context.redoContext
}
})
}
private fun documentChangedByAction(event: NotebookCellLinesEvent,
documentChangedContext: DocumentChangedContext?): NotebookIntervalPointersEvent {
val eventChanges = NotebookIntervalPointersEventChanges()
updateChangedIntervals(event, eventChanges)
updateShiftedIntervals(event)
undoManager?.undoableActionPerformed(object : BasicUndoableAction(documentReference) {
override fun undo() {
changesContext = UndoContext(eventChanges)
}
override fun redo() {}
})
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)
}
private fun updatePointersByChanges(changes: List<Change>) {
for (change in changes) {
when (change) {
is OnEdited -> (change.pointer as NotebookIntervalPointerImpl).interval = change.intervalAfter
is OnInserted -> {
for (p in change.subsequentPointers) {
(p.pointer as NotebookIntervalPointerImpl).interval = p.interval
}
pointers.addAll(change.ordinals.first, change.subsequentPointers.map { it.pointer as NotebookIntervalPointerImpl })
}
is OnRemoved -> {
for (p in change.subsequentPointers.asReversed()) {
pointers.removeAt(p.interval.ordinal)
(p.pointer as NotebookIntervalPointerImpl).interval = null
}
}
is OnSwapped -> {
trySwapPointers(null, NotebookIntervalPointerFactory.Swap(change.firstOrdinal, change.secondOrdinal))
}
}
}
}
private fun makeSnapshot(interval: NotebookCellLines.Interval) =
PointerSnapshot(pointers[interval.ordinal], interval)
private fun updateChangedIntervals(e: NotebookCellLinesEvent, eventChanges: NotebookIntervalPointersEventChanges) {
when {
!e.isIntervalsChanged() -> {
// content edited without affecting intervals values
eventBuilder.onEdited((e.oldAffectedIntervals + e.newAffectedIntervals).distinct().sortedBy { it.ordinal })
for (editedInterval in LinkedHashSet(e.oldAffectedIntervals) + e.newAffectedIntervals) {
eventChanges.add(OnEdited(pointers[editedInterval.ordinal], editedInterval, editedInterval))
}
}
e.oldIntervals.size == 1 && e.newIntervals.size == 1 && e.oldIntervals.first().type == e.newIntervals.first().type -> {
// only one interval changed size
pointers[e.newIntervals.first().ordinal].interval = e.newIntervals.first()
eventBuilder.onEdited(e.newAffectedIntervals)
if (e.newIntervals.first() !in e.newAffectedIntervals) {
eventBuilder.onEdited(e.newIntervals.first())
for (editedInterval in e.newAffectedIntervals) {
val ptr = pointers[editedInterval.ordinal]
eventChanges.add(OnEdited(ptr, ptr.interval!!, editedInterval))
}
if (e.newIntervals.first() !in e.newAffectedIntervals) {
val ptr = pointers[e.newIntervals.first().ordinal]
eventChanges.add(OnEdited(ptr, ptr.interval!!, e.newIntervals.first()))
}
pointers[e.newIntervals.first().ordinal].interval = e.newIntervals.first()
}
else -> {
for (old in e.oldIntervals.asReversed()) {
pointers[old.ordinal].interval = null
pointers.removeAt(old.ordinal)
}
eventBuilder.onRemoved(e.oldIntervals)
if (e.oldIntervals.isNotEmpty()) {
eventChanges.add(OnRemoved(e.oldIntervals.map(::makeSnapshot)))
e.newIntervals.firstOrNull()?.also { firstNew ->
pointers.addAll(firstNew.ordinal, e.newIntervals.map { NotebookIntervalPointerImpl(it) })
for (old in e.oldIntervals.asReversed()) {
pointers[old.ordinal].interval = null
pointers.removeAt(old.ordinal)
}
}
if (e.newIntervals.isNotEmpty()) {
pointers.addAll(e.newIntervals.first().ordinal, e.newIntervals.map { NotebookIntervalPointerImpl(it) })
eventChanges.add(OnInserted(e.newIntervals.map(::makeSnapshot)))
}
for (interval in e.newAffectedIntervals - e.newIntervals.toSet()) {
val ptr = pointers[interval.ordinal]
eventChanges.add(OnEdited(ptr, ptr.interval!!, interval))
}
eventBuilder.onInserted(e.newIntervals)
eventBuilder.onEdited(e.newAffectedIntervals, excluded = e.newIntervals)
}
}
}
private fun updateShiftedIntervals(event: NotebookCellLinesEvent) {
val invalidPointersStart =
e.newIntervals.firstOrNull()?.let { it.ordinal + e.newIntervals.size }
?: e.oldIntervals.firstOrNull()?.ordinal
event.newIntervals.firstOrNull()?.let { it.ordinal + event.newIntervals.size }
?: event.oldIntervals.firstOrNull()?.ordinal
?: pointers.size
updatePointersFrom(invalidPointersStart)
for (i in invalidPointersStart until pointers.size) {
pointers[i].interval = notebookCellLines.intervals[i]
}
}
private fun applyChanges(changes: Iterable<NotebookIntervalPointerFactory.Change>, eventBuilder: NotebookIntervalPointersEventBuilder) {
for(hint in changes) {
private fun applyChanges(changes: Iterable<NotebookIntervalPointerFactory.Change>, eventChanges: NotebookIntervalPointersEventChanges){
for (hint in changes) {
when (hint) {
is NotebookIntervalPointerFactory.Invalidate -> applyChange(eventBuilder, hint)
is NotebookIntervalPointerFactory.Swap -> applyChange(eventBuilder, hint)
is NotebookIntervalPointerFactory.Invalidate -> {
val ptr = create(hint.interval) as NotebookIntervalPointerImpl
invalidatePointer(eventChanges, ptr)
}
is NotebookIntervalPointerFactory.Swap ->
trySwapPointers(eventChanges, hint)
}
}
}
private fun applyChange(eventBuilder: NotebookIntervalPointersEventBuilder,
hint: NotebookIntervalPointerFactory.Invalidate) {
val ptr = hint.ptr as NotebookIntervalPointerImpl
private fun invalidatePointer(eventChanges: NotebookIntervalPointersEventChanges,
ptr: NotebookIntervalPointerImpl) {
val interval = ptr.interval
if (interval == null) return
invalidate(ptr)
eventBuilder.onRemoved(interval)
eventBuilder.onInserted(interval)
val newPtr = NotebookIntervalPointerImpl(interval)
pointers[interval.ordinal] = newPtr
ptr.interval = null
eventChanges.add(OnRemoved(listOf(PointerSnapshot(ptr, interval))))
eventChanges.add(OnInserted(listOf(PointerSnapshot(newPtr, interval))))
}
private fun applyChange(eventBuilder: NotebookIntervalPointersEventBuilder,
hint: NotebookIntervalPointerFactory.Swap) {
val success = trySwapPointers(eventBuilder, hint)
}
private fun trySwapPointers(eventBuilder: NotebookIntervalPointersEventBuilder,
hint: NotebookIntervalPointerFactory.Swap): Boolean {
private fun trySwapPointers(eventChanges: NotebookIntervalPointersEventChanges?,
hint: NotebookIntervalPointerFactory.Swap) {
val firstPtr = pointers.getOrNull(hint.firstOrdinal)
val secondPtr = pointers.getOrNull(hint.secondOrdinal)
if (firstPtr == null || secondPtr == null) {
thisLogger().error("cannot swap invalid NotebookIntervalPointers: ${hint.firstOrdinal} and ${hint.secondOrdinal}")
return false
return
}
if (hint.firstOrdinal == hint.secondOrdinal) return false // nothing to do
if (hint.firstOrdinal == hint.secondOrdinal) return // nothing to do
val interval = firstPtr.interval!!
firstPtr.interval = secondPtr.interval
@@ -149,71 +282,22 @@ class NotebookIntervalPointerFactoryImpl(private val notebookCellLines: Notebook
pointers[hint.firstOrdinal] = secondPtr
pointers[hint.secondOrdinal] = firstPtr
eventBuilder.onSwapped(hint.firstOrdinal, hint.secondOrdinal)
return true
eventChanges?.add(OnSwapped(PointerSnapshot(firstPtr, firstPtr.interval!!),
PointerSnapshot(secondPtr, secondPtr.interval!!)))
}
private fun invalidate(ptr: NotebookIntervalPointerImpl) {
ptr.interval?.let { interval ->
pointers[interval.ordinal] = NotebookIntervalPointerImpl(interval)
ptr.interval = null
}
}
private fun invertChanges(changes: List<Change>): List<Change> =
changes.asReversed().map(::invertChange)
private fun updatePointersFrom(pos: Int) {
for (i in pos until pointers.size) {
pointers[i].interval = notebookCellLines.intervals[i]
private fun invertChange(change: Change): Change =
when (change) {
is OnEdited -> change.copy(intervalAfter = change.intervalBefore, intervalBefore = change.intervalAfter)
is OnInserted -> OnRemoved(change.subsequentPointers)
is OnRemoved -> OnInserted(change.subsequentPointers)
is OnSwapped -> OnSwapped(first = PointerSnapshot(change.first.pointer, change.second.interval),
second = PointerSnapshot(change.second.pointer, change.first.interval))
}
}
@TestOnly
fun pointersCount(): Int = pointers.size
}
@JvmInline
private value class NotebookIntervalPointersEventBuilder(val accumulatedChanges: MutableList<Change> = mutableListOf<Change>()) {
fun applyEvent(eventDispatcher: EventDispatcher<NotebookIntervalPointerFactory.ChangeListener>,
cellLinesEvent: NotebookCellLinesEvent?) {
val event = NotebookIntervalPointersEvent(accumulatedChanges, cellLinesEvent)
eventDispatcher.multicaster.onUpdated(event)
}
fun onEdited(interval: NotebookCellLines.Interval) {
accumulatedChanges.add(OnEdited(interval.ordinal))
}
fun onEdited(intervals: List<NotebookCellLines.Interval>, excluded: List<NotebookCellLines.Interval> = emptyList()) {
if (intervals.isEmpty()) return
val overLast = intervals.last().ordinal + 1
val excludedRange = (excluded.firstOrNull()?.ordinal ?: overLast)..(excluded.lastOrNull()?.ordinal ?: overLast)
for (interval in intervals) {
if (interval.ordinal !in excludedRange) {
accumulatedChanges.add(OnEdited(interval.ordinal))
}
}
}
fun onRemoved(interval: NotebookCellLines.Interval) {
accumulatedChanges.add(OnRemoved(interval.ordinal..interval.ordinal))
}
fun onRemoved(sequentialIntervals: List<NotebookCellLines.Interval>) {
if (sequentialIntervals.isNotEmpty()) {
accumulatedChanges.add(OnRemoved(sequentialIntervals.first().ordinal..sequentialIntervals.last().ordinal))
}
}
fun onInserted(interval: NotebookCellLines.Interval) {
accumulatedChanges.add(OnInserted(interval.ordinal..interval.ordinal))
}
fun onInserted(sequentialIntervals: List<NotebookCellLines.Interval>) {
if (sequentialIntervals.isNotEmpty()) {
accumulatedChanges.add(OnInserted(sequentialIntervals.first().ordinal..sequentialIntervals.last().ordinal))
}
}
fun onSwapped(fromOrdinal: Int, toOrdinal: Int) {
accumulatedChanges.add(OnSwapped(fromOrdinal, toOrdinal))
}
}

View File

@@ -1,11 +1,51 @@
package org.jetbrains.plugins.notebooks.visualization
/**
* passed to [NotebookIntervalPointerFactory.ChangeListener] in next cases:
* * Underlying document is changed. (in such case cellLinesEvent != null)
* * Someone explicitly swapped two pointers or invalidated them by calling [NotebookIntervalPointerFactory.modifyPointers]
* * one of upper changes was reverted or redone. See corresponding [EventSource]
*
* Changes represented as list of trivial changes. [Change]
* Intervals which was just moved are not mentioned in changes. For example, when inserting code before them.
*/
data class NotebookIntervalPointersEvent(val changes: List<Change>,
val cellLinesEvent: NotebookCellLinesEvent?) {
val cellLinesEvent: NotebookCellLinesEvent?,
val source: EventSource) {
enum class EventSource {
ACTION, UNDO_ACTION, REDO_ACTION
}
data class PointerSnapshot(val pointer: NotebookIntervalPointer, val interval: NotebookCellLines.Interval)
/**
* any change contains enough information to be inverted. It simplifies undo/redo actions.
*/
sealed interface Change
data class OnInserted(val ordinals: IntRange) : Change
data class OnEdited(val ordinal: Int) : Change
data class OnRemoved(val ordinals: IntRange) : Change
data class OnSwapped(val firstOrdinal: Int, val secondOrdinal: Int) : Change
data class OnInserted(val subsequentPointers: List<PointerSnapshot>) : Change {
val ordinals = subsequentPointers.first().interval.ordinal..subsequentPointers.last().interval.ordinal
}
/* snapshots contains intervals before removal */
data class OnRemoved(val subsequentPointers: List<PointerSnapshot>) : Change {
val ordinals = subsequentPointers.first().interval.ordinal..subsequentPointers.last().interval.ordinal
}
data class OnEdited(val pointer: NotebookIntervalPointer,
val intervalBefore: NotebookCellLines.Interval,
val intervalAfter: NotebookCellLines.Interval) : Change {
val ordinal: Int
get() = intervalAfter.ordinal
}
/* snapshots contains intervals after swap */
data class OnSwapped(val first: PointerSnapshot, val second: PointerSnapshot) : Change {
val firstOrdinal: Int
get() = first.interval.ordinal
val secondOrdinal: Int
get() = second.interval.ordinal
}
}

View File

@@ -1,211 +0,0 @@
package org.jetbrains.plugins.notebooks.visualization
import com.intellij.mock.MockDocument
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.event.MockDocumentEvent
import com.intellij.util.EventDispatcher
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.assertj.core.api.Descriptable
import org.assertj.core.description.Description
import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines.Interval
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class NotebookIntervalPointerTest {
private val exampleIntervals = listOf(
makeIntervals(),
makeIntervals(0..1),
makeIntervals(0..1, 2..4),
makeIntervals(0..1, 2..4, 5..8),
)
@Test
fun testInitialization() {
for (intervals in exampleIntervals) {
val env = TestEnv(intervals)
env.shouldBeValid(intervals)
env.shouldBeInvalid(makeInterval(10, 10..11))
}
}
@Test
fun testAddAllIntervals() {
for (intervals in exampleIntervals) {
val env = TestEnv(listOf())
env.shouldBeInvalid(intervals)
env.changeSegment(listOf(), intervals, intervals)
env.shouldBeValid(intervals)
}
}
@Test
fun testRemoveAllIntervals() {
for (intervals in exampleIntervals) {
val env = TestEnv(intervals)
env.shouldBeValid(intervals)
env.changeSegment(intervals, listOf(), listOf())
env.shouldBeInvalid(intervals)
}
}
@Test
fun testChangeElements() {
val initialIntervals = makeIntervals(0..1, 2..4, 5..8, 9..13)
val optionsToRemove = listOf(
listOf(),
initialIntervals.subList(1, 2),
initialIntervals.subList(1, 3),
initialIntervals.subList(1, 4)
)
val optionsToAdd = listOf(
listOf(),
makeIntervals(2..10).map { it.copy(ordinal = it.ordinal + 1, type = NotebookCellLines.CellType.CODE) },
makeIntervals(2..10, 11..20).map { it.copy(ordinal = it.ordinal + 1, type = NotebookCellLines.CellType.CODE) }
)
for (toRemove in optionsToRemove) {
for (toAdd in optionsToAdd) {
val start = initialIntervals.subList(0, 1)
val end = initialIntervals.subList(1 + toRemove.size, 4)
val finalIntervals = fixOrdinalsAndOffsets(start + toAdd + end)
val env = TestEnv(initialIntervals)
val pointersToUnchangedIntervals = (start + end).map { env.pointersFactory.create(it) }
val pointersToRemovedIntervals = toRemove.map { env.pointersFactory.create(it) }
pointersToUnchangedIntervals.forEach { assertThat(it.get()).isNotNull() }
pointersToRemovedIntervals.forEach { assertThat(it.get()).isNotNull }
env.changeSegment(toRemove, toAdd, finalIntervals)
pointersToUnchangedIntervals.forEach { pointer -> assertThat(pointer.get()).isNotNull() }
pointersToRemovedIntervals.forEach { pointer -> assertThat(pointer.get()).isNull() }
env.shouldBeValid(finalIntervals)
env.shouldBeInvalid(toRemove)
}
}
}
@Test
fun testReuseInterval() {
val initialIntervals = makeIntervals(0..1, 2..19, 20..199)
for ((i, selected) in initialIntervals.withIndex()) {
val dsize = 1
val changed = selected.copy(lines = selected.lines.first..selected.lines.last + dsize)
val allIntervals = fixOrdinalsAndOffsets(initialIntervals.subList(0, i) + listOf(changed) + initialIntervals.subList(i + 1, initialIntervals.size))
val env = TestEnv(initialIntervals)
val pointers = initialIntervals.map { env.pointersFactory.create(it) }
pointers.zip(initialIntervals).forEach { (pointer, interval) -> assertThat(pointer.get()).isEqualTo(interval) }
env.changeSegment(listOf(selected), listOf(changed), allIntervals)
pointers.zip(allIntervals).forEach { (pointer, interval) -> assertThat(pointer.get()).isEqualTo(interval)}
}
}
private fun fixOrdinalsAndOffsets(intervals: List<Interval>): List<Interval> {
val result = mutableListOf<Interval>()
for ((index, interval) in intervals.withIndex()) {
val expectedOffset = result.lastOrNull()?.let { it.lines.last + 1 } ?: 0
val correctInterval =
if (interval.lines.first == expectedOffset && interval.ordinal == index)
interval
else
interval.copy(ordinal = index, lines = expectedOffset..(expectedOffset + interval.lines.last - interval.lines.first))
result.add(correctInterval)
}
return result
}
private fun makeInterval(ordinal: Int, lines: IntRange) =
Interval(ordinal, NotebookCellLines.CellType.RAW, lines, NotebookCellLines.MarkersAtLines.NO)
private fun makeIntervals(vararg lines: IntRange): List<Interval> =
lines.withIndex().map { (index, lines) -> makeInterval(index, lines) }
}
private class TestEnv(intervals: List<Interval>, val document: Document = MockDocument()) {
val notebookCellLines = MockNotebookCellLines(intervals = mutableListOf(*intervals.toTypedArray()))
val pointersFactory = NotebookIntervalPointerFactoryImpl(notebookCellLines)
fun changeSegment(old: List<Interval>, new: List<Interval>, allIntervals: List<Interval>) {
old.firstOrNull()?.let { firstOld ->
new.firstOrNull()?.let { firstNew ->
assertThat(firstOld.ordinal).isEqualTo(firstNew.ordinal)
}
}
notebookCellLines.intervals.clear()
notebookCellLines.intervals.addAll(allIntervals)
val documentEvent = MockDocumentEvent(document, 0)
val event = NotebookCellLinesEvent(documentEvent, old, old, new, new, 0)
notebookCellLines.intervalListeners.multicaster.documentChanged(event)
}
fun shouldBeValid(interval: Interval) {
assertThat(pointersFactory.create(interval).get())
.describedAs { "pointer for ${interval} should be valid, but current pointers = ${describe(notebookCellLines.intervals)}" }
.isEqualTo(interval)
}
fun shouldBeInvalid(interval: Interval) {
val error = catchThrowable {
pointersFactory.create(interval).get()
}
assertThat(error)
.describedAs { "pointer for ${interval} should be null, but current pointers = ${describe(notebookCellLines.intervals)}" }
.isNotNull()
}
fun shouldBeValid(intervals: List<Interval>): Unit = intervals.forEach { shouldBeValid(it) }
fun shouldBeInvalid(intervals: List<Interval>): Unit = intervals.forEach { shouldBeInvalid(it) }
}
private class MockNotebookCellLines(override val intervals: MutableList<Interval> = mutableListOf()) : NotebookCellLines {
init {
checkIntervals(intervals)
}
override val intervalListeners = EventDispatcher.create(NotebookCellLines.IntervalListener::class.java)
override fun intervalsIterator(startLine: Int): ListIterator<Interval> = TODO("stub")
override val modificationStamp: Long
get() = TODO("stub")
}
private fun describe(intervals: List<Interval>): String =
intervals.joinToString(separator = ",", prefix = "[", postfix = "]")
private fun checkIntervals(intervals: List<Interval>) {
intervals.zipWithNext().forEach { (prev, next) ->
assertThat(prev.lines.last + 1).describedAs { "wrong intervals: ${describe(intervals)}" }.isEqualTo(next.lines.first)
}
intervals.withIndex().forEach { (index, interval) ->
assertThat(interval.ordinal).describedAs { "wrong interval ordinal: ${describe(intervals)}" }.isEqualTo(index)
}
}
fun <Self> Descriptable<Self>.describedAs(lazyMsg: () -> String): Self =
describedAs(object : Description() {
override fun value(): String = lazyMsg()
})