public void testPsi2DocMergeMultipleAdditionsWithReplace() throws Exception {
    StringBuilder buffer = new StringBuilder("0123456789");
    RangeMarker marker = createMarker(buffer.toString(), 2, 5);
    synchronizer.startTransaction(getProject(), document, psiFile);
    final PsiToDocumentSynchronizer.DocumentChangeTransaction transaction =
        synchronizer.getTransaction(document);
    final Set<Pair<PsiToDocumentSynchronizer.MutableTextRange, StringBuffer>> affectedFragments =
        transaction.getAffectedFragments();

    for (int i = 0; i < 10; i++) {
      synchronizer.insertString(document, i, "" + i);
      buffer.insert(i, "" + i);
    }

    assertEquals(1, affectedFragments.size());
    synchronizer.replaceString(document, 0, 20, "0123456789");
    buffer.replace(0, 20, "0123456789");

    assertEquals(1, affectedFragments.size());

    synchronizer.commitTransaction(document);

    assertEquals(buffer.toString(), document.getText());

    assertValidMarker(marker, 2, 5);
  }
  public void testDocSynchronizerPrefersLineBoundaryChanges() throws Exception {
    String text =
        "import java.awt.List;\n"
            + "[import java.util.ArrayList;\n]"
            + "import java.util.HashMap;\n"
            + "import java.util.Map;";
    RangeMarker marker = createMarker(text);
    synchronizer.startTransaction(getProject(), document, psiFile);

    String newText = StringUtil.replaceSubstring(document.getText(), TextRange.create(marker), "");
    synchronizer.replaceString(document, 0, document.getTextLength(), newText);

    final List<DocumentEvent> events = new ArrayList<DocumentEvent>();
    document.addDocumentListener(
        new DocumentAdapter() {
          @Override
          public void documentChanged(DocumentEvent e) {
            events.add(e);
          }
        });
    synchronizer.commitTransaction(document);

    assertEquals(newText, document.getText());
    DocumentEvent event = assertOneElement(events);
    assertEquals(
        "DocumentEventImpl[myOffset=22, myOldLength=28, myNewLength=0, myOldString='import java.util.ArrayList;\n', myNewString=''].",
        event.toString());
  }
  @Nullable
  public static Document getDocumentToBeUsedFor(final PsiFile file) {
    final Project project = file.getProject();
    final Document document = PsiDocumentManager.getInstance(project).getDocument(file);
    if (document == null) return null;
    if (PsiDocumentManager.getInstance(project).isUncommited(document)) return null;
    PsiToDocumentSynchronizer synchronizer =
        ((PsiDocumentManagerImpl) PsiDocumentManager.getInstance(project)).getSynchronizer();
    if (synchronizer.isDocumentAffectedByTransactions(document)) return null;

    return document;
  }
  public void testPsi2Doc1() throws Exception {
    StringBuilder buffer = new StringBuilder("0123456789");
    RangeMarker marker = createMarker(buffer.toString(), 2, 5);
    synchronizer.startTransaction(getProject(), document, psiFile);

    synchronizer.insertString(document, 3, "a");
    buffer.insert(3, "a");

    synchronizer.commitTransaction(this.document);

    assertEquals(buffer.toString(), document.getText());

    assertValidMarker(marker, 2, 6);
  }
  public boolean finishCommit(
      @NotNull final Document document,
      @NotNull final List<Processor<Document>> finishProcessors,
      final boolean synchronously,
      @NotNull final Object reason) {
    assert !myProject.isDisposed() : "Already disposed";
    final boolean[] ok = {true};
    ApplicationManager.getApplication()
        .runWriteAction(
            new CommitToPsiFileAction(document, myProject) {
              @Override
              public void run() {
                ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously);
              }
            });

    if (ok[0]) {
      // otherwise changes maybe not synced to the document yet, and injectors will crash
      if (!mySynchronizer.isDocumentAffectedByTransactions(document)) {
        final InjectedLanguageManager injectedLanguageManager =
            InjectedLanguageManager.getInstance(myProject);
        if (injectedLanguageManager != null)
          injectedLanguageManager.startRunInjectors(document, synchronously);
      }
      // run after commit actions outside write action
      runAfterCommitActions(document);
      if (DebugUtil.DO_EXPENSIVE_CHECKS) {
        checkAllElementsValid(document, reason);
      }
    }
    return ok[0];
  }
  public void testPsi2DocForwardRangesChanges() throws Exception {
    StringBuilder buffer = new StringBuilder("0123456789");
    RangeMarker marker = createMarker(buffer.toString(), 2, 5);
    synchronizer.startTransaction(getProject(), document, psiFile);

    synchronizer.replaceString(document, 4, 5, "3a4");
    buffer.replace(4, 5, "3a4");

    synchronizer.insertString(document, 7, "b");
    buffer.insert(7, "b");

    synchronizer.insertString(document, 1, "b");
    buffer.insert(1, "b");

    synchronizer.commitTransaction(document);

    assertEquals(buffer.toString(), document.getText());

    assertValidMarker(marker, 3, 8);
  }
  public void testPsi2DocSurround() throws Exception {
    StringBuilder buffer = new StringBuilder("0123456789");
    RangeMarker marker = createMarker(buffer.toString(), 2, 5);
    synchronizer.startTransaction(getProject(), document, psiFile);

    synchronizer.replaceString(document, 3, 5, "3a4");
    buffer.replace(3, 5, "3a4");

    synchronizer.insertString(document, 3, "b");
    buffer.insert(3, "b");

    synchronizer.insertString(document, 7, "d");
    buffer.insert(7, "d");

    final PsiToDocumentSynchronizer.DocumentChangeTransaction transaction =
        synchronizer.getTransaction(document);
    final Set<Pair<PsiToDocumentSynchronizer.MutableTextRange, StringBuffer>> affectedFragments =
        transaction.getAffectedFragments();
    assertEquals(3, affectedFragments.size());

    synchronizer.commitTransaction(document);

    assertEquals(buffer.toString(), document.getText());

    assertValidMarker(marker, 2, 7);
  }
  @Override
  public void beforeDocumentChange(@NotNull DocumentEvent event) {
    if (myStopTrackingDocuments) return;

    final Document document = event.getDocument();
    if (!(document instanceof DocumentWindow) && !myLastCommittedTexts.containsKey(document)) {
      myLastCommittedTexts.put(
          document,
          Pair.create(document.getImmutableCharSequence(), document.getModificationStamp()));
    }

    VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    boolean isRelevant = virtualFile != null && isRelevant(virtualFile);

    final FileViewProvider viewProvider = getCachedViewProvider(document);
    boolean inMyProject = viewProvider != null && viewProvider.getManager() == myPsiManager;
    if (!isRelevant || !inMyProject) {
      return;
    }

    final List<PsiFile> files = viewProvider.getAllFiles();
    PsiFile psiCause = null;
    for (PsiFile file : files) {
      if (file == null) {
        throw new AssertionError(
            "View provider "
                + viewProvider
                + " ("
                + viewProvider.getClass()
                + ") returned null in its files array: "
                + files
                + " for file "
                + viewProvider.getVirtualFile());
      }

      if (mySynchronizer.isInsideAtomicChange(file)) {
        psiCause = file;
      }
    }

    if (psiCause == null) {
      beforeDocumentChangeOnUnlockedDocument(viewProvider);
    }

    ((SingleRootFileViewProvider) viewProvider).beforeDocumentChanged(psiCause);
  }
 @TestOnly
 public void clearUncommittedDocuments() {
   myUncommittedDocuments.clear();
   mySynchronizer.cleanupForNextTest();
 }
  @Override
  public void documentChanged(DocumentEvent event) {
    if (myStopTrackingDocuments) return;

    final Document document = event.getDocument();
    VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    boolean isRelevant = virtualFile != null && isRelevant(virtualFile);

    final FileViewProvider viewProvider = getCachedViewProvider(document);
    if (viewProvider == null) {
      handleCommitWithoutPsi(document);
      return;
    }
    boolean inMyProject = viewProvider.getManager() == myPsiManager;
    if (!isRelevant || !inMyProject) {
      myLastCommittedTexts.remove(document);
      return;
    }

    ApplicationManager.getApplication().assertWriteAccessAllowed();
    final List<PsiFile> files = viewProvider.getAllFiles();
    boolean commitNecessary = true;
    for (PsiFile file : files) {

      if (mySynchronizer.isInsideAtomicChange(file)) {
        commitNecessary = false;
        continue;
      }

      assert file instanceof PsiFileImpl
              || "mock.file".equals(file.getName())
                  && ApplicationManager.getApplication().isUnitTestMode()
          : event + "; file=" + file + "; allFiles=" + files + "; viewProvider=" + viewProvider;
    }

    boolean forceCommit =
        ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class)
            && (SystemProperties.getBooleanProperty("idea.force.commit.on.external.change", false)
                || ApplicationManager.getApplication().isHeadlessEnvironment()
                    && !ApplicationManager.getApplication().isUnitTestMode());

    // Consider that it's worth to perform complete re-parse instead of merge if the whole document
    // text is replaced and
    // current document lines number is roughly above 5000. This makes sense in situations when
    // external change is performed
    // for the huge file (that causes the whole document to be reloaded and 'merge' way takes a
    // while to complete).
    if (event.isWholeTextReplaced() && document.getTextLength() > 100000) {
      document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
    }

    if (commitNecessary) {
      assert !(document instanceof DocumentWindow);
      myUncommittedDocuments.add(document);
      myDocumentCommitProcessor.log(
          "added uncommitted doc",
          null,
          false,
          myProject,
          document,
          ((DocumentEx) document).isInBulkUpdate());
      if (forceCommit) {
        commitDocument(document);
      } else if (!((DocumentEx) document).isInBulkUpdate() && myPerformBackgroundCommit) {
        myDocumentCommitProcessor.commitAsynchronously(myProject, document, event);
      }
    } else {
      myLastCommittedTexts.remove(document);
    }
  }