transform.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
  6. import deltaTransform from '../../../../src/model/delta/transform';
  7. const transformDeltaSets = deltaTransform.transformDeltaSets;
  8. import Model from '../../../../src/model/model';
  9. import Element from '../../../../src/model/element';
  10. import Text from '../../../../src/model/text';
  11. import Position from '../../../../src/model/position';
  12. import Delta from '../../../../src/model/delta/delta';
  13. import InsertDelta from '../../../../src/model/delta/insertdelta';
  14. import RemoveDelta from '../../../../src/model/delta/removedelta';
  15. import MoveDelta from '../../../../src/model/delta/movedelta';
  16. import SplitDelta from '../../../../src/model/delta/splitdelta';
  17. import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
  18. import NoOperation from '../../../../src/model/operation/nooperation';
  19. import MoveOperation from '../../../../src/model/operation/moveoperation';
  20. import RemoveOperation from '../../../../src/model/operation/removeoperation';
  21. import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
  22. import InsertOperation from '../../../../src/model/operation/insertoperation';
  23. import {
  24. expectDelta,
  25. getInsertDelta,
  26. getSplitDelta,
  27. getRemoveDelta,
  28. getMoveDelta,
  29. getMergeDelta,
  30. getUnwrapDelta
  31. } from '../../../../tests/model/delta/transform/_utils/utils';
  32. describe( 'transform', () => {
  33. let doc, root, baseVersion;
  34. beforeEach( () => {
  35. const model = new Model();
  36. doc = model.document;
  37. root = doc.createRoot();
  38. root.appendChildren( new Element( 'p', null, new Text( 'foobar' ) ) );
  39. baseVersion = doc.version;
  40. } );
  41. describe( 'transformDeltaSets', () => {
  42. it( 'should use deltaTransform.transform', () => {
  43. sinon.spy( deltaTransform, 'transform' );
  44. const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
  45. const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion );
  46. transformDeltaSets( [ insertDelta ], [ removeDelta ] );
  47. expect( deltaTransform.transform.called ).to.be.true;
  48. deltaTransform.transform.restore();
  49. } );
  50. it( 'should transform two arrays of deltas', () => {
  51. const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion );
  52. const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 );
  53. const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion );
  54. const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 );
  55. const { deltasA, deltasB } = transformDeltaSets( [ splitDelta, insertDeltaX ], [ removeDelta, insertDeltaY ] );
  56. expect( deltasA.length ).to.equal( 3 );
  57. expect( deltasB.length ).to.equal( 3 );
  58. expectDelta( deltasA[ 0 ], {
  59. type: SplitDelta,
  60. operations: [
  61. {
  62. type: InsertOperation,
  63. position: new Position( root, [ 1 ] ),
  64. baseVersion: 2
  65. },
  66. {
  67. type: MoveOperation,
  68. sourcePosition: new Position( root, [ 0, 2 ] ),
  69. howMany: 5,
  70. targetPosition: new Position( root, [ 1, 0 ] ),
  71. baseVersion: 3
  72. }
  73. ]
  74. } );
  75. expectDelta( deltasA[ 1 ], {
  76. type: InsertDelta,
  77. operations: [
  78. {
  79. type: InsertOperation,
  80. position: new Position( root, [ 0, 2 ] ),
  81. baseVersion: 4
  82. }
  83. ]
  84. } );
  85. expectDelta( deltasA[ 2 ], {
  86. type: Delta,
  87. operations: [
  88. {
  89. type: NoOperation,
  90. baseVersion: 5
  91. }
  92. ]
  93. } );
  94. expectDelta( deltasB[ 0 ], {
  95. type: RemoveDelta,
  96. operations: [
  97. {
  98. type: RemoveOperation,
  99. sourcePosition: new Position( root, [ 0, 2 ] ),
  100. howMany: 1,
  101. baseVersion: 3
  102. }
  103. ]
  104. } );
  105. expectDelta( deltasB[ 1 ], {
  106. type: RemoveDelta,
  107. operations: [
  108. {
  109. type: RemoveOperation,
  110. sourcePosition: new Position( root, [ 1, 0 ] ),
  111. howMany: 1,
  112. baseVersion: 4
  113. }
  114. ]
  115. } );
  116. expectDelta( deltasB[ 2 ], {
  117. type: InsertDelta,
  118. operations: [
  119. {
  120. type: InsertOperation,
  121. position: new Position( root, [ 1, 2 ] ),
  122. baseVersion: 5
  123. }
  124. ]
  125. } );
  126. } );
  127. it( 'should transform two arrays of deltas - reverse', () => {
  128. const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion );
  129. const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 );
  130. const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion );
  131. const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 );
  132. const { deltasA, deltasB } = transformDeltaSets( [ removeDelta, insertDeltaY ], [ splitDelta, insertDeltaX ] );
  133. expect( deltasA.length ).to.equal( 3 );
  134. expect( deltasB.length ).to.equal( 3 );
  135. expectDelta( deltasA[ 0 ], {
  136. type: RemoveDelta,
  137. operations: [
  138. {
  139. type: RemoveOperation,
  140. sourcePosition: new Position( root, [ 0, 2 ] ),
  141. howMany: 1,
  142. baseVersion: 3
  143. }
  144. ]
  145. } );
  146. expectDelta( deltasA[ 1 ], {
  147. type: RemoveDelta,
  148. operations: [
  149. {
  150. type: RemoveOperation,
  151. sourcePosition: new Position( root, [ 1, 0 ] ),
  152. howMany: 1,
  153. baseVersion: 4
  154. }
  155. ]
  156. } );
  157. expectDelta( deltasA[ 2 ], {
  158. type: InsertDelta,
  159. operations: [
  160. {
  161. type: InsertOperation,
  162. position: new Position( root, [ 1, 2 ] ),
  163. baseVersion: 5
  164. }
  165. ]
  166. } );
  167. expectDelta( deltasB[ 0 ], {
  168. type: SplitDelta,
  169. operations: [
  170. {
  171. type: InsertOperation,
  172. position: new Position( root, [ 1 ] ),
  173. baseVersion: 2
  174. },
  175. {
  176. type: MoveOperation,
  177. sourcePosition: new Position( root, [ 0, 2 ] ),
  178. howMany: 5,
  179. targetPosition: new Position( root, [ 1, 0 ] ),
  180. baseVersion: 3
  181. }
  182. ]
  183. } );
  184. expectDelta( deltasB[ 1 ], {
  185. type: InsertDelta,
  186. operations: [
  187. {
  188. type: InsertOperation,
  189. position: new Position( root, [ 0, 2 ] ),
  190. baseVersion: 4
  191. }
  192. ]
  193. } );
  194. expectDelta( deltasB[ 2 ], {
  195. type: Delta,
  196. operations: [
  197. {
  198. type: NoOperation,
  199. baseVersion: 5
  200. }
  201. ]
  202. } );
  203. } );
  204. it( 'should not modify original deltas or arrays', () => {
  205. const insertDeltaA = getInsertDelta( new Position( root, [ 0, 0 ] ), new Text( 'x' ), baseVersion );
  206. const insertDeltaB = getInsertDelta( new Position( root, [ 1, 0 ] ), new Text( 'y' ), baseVersion );
  207. const originalDeltasA = [ insertDeltaA ];
  208. const originalDeltasB = [ insertDeltaB ];
  209. const { deltasA, deltasB } = transformDeltaSets( originalDeltasA, originalDeltasB );
  210. expect( deltasA ).to.not.equal( originalDeltasA );
  211. expect( deltasB ).to.not.equal( originalDeltasB );
  212. expect( deltasA[ 0 ] ).to.not.equal( originalDeltasA[ 0 ] );
  213. expect( deltasB[ 0 ] ).to.not.equal( originalDeltasB[ 0 ] );
  214. } );
  215. describe( 'context', () => {
  216. it( 'first set is important', () => {
  217. const insertDeltaA = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
  218. const insertDeltaB = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion );
  219. const { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ] );
  220. expectDelta( deltasA[ 0 ], {
  221. type: InsertDelta,
  222. operations: [
  223. {
  224. type: InsertOperation,
  225. position: new Position( root, [ 0, 4 ] ),
  226. baseVersion: 1
  227. }
  228. ]
  229. } );
  230. expectDelta( deltasB[ 0 ], {
  231. type: InsertDelta,
  232. operations: [
  233. {
  234. type: InsertOperation,
  235. position: new Position( root, [ 0, 7 ] ),
  236. baseVersion: 1
  237. }
  238. ]
  239. } );
  240. } );
  241. it( 'remove delta is more important if additional context is not used', () => {
  242. const moveDelta = getMoveDelta( new Position( root, [ 0, 4 ] ), 3, new Position( root, [ 1, 0 ] ), baseVersion );
  243. const removeDelta = getRemoveDelta( new Position( root, [ 0, 4 ] ), 3, baseVersion );
  244. const { deltasA, deltasB } = transformDeltaSets( [ moveDelta ], [ removeDelta ] );
  245. expectDelta( deltasA[ 0 ], {
  246. type: Delta,
  247. operations: [
  248. {
  249. type: NoOperation,
  250. baseVersion: 1
  251. }
  252. ]
  253. } );
  254. expectDelta( deltasB[ 0 ], {
  255. type: RemoveDelta,
  256. operations: [
  257. {
  258. type: RemoveOperation,
  259. sourcePosition: new Position( root, [ 1, 0 ] ),
  260. howMany: 3,
  261. targetPosition: new Position( doc.graveyard, [ 0 ] ),
  262. baseVersion: 1
  263. }
  264. ]
  265. } );
  266. } );
  267. it( 'remove delta may be less important if additional context is used and the delta was undone', () => {
  268. const moveDelta = getMoveDelta( new Position( root, [ 0, 4 ] ), 3, new Position( root, [ 1, 0 ] ), baseVersion );
  269. const removeDelta = getRemoveDelta( new Position( root, [ 0, 4 ] ), 3, baseVersion );
  270. // "Fake" delta undoing.
  271. doc.history.setDeltaAsUndone( removeDelta, new Delta() );
  272. const { deltasA, deltasB } = transformDeltaSets( [ moveDelta ], [ removeDelta ], doc );
  273. expectDelta( deltasA[ 0 ], {
  274. type: MoveDelta,
  275. operations: [
  276. {
  277. type: MoveOperation,
  278. sourcePosition: new Position( doc.graveyard, [ 0 ] ),
  279. howMany: 3,
  280. targetPosition: new Position( root, [ 1, 0 ] ),
  281. baseVersion: 1
  282. }
  283. ]
  284. } );
  285. expectDelta( deltasB[ 0 ], {
  286. type: Delta,
  287. operations: [
  288. {
  289. type: NoOperation,
  290. baseVersion: 1
  291. }
  292. ]
  293. } );
  294. } );
  295. // #1053.
  296. it( 'remove operation importance should be set correctly also for deltas other than remove delta', () => {
  297. const unwrapDelta = getUnwrapDelta( new Position( root, [ 0 ] ), 2, baseVersion );
  298. const nodeCopy = new Element( 'p' );
  299. const splitDelta = getSplitDelta( new Position( root, [ 0, 0, 1 ] ), nodeCopy, 1, baseVersion );
  300. const mergeDelta = splitDelta.getReversed();
  301. const { deltasA } = transformDeltaSets( [ unwrapDelta ], [ splitDelta, mergeDelta ], doc );
  302. expect( deltasA.length ).to.equal( 2 );
  303. expectDelta( deltasA[ 0 ], {
  304. type: MoveDelta,
  305. operations: [
  306. {
  307. type: MoveOperation,
  308. sourcePosition: new Position( root, [ 0, 0 ] ),
  309. howMany: 1,
  310. targetPosition: new Position( root, [ 0 ] ),
  311. baseVersion: baseVersion + 4
  312. }
  313. ]
  314. } );
  315. expectDelta( deltasA[ 1 ], {
  316. type: UnwrapDelta,
  317. operations: [
  318. {
  319. type: MoveOperation,
  320. sourcePosition: new Position( root, [ 1, 0 ] ),
  321. howMany: 1,
  322. targetPosition: new Position( root, [ 1 ] ),
  323. baseVersion: baseVersion + 5
  324. },
  325. {
  326. type: RemoveOperation,
  327. sourcePosition: new Position( root, [ 2 ] ),
  328. howMany: 1,
  329. baseVersion: baseVersion + 6
  330. }
  331. ]
  332. } );
  333. } );
  334. it( 'should keep correct order of nodes if additional context is used (insert before)', () => {
  335. // Assume document state: 'a^bcd'. User presses 'delete' key twice.
  336. const removeDelta1 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion ); // 'acd'.
  337. const removeDelta2 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion + 1 ); // 'ad'.
  338. const reinsertDelta2 = removeDelta2.getReversed(); // 'acd'.
  339. doc.history.addDelta( removeDelta1 );
  340. doc.history.addDelta( removeDelta2 );
  341. doc.history.addDelta( reinsertDelta2 );
  342. doc.history.setDeltaAsUndone( removeDelta2, reinsertDelta2 );
  343. // This should insert 'b' at offset 1.
  344. // However this delta will be transformed by `reinsertDelta2` which also inserts a node at offset 1.
  345. // This leads to a conflict and history needs to be check how deltas relate to each other.
  346. // Compare this scenario to the scenario below.
  347. const reinsertDelta1 = removeDelta1.getReversed();
  348. const { deltasA } = transformDeltaSets( [ reinsertDelta1 ], [ removeDelta2, reinsertDelta2 ], doc );
  349. expectDelta( deltasA[ 0 ], {
  350. type: MoveDelta,
  351. operations: [
  352. {
  353. type: ReinsertOperation,
  354. targetPosition: new Position( root, [ 1 ] ),
  355. howMany: 1,
  356. baseVersion: 3
  357. }
  358. ]
  359. } );
  360. } );
  361. it( 'should keep correct order of nodes if additional context is used (insert after)', () => {
  362. // Assume document state: 'abc^d'. User presses 'backspace' key twice.
  363. const removeDelta1 = getRemoveDelta( new Position( root, [ 2 ] ), 1, baseVersion ); // 'abd'.
  364. const removeDelta2 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion + 1 ); // 'ad'.
  365. const reinsertDelta2 = removeDelta2.getReversed(); // 'abd'.
  366. doc.history.addDelta( removeDelta1 );
  367. doc.history.addDelta( removeDelta2 );
  368. doc.history.addDelta( reinsertDelta2 );
  369. doc.history.setDeltaAsUndone( removeDelta2, reinsertDelta2 );
  370. // This should insert 'c' at offset 2.
  371. // However this delta will be first transformed by `removeDelta2`. After that transformation
  372. // the delta will try to insert 'c' at offset 1. Then, it will be transformed by `reinsertDelta2`,
  373. // which also tries to insert node at offset 1.
  374. // This leads to a conflict and history needs to be check how deltas relate to each other.
  375. // Compare this scenario to the scenario below.
  376. // In both cases transformed delta was inserting something at the same position. However, in
  377. // both cases the expected outcome is different.
  378. const reinsertDelta1 = removeDelta1.getReversed();
  379. const { deltasA } = transformDeltaSets( [ reinsertDelta1 ], [ removeDelta2, reinsertDelta2 ], doc );
  380. expectDelta( deltasA[ 0 ], {
  381. type: MoveDelta,
  382. operations: [
  383. {
  384. type: ReinsertOperation,
  385. targetPosition: new Position( root, [ 2 ] ),
  386. howMany: 1,
  387. baseVersion: 3
  388. }
  389. ]
  390. } );
  391. } );
  392. it( 'should not throw if delta transformed by undoing delta was not transformed by undone delta', () => {
  393. // This scenario may happen when deltas are undone in different order than usual LIFO order.
  394. const removeDelta1 = getRemoveDelta( new Position( root, [ 0 ] ), 1, baseVersion );
  395. const removeDelta2 = getRemoveDelta( new Position( root, [ 2 ] ), 1, baseVersion + 1 );
  396. const reinsertDelta1 = removeDelta1.getReversed();
  397. reinsertDelta1.baseVersion = 2;
  398. doc.history.addDelta( removeDelta1 );
  399. doc.history.addDelta( removeDelta2 );
  400. doc.history.addDelta( reinsertDelta1 );
  401. doc.history.setDeltaAsUndone( removeDelta1, reinsertDelta1 );
  402. const reinsertDelta2 = removeDelta2.getReversed();
  403. const { deltasA } = transformDeltaSets( [ reinsertDelta2 ], [ reinsertDelta1 ], doc );
  404. expectDelta( deltasA[ 0 ], {
  405. type: MoveDelta,
  406. operations: [
  407. {
  408. type: MoveOperation,
  409. sourcePosition: new Position( root, [ 0 ] ),
  410. targetPosition: new Position( root, [ 3 ] ),
  411. howMany: 1,
  412. baseVersion: 3
  413. }
  414. ]
  415. } );
  416. } );
  417. it( 'should use stickiness if additional context is not used', () => {
  418. const moveDeltaA = getMoveDelta( new Position( root, [ 0, 0 ] ), 1, new Position( root, [ 1, 0 ] ), baseVersion );
  419. const moveDeltaB = getMoveDelta( new Position( root, [ 1, 0 ] ), 3, new Position( root, [ 2, 2 ] ), baseVersion );
  420. moveDeltaB.operations[ 0 ].isSticky = true;
  421. const { deltasA } = transformDeltaSets( [ moveDeltaA ], [ moveDeltaB ], doc );
  422. expectDelta( deltasA[ 0 ], {
  423. type: MoveDelta,
  424. operations: [
  425. {
  426. type: MoveOperation,
  427. targetPosition: new Position( root, [ 2, 2 ] ),
  428. howMany: 1,
  429. baseVersion: 1
  430. }
  431. ]
  432. } );
  433. } );
  434. it( 'should discard stickiness if additional context is used and delta with sticky move operation has been undone', () => {
  435. // Assume following data model: <paragraph>ab</paragraph><paragraph>cd</paragraph>
  436. // Then we remove 'b': <paragraph>a</paragraph><paragraph>cd</paragraph>
  437. const removeDelta = getRemoveDelta( new Position( root, [ 0, 1 ] ), 1, baseVersion );
  438. // Then we merge paragraphs: <paragraph>acd</paragraph>
  439. // Keep in mind that merging has sticky move operation ('cd' is sticky-moved to the first paragraph).
  440. const mergeDelta = getMergeDelta( new Position( root, [ 1 ] ), 2, 1, baseVersion + 1 );
  441. // Then we reverse merging: <paragraph>a</paragraph><paragraph>cd</paragraph>
  442. // Keep in mind that now we have two deltas with sticky moves in a history.
  443. const splitDelta = mergeDelta.getReversed();
  444. doc.history.addDelta( removeDelta );
  445. doc.history.addDelta( mergeDelta );
  446. doc.history.addDelta( splitDelta );
  447. doc.history.setDeltaAsUndone( mergeDelta, splitDelta );
  448. // This delta should insert 'b' at position [ 0, 1 ].
  449. // However, without context data, after transforming `reinsertDelta` by `splitDelta`,
  450. // 'b' would "go away" together with 'cd'.
  451. const reinsertDelta = removeDelta.getReversed();
  452. const { deltasA } = transformDeltaSets( [ reinsertDelta ], [ mergeDelta, splitDelta ], doc );
  453. expectDelta( deltasA[ 0 ], {
  454. type: MoveDelta,
  455. operations: [
  456. {
  457. type: ReinsertOperation,
  458. targetPosition: new Position( root, [ 0, 1 ] ),
  459. howMany: 1,
  460. baseVersion: 5
  461. }
  462. ]
  463. } );
  464. } );
  465. it( 'should set `context.undoMode` in undo mode (when document is passed)', () => {
  466. const deltaA = new Delta();
  467. deltaA.addOperation( new NoOperation( 0 ) );
  468. const deltaB = new Delta();
  469. deltaB.addOperation( new NoOperation( 0 ) );
  470. sinon.spy( deltaTransform, 'transform' );
  471. transformDeltaSets( [ deltaA ], [ deltaB ], doc );
  472. const contextAB = deltaTransform.transform.args[ 0 ][ 2 ];
  473. const contextBA = deltaTransform.transform.args[ 1 ][ 2 ];
  474. expect( contextAB.undoMode ).to.be.true;
  475. expect( contextBA.undoMode ).to.be.true;
  476. } );
  477. } );
  478. } );
  479. } );