private @NotNull List<T> updateAffectedNodes(@NotNull DocumentEvent e, int reTargetShift,
                                      @NotNull List<? extends IntervalNode<T>> affected) {
    List<T> invalidated = affected.isEmpty() ? Collections.emptyList() : new ArrayList<>(affected.size());
    // reverse direction to visit leaves first - it's cheaper to compute maxEndOf for them first
    for (int i = affected.size() - 1; i >= 0; i--) {
      IntervalNode<T> node = affected.get(i);
      // assumption: interval.getEndOffset() will never be accessed during remove()
      int startOffset = node.intervalStart();
      int endOffset = node.intervalEnd();
      removeNode(node);
      checkMax(false);
      node.setParent(null);
      node.setLeft(null);
      node.setRight(null);
      node.setValid(true);
      if (reTargetShift == 0) {
        // we can do it because all the deltas up from the root to this node were cleared in the collectAffectedMarkersAndShiftSubtrees
        node.clearDelta();
        assert node.intervalStart() == startOffset;
        assert node.intervalEnd() == endOffset;
      }
      else {
        node.changeDelta(reTargetShift);
        pushDelta(node);
      }
    }
    checkMax(true);
    for (IntervalNode<T> node : affected) {
      RangeMarkerImpl marker = getAnyNodeMarker(node, invalidated);
      if (marker == null) continue; // node remains removed from the tree

      if (reTargetShift == 0) {
        marker.onDocumentChanged(e);
      }
      else {
        marker.onReTarget(e);
      }

      if (marker.isValid()) {
        findOrInsertWithIntervals(node);
      }
      else {
        node.setValid(false);
        ((RMNode<?>)node).onRemoved();
      }
      if (!node.isValid()) {
        node.processAliveKeys(t->invalidated.add(t));
      }
    }
    checkMax(true);
    return invalidated;
  }
private @NotNull List<T> updateAffectedNodes(@NotNull DocumentEvent e, int reTargetShift,
                                      @NotNull List<? extends IntervalNode<T>> affected)