|
|
@@ -220,6 +220,59 @@ define [
|
|
|
rejectChangeIds: (change_ids) ->
|
|
|
changes = @rangesTracker.getChanges(change_ids)
|
|
|
return if changes.length == 0
|
|
|
+
|
|
|
+ # When doing bulk rejections, adjacent changes might interact with each other.
|
|
|
+ # Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
|
|
|
+ #
|
|
|
+ # "foo bar baz" -> "foo quux baz"
|
|
|
+ #
|
|
|
+ # The change above will be modeled with two ops, with the insertion going first:
|
|
|
+ #
|
|
|
+ # foo quux baz
|
|
|
+ # |--| -> insertion of "quux", op 1, at position 4
|
|
|
+ # | -> deletion of "bar", op 2, pushed forward by "quux" to position 8
|
|
|
+ #
|
|
|
+ # When rejecting these changes at once, if the insertion is rejected first, we get unexpected
|
|
|
+ # results. What happens is:
|
|
|
+ #
|
|
|
+ # 1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
|
|
|
+ # starting from position 4;
|
|
|
+ #
|
|
|
+ # "foo quux baz" -> "foo baz"
|
|
|
+ # |--| -> 4 characters to be removed
|
|
|
+ #
|
|
|
+ # 2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
|
|
|
+ # the word "quuux" was still present).
|
|
|
+ #
|
|
|
+ # "foo baz" -> "foo bazbar"
|
|
|
+ # | -> deletion of "bar" is reverted by reinserting "bar" at position 8
|
|
|
+ #
|
|
|
+ # While the intended result would be "foo bar baz", what we get is:
|
|
|
+ #
|
|
|
+ # "foo bazbar" (note "bar" readded at position 8)
|
|
|
+ #
|
|
|
+ # The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
|
|
|
+ # from position 4. This includes the position where the deletion exists; when that position is
|
|
|
+ # cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
|
|
|
+ # As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
|
|
|
+ # does so at the outdated position (position 8, which was valid when "quux" was present).
|
|
|
+ #
|
|
|
+ # To avoid this kind of problem, we need to make sure that reverting operations doesn't affect
|
|
|
+ # subsequent operations that come after. Reverse sorting the operations based on position will
|
|
|
+ # achieve it; in the case above, it makes sure that the the deletion is reverted first:
|
|
|
+ #
|
|
|
+ # 1) Rejecting the deletion adds the deleted word "bar" at position 8
|
|
|
+ #
|
|
|
+ # "foo quux baz" -> "foo quuxbar baz"
|
|
|
+ # | -> deletion of "bar" is reverted by
|
|
|
+ # reinserting "bar" at position 8
|
|
|
+ #
|
|
|
+ # 2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
|
|
|
+ # starting from position 4 and achieves the expected result:
|
|
|
+ #
|
|
|
+ # "foo quuxbar baz" -> "foo bar baz"
|
|
|
+ # |--| -> 4 characters to be removed
|
|
|
+
|
|
|
changes.sort((a, b) -> b.op.p - a.op.p)
|
|
|
|
|
|
session = @editor.getSession()
|