mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[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:
committed by
intellij-monorepo-bot
parent
df9885fe22
commit
4aaad8916f
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
Reference in New Issue
Block a user