1- import { cloneDeepWith , first , initial , isEmpty , last , times } from 'lodash-es'
1+ import { cloneDeepWith , first , initial , isEmpty , isEqual , last , times } from 'lodash-es'
22import {
33 compileJSONPointer ,
44 existsIn ,
@@ -11,6 +11,7 @@ import {
1111 type JSONPatchAdd ,
1212 type JSONPatchCopy ,
1313 type JSONPatchDocument ,
14+ type JSONPatchMove ,
1415 type JSONPatchOperation ,
1516 type JSONPath ,
1617 parseJSONPointer ,
@@ -39,7 +40,6 @@ import {
3940} from './selection.js'
4041import type { ClipboardValues , DragInsideAction , JSONParser , JSONSelection } from '$lib/types'
4142import { int } from '../utils/numberUtils.js'
42- import { dedupeKeepLast } from '$lib/utils/arrayUtils'
4343
4444/**
4545 * Create a JSONPatch for an insert operation.
@@ -722,30 +722,146 @@ export function revertJSONPatchWithMoveOperations(
722722 json : unknown ,
723723 operations : JSONPatchDocument
724724) : JSONPatchDocument {
725- return dedupeKeepLast (
726- revertJSONPatch ( json , operations , {
727- before : ( json , operation , revertOperations ) => {
728- if ( isJSONPatchRemove ( operation ) ) {
729- const path = parseJSONPointer ( operation . path )
730- return {
731- revertOperations : [ ...revertOperations , ...createRevertMoveOperations ( json , path ) ]
732- }
725+ // first, we merge series of move operations inside an object. These move operations are used
726+ // to ensure a specific ordering of the keys in the object. Reverting those move events one by
727+ // one would explode the amount of move operations in the revert operations and contain mostly
728+ // duplicates.
729+ const filteredOperations = _filterRedundantMoveOperations ( json , operations )
730+
731+ return revertJSONPatch ( json , filteredOperations , {
732+ before : ( json , operation , revertOperations ) => {
733+ if ( isJSONPatchRemove ( operation ) ) {
734+ // we must restore the key order when reverting removing of an object key
735+ const path = parseJSONPointer ( operation . path )
736+ return {
737+ revertOperations : [ ...revertOperations , ...createRevertMoveOperations ( json , path ) ]
733738 }
739+ }
734740
735- if ( isJSONPatchMove ( operation ) ) {
736- const from = parseJSONPointer ( operation . from )
737- return {
738- revertOperations :
739- operation . from === operation . path
740- ? [ operation , ...createRevertMoveOperations ( json , from ) ] // move in-place (just for re-ordering object keys)
741- : [ ...revertOperations , ...createRevertMoveOperations ( json , from ) ]
742- }
741+ if ( isJSONPatchMove ( operation ) ) {
742+ const from = parseJSONPointer ( operation . from )
743+ return {
744+ revertOperations :
745+ operation . from === operation . path
746+ ? [ operation , ...createRevertMoveOperations ( json , from ) ] // move in-place (just for re-ordering object keys)
747+ : [ ...revertOperations , ...createRevertMoveOperations ( json , from ) ]
743748 }
744-
745- return { document : json }
746749 }
747- } )
748- )
750+
751+ return { document : json }
752+ }
753+ } )
754+ }
755+
756+ /**
757+ * Filter move operations which are redundant when creating the reverse patch operations.
758+ *
759+ * This is a preprocessing step for creating the revert patch operations. For example, having
760+ * json {b:2, a:1, c:3} and the following move operations (this will reorder the keys):
761+ *
762+ * [
763+ * { op: 'move', from: '/a', path: '/a' },
764+ * { op: 'move', from: '/b', path: '/b' },
765+ * { op: 'move', from: '/c', path: '/c' }
766+ * ]
767+ *
768+ * This function will return only moving of b, since that is the first of all the moved keys:
769+ *
770+ * [
771+ * { op: 'move', from: '/b', path: '/b' }
772+ * ]
773+ *
774+ * So, when creating the reverse operations, this single operation is enough to create the needed
775+ * move operations for all lower down keys to restore the original order of the keys.
776+ */
777+ function _filterRedundantMoveOperations (
778+ json : unknown ,
779+ operations : JSONPatchOperation [ ]
780+ ) : JSONPatchOperation [ ] {
781+ // check whether this is a set of move operations (cheap check)
782+ if ( isEmpty ( operations ) || ! operations . every ( isJSONPatchMove ) ) {
783+ return operations
784+ }
785+
786+ // parse the JSON pointers once and split them in parent paths and keys
787+ const processedOps : ProcessedOperation [ ] = [ ]
788+ for ( const operation of operations ) {
789+ const from = splitParentKey ( parseJSONPointer ( operation . from ) )
790+ const path = splitParentKey ( parseJSONPointer ( operation . path ) )
791+ if ( ! from || ! path ) {
792+ return operations
793+ }
794+ processedOps . push ( { from, path, operation } )
795+ }
796+
797+ // check whether the first movement is inside an object (and not an array)
798+ const parentPath = processedOps [ 0 ] . path . parent
799+ const parent = getIn ( json , parentPath )
800+ if ( ! isJSONObject ( parent ) ) {
801+ return operations
802+ }
803+
804+ // check whether all movements are within the same parent object
805+ if ( ! processedOps . every ( ( op ) => _equalParentPath ( op , parentPath ) ) ) {
806+ return operations
807+ }
808+
809+ const firstKey = _findKeyWithLowestKeyIndex ( processedOps , json )
810+ const getOperation = ( op : ProcessedOperation ) => op . operation
811+
812+ // only return the operation moving the firstKey back, and all rename operations
813+ const renameOps = processedOps . filter ( ( op ) => op . operation . from !== op . operation . path )
814+ return renameOps . some ( ( op ) => op . path . key === firstKey )
815+ ? renameOps . map ( getOperation ) // no need to add the firstKey move operation
816+ : [ moveDown ( parentPath , firstKey ) , ...renameOps . map ( getOperation ) ]
817+ }
818+
819+ interface ParentKey {
820+ parent : string [ ]
821+ key : string
822+ }
823+
824+ interface ProcessedOperation {
825+ from : ParentKey
826+ path : ParentKey
827+ operation : JSONPatchMove
828+ }
829+
830+ function splitParentKey ( path : JSONPath ) : ParentKey | undefined {
831+ return path . length > 0 ? { parent : initial ( path ) , key : last ( path ) as string } : undefined
832+ }
833+
834+ /**
835+ * Test whether a move operation has a specific parent path for both `from` and `path`
836+ */
837+ function _equalParentPath (
838+ operation : ProcessedOperation ,
839+ parentPath : JSONPath | undefined
840+ ) : boolean {
841+ return isEqual ( operation . from . parent , parentPath ) && isEqual ( operation . path . parent , parentPath )
842+ }
843+
844+ function _findKeyWithLowestKeyIndex ( processedOps : ProcessedOperation [ ] , json : unknown ) : string {
845+ const keys = Object . keys ( json as Record < string , unknown > )
846+ const movedKeys = keys . slice ( )
847+
848+ // first execute all move operations on the list with keys
849+ // here we assume all move operations have the same parent and are keys in an object
850+ for ( const op of processedOps ) {
851+ const index = movedKeys . indexOf ( op . from . key )
852+ if ( index !== - 1 ) {
853+ movedKeys . splice ( index , 1 )
854+ movedKeys . push ( op . path . key )
855+ }
856+ }
857+
858+ // find the first changed key
859+ let i = 0
860+ while ( i < keys . length && keys [ i ] === movedKeys [ i ] ) {
861+ i ++
862+ }
863+
864+ return movedKeys [ i ]
749865}
750866
751867function createRevertMoveOperations ( json : unknown , path : JSONPath ) : JSONPatchOperation [ ] {
0 commit comments