@@ -19,11 +19,14 @@ import type {
1919 ObjectDifference ,
2020 ObjectDiffState ,
2121 ValueDifference ,
22+ CompositeListLayoutChangeItemRemoved ,
23+ CompositeListLayoutChange ,
2224} from "./types" ;
2325import * as Value from "../values" ;
2426import * as Difference from "./difference" ;
2527import { DiffErrorKind } from "./types" ;
2628import { ValueKind } from "../values/types" ;
29+ import * as ChangeKind from "./itemChangeKind" ;
2730
2831/**
2932 * Compares base object version with model version and returns a normalized Difference object,
@@ -420,8 +423,15 @@ function diffCompositeListValue(
420423 return undefined ;
421424 }
422425
423- const layoutDiffResult =
424- diff ?. layout ?? diffCompositeListLayout ( context , base , model ) ;
426+ let layoutDiffResult : any = diff ?. layout ;
427+ let itemsChanges : CompositeListLayoutChange [ ] = [ ] ;
428+
429+ if ( ! layoutDiffResult ) {
430+ const { deletedItems : newDeletedItems , layout : newLayout } =
431+ diffCompositeListLayout ( context , base , model ) ;
432+ itemsChanges = newDeletedItems ;
433+ layoutDiffResult = newLayout ;
434+ }
425435
426436 if ( layoutDiffResult === "BREAK" ) {
427437 // Fast-path, no further diffing necessary
@@ -432,6 +442,7 @@ function diffCompositeListValue(
432442 if ( layoutDiffResult ) {
433443 diff = diff ?? Difference . createCompositeListDifference ( ) ;
434444 diff . layout = layoutDiffResult ;
445+ diff . itemsChanges = itemsChanges ;
435446 }
436447
437448 for ( const index of itemQueue ) {
@@ -478,7 +489,10 @@ function diffCompositeListLayout(
478489 context : DiffContext ,
479490 base : CompositeListValue ,
480491 model : CompositeListValue ,
481- ) : CompositeListLayoutDifference | undefined | "BREAK" {
492+ ) : {
493+ deletedItems : CompositeListLayoutChange [ ] ;
494+ layout : CompositeListLayoutDifference | undefined | "BREAK" ;
495+ } {
482496 // What constitutes layout change?
483497 // - Change of "keyed object" position in the list
484498 // - Change of list length
@@ -489,6 +503,11 @@ function diffCompositeListLayout(
489503
490504 const baseChunk = Value . isAggregate ( base ) ? base . chunks [ 0 ] : base ;
491505 const modelChunk = Value . isAggregate ( model ) ? model . chunks [ 0 ] : model ;
506+ const unusedBaseIndixes = new Set < number > ( ) ;
507+ const itemChanges : CompositeListLayoutChange [ ] = [ ] ;
508+ for ( let i = 0 ; i < baseLen ; i ++ ) {
509+ unusedBaseIndixes . add ( i ) ;
510+ }
492511
493512 let itemDiffRequired = false ;
494513 let firstDirtyIndex = - 1 ;
@@ -508,17 +527,32 @@ function diffCompositeListLayout(
508527 firstDirtyIndex = i ;
509528 break ;
510529 }
530+ unusedBaseIndixes . delete ( i ) ;
511531 }
512532 // Fast-path: no layout difference found
513533 if ( firstDirtyIndex === - 1 ) {
534+ const deletedItems : CompositeListLayoutChangeItemRemoved [ ] = [ ] ;
535+ for ( const index of unusedBaseIndixes ) {
536+ deletedItems . push ( {
537+ kind : ChangeKind . ItemRemove ,
538+ oldIndex : index ,
539+ data : baseChunk . data [ index ] ,
540+ } ) ;
541+ }
514542 if ( baseLen > modelLen ) {
515543 const layout : CompositeListLayoutDifference = [ ] ;
516544 for ( let i = 0 ; i < modelLen ; i ++ ) {
517545 layout . push ( i ) ;
518546 }
519- return layout ;
547+ return {
548+ deletedItems,
549+ layout,
550+ } ;
520551 }
521- return ! itemDiffRequired ? "BREAK" : undefined ;
552+ return {
553+ deletedItems,
554+ layout : ! itemDiffRequired ? "BREAK" : undefined ,
555+ } ;
522556 }
523557 // TODO: lastDirtyIndex to isolate changed segment (prepend case)
524558
@@ -527,12 +561,19 @@ function diffCompositeListLayout(
527561 layout . push ( i ) ;
528562 }
529563 let plainObjectLookupStartIndex = firstDirtyIndex ;
530- for ( let i = firstDirtyIndex ; i < modelLen ; i ++ ) {
531- if ( modelChunk . data [ i ] === null ) {
564+ for ( let index = firstDirtyIndex ; index < modelLen ; index ++ ) {
565+ if ( modelChunk . data [ index ] === null ) {
532566 layout . push ( null ) ;
567+ if ( baseChunk . data [ index ] !== null ) {
568+ itemChanges . push ( {
569+ kind : ChangeKind . ItemAdd ,
570+ index,
571+ data : null ,
572+ } ) ;
573+ }
533574 continue ;
534575 }
535- const modelKey = resolveItemKey ( env , modelChunk , i ) ;
576+ const modelKey = resolveItemKey ( env , modelChunk , index ) ;
536577 const lookupStartIndex =
537578 modelKey === false ? plainObjectLookupStartIndex : 0 ; // TODO: should be firstDirtyIndex; (0 is necessary only for cases when array contains duplicates - we should detect such arrays when indexing and special-case it instead)
538579
@@ -545,8 +586,17 @@ function diffCompositeListLayout(
545586 ) ;
546587 if ( baseIndex !== - 1 ) {
547588 layout . push ( baseIndex ) ;
589+ unusedBaseIndixes . delete ( baseIndex ) ;
590+ if ( index !== baseIndex ) {
591+ itemChanges . push ( {
592+ kind : ChangeKind . ItemIndexChange ,
593+ index,
594+ oldIndex : baseIndex ,
595+ data : baseChunk . data [ baseIndex ] ,
596+ } ) ;
597+ }
548598 } else {
549- const value = Value . aggregateListItemValue ( model , i ) ;
599+ const value = Value . aggregateListItemValue ( model , index ) ;
550600 if ( Value . isCompositeNullValue ( value ) ) {
551601 layout . push ( null ) ;
552602 } else if (
@@ -556,13 +606,24 @@ function diffCompositeListLayout(
556606 layout . push ( value ) ;
557607 } else {
558608 throw new Error (
559- `Unexpected list item value at index #${ i } \n` +
609+ `Unexpected list item value at index #${ index } \n` +
560610 ` original list: ${ JSON . stringify ( model . data ) } ` ,
561611 ) ;
562612 }
563613 }
564614 }
565- return layout ;
615+
616+ for ( const oldIndex of unusedBaseIndixes ) {
617+ itemChanges . push ( {
618+ kind : ChangeKind . ItemRemove ,
619+ oldIndex,
620+ data : baseChunk . data [ oldIndex ] ,
621+ } ) ;
622+ }
623+ return {
624+ deletedItems : itemChanges ,
625+ layout,
626+ } ;
566627}
567628
568629function resolveItemKey (
0 commit comments