public abstract class PsiDocumentManagerBase extends PsiDocumentManager
    implements ProjectComponent, DocumentListener {
  protected static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl");
  protected static final Key<PsiFile> HARD_REF_TO_PSI = new Key<PsiFile>("HARD_REFERENCE_TO_PSI");
  protected static final Key<List<Runnable>> ACTION_AFTER_COMMIT =
      Key.create("ACTION_AFTER_COMMIT");

  protected final Project myProject;
  protected final PsiManager myPsiManager;
  protected final DocumentCommitProcessor myDocumentCommitProcessor;
  protected final Set<Document> myUncommittedDocuments =
      Collections.synchronizedSet(new HashSet<Document>());

  protected volatile boolean myIsCommitInProgress;
  protected final PsiToDocumentSynchronizer mySynchronizer;

  protected final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  protected final SmartPointerManagerImpl mySmartPointerManager;

  public PsiDocumentManagerBase(
      @NotNull final Project project,
      @NotNull PsiManager psiManager,
      @NotNull SmartPointerManager smartPointerManager,
      @NotNull MessageBus bus,
      @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) {
    myProject = project;
    myPsiManager = psiManager;
    myDocumentCommitProcessor = documentCommitProcessor;
    mySmartPointerManager = (SmartPointerManagerImpl) smartPointerManager;
    mySynchronizer = new PsiToDocumentSynchronizer(this, bus);
    myPsiManager.addPsiTreeChangeListener(mySynchronizer);
  }

  @Override
  public void projectOpened() {}

  @Override
  public void projectClosed() {}

  @Override
  @NotNull
  public String getComponentName() {
    return "PsiDocumentManager";
  }

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {}

  @Override
  @Nullable
  public PsiFile getPsiFile(@NotNull Document document) {
    final PsiFile userData = document.getUserData(HARD_REF_TO_PSI);
    if (userData != null) return userData;

    PsiFile psiFile = getCachedPsiFile(document);
    if (psiFile != null) return psiFile;

    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;

    psiFile = getPsiFile(virtualFile);
    if (psiFile == null) return null;

    fireFileCreated(document, psiFile);

    return psiFile;
  }

  public static void cachePsi(@NotNull Document document, @NotNull PsiFile file) {
    document.putUserData(HARD_REF_TO_PSI, file);
  }

  @Override
  public PsiFile getCachedPsiFile(@NotNull Document document) {
    final PsiFile userData = document.getUserData(HARD_REF_TO_PSI);
    if (userData != null) return userData;

    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;
    return getCachedPsiFile(virtualFile);
  }

  @Nullable
  public FileViewProvider getCachedViewProvider(@NotNull Document document) {
    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;
    return ((PsiManagerEx) myPsiManager).getFileManager().findCachedViewProvider(virtualFile);
  }

  @Nullable
  protected PsiFile getCachedPsiFile(VirtualFile virtualFile) {
    return ((PsiManagerEx) myPsiManager).getFileManager().getCachedPsiFile(virtualFile);
  }

  @Nullable
  protected PsiFile getPsiFile(VirtualFile virtualFile) {
    return ((PsiManagerEx) myPsiManager).getFileManager().findFile(virtualFile);
  }

  @Nullable
  @Override
  public Document getDocument(@NotNull PsiFile file) {
    if (file instanceof PsiBinaryFile) return null;

    Document document = getCachedDocument(file);
    if (document != null) {
      if (!file.getViewProvider().isPhysical() && document.getUserData(HARD_REF_TO_PSI) == null) {
        cachePsi(document, file);
      }
      return document;
    }

    if (!file.getViewProvider().isEventSystemEnabled()) return null;
    document =
        FileDocumentManager.getInstance().getDocument(file.getViewProvider().getVirtualFile());

    if (document != null) {
      if (document.getTextLength() != file.getTextLength()) {
        throw new AssertionError(
            "Modified PSI with no document: "
                + file
                + "; physical="
                + file.getViewProvider().isPhysical());
      }

      if (!file.getViewProvider().isPhysical()) {
        cachePsi(document, file);
      }
    }

    return document;
  }

  @Override
  public Document getCachedDocument(@NotNull PsiFile file) {
    if (!file.isPhysical()) return null;
    VirtualFile vFile = file.getViewProvider().getVirtualFile();
    return FileDocumentManager.getInstance().getCachedDocument(vFile);
  }

  @Override
  public void commitAllDocuments() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (myUncommittedDocuments.isEmpty()) return;

    final Document[] documents = getUncommittedDocuments();
    for (Document document : documents) {
      commitDocument(document);
    }

    LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments);
  }

  @Override
  public void performForCommittedDocument(
      @NotNull final Document doc, @NotNull final Runnable action) {
    final Document document =
        doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc;
    if (isCommitted(document)) {
      action.run();
    } else {
      addRunOnCommit(document, action);
    }
  }

  private final Map<Object, Runnable> actionsWhenAllDocumentsAreCommitted =
      new LinkedHashMap<Object, Runnable>(); // accessed from EDT only
  private static final Object PERFORM_ALWAYS_KEY =
      new Object() {
        @Override
        @NonNls
        public String toString() {
          return "PERFORM_ALWAYS";
        }
      };

  /**
   * Cancel previously registered action and schedules (new) action to be executed when all
   * documents are committed.
   *
   * @param key the (unique) id of the action.
   * @param action The action to be executed after automatic commit. This action will overwrite any
   *     action which was registered under this key earlier. The action will be executed in EDT.
   * @return true if action has been run immediately, or false if action was scheduled for execution
   *     later.
   */
  public boolean cancelAndRunWhenAllCommitted(
      @NonNls @NotNull Object key, @NotNull final Runnable action) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (myProject.isDisposed()) {
      action.run();
      return true;
    }
    if (myUncommittedDocuments.isEmpty()) {
      action.run();
      assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted;
      return true;
    }
    actionsWhenAllDocumentsAreCommitted.put(key, action);
    return false;
  }

  public static void addRunOnCommit(@NotNull Document document, @NotNull Runnable action) {
    synchronized (ACTION_AFTER_COMMIT) {
      List<Runnable> list = document.getUserData(ACTION_AFTER_COMMIT);
      if (list == null) {
        document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<Runnable>());
      }
      list.add(action);
    }
  }

  @Override
  public void commitDocument(@NotNull final Document doc) {
    final Document document =
        doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc;
    if (!isCommitted(document)) {
      doCommit(document, null);
    }
  }

  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];
  }

  protected boolean finishCommitInWriteAction(
      @NotNull final Document document,
      @NotNull final List<Processor<Document>> finishProcessors,
      final boolean synchronously) {
    if (myProject.isDisposed()) return false;

    assert !(document instanceof DocumentWindow);
    myIsCommitInProgress = true;
    boolean success = true;
    try {
      final FileViewProvider viewProvider = getCachedViewProvider(document);
      if (viewProvider != null) {
        for (Processor<Document> finishRunnable : finishProcessors) {
          success = finishRunnable.process(document);
          if (synchronously) {
            assert success;
          }
          if (!success) {
            break;
          }
        }
        viewProvider.contentsSynchronized();
      }
    } finally {
      myDocumentCommitProcessor.log(
          "in PDI.finishDoc: ", null, synchronously, success, myUncommittedDocuments);
      if (success) {
        myUncommittedDocuments.remove(document);
        myDocumentCommitProcessor.log(
            "in PDI.finishDoc: removed doc", null, synchronously, success, myUncommittedDocuments);
      }
      myIsCommitInProgress = false;
      myDocumentCommitProcessor.log(
          "in PDI.finishDoc: exit", null, synchronously, success, myUncommittedDocuments);
    }

    return success;
  }

  private void checkAllElementsValid(@NotNull Document document, @NotNull final Object reason) {
    final PsiFile psiFile = getCachedPsiFile(document);
    if (psiFile != null) {
      psiFile.accept(
          new PsiRecursiveElementWalkingVisitor() {
            @Override
            public void visitElement(PsiElement element) {
              if (!element.isValid()) {
                throw new AssertionError(
                    "Commit to '"
                        + psiFile.getVirtualFile()
                        + "' has led to invalid element: "
                        + element
                        + "; Reason: '"
                        + reason
                        + "'");
              }
            }
          });
    }
  }

  protected void doCommit(@NotNull final Document document, final PsiFile excludeFile) {
    assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener";
    ApplicationManager.getApplication()
        .runWriteAction(
            new Runnable() {
              @Override
              public void run() {
                // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged()
                if (getSynchronizer().isDocumentAffectedByTransactions(document)
                    && excludeFile == null) return;

                myIsCommitInProgress = true;
                try {
                  myDocumentCommitProcessor.commitSynchronously(document, myProject, excludeFile);
                } finally {
                  myIsCommitInProgress = false;
                }
                assert !myUncommittedDocuments.contains(document) : "Document :" + document;
              }
            });
  }

  public void commitOtherFilesAssociatedWithDocument(
      final Document document, final PsiFile psiFile) {}

  @Override
  public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) {
    final Ref<T> ref = Ref.create(null);
    commitAndRunReadAction(
        new Runnable() {
          @Override
          public void run() {
            ref.set(computation.compute());
          }
        });
    return ref.get();
  }

  @Override
  public void commitAndRunReadAction(@NotNull final Runnable runnable) {
    final Application application = ApplicationManager.getApplication();
    if (SwingUtilities.isEventDispatchThread()) {
      commitAllDocuments();
      runnable.run();
    } else {
      LOG.assertTrue(
          !ApplicationManager.getApplication().isReadAccessAllowed(),
          "Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock otherwise.");

      final Semaphore s1 = new Semaphore();
      final Semaphore s2 = new Semaphore();
      final boolean[] committed = {false};

      application.runReadAction(
          new Runnable() {
            @Override
            public void run() {
              if (myUncommittedDocuments.isEmpty()) {
                runnable.run();
                committed[0] = true;
              } else {
                s1.down();
                s2.down();
                final Runnable commitRunnable =
                    new Runnable() {
                      @Override
                      public void run() {
                        commitAllDocuments();
                        s1.up();
                        s2.waitFor();
                      }
                    };
                final ProgressIndicator progressIndicator =
                    ProgressManager.getInstance().getProgressIndicator();
                if (progressIndicator == null) {
                  ApplicationManager.getApplication().invokeLater(commitRunnable);
                } else {
                  ApplicationManager.getApplication()
                      .invokeLater(commitRunnable, progressIndicator.getModalityState());
                }
              }
            }
          });

      if (!committed[0]) {
        s1.waitFor();
        application.runReadAction(
            new Runnable() {
              @Override
              public void run() {
                s2.up();
                runnable.run();
              }
            });
      }
    }
  }

  /**
   * Schedules action to be executed when all documents are committed.
   *
   * @return true if action has been run immediately, or false if action was scheduled for execution
   *     later.
   */
  @Override
  public boolean performWhenAllCommitted(@NotNull final Runnable action) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    assert !myProject.isDisposed() : "Already disposed: " + myProject;
    if (myUncommittedDocuments.isEmpty()) {
      action.run();
      return true;
    }
    CompositeRunnable actions =
        (CompositeRunnable) actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY);
    if (actions == null) {
      actions = new CompositeRunnable();
      actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions);
    }
    actions.add(action);
    myDocumentCommitProcessor.log(
        "PDI: added performWhenAllCommitted", null, false, action, myUncommittedDocuments);
    return false;
  }

  private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable {
    @Override
    public void run() {
      for (Runnable runnable : this) {
        runnable.run();
      }
    }
  }

  private void runAfterCommitActions(@NotNull Document document) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    List<Runnable> list;
    synchronized (ACTION_AFTER_COMMIT) {
      list = document.getUserData(ACTION_AFTER_COMMIT);
      if (list != null) {
        list = new ArrayList<Runnable>(list);
        document.putUserData(ACTION_AFTER_COMMIT, null);
      }
    }
    if (list != null) {
      for (final Runnable runnable : list) {
        runnable.run();
      }
    }

    if (!hasUncommitedDocuments() && !actionsWhenAllDocumentsAreCommitted.isEmpty()) {
      List<Object> keys = new ArrayList<Object>(actionsWhenAllDocumentsAreCommitted.keySet());
      for (Object key : keys) {
        Runnable action = actionsWhenAllDocumentsAreCommitted.remove(key);
        myDocumentCommitProcessor.log("Running after commit runnable: ", null, false, key, action);
        action.run();
      }
    }
  }

  @Override
  public void addListener(@NotNull Listener listener) {
    myListeners.add(listener);
  }

  @Override
  public void removeListener(@NotNull Listener listener) {
    myListeners.remove(listener);
  }

  @Override
  public boolean isDocumentBlockedByPsi(@NotNull Document doc) {
    return false;
  }

  @Override
  public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) {}

  protected void fireDocumentCreated(@NotNull Document document, PsiFile file) {
    for (Listener listener : myListeners) {
      listener.documentCreated(document, file);
    }
  }

  private void fireFileCreated(Document document, PsiFile file) {
    for (Listener listener : myListeners) {
      listener.fileCreated(file, document);
    }
  }

  @Override
  @NotNull
  public Document[] getUncommittedDocuments() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    return myUncommittedDocuments.toArray(new Document[myUncommittedDocuments.size()]);
  }

  public Collection<Document> getUncommittedDocumentsUnsafe() {
    return myUncommittedDocuments;
  }

  @Override
  public boolean isUncommited(@NotNull Document document) {
    return !isCommitted(document);
  }

  @Override
  public boolean isCommitted(@NotNull Document document) {
    if (getSynchronizer().isInSynchronization(document)) return true;
    return !((DocumentEx) document).isInEventsHandling()
        && !myUncommittedDocuments.contains(document);
  }

  @Override
  public boolean hasUncommitedDocuments() {
    return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty();
  }

  @Override
  public void beforeDocumentChange(DocumentEvent event) {
    final Document document = event.getDocument();

    final FileViewProvider viewProvider = getCachedViewProvider(document);
    if (viewProvider == null) return;
    if (!isRelevant(viewProvider)) return;

    VirtualFile virtualFile = viewProvider.getVirtualFile();
    if (virtualFile.getFileType().isBinary()) return;

    final List<PsiFile> files = viewProvider.getAllFiles();
    PsiFile psiCause = null;
    for (PsiFile file : files) {
      mySmartPointerManager.fastenBelts(file, event.getOffset(), null);

      if (TextBlock.get(file).isLocked()) {
        psiCause = file;
      }
    }

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

    ((SingleRootFileViewProvider) viewProvider).beforeDocumentChanged(psiCause);
  }

  protected void beforeDocumentChangeOnUnlockedDocument(
      @NotNull final FileViewProvider viewProvider) {}

  @Override
  public void documentChanged(DocumentEvent event) {
    final Document document = event.getDocument();
    final FileViewProvider viewProvider = getCachedViewProvider(document);
    if (viewProvider == null) return;
    if (!isRelevant(viewProvider)) return;

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

      final TextBlock textBlock = TextBlock.get(file);
      if (textBlock.isLocked()) {
        commitNecessary = false;
        continue;
      }

      textBlock.documentChanged(event);
      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());

    // 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) {
      myUncommittedDocuments.add(document);
      if (forceCommit) {
        commitDocument(document);
      } else if (!((DocumentEx) document).isInBulkUpdate()) {
        myDocumentCommitProcessor.commitAsynchronously(myProject, document, event);
      }
    }
  }

  private boolean isRelevant(@NotNull FileViewProvider viewProvider) {
    VirtualFile virtualFile = viewProvider.getVirtualFile();
    return !virtualFile.getFileType().isBinary()
        && viewProvider.getManager() == myPsiManager
        && !myPsiManager.getProject().isDisposed();
  }

  public static boolean checkConsistency(PsiFile psiFile, Document document) {
    // todo hack
    if (psiFile.getVirtualFile() == null) return true;

    CharSequence editorText = document.getCharsSequence();
    int documentLength = document.getTextLength();
    if (psiFile.textMatches(editorText)) {
      LOG.assertTrue(psiFile.getTextLength() == documentLength);
      return true;
    }

    char[] fileText = psiFile.textToCharArray();
    @SuppressWarnings({"NonConstantStringShouldBeStringBuffer"})
    @NonNls
    String error =
        "File '"
            + psiFile.getName()
            + "' text mismatch after reparse. "
            + "File length="
            + fileText.length
            + "; Doc length="
            + documentLength
            + "\n";
    int i = 0;
    for (; i < documentLength; i++) {
      if (i >= fileText.length) {
        error += "editorText.length > psiText.length i=" + i + "\n";
        break;
      }
      if (i >= editorText.length()) {
        error += "editorText.length > psiText.length i=" + i + "\n";
        break;
      }
      if (editorText.charAt(i) != fileText[i]) {
        error += "first unequal char i=" + i + "\n";
        break;
      }
    }
    // error += "*********************************************" + "\n";
    // if (i <= 500){
    //  error += "Equal part:" + editorText.subSequence(0, i) + "\n";
    // }
    // else{
    //  error += "Equal part start:\n" + editorText.subSequence(0, 200) + "\n";
    //  error += "................................................" + "\n";
    //  error += "................................................" + "\n";
    //  error += "................................................" + "\n";
    //  error += "Equal part end:\n" + editorText.subSequence(i - 200, i) + "\n";
    // }
    error += "*********************************************" + "\n";
    error +=
        "Editor Text tail:("
            + (documentLength - i)
            + ")\n"; // + editorText.subSequence(i, Math.min(i + 300, documentLength)) + "\n";
    error += "*********************************************" + "\n";
    error += "Psi Text tail:(" + (fileText.length - i) + ")\n";
    error += "*********************************************" + "\n";

    if (document instanceof DocumentWindow) {
      error += "doc: '" + document.getText() + "'\n";
      error += "psi: '" + psiFile.getText() + "'\n";
      error += "ast: '" + psiFile.getNode().getText() + "'\n";
      error += psiFile.getLanguage() + "\n";
      PsiElement context =
          InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile);
      if (context != null) {
        error += "context: " + context + "; text: '" + context.getText() + "'\n";
        error += "context file: " + context.getContainingFile() + "\n";
      }
      error +=
          "document window ranges: "
              + Arrays.asList(((DocumentWindow) document).getHostRanges())
              + "\n";
    }
    LOG.error(error);
    // document.replaceString(0, documentLength, psiFile.getText());
    return false;
  }

  @TestOnly
  public void clearUncommittedDocuments() {
    myUncommittedDocuments.clear();
    mySynchronizer.cleanupForNextTest();
  }

  public PsiToDocumentSynchronizer getSynchronizer() {
    return mySynchronizer;
  }
}
  @Override
  protected void collectInformationWithProgress(@NotNull final ProgressIndicator progress) {
    if (!Registry.is("editor.injected.highlighting.enabled")) return;

    final Set<HighlightInfo> gotHighlights = new THashSet<HighlightInfo>(100);

    final List<PsiElement> inside = new ArrayList<PsiElement>();
    final List<PsiElement> outside = new ArrayList<PsiElement>();
    List<ProperTextRange> insideRanges = new ArrayList<ProperTextRange>();
    List<ProperTextRange> outsideRanges = new ArrayList<ProperTextRange>();
    // TODO: this thing is just called TWICE with same arguments eating CPU on huge files :(
    Divider.divideInsideAndOutside(
        myFile,
        myRestrictRange.getStartOffset(),
        myRestrictRange.getEndOffset(),
        myPriorityRange,
        inside,
        insideRanges,
        outside,
        outsideRanges,
        false,
        SHOULD_HIGHIGHT_FILTER);

    // all infos for the "injected fragment for the host which is inside" are indeed inside
    // but some of the infos for the "injected fragment for the host which is outside" can be still
    // inside
    Set<HighlightInfo> injectedResult = new THashSet<HighlightInfo>();
    Set<PsiFile> injected = getInjectedPsiFiles(inside, outside, progress);
    setProgressLimit(injected.size());

    if (!addInjectedPsiHighlights(
        injected, progress, Collections.synchronizedSet(injectedResult))) {
      throw new ProcessCanceledException();
    }
    final List<HighlightInfo> injectionsOutside =
        new ArrayList<HighlightInfo>(gotHighlights.size());

    Set<HighlightInfo> result;
    synchronized (injectedResult) {
      // sync here because all writes happened in another thread
      result = injectedResult;
    }
    for (HighlightInfo info : result) {
      if (myRestrictRange.contains(info)) {
        gotHighlights.add(info);
      } else {
        // nonconditionally apply injected results regardless whether they are in
        // myStartOffset,myEndOffset
        injectionsOutside.add(info);
      }
    }

    if (!injectionsOutside.isEmpty()) {
      final ProperTextRange priorityIntersection = myPriorityRange.intersection(myRestrictRange);
      if ((!inside.isEmpty() || !gotHighlights.isEmpty())
          && priorityIntersection
              != null) { // do not apply when there were no elements to highlight
        // clear infos found in visible area to avoid applying them twice
        final List<HighlightInfo> toApplyInside = new ArrayList<HighlightInfo>(gotHighlights);
        myHighlights.addAll(toApplyInside);
        gotHighlights.clear();

        myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(
            myHighlightingSession, toApplyInside, myPriorityRange, myRestrictRange, getId());
      }

      List<HighlightInfo> toApply = new ArrayList<HighlightInfo>();
      for (HighlightInfo info : gotHighlights) {
        if (!myRestrictRange.containsRange(info.getStartOffset(), info.getEndOffset())) continue;
        if (!myPriorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) {
          toApply.add(info);
        }
      }
      toApply.addAll(injectionsOutside);

      myHighlightInfoProcessor.highlightsOutsideVisiblePartAreProduced(
          myHighlightingSession,
          toApply,
          myRestrictRange,
          new ProperTextRange(0, myDocument.getTextLength()),
          getId());
    } else {
      // else apply only result (by default apply command) and only within inside
      myHighlights.addAll(gotHighlights);
      myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(
          myHighlightingSession, myHighlights, myRestrictRange, myRestrictRange, getId());
    }
  }
  private void processBatch(
      @NotNull final ProgressIndicator indicator, @NotNull Set<VirtualFile> files) {
    assert !myApplication.isDispatchThread();
    final int resolvedInPreviousBatch = this.resolvedInPreviousBatch;
    final int totalSize = files.size() + resolvedInPreviousBatch;
    final ConcurrentIntObjectMap<int[]> fileToForwardIds =
        ContainerUtil.createConcurrentIntObjectMap();
    final Set<VirtualFile> toProcess = Collections.synchronizedSet(files);
    indicator.setIndeterminate(false);
    ProgressIndicatorUtils.forceWriteActionPriority(indicator, (Disposable) indicator);
    long start = System.currentTimeMillis();
    Processor<VirtualFile> processor =
        file -> {
          double fraction = 1 - toProcess.size() * 1.0 / totalSize;
          indicator.setFraction(fraction);
          try {
            if (!file.isDirectory() && toResolve(file, myProject)) {
              int fileId = getAbsId(file);
              int i = totalSize - toProcess.size();
              indicator.setText(i + "/" + totalSize + ": Resolving " + file.getPresentableUrl());
              int[] forwardIds = processFile(file, fileId, indicator);
              if (forwardIds == null) {
                // queueUpdate(file);
                return false;
              }
              fileToForwardIds.put(fileId, forwardIds);
            }
            toProcess.remove(file);
            return true;
          } catch (RuntimeException e) {
            indicator.checkCanceled();
          }
          return true;
        };
    boolean success = true;
    try {
      success = processFilesConcurrently(files, indicator, processor);
    } finally {
      this.resolvedInPreviousBatch = toProcess.isEmpty() ? 0 : totalSize - toProcess.size();
      queue(toProcess, "re-added after fail. success=" + success);
      storeIds(fileToForwardIds);

      long end = System.currentTimeMillis();
      log(
          "Resolved batch of "
              + (totalSize - toProcess.size())
              + " from "
              + totalSize
              + " files in "
              + ((end - start) / 1000)
              + "sec. (Gap: "
              + storage.gap
              + ")");
      synchronized (filesToResolve) {
        upToDate = filesToResolve.isEmpty();
        log("upToDate = " + upToDate);
        if (upToDate) {
          for (Listener listener : myListeners) {
            listener.allFilesResolved();
          }
        }
      }
    }
  }
  public void inlineElement(
      final Project project, final Editor editor, final PsiElement psiElement) {
    final PsiParameter psiParameter = (PsiParameter) psiElement;
    final PsiParameterList parameterList = (PsiParameterList) psiParameter.getParent();
    if (!(parameterList.getParent() instanceof PsiMethod)) {
      return;
    }
    final int index = parameterList.getParameterIndex(psiParameter);
    final PsiMethod method = (PsiMethod) parameterList.getParent();

    String errorMessage = getCannotInlineMessage(psiParameter, method);
    if (errorMessage != null) {
      CommonRefactoringUtil.showErrorHint(
          project,
          editor,
          errorMessage,
          RefactoringBundle.message("inline.parameter.refactoring"),
          null);
      return;
    }

    final Ref<PsiExpression> refInitializer = new Ref<PsiExpression>();
    final Ref<PsiExpression> refConstantInitializer = new Ref<PsiExpression>();
    final Ref<PsiCallExpression> refMethodCall = new Ref<PsiCallExpression>();
    final List<PsiReference> occurrences =
        Collections.synchronizedList(new ArrayList<PsiReference>());
    final Collection<PsiFile> containingFiles = Collections.synchronizedSet(new HashSet<PsiFile>());
    containingFiles.add(psiParameter.getContainingFile());
    boolean result =
        ReferencesSearch.search(method)
            .forEach(
                new Processor<PsiReference>() {
                  public boolean process(final PsiReference psiReference) {
                    PsiElement element = psiReference.getElement();
                    final PsiElement parent = element.getParent();
                    if (parent instanceof PsiCallExpression) {
                      final PsiCallExpression methodCall = (PsiCallExpression) parent;
                      occurrences.add(psiReference);
                      containingFiles.add(element.getContainingFile());
                      final PsiExpression[] expressions =
                          methodCall.getArgumentList().getExpressions();
                      if (expressions.length <= index) return false;
                      PsiExpression argument = expressions[index];
                      if (!refInitializer.isNull()) {
                        return argument != null
                            && PsiEquivalenceUtil.areElementsEquivalent(
                                refInitializer.get(), argument)
                            && PsiEquivalenceUtil.areElementsEquivalent(
                                refMethodCall.get(), methodCall);
                      }
                      if (InlineToAnonymousConstructorProcessor.isConstant(argument)
                          || getReferencedFinalField(argument) != null) {
                        if (refConstantInitializer.isNull()) {
                          refConstantInitializer.set(argument);
                        } else if (!isSameConstant(argument, refConstantInitializer.get())) {
                          return false;
                        }
                      } else if (!isRecursiveReferencedParameter(argument, psiParameter)) {
                        if (!refConstantInitializer.isNull()) return false;
                        refInitializer.set(argument);
                        refMethodCall.set(methodCall);
                      }
                    }
                    return true;
                  }
                });
    final int offset = editor.getCaretModel().getOffset();
    final PsiElement refExpr = psiElement.getContainingFile().findElementAt(offset);
    final PsiCodeBlock codeBlock = PsiTreeUtil.getParentOfType(refExpr, PsiCodeBlock.class);
    if (codeBlock != null) {
      final PsiElement[] defs = DefUseUtil.getDefs(codeBlock, psiParameter, refExpr);
      if (defs.length == 1) {
        final PsiElement def = defs[0];
        if (def instanceof PsiReferenceExpression
            && PsiUtil.isOnAssignmentLeftHand((PsiExpression) def)) {
          final PsiExpression rExpr = ((PsiAssignmentExpression) def.getParent()).getRExpression();
          if (rExpr != null) {
            final PsiElement[] refs = DefUseUtil.getRefs(codeBlock, psiParameter, refExpr);

            if (InlineLocalHandler.checkRefsInAugmentedAssignmentOrUnaryModified(refs, def)
                == null) {
              new WriteCommandAction(project) {
                @Override
                protected void run(Result result) throws Throwable {
                  for (final PsiElement ref : refs) {
                    InlineUtil.inlineVariable(
                        psiParameter, rExpr, (PsiJavaCodeReferenceElement) ref);
                  }
                  def.getParent().delete();
                }
              }.execute();
              return;
            }
          }
        }
      }
    }
    if (occurrences.isEmpty()) {
      CommonRefactoringUtil.showErrorHint(
          project,
          editor,
          "Method has no usages",
          RefactoringBundle.message("inline.parameter.refactoring"),
          null);
      return;
    }
    if (!result) {
      CommonRefactoringUtil.showErrorHint(
          project,
          editor,
          "Cannot find constant initializer for parameter",
          RefactoringBundle.message("inline.parameter.refactoring"),
          null);
      return;
    }
    if (!refInitializer.isNull()) {
      if (ApplicationManager.getApplication().isUnitTestMode()) {
        final InlineParameterExpressionProcessor processor =
            new InlineParameterExpressionProcessor(
                refMethodCall.get(),
                method,
                psiParameter,
                refInitializer.get(),
                method
                    .getProject()
                    .getUserData(InlineParameterExpressionProcessor.CREATE_LOCAL_FOR_TESTS));
        processor.run();
      } else {
        final boolean createLocal = ReferencesSearch.search(psiParameter).findAll().size() > 1;
        InlineParameterDialog dlg =
            new InlineParameterDialog(
                refMethodCall.get(), method, psiParameter, refInitializer.get(), createLocal);
        dlg.show();
      }
      return;
    }
    if (refConstantInitializer.isNull()) {
      CommonRefactoringUtil.showErrorHint(
          project,
          editor,
          "Cannot find constant initializer for parameter",
          RefactoringBundle.message("inline.parameter.refactoring"),
          null);
      return;
    }

    final Ref<Boolean> isNotConstantAccessible = new Ref<Boolean>();
    final PsiExpression constantExpression = refConstantInitializer.get();
    constantExpression.accept(
        new JavaRecursiveElementVisitor() {
          @Override
          public void visitReferenceExpression(PsiReferenceExpression expression) {
            super.visitReferenceExpression(expression);
            final PsiElement resolved = expression.resolve();
            if (resolved instanceof PsiMember
                && !PsiUtil.isAccessible((PsiMember) resolved, method, null)) {
              isNotConstantAccessible.set(Boolean.TRUE);
            }
          }
        });
    if (!isNotConstantAccessible.isNull() && isNotConstantAccessible.get()) {
      CommonRefactoringUtil.showErrorHint(
          project,
          editor,
          "Constant initializer is not accessible in method body",
          RefactoringBundle.message("inline.parameter.refactoring"),
          null);
      return;
    }

    for (PsiReference psiReference : ReferencesSearch.search(psiParameter)) {
      final PsiElement element = psiReference.getElement();
      if (element instanceof PsiExpression
          && PsiUtil.isAccessedForWriting((PsiExpression) element)) {
        CommonRefactoringUtil.showErrorHint(
            project,
            editor,
            "Inline parameter which has write usages is not supported",
            RefactoringBundle.message("inline.parameter.refactoring"),
            null);
        return;
      }
    }

    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      String occurencesString = RefactoringBundle.message("occurences.string", occurrences.size());
      String question =
          RefactoringBundle.message(
                  "inline.parameter.confirmation",
                  psiParameter.getName(),
                  constantExpression.getText())
              + " "
              + occurencesString;
      RefactoringMessageDialog dialog =
          new RefactoringMessageDialog(
              REFACTORING_NAME,
              question,
              HelpID.INLINE_VARIABLE,
              "OptionPane.questionIcon",
              true,
              project);
      dialog.show();
      if (!dialog.isOK()) {
        return;
      }
    }

    final RefactoringEventData data = new RefactoringEventData();
    data.addElement(psiElement.copy());
    project
        .getMessageBus()
        .syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC)
        .refactoringStarted(REFACTORING_ID, data);

    SameParameterValueInspection.InlineParameterValueFix.inlineSameParameterValue(
        method, psiParameter, constantExpression);

    project
        .getMessageBus()
        .syncPublisher(RefactoringEventListener.REFACTORING_EVENT_TOPIC)
        .refactoringDone(REFACTORING_ID, null);
  }
/** @author cdr */
public class InjectedLanguageManagerImpl extends InjectedLanguageManager implements Disposable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl");
  static final Object ourInjectionPsiLock = new String("injectionPsiLock");
  private final Project myProject;
  private final DumbService myDumbService;
  private volatile DaemonProgressIndicator myProgress;

  public static InjectedLanguageManagerImpl getInstanceImpl(Project project) {
    return (InjectedLanguageManagerImpl) InjectedLanguageManager.getInstance(project);
  }

  public InjectedLanguageManagerImpl(Project project, DumbService dumbService) {
    myProject = project;
    myDumbService = dumbService;

    final ExtensionPoint<MultiHostInjector> multiPoint =
        Extensions.getArea(project).getExtensionPoint(MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME);
    ((ExtensionPointImpl<MultiHostInjector>) multiPoint)
        .addExtensionPointListener(
            new ExtensionPointListener<MultiHostInjector>() {
              @Override
              public void extensionAdded(
                  @NotNull MultiHostInjector injector,
                  @Nullable PluginDescriptor pluginDescriptor) {
                clearInjectorCache();
              }

              @Override
              public void extensionRemoved(
                  @NotNull MultiHostInjector injector,
                  @Nullable PluginDescriptor pluginDescriptor) {
                clearInjectorCache();
              }
            },
            false,
            this);
    final ExtensionPointListener<LanguageInjector> myListener =
        new ExtensionPointListener<LanguageInjector>() {
          @Override
          public void extensionAdded(
              @NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) {
            clearInjectorCache();
          }

          @Override
          public void extensionRemoved(
              @NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) {
            clearInjectorCache();
          }
        };
    final ExtensionPoint<LanguageInjector> psiManagerPoint =
        Extensions.getRootArea().getExtensionPoint(LanguageInjector.EXTENSION_POINT_NAME);
    ((ExtensionPointImpl<LanguageInjector>) psiManagerPoint)
        .addExtensionPointListener(myListener, false, this);
    myProgress = new DaemonProgressIndicator();
    project
        .getMessageBus()
        .connect(this)
        .subscribe(
            DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
            new DaemonCodeAnalyzer.DaemonListenerAdapter() {
              @Override
              public void daemonCancelEventOccurred(@NotNull String reason) {
                if (!myProgress.isCanceled()) myProgress.cancel();
              }
            });
  }

  @Override
  public void dispose() {
    myProgress.cancel();
    EditorWindowImpl.disposeInvalidEditors();
  }

  @Override
  public void startRunInjectors(@NotNull final Document hostDocument, final boolean synchronously) {
    if (myProject.isDisposed()) return;
    if (!synchronously && ApplicationManager.getApplication().isWriteAccessAllowed()) return;
    // use cached to avoid recreate PSI in alien project
    final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
    final PsiFile hostPsiFile = documentManager.getCachedPsiFile(hostDocument);
    if (hostPsiFile == null) return;

    final ConcurrentList<DocumentWindow> injected =
        InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile);
    if (injected.isEmpty()) return;

    if (myProgress.isCanceled()) {
      myProgress = new DaemonProgressIndicator();
    }
    final Set<DocumentWindow> newDocuments = Collections.synchronizedSet(new THashSet<>());
    final Processor<DocumentWindow> commitProcessor =
        documentWindow -> {
          if (myProject.isDisposed()) return false;
          ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
          if (indicator != null && indicator.isCanceled()) return false;
          if (documentManager.isUncommited(hostDocument) || !hostPsiFile.isValid())
            return false; // will be committed later

          // it is here where the reparse happens and old file contents replaced
          InjectedLanguageUtil.enumerate(
              documentWindow,
              hostPsiFile,
              (injectedPsi, places) -> {
                DocumentWindow newDocument =
                    (DocumentWindow) injectedPsi.getViewProvider().getDocument();
                if (newDocument != null) {
                  PsiDocumentManagerBase.checkConsistency(injectedPsi, newDocument);
                  newDocuments.add(newDocument);
                }
              });
          return true;
        };
    final Runnable commitInjectionsRunnable =
        () -> {
          if (myProgress.isCanceled()) return;
          JobLauncher.getInstance()
              .invokeConcurrentlyUnderProgress(
                  new ArrayList<>(injected), myProgress, true, commitProcessor);

          synchronized (ourInjectionPsiLock) {
            injected.clear();
            injected.addAll(newDocuments);
          }
        };

    if (synchronously) {
      if (Thread.holdsLock(PsiLock.LOCK)) {
        // hack for the case when docCommit was called from within PSI modification, e.g. in
        // formatter.
        // we can't spawn threads to do injections there, otherwise a deadlock is imminent
        ContainerUtil.process(new ArrayList<>(injected), commitProcessor);
      } else {
        commitInjectionsRunnable.run();
      }
    } else {
      JobLauncher.getInstance()
          .submitToJobThread(
              () ->
                  ApplicationManagerEx.getApplicationEx()
                      .tryRunReadAction(commitInjectionsRunnable),
              null);
    }
  }

  @Override
  public PsiLanguageInjectionHost getInjectionHost(@NotNull PsiElement element) {
    final PsiFile file = element.getContainingFile();
    final VirtualFile virtualFile = file == null ? null : file.getVirtualFile();
    if (virtualFile instanceof VirtualFileWindow) {
      PsiElement host =
          FileContextUtil.getFileContext(
              file); // use utility method in case the file's overridden getContext()
      if (host instanceof PsiLanguageInjectionHost) {
        return (PsiLanguageInjectionHost) host;
      }
    }
    return null;
  }

  @Override
  @NotNull
  public TextRange injectedToHost(
      @NotNull PsiElement injectedContext, @NotNull TextRange injectedTextRange) {
    PsiFile file = injectedContext.getContainingFile();
    if (file == null) return injectedTextRange;
    Document document = PsiDocumentManager.getInstance(file.getProject()).getCachedDocument(file);
    if (!(document instanceof DocumentWindowImpl)) return injectedTextRange;
    DocumentWindowImpl documentWindow = (DocumentWindowImpl) document;
    return documentWindow.injectedToHost(injectedTextRange);
  }

  @Override
  public int injectedToHost(@NotNull PsiElement element, int offset) {
    PsiFile file = element.getContainingFile();
    if (file == null) return offset;
    Document document = PsiDocumentManager.getInstance(file.getProject()).getCachedDocument(file);
    if (!(document instanceof DocumentWindowImpl)) return offset;
    DocumentWindowImpl documentWindow = (DocumentWindowImpl) document;
    return documentWindow.injectedToHost(offset);
  }

  // used only from tests => no need for complex synchronization
  private final Set<MultiHostInjector> myManualInjectors =
      Collections.synchronizedSet(new LinkedHashSet<MultiHostInjector>());
  private volatile ClassMapCachingNulls<MultiHostInjector> cachedInjectors;

  public void processInjectableElements(
      Collection<PsiElement> in, Processor<PsiElement> processor) {
    ClassMapCachingNulls<MultiHostInjector> map = getInjectorMap();
    for (PsiElement element : in) {
      if (map.get(element.getClass()) != null) processor.process(element);
    }
  }

  private ClassMapCachingNulls<MultiHostInjector> getInjectorMap() {
    ClassMapCachingNulls<MultiHostInjector> cached = cachedInjectors;
    if (cached != null) {
      return cached;
    }

    Map<Class, MultiHostInjector[]> injectors = ContainerUtil.newHashMap();

    List<MultiHostInjector> allInjectors = ContainerUtil.newArrayList();
    allInjectors.addAll(myManualInjectors);
    Collections.addAll(
        allInjectors, MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME.getExtensions(myProject));
    if (LanguageInjector.EXTENSION_POINT_NAME.getExtensions().length > 0) {
      allInjectors.add(PsiManagerRegisteredInjectorsAdapter.INSTANCE);
    }

    for (MultiHostInjector injector : allInjectors) {
      for (Class<? extends PsiElement> place : injector.elementsToInjectIn()) {
        LOG.assertTrue(place != null, injector);
        MultiHostInjector[] existing = injectors.get(place);
        injectors.put(
            place,
            existing == null
                ? new MultiHostInjector[] {injector}
                : ArrayUtil.append(existing, injector));
      }
    }

    ClassMapCachingNulls<MultiHostInjector> result =
        new ClassMapCachingNulls<>(injectors, new MultiHostInjector[0], allInjectors);
    cachedInjectors = result;
    return result;
  }

  private void clearInjectorCache() {
    cachedInjectors = null;
  }

  @Override
  public void registerMultiHostInjector(@NotNull MultiHostInjector injector) {
    myManualInjectors.add(injector);
    clearInjectorCache();
  }

  @Override
  public boolean unregisterMultiHostInjector(@NotNull MultiHostInjector injector) {
    try {
      return myManualInjectors.remove(injector);
    } finally {
      clearInjectorCache();
    }
  }

  @Override
  public String getUnescapedText(@NotNull final PsiElement injectedNode) {
    final StringBuilder text = new StringBuilder(injectedNode.getTextLength());
    // gather text from (patched) leaves
    injectedNode.accept(
        new PsiRecursiveElementWalkingVisitor() {
          @Override
          public void visitElement(PsiElement element) {
            String leafText = InjectedLanguageUtil.getUnescapedLeafText(element, false);
            if (leafText != null) {
              text.append(leafText);
              return;
            }
            super.visitElement(element);
          }
        });
    return text.toString();
  }

  /**
   * intersection may spread over several injected fragments
   *
   * @param rangeToEdit range in encoded(raw) PSI
   * @return list of ranges in encoded (raw) PSI
   */
  @Override
  @SuppressWarnings({"ConstantConditions", "unchecked"})
  @NotNull
  public List<TextRange> intersectWithAllEditableFragments(
      @NotNull PsiFile injectedPsi, @NotNull TextRange rangeToEdit) {
    Place shreds = InjectedLanguageUtil.getShreds(injectedPsi);
    if (shreds == null) return Collections.emptyList();
    Object result = null; // optimization: TextRange or ArrayList
    int count = 0;
    int offset = 0;
    for (PsiLanguageInjectionHost.Shred shred : shreds) {
      TextRange encodedRange =
          TextRange.from(
              offset + shred.getPrefix().length(), shred.getRangeInsideHost().getLength());
      TextRange intersection = encodedRange.intersection(rangeToEdit);
      if (intersection != null) {
        count++;
        if (count == 1) {
          result = intersection;
        } else if (count == 2) {
          TextRange range = (TextRange) result;
          if (range.isEmpty()) {
            result = intersection;
            count = 1;
          } else if (intersection.isEmpty()) {
            count = 1;
          } else {
            List<TextRange> list = new ArrayList<>();
            list.add(range);
            list.add(intersection);
            result = list;
          }
        } else if (intersection.isEmpty()) {
          count--;
        } else {
          ((List<TextRange>) result).add(intersection);
        }
      }
      offset +=
          shred.getPrefix().length()
              + shred.getRangeInsideHost().getLength()
              + shred.getSuffix().length();
    }
    return count == 0
        ? Collections.emptyList()
        : count == 1 ? Collections.singletonList((TextRange) result) : (List<TextRange>) result;
  }

  @Override
  public boolean isInjectedFragment(@NotNull final PsiFile file) {
    return file.getViewProvider() instanceof InjectedFileViewProvider;
  }

  @Override
  public PsiElement findInjectedElementAt(@NotNull PsiFile hostFile, int hostDocumentOffset) {
    return InjectedLanguageUtil.findInjectedElementNoCommit(hostFile, hostDocumentOffset);
  }

  @Override
  public void dropFileCaches(@NotNull PsiFile file) {
    InjectedLanguageUtil.clearCachedInjectedFragmentsForFile(file);
  }

  @Override
  public PsiFile getTopLevelFile(@NotNull PsiElement element) {
    return InjectedLanguageUtil.getTopLevelFile(element);
  }

  @NotNull
  @Override
  public List<DocumentWindow> getCachedInjectedDocuments(@NotNull PsiFile hostPsiFile) {
    return InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile);
  }

  @Override
  public void enumerate(
      @NotNull PsiElement host, @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) {
    InjectedLanguageUtil.enumerate(host, visitor);
  }

  @Override
  public void enumerateEx(
      @NotNull PsiElement host,
      @NotNull PsiFile containingFile,
      boolean probeUp,
      @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) {
    InjectedLanguageUtil.enumerate(host, containingFile, probeUp, visitor);
  }

  @NotNull
  @Override
  public List<TextRange> getNonEditableFragments(@NotNull DocumentWindow window) {
    List<TextRange> result = ContainerUtil.newArrayList();
    int offset = 0;
    for (PsiLanguageInjectionHost.Shred shred : ((DocumentWindowImpl) window).getShreds()) {
      Segment hostRange = shred.getHostRangeMarker();
      if (hostRange == null) continue;

      offset = appendRange(result, offset, shred.getPrefix().length());
      offset += hostRange.getEndOffset() - hostRange.getStartOffset();
      offset = appendRange(result, offset, shred.getSuffix().length());
    }

    return result;
  }

  private static int appendRange(List<TextRange> result, int start, int length) {
    if (length > 0) {
      int lastIndex = result.size() - 1;
      TextRange lastRange = lastIndex >= 0 ? result.get(lastIndex) : null;
      if (lastRange != null && lastRange.getEndOffset() == start) {
        result.set(lastIndex, lastRange.grown(length));
      } else {
        result.add(TextRange.from(start, length));
      }
    }
    return start + length;
  }

  private final Map<Class, MultiHostInjector[]> myInjectorsClone = new HashMap<>();

  @TestOnly
  public static void pushInjectors(@NotNull Project project) {
    InjectedLanguageManagerImpl cachedManager =
        (InjectedLanguageManagerImpl) project.getUserData(INSTANCE_CACHE);
    if (cachedManager == null) return;
    try {
      assert cachedManager.myInjectorsClone.isEmpty() : cachedManager.myInjectorsClone;
    } finally {
      cachedManager.myInjectorsClone.clear();
    }
    cachedManager.myInjectorsClone.putAll(cachedManager.getInjectorMap().getBackingMap());
  }

  @TestOnly
  public static void checkInjectorsAreDisposed(@NotNull Project project) {
    InjectedLanguageManagerImpl cachedManager =
        (InjectedLanguageManagerImpl) project.getUserData(INSTANCE_CACHE);
    if (cachedManager == null) {
      return;
    }
    try {
      ClassMapCachingNulls<MultiHostInjector> cached = cachedManager.cachedInjectors;
      if (cached == null) return;
      for (Map.Entry<Class, MultiHostInjector[]> entry : cached.getBackingMap().entrySet()) {
        Class key = entry.getKey();
        if (cachedManager.myInjectorsClone.isEmpty()) return;
        MultiHostInjector[] oldInjectors = cachedManager.myInjectorsClone.get(key);
        for (MultiHostInjector injector : entry.getValue()) {
          if (ArrayUtil.indexOf(oldInjectors, injector) == -1) {
            throw new AssertionError("Injector was not disposed: " + key + " -> " + injector);
          }
        }
      }
    } finally {
      cachedManager.myInjectorsClone.clear();
    }
  }

  interface InjProcessor {
    boolean process(PsiElement element, MultiHostInjector injector);
  }

  void processInPlaceInjectorsFor(@NotNull PsiElement element, @NotNull InjProcessor processor) {
    MultiHostInjector[] infos = getInjectorMap().get(element.getClass());
    if (infos != null) {
      final boolean dumb = myDumbService.isDumb();
      for (MultiHostInjector injector : infos) {
        if (dumb && !DumbService.isDumbAware(injector)) {
          continue;
        }

        if (!processor.process(element, injector)) return;
      }
    }
  }

  @Override
  @Nullable
  public List<Pair<PsiElement, TextRange>> getInjectedPsiFiles(@NotNull final PsiElement host) {
    if (!(host instanceof PsiLanguageInjectionHost)
        || !((PsiLanguageInjectionHost) host).isValidHost()) {
      return null;
    }
    final PsiElement inTree = InjectedLanguageUtil.loadTree(host, host.getContainingFile());
    final List<Pair<PsiElement, TextRange>> result = new SmartList<>();
    InjectedLanguageUtil.enumerate(
        inTree,
        (injectedPsi, places) -> {
          for (PsiLanguageInjectionHost.Shred place : places) {
            if (place.getHost() == inTree) {
              result.add(new Pair<>(injectedPsi, place.getRangeInsideHost()));
            }
          }
        });
    return result.isEmpty() ? null : result;
  }

  private static class PsiManagerRegisteredInjectorsAdapter implements MultiHostInjector {
    public static final PsiManagerRegisteredInjectorsAdapter INSTANCE =
        new PsiManagerRegisteredInjectorsAdapter();

    @Override
    public void getLanguagesToInject(
        @NotNull final MultiHostRegistrar injectionPlacesRegistrar, @NotNull PsiElement context) {
      final PsiLanguageInjectionHost host = (PsiLanguageInjectionHost) context;
      InjectedLanguagePlaces placesRegistrar =
          (language, rangeInsideHost, prefix, suffix) ->
              injectionPlacesRegistrar
                  .startInjecting(language)
                  .addPlace(prefix, suffix, host, rangeInsideHost)
                  .doneInjecting();
      for (LanguageInjector injector :
          Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME)) {
        injector.getLanguagesToInject(host, placesRegistrar);
      }
    }

    @Override
    @NotNull
    public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
      return Collections.singletonList(PsiLanguageInjectionHost.class);
    }
  }
}
  @Override
  public void startRunInjectors(@NotNull final Document hostDocument, final boolean synchronously) {
    if (myProject.isDisposed()) return;
    if (!synchronously && ApplicationManager.getApplication().isWriteAccessAllowed()) return;
    // use cached to avoid recreate PSI in alien project
    final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
    final PsiFile hostPsiFile = documentManager.getCachedPsiFile(hostDocument);
    if (hostPsiFile == null) return;

    final ConcurrentList<DocumentWindow> injected =
        InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile);
    if (injected.isEmpty()) return;

    if (myProgress.isCanceled()) {
      myProgress = new DaemonProgressIndicator();
    }
    final Set<DocumentWindow> newDocuments = Collections.synchronizedSet(new THashSet<>());
    final Processor<DocumentWindow> commitProcessor =
        documentWindow -> {
          if (myProject.isDisposed()) return false;
          ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
          if (indicator != null && indicator.isCanceled()) return false;
          if (documentManager.isUncommited(hostDocument) || !hostPsiFile.isValid())
            return false; // will be committed later

          // it is here where the reparse happens and old file contents replaced
          InjectedLanguageUtil.enumerate(
              documentWindow,
              hostPsiFile,
              (injectedPsi, places) -> {
                DocumentWindow newDocument =
                    (DocumentWindow) injectedPsi.getViewProvider().getDocument();
                if (newDocument != null) {
                  PsiDocumentManagerBase.checkConsistency(injectedPsi, newDocument);
                  newDocuments.add(newDocument);
                }
              });
          return true;
        };
    final Runnable commitInjectionsRunnable =
        () -> {
          if (myProgress.isCanceled()) return;
          JobLauncher.getInstance()
              .invokeConcurrentlyUnderProgress(
                  new ArrayList<>(injected), myProgress, true, commitProcessor);

          synchronized (ourInjectionPsiLock) {
            injected.clear();
            injected.addAll(newDocuments);
          }
        };

    if (synchronously) {
      if (Thread.holdsLock(PsiLock.LOCK)) {
        // hack for the case when docCommit was called from within PSI modification, e.g. in
        // formatter.
        // we can't spawn threads to do injections there, otherwise a deadlock is imminent
        ContainerUtil.process(new ArrayList<>(injected), commitProcessor);
      } else {
        commitInjectionsRunnable.run();
      }
    } else {
      JobLauncher.getInstance()
          .submitToJobThread(
              () ->
                  ApplicationManagerEx.getApplicationEx()
                      .tryRunReadAction(commitInjectionsRunnable),
              null);
    }
  }