From bc94b5c032790eee1726973a0c508bcbb72e55c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Mon, 30 Mar 2020 16:08:34 +0200
Subject: [PATCH 1/7] Do not fix graveyard range if it would intersect with
 existing selection.

---
 src/model/documentselection.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/model/documentselection.js b/src/model/documentselection.js
index 0ebf2f1aa..947d1fd69 100644
--- a/src/model/documentselection.js
+++ b/src/model/documentselection.js
@@ -1137,7 +1137,7 @@ class LiveSelection extends Selection {
 		liveRange.detach();
 
 		// If nearest valid selection range has been found - add it in the place of old range.
-		if ( selectionRange ) {
+		if ( selectionRange && !isIntersecting( this._ranges, selectionRange ) ) {
 			// Check the range, convert it to live range, bind events, etc.
 			const newRange = this._prepareRange( selectionRange );
 
@@ -1190,3 +1190,15 @@ function clearAttributesStoredInElement( model, batch ) {
 		}
 	}
 }
+
+// Checks if range intersects with any given ranges.
+function isIntersecting( ranges, selectionRange ) {
+	for ( let i = 0; i < ranges.length; i++ ) {
+		if ( selectionRange.isIntersecting( ranges[ i ] ) ) {
+			// It is also fine - as we can have multiple ranges in the selection.
+			return true;
+		}
+	}
+
+	return false;
+}

From 3e0cfb5442b08baf5c72264295059722daaae4da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Tue, 31 Mar 2020 10:43:41 +0200
Subject: [PATCH 2/7] Define range intersection of collapsed ranges.

---
 tests/model/range.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/tests/model/range.js b/tests/model/range.js
index a6ee47035..48541ce54 100644
--- a/tests/model/range.js
+++ b/tests/model/range.js
@@ -156,6 +156,13 @@ describe( 'Range', () => {
 			const otherRange = new Range( new Position( otherRoot, [ 0 ] ), new Position( otherRoot, [ 1, 4 ] ) );
 			expect( range.isIntersecting( otherRange ) ).to.be.false;
 		} );
+
+		it( 'should return false if ranges are collapsed and equal', () => {
+			range = new Range( new Position( root, [ 1, 1 ] ) );
+			const otherRange = new Range( new Position( otherRoot, [ 1, 1 ] ) );
+
+			expect( range.isIntersecting( otherRange ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'static constructors', () => {

From 712efbe6882bbb7f72695006251b4be1246d1688 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Tue, 31 Mar 2020 10:46:40 +0200
Subject: [PATCH 3/7] Fix test names for fixing selection after move operation
 to graveyard.

---
 tests/model/documentselection.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/tests/model/documentselection.js b/tests/model/documentselection.js
index 84cba4fbc..ca91c1e1c 100644
--- a/tests/model/documentselection.js
+++ b/tests/model/documentselection.js
@@ -1710,7 +1710,7 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		describe( 'MoveOperation to graveyard', () => {
-			it( 'fix selection range if it ends up in graveyard #1', () => {
+			it( 'fix selection range if it ends up in graveyard - collapsed selection', () => {
 				selection._setTo( new Position( root, [ 1, 3 ] ) );
 
 				model.applyOperation(
@@ -1725,7 +1725,7 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 2 ] );
 			} );
 
-			it( 'fix selection range if it ends up in graveyard #2', () => {
+			it( 'fix selection range if it ends up in graveyard - text from non-collapsed selection is moved', () => {
 				selection._setTo( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
 
 				model.applyOperation(
@@ -1740,7 +1740,7 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 2 ] );
 			} );
 
-			it( 'fix selection range if it ends up in graveyard #3', () => {
+			it( 'fix selection range if it ends up in graveyard - parent of non-collapsed selection is moved', () => {
 				selection._setTo( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
 
 				model.applyOperation(
@@ -1755,7 +1755,7 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 6 ] );
 			} );
 
-			it( 'fix selection range if it ends up in graveyard #4 - whole content removed', () => {
+			it( 'fix selection range if it ends up in graveyard - whole content removed', () => {
 				model.applyOperation(
 					new MoveOperation(
 						new Position( root, [ 0 ] ),

From b0b3b5a4b59b93f42e90a0517ba94b31b927090d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Tue, 31 Mar 2020 10:47:35 +0200
Subject: [PATCH 4/7] Add test for fixing selection after move operation to
 graveyard for multi-range selection in text node.

---
 tests/model/documentselection.js | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/tests/model/documentselection.js b/tests/model/documentselection.js
index ca91c1e1c..8e4b72814 100644
--- a/tests/model/documentselection.js
+++ b/tests/model/documentselection.js
@@ -1778,6 +1778,28 @@ describe( 'DocumentSelection', () => {
 				// Now it's clear that it's the default range.
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
 			} );
+
+			it( 'does not break if multi-range selection is moved (inside text nodes - resulting ranges are collapsed)', () => {
+				const ranges = [
+					new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ),
+					new Range( new Position( root, [ 1, 3 ] ), new Position( root, [ 1, 4 ] ) )
+				];
+
+				selection._setTo( ranges );
+
+				model.applyOperation(
+					new MoveOperation(
+						new Position( root, [ 1, 1 ] ),
+						4,
+						new Position( doc.graveyard, [ 0 ] ),
+						doc.version
+					)
+				);
+
+				expect( selection.rangeCount ).to.equal( 2 );
+				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 1 ] );
+				expect( selection.getLastPosition().path ).to.deep.equal( [ 1, 1 ] );
+			} );
 		} );
 
 		it( '`DocumentSelection#change:range` event should be fire once even if selection contains multi-ranges', () => {

From 1d9d8cee8685e574eabc119c56459977c2eb5fc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Tue, 31 Mar 2020 11:45:56 +0200
Subject: [PATCH 5/7] Add test for intersecting ranges after multi-range
 selection moved to graveyard.

---
 tests/model/documentselection.js | 37 +++++++++++++++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)

diff --git a/tests/model/documentselection.js b/tests/model/documentselection.js
index 8e4b72814..6b1da29eb 100644
--- a/tests/model/documentselection.js
+++ b/tests/model/documentselection.js
@@ -1779,7 +1779,7 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
 			} );
 
-			it( 'does not break if multi-range selection is moved (inside text nodes - resulting ranges are collapsed)', () => {
+			it( 'does not break if multi-range selection is inside text nodes', () => {
 				const ranges = [
 					new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ),
 					new Range( new Position( root, [ 1, 3 ] ), new Position( root, [ 1, 4 ] ) )
@@ -1800,6 +1800,41 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 1 ] );
 				expect( selection.getLastPosition().path ).to.deep.equal( [ 1, 1 ] );
 			} );
+
+			it( 'does not break if multi-range selection is set on object nodes and resulting ranges will intersect', () => {
+				model.schema.register( 'outer', {
+					isObject: true
+				} );
+				model.schema.register( 'inner', {
+					isObject: true,
+					allowIn: 'outer'
+				} );
+
+				root._removeChildren( 0, root.childCount );
+				root._insertChild( 0, [
+					new Element( 'outer', [], [ new Element( 'inner' ), new Element( 'inner' ), new Element( 'inner' ) ] )
+				] );
+
+				const ranges = [
+					new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 1 ] ) ),
+					new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 2 ] ) )
+				];
+
+				selection._setTo( ranges );
+
+				model.applyOperation(
+					new MoveOperation(
+						new Position( root, [ 0, 0 ] ),
+						2,
+						new Position( doc.graveyard, [ 0 ] ),
+						doc.version
+					)
+				);
+
+				expect( selection.rangeCount ).to.equal( 1 );
+				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
+				expect( selection.getLastPosition().path ).to.deep.equal( [ 0, 1 ] );
+			} );
 		} );
 
 		it( '`DocumentSelection#change:range` event should be fire once even if selection contains multi-ranges', () => {

From ac8078d13000171cca8e90eedfdbdc9b5da71f97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Tue, 31 Mar 2020 12:07:33 +0200
Subject: [PATCH 6/7] Refactor internal helpers in
 DocumentSelection#_fixGraveyardSelection.

---
 src/model/documentselection.js | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/src/model/documentselection.js b/src/model/documentselection.js
index 947d1fd69..281d5afe0 100644
--- a/src/model/documentselection.js
+++ b/src/model/documentselection.js
@@ -1137,14 +1137,16 @@ class LiveSelection extends Selection {
 		liveRange.detach();
 
 		// If nearest valid selection range has been found - add it in the place of old range.
-		if ( selectionRange && !isIntersecting( this._ranges, selectionRange ) ) {
+		// If range is intersecting with other selection ranges then it is probably due to contents
+		// of a multi-range selection being removed. See ckeditor/ckeditor5#6501.
+		if ( selectionRange && !isRangeIntersectingWithSelection( selectionRange, this ) ) {
 			// Check the range, convert it to live range, bind events, etc.
 			const newRange = this._prepareRange( selectionRange );
 
 			// Add new range in the place of old range.
 			this._ranges.splice( index, 0, newRange );
 		}
-		// If nearest valid selection range cannot be found - just removing the old range is fine.
+		// If nearest valid selection range cannot be found or is intersecting with other selection ranges removing the old range is fine.
 	}
 }
 
@@ -1164,7 +1166,6 @@ function getAttrsIfCharacter( node ) {
 
 // Removes selection attributes from element which is not empty anymore.
 //
-// @private
 // @param {module:engine/model/model~Model} model
 // @param {module:engine/model/batch~Batch} batch
 function clearAttributesStoredInElement( model, batch ) {
@@ -1191,14 +1192,7 @@ function clearAttributesStoredInElement( model, batch ) {
 	}
 }
 
-// Checks if range intersects with any given ranges.
-function isIntersecting( ranges, selectionRange ) {
-	for ( let i = 0; i < ranges.length; i++ ) {
-		if ( selectionRange.isIntersecting( ranges[ i ] ) ) {
-			// It is also fine - as we can have multiple ranges in the selection.
-			return true;
-		}
-	}
-
-	return false;
+// Checks if range intersects with any of selection ranges.
+function isRangeIntersectingWithSelection( range, selection ) {
+	return !selection._ranges.every( selectionRange => !range.isIntersecting( selectionRange ) );
 }

From e4fea1664aa3644634a528f1035cee7eafb18cf7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= <jodator@jodator.net>
Date: Tue, 14 Apr 2020 11:18:41 +0200
Subject: [PATCH 7/7] Fixed conditions on checking ranges when fixing graveyard
 selection.

---
 src/model/documentselection.js   | 10 +++++-----
 tests/model/documentselection.js |  6 +++---
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/model/documentselection.js b/src/model/documentselection.js
index 281d5afe0..214bfd48b 100644
--- a/src/model/documentselection.js
+++ b/src/model/documentselection.js
@@ -1137,9 +1137,9 @@ class LiveSelection extends Selection {
 		liveRange.detach();
 
 		// If nearest valid selection range has been found - add it in the place of old range.
-		// If range is intersecting with other selection ranges then it is probably due to contents
+		// If range is equal to any other selection ranges then it is probably due to contents
 		// of a multi-range selection being removed. See ckeditor/ckeditor5#6501.
-		if ( selectionRange && !isRangeIntersectingWithSelection( selectionRange, this ) ) {
+		if ( selectionRange && !isRangeCollidingWithSelection( selectionRange, this ) ) {
 			// Check the range, convert it to live range, bind events, etc.
 			const newRange = this._prepareRange( selectionRange );
 
@@ -1192,7 +1192,7 @@ function clearAttributesStoredInElement( model, batch ) {
 	}
 }
 
-// Checks if range intersects with any of selection ranges.
-function isRangeIntersectingWithSelection( range, selection ) {
-	return !selection._ranges.every( selectionRange => !range.isIntersecting( selectionRange ) );
+// Checks if range collides with any of selection ranges.
+function isRangeCollidingWithSelection( range, selection ) {
+	return !selection._ranges.every( selectionRange => !range.isEqual( selectionRange ) );
 }
diff --git a/tests/model/documentselection.js b/tests/model/documentselection.js
index 6b1da29eb..17c9dd77e 100644
--- a/tests/model/documentselection.js
+++ b/tests/model/documentselection.js
@@ -1779,7 +1779,7 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
 			} );
 
-			it( 'does not break if multi-range selection is inside text nodes', () => {
+			it( 'handles multi-range selection in a text node by merging it into one range (resulting in collapsed ranges)', () => {
 				const ranges = [
 					new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ),
 					new Range( new Position( root, [ 1, 3 ] ), new Position( root, [ 1, 4 ] ) )
@@ -1796,12 +1796,12 @@ describe( 'DocumentSelection', () => {
 					)
 				);
 
-				expect( selection.rangeCount ).to.equal( 2 );
+				expect( selection.rangeCount ).to.equal( 1 );
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 1 ] );
 				expect( selection.getLastPosition().path ).to.deep.equal( [ 1, 1 ] );
 			} );
 
-			it( 'does not break if multi-range selection is set on object nodes and resulting ranges will intersect', () => {
+			it( 'handles multi-range selection on object nodes by merging it into one range (resulting in non-collapsed ranges)', () => {
 				model.schema.register( 'outer', {
 					isObject: true
 				} );