@Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    DocumentFoldingInfo info = (DocumentFoldingInfo) o;

    if (myFile != null ? !myFile.equals(info.myFile) : info.myFile != null) {
      return false;
    }
    if (!myProject.equals(info.myProject)
        || !myPsiElements.equals(info.myPsiElements)
        || !mySerializedElements.equals(info.mySerializedElements)) {
      return false;
    }

    if (myRangeMarkers.size() != info.myRangeMarkers.size()) return false;
    for (int i = 0; i < myRangeMarkers.size(); i++) {
      RangeMarker marker = myRangeMarkers.get(i);
      RangeMarker other = info.myRangeMarkers.get(i);
      if (marker == other || !marker.isValid() || !other.isValid()) {
        continue;
      }
      if (!TextRange.areSegmentsEqual(marker, other)) return false;

      FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
      FoldingInfo ofi = other.getUserData(FOLDING_INFO_KEY);
      if (!Comparing.equal(fi, ofi)) return false;
    }
    return true;
  }
  private void altCommitToOriginal(@NotNull DocumentEvent e) {
    final PsiFile origPsiFile =
        PsiDocumentManager.getInstance(myProject).getPsiFile(myOrigDocument);
    String newText = myNewDocument.getText();
    // prepare guarded blocks
    LinkedHashMap<String, String> replacementMap = new LinkedHashMap<String, String>();
    int count = 0;
    for (RangeMarker o : ContainerUtil.reverse(((DocumentEx) myNewDocument).getGuardedBlocks())) {
      String replacement = o.getUserData(REPLACEMENT_KEY);
      String tempText = "REPLACE" + (count++) + Long.toHexString(StringHash.calc(replacement));
      newText =
          newText.substring(0, o.getStartOffset()) + tempText + newText.substring(o.getEndOffset());
      replacementMap.put(tempText, replacement);
    }
    // run preformat processors
    final int hostStartOffset = myAltFullRange.getStartOffset();
    myEditor.getCaretModel().moveToOffset(hostStartOffset);
    for (CopyPastePreProcessor preProcessor :
        Extensions.getExtensions(CopyPastePreProcessor.EP_NAME)) {
      newText = preProcessor.preprocessOnPaste(myProject, origPsiFile, myEditor, newText, null);
    }
    myOrigDocument.replaceString(hostStartOffset, myAltFullRange.getEndOffset(), newText);
    // replace temp strings for guarded blocks
    for (String tempText : replacementMap.keySet()) {
      int idx =
          CharArrayUtil.indexOf(
              myOrigDocument.getCharsSequence(),
              tempText,
              hostStartOffset,
              myAltFullRange.getEndOffset());
      myOrigDocument.replaceString(idx, idx + tempText.length(), replacementMap.get(tempText));
    }
    // JAVA: fix occasional char literal concatenation
    fixDocumentQuotes(myOrigDocument, hostStartOffset - 1);
    fixDocumentQuotes(myOrigDocument, myAltFullRange.getEndOffset());

    // reformat
    PsiDocumentManager.getInstance(myProject).commitDocument(myOrigDocument);
    Runnable task =
        () -> {
          try {
            CodeStyleManager.getInstance(myProject)
                .reformatRange(origPsiFile, hostStartOffset, myAltFullRange.getEndOffset(), true);
          } catch (IncorrectOperationException e1) {
            // LOG.error(e);
          }
        };
    DocumentUtil.executeInBulk(myOrigDocument, true, task);

    PsiElement newInjected =
        InjectedLanguageManager.getInstance(myProject)
            .findInjectedElementAt(origPsiFile, hostStartOffset);
    DocumentWindow documentWindow =
        newInjected == null ? null : InjectedLanguageUtil.getDocumentWindow(newInjected);
    if (documentWindow != null) {
      myEditor.getCaretModel().moveToOffset(documentWindow.injectedToHost(e.getOffset()));
      myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
    }
  }
  @Override
  public void writeExternal(Element element) throws WriteExternalException {
    PsiDocumentManager.getInstance(myProject).commitAllDocuments();

    if (myPsiElements.isEmpty() && myRangeMarkers.isEmpty() && mySerializedElements.isEmpty()) {
      throw new WriteExternalException();
    }

    if (mySerializedElements.isEmpty()) {
      for (SmartPsiElementPointer<PsiElement> ptr : myPsiElements) {
        PsiElement psiElement = ptr.getElement();
        if (psiElement == null || !psiElement.isValid()) {
          continue;
        }
        FoldingInfo fi = psiElement.getUserData(FOLDING_INFO_KEY);
        boolean state = fi != null && fi.expanded;
        String signature = FoldingPolicy.getSignature(psiElement);
        if (signature == null) {
          continue;
        }

        PsiFile containingFile = psiElement.getContainingFile();
        PsiElement restoredElement = FoldingPolicy.restoreBySignature(containingFile, signature);
        if (!psiElement.equals(restoredElement)) {
          StringBuilder trace = new StringBuilder();
          PsiElement restoredAgain =
              FoldingPolicy.restoreBySignature(containingFile, signature, trace);
          LOG.error(
              "element: "
                  + psiElement
                  + "("
                  + psiElement.getText()
                  + "); restoredElement: "
                  + restoredElement
                  + "; signature: '"
                  + signature
                  + "'; file: "
                  + containingFile
                  + "; injected: "
                  + InjectedLanguageManager.getInstance(myProject)
                      .isInjectedFragment(containingFile)
                  + "; languages: "
                  + containingFile.getViewProvider().getLanguages()
                  + "; restored again: "
                  + restoredAgain
                  + "; restore produces same results: "
                  + (restoredAgain == restoredElement)
                  + "; trace:\n"
                  + trace);
        }

        Element e = new Element(ELEMENT_TAG);
        e.setAttribute(SIGNATURE_ATT, signature);
        e.setAttribute(EXPANDED_ATT, Boolean.toString(state));
        element.addContent(e);
      }
    } else {
      // get back postponed state (before folding initialization)
      for (SerializedPsiElement entry : mySerializedElements) {
        Element e = new Element(ELEMENT_TAG);
        e.setAttribute(SIGNATURE_ATT, entry.mySerializedElement);
        e.setAttribute(EXPANDED_ATT, Boolean.toString(entry.myFoldingInfo.getExpanded()));
        element.addContent(e);
      }
    }
    String date = null;
    for (RangeMarker marker : myRangeMarkers) {
      FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
      boolean state = fi != null && fi.expanded;

      Element e = new Element(MARKER_TAG);
      if (date == null) {
        date = getTimeStamp();
      }
      if (date.isEmpty()) {
        continue;
      }

      e.setAttribute(DATE_ATT, date);
      e.setAttribute(EXPANDED_ATT, Boolean.toString(state));
      String signature =
          Integer.valueOf(marker.getStartOffset()) + ":" + Integer.valueOf(marker.getEndOffset());
      e.setAttribute(SIGNATURE_ATT, signature);
      String placeHolderText = fi == null ? DEFAULT_PLACEHOLDER : fi.placeHolder;
      e.setAttribute(PLACEHOLDER_ATT, placeHolderText);
      element.addContent(e);
    }
  }
  @Override
  public void setToEditor(@NotNull final Editor editor) {
    assertDispatchThread();
    final PsiManager psiManager = PsiManager.getInstance(myProject);
    if (psiManager.isDisposed()) return;

    if (!myFile.isValid()) return;
    final PsiFile psiFile = psiManager.findFile(myFile);
    if (psiFile == null) return;

    if (!mySerializedElements.isEmpty()) {
      // Restore postponed state
      assert myPsiElements.isEmpty() : "Sequential deserialization";
      for (SerializedPsiElement entry : mySerializedElements) {
        PsiElement restoredElement =
            FoldingPolicy.restoreBySignature(psiFile, entry.mySerializedElement);
        if (restoredElement != null && restoredElement.isValid()) {
          myPsiElements.add(
              SmartPointerManager.getInstance(myProject)
                  .createSmartPsiElementPointer(restoredElement));
          restoredElement.putUserData(FOLDING_INFO_KEY, entry.myFoldingInfo);
        }
      }
      mySerializedElements.clear();
    }

    Map<PsiElement, FoldingDescriptor> ranges = null;
    for (SmartPsiElementPointer<PsiElement> ptr : myPsiElements) {
      PsiElement element = ptr.getElement();
      if (element == null || !element.isValid()) {
        continue;
      }

      if (ranges == null) {
        ranges = buildRanges(editor, psiFile);
      }
      FoldingDescriptor descriptor = ranges.get(element);
      if (descriptor == null) {
        continue;
      }

      TextRange range = descriptor.getRange();
      FoldRegion region =
          FoldingUtil.findFoldRegion(editor, range.getStartOffset(), range.getEndOffset());
      if (region != null) {
        FoldingInfo fi = element.getUserData(FOLDING_INFO_KEY);
        boolean state = fi != null && fi.expanded;
        region.setExpanded(state);
      }
    }
    for (RangeMarker marker : myRangeMarkers) {
      if (!marker.isValid()) {
        continue;
      }
      FoldRegion region =
          FoldingUtil.findFoldRegion(editor, marker.getStartOffset(), marker.getEndOffset());
      FoldingInfo info = marker.getUserData(FOLDING_INFO_KEY);
      if (region == null) {
        if (info != null) {
          region =
              editor
                  .getFoldingModel()
                  .addFoldRegion(marker.getStartOffset(), marker.getEndOffset(), info.placeHolder);
        }
        if (region == null) {
          return;
        }
      }

      boolean state = info != null && info.expanded;
      region.setExpanded(state);
    }
  }
 public void navigate(int injectedOffset) {
   if (myAction.isShowInBalloon()) {
     final JComponent component = myAction.createBalloonComponent(myNewFile);
     if (component != null) {
       final Balloon balloon =
           JBPopupFactory.getInstance()
               .createBalloonBuilder(component)
               .setShadow(true)
               .setAnimationCycle(0)
               .setHideOnClickOutside(true)
               .setHideOnKeyOutside(true)
               .setHideOnAction(false)
               .setFillColor(UIUtil.getControlColor())
               .createBalloon();
       new AnAction() {
         @Override
         public void actionPerformed(AnActionEvent e) {
           balloon.hide();
         }
       }.registerCustomShortcutSet(CommonShortcuts.ESCAPE, component);
       Disposer.register(myNewFile.getProject(), balloon);
       final Balloon.Position position = QuickEditAction.getBalloonPosition(myEditor);
       RelativePoint point = JBPopupFactory.getInstance().guessBestPopupLocation(myEditor);
       if (position == Balloon.Position.above) {
         final Point p = point.getPoint();
         point =
             new RelativePoint(
                 point.getComponent(), new Point(p.x, p.y - myEditor.getLineHeight()));
       }
       balloon.show(point, position);
     }
   } else {
     final FileEditorManagerEx fileEditorManager = FileEditorManagerEx.getInstanceEx(myProject);
     final FileEditor[] editors = fileEditorManager.getEditors(myNewVirtualFile);
     if (editors.length == 0) {
       final EditorWindow curWindow = fileEditorManager.getCurrentWindow();
       mySplittedWindow =
           curWindow.split(SwingConstants.HORIZONTAL, false, myNewVirtualFile, true);
     }
     Editor editor =
         fileEditorManager.openTextEditor(
             new OpenFileDescriptor(myProject, myNewVirtualFile, injectedOffset), true);
     // fold missing values
     if (editor != null) {
       editor.putUserData(QuickEditAction.QUICK_EDIT_HANDLER, this);
       final FoldingModel foldingModel = editor.getFoldingModel();
       foldingModel.runBatchFoldingOperation(
           () -> {
             for (RangeMarker o :
                 ContainerUtil.reverse(((DocumentEx) myNewDocument).getGuardedBlocks())) {
               String replacement = o.getUserData(REPLACEMENT_KEY);
               if (StringUtil.isEmpty(replacement)) continue;
               FoldRegion region =
                   foldingModel.addFoldRegion(o.getStartOffset(), o.getEndOffset(), replacement);
               if (region != null) region.setExpanded(false);
             }
           });
     }
     SwingUtilities.invokeLater(
         () -> myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE));
   }
 }