void updateCachedOffsets() {
    if (!isFoldingEnabled()) {
      return;
    }
    if (myCachedVisible == null) {
      rebuild();
      return;
    }

    for (FoldRegion foldRegion : myCachedVisible) {
      if (!foldRegion.isValid()) {
        rebuild();
        return;
      }
    }

    int length = myCachedTopLevelRegions.length;
    if (myCachedEndOffsets == null || myCachedEndOffsets.length != length) {
      if (length != 0) {
        myCachedEndOffsets = new int[length];
        myCachedStartOffsets = new int[length];
        myCachedFoldedLines = new int[length];
      } else {
        myCachedEndOffsets = ArrayUtil.EMPTY_INT_ARRAY;
        myCachedStartOffsets = ArrayUtil.EMPTY_INT_ARRAY;
        myCachedFoldedLines = ArrayUtil.EMPTY_INT_ARRAY;
      }
    }

    int sum = 0;
    for (int i = 0; i < length; i++) {
      FoldRegion region = myCachedTopLevelRegions[i];
      myCachedStartOffsets[i] = region.getStartOffset();
      myCachedEndOffsets[i] = region.getEndOffset() - 1;
      Document document = region.getDocument();
      sum +=
          document.getLineNumber(region.getEndOffset())
              - document.getLineNumber(region.getStartOffset());
      myCachedFoldedLines[i] = sum;
    }
  }
  @Nullable
  FoldRegion fetchOutermost(int offset) {
    if (!isFoldingEnabledAndUpToDate()) return null;

    final int[] starts = myCachedStartOffsets;
    final int[] ends = myCachedEndOffsets;
    if (starts == null || ends == null) {
      return null;
    }

    int start = 0;
    int end = ends.length - 1;

    while (start <= end) {
      int i = (start + end) / 2;
      if (offset < starts[i]) {
        end = i - 1;
      } else if (offset > ends[i]) {
        start = i + 1;
      } else {
        // We encountered situation when cached data is inconsistent. It's not clear what produced
        // that, so, the following was done:
        //     1. Corresponding check was added and cached data is rebuilt in case of inconsistency;
        //     2. Debug asserts are activated if dedicated flag is on (it's off by default);
        if (myCachedStartOffsets[i] != myCachedTopLevelRegions[i].getStartOffset()) {
          if (DEBUG) {
            assert false
                : "inconsistent cached fold data detected. Start offsets: "
                    + Arrays.toString(myCachedStartOffsets)
                    + ", end offsets: "
                    + Arrays.toString(myCachedEndOffsets)
                    + ", top regions: "
                    + Arrays.toString(myCachedTopLevelRegions)
                    + ", visible regions: "
                    + Arrays.toString(myCachedVisible);
          }
          rebuild();
          return fetchOutermost(offset);
        }
        return myCachedTopLevelRegions[i];
      }
    }

    return null;
  }
 void onBulkDocumentUpdateFinished() {
   myFoldTree.rebuild();
 }
 @Override
 public void rebuild() {
   if (!myEditor.getDocument().isInBulkUpdate()) {
     myFoldTree.rebuild();
   }
 }
 @Override
 public void rebuild() {
   myFoldTree.rebuild();
 }
  private void doNotifyBatchFoldingProcessingDone() {
    myFoldTree.rebuild();

    for (FoldingListener listener : myListeners) {
      listener.onFoldProcessingEnd();
    }

    myEditor.updateCaretCursor();
    myEditor.recalculateSizeAndRepaint();
    if (myEditor.getGutterComponentEx().isFoldingOutlineShown()) {
      myEditor.getGutterComponentEx().repaint();
    }

    LogicalPosition caretPosition = myEditor.getCaretModel().getLogicalPosition();
    // There is a possible case that caret position is already visual position aware. But visual
    // position depends on number of folded
    // logical lines as well, hence, we can't be sure that target logical position defines correct
    // visual position because fold
    // regions have just changed. Hence, we use 'raw' logical position instead.
    if (caretPosition.visualPositionAware) {
      caretPosition = new LogicalPosition(caretPosition.line, caretPosition.column);
    }
    int caretOffset = myEditor.logicalPositionToOffset(caretPosition);
    boolean hasBlockSelection = myEditor.getSelectionModel().hasBlockSelection();
    int selectionStart = myEditor.getSelectionModel().getSelectionStart();
    int selectionEnd = myEditor.getSelectionModel().getSelectionEnd();

    int column = -1;
    int line = -1;
    int offsetToUse = -1;

    FoldRegion collapsed = myFoldTree.fetchOutermost(caretOffset);
    if (myCaretPositionSaved) {
      int savedOffset =
          myEditor.logicalPositionToOffset(new LogicalPosition(mySavedCaretY, mySavedCaretX));
      FoldRegion collapsedAtSaved = myFoldTree.fetchOutermost(savedOffset);
      if (collapsedAtSaved == null) {
        column = mySavedCaretX;
        line = mySavedCaretY;
      } else {
        offsetToUse = collapsedAtSaved.getStartOffset();
      }
    }

    if (collapsed != null && column == -1) {
      line = collapsed.getDocument().getLineNumber(collapsed.getStartOffset());
      column = myEditor.offsetToLogicalPosition(collapsed.getStartOffset()).column;
    }

    boolean oldCaretPositionSaved = myCaretPositionSaved;

    if (offsetToUse >= 0) {
      myEditor.getCaretModel().moveToOffset(offsetToUse);
    } else if (column != -1) {
      myEditor.getCaretModel().moveToLogicalPosition(new LogicalPosition(line, column));
    } else {
      myEditor.getCaretModel().moveToLogicalPosition(caretPosition);
    }

    myCaretPositionSaved = oldCaretPositionSaved;

    if (!hasBlockSelection && selectionStart < myEditor.getDocument().getTextLength()) {
      myEditor.getSelectionModel().setSelection(selectionStart, selectionEnd);
    }

    if (mySavedCaretShift > 0) {
      myEditor.getScrollingModel().disableAnimation();
      int scrollTo =
          myEditor.visibleLineToY(myEditor.getCaretModel().getVisualPosition().line)
              - mySavedCaretShift;
      myEditor.getScrollingModel().scrollVertically(scrollTo);
      myEditor.getScrollingModel().enableAnimation();
    }
  }