Beispiel #1
0
 @NotNull
 @Override
 public Navigatable createNavigatable(@NotNull Project project) {
   if (ALTERNATIVE_SOURCE_KEY.get(myFile) != null) {
     return new OpenFileDescriptor(project, getFile(), getLine(), 0);
   }
   return XSourcePositionImpl.doCreateOpenFileDescriptor(project, this);
 }
Beispiel #2
0
 @NotNull
 @Override
 public VirtualFile getFile() {
   VirtualFile file = ALTERNATIVE_SOURCE_KEY.get(myFile);
   if (file != null) {
     return file;
   }
   return myFile;
 }
public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingPass
    implements DumbAware {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.daemon.impl.GeneralHighlightingPass");
  static final String PRESENTABLE_NAME = DaemonBundle.message("pass.syntax");
  private static final Key<Boolean> HAS_ERROR_ELEMENT = Key.create("HAS_ERROR_ELEMENT");

  private final int myStartOffset;
  private final int myEndOffset;
  private final boolean myUpdateAll;
  private final ProperTextRange myPriorityRange;
  private final Editor myEditor;

  private final List<HighlightInfo> myHighlights = new ArrayList<HighlightInfo>();

  protected volatile boolean myHasErrorElement;
  private volatile boolean myErrorFound;
  private static final Comparator<HighlightVisitor> VISITOR_ORDER_COMPARATOR =
      new Comparator<HighlightVisitor>() {
        @Override
        public int compare(final HighlightVisitor o1, final HighlightVisitor o2) {
          return o1.order() - o2.order();
        }
      };
  private Runnable myApplyCommand;
  private final EditorColorsScheme myGlobalScheme;
  private boolean myFailFastOnAcquireReadAction = true;

  public GeneralHighlightingPass(
      @NotNull Project project,
      @NotNull PsiFile file,
      @NotNull Document document,
      int startOffset,
      int endOffset,
      boolean updateAll) {
    this(
        project,
        file,
        document,
        startOffset,
        endOffset,
        updateAll,
        new ProperTextRange(0, document.getTextLength()),
        null);
  }

  public GeneralHighlightingPass(
      @NotNull Project project,
      @NotNull PsiFile file,
      @NotNull Document document,
      int startOffset,
      int endOffset,
      boolean updateAll,
      @NotNull ProperTextRange priorityRange,
      @Nullable Editor editor) {
    super(project, document, PRESENTABLE_NAME, file, true);
    myStartOffset = startOffset;
    myEndOffset = endOffset;
    myUpdateAll = updateAll;
    myPriorityRange = priorityRange;
    myEditor = editor;

    LOG.assertTrue(file.isValid());
    setId(Pass.UPDATE_ALL);
    myHasErrorElement =
        !isWholeFileHighlighting() && Boolean.TRUE.equals(myFile.getUserData(HAS_ERROR_ELEMENT));
    FileStatusMap fileStatusMap =
        ((DaemonCodeAnalyzerImpl) DaemonCodeAnalyzer.getInstance(myProject)).getFileStatusMap();
    myErrorFound = !isWholeFileHighlighting() && fileStatusMap.wasErrorFound(myDocument);

    myApplyCommand =
        new Runnable() {
          @Override
          public void run() {
            ProperTextRange range = new ProperTextRange(myStartOffset, myEndOffset);
            MarkupModel model = DocumentMarkupModel.forDocument(myDocument, myProject, true);
            UpdateHighlightersUtil.cleanFileLevelHighlights(myProject, Pass.UPDATE_ALL, myFile);
            final EditorColorsScheme colorsScheme = getColorsScheme();
            UpdateHighlightersUtil.setHighlightersInRange(
                myProject,
                myDocument,
                range,
                colorsScheme,
                myHighlights,
                (MarkupModelEx) model,
                Pass.UPDATE_ALL);
          }
        };

    // initial guess to show correct progress in the traffic light icon
    setProgressLimit(document.getTextLength() / 2); // approx number of PSI elements = file length/2
    myGlobalScheme = EditorColorsManager.getInstance().getGlobalScheme();
  }

  private static final Key<AtomicInteger> HIGHLIGHT_VISITOR_INSTANCE_COUNT =
      new Key<AtomicInteger>("HIGHLIGHT_VISITOR_INSTANCE_COUNT");

  @NotNull
  private HighlightVisitor[] getHighlightVisitors() {
    int oldCount = incVisitorUsageCount(1);
    HighlightVisitor[] highlightVisitors = createHighlightVisitors();
    if (oldCount != 0) {
      HighlightVisitor[] clones = new HighlightVisitor[highlightVisitors.length];
      for (int i = 0; i < highlightVisitors.length; i++) {
        HighlightVisitor highlightVisitor = highlightVisitors[i];
        clones[i] = highlightVisitor.clone();
      }
      highlightVisitors = clones;
    }
    return highlightVisitors;
  }

  protected HighlightVisitor[] createHighlightVisitors() {
    return Extensions.getExtensions(HighlightVisitor.EP_HIGHLIGHT_VISITOR, myProject);
  }

  // returns old value
  private int incVisitorUsageCount(int delta) {
    AtomicInteger count = myProject.getUserData(HIGHLIGHT_VISITOR_INSTANCE_COUNT);
    if (count == null) {
      count =
          ((UserDataHolderEx) myProject)
              .putUserDataIfAbsent(HIGHLIGHT_VISITOR_INSTANCE_COUNT, new AtomicInteger(0));
    }
    int old = count.getAndAdd(delta);
    assert old + delta >= 0 : old + ";" + delta;
    return old;
  }

  @Override
  protected void collectInformationWithProgress(final ProgressIndicator progress) {
    final Set<HighlightInfo> gotHighlights = new THashSet<HighlightInfo>(100);
    final Set<HighlightInfo> outsideResult = new THashSet<HighlightInfo>(100);

    DaemonCodeAnalyzer daemonCodeAnalyzer = DaemonCodeAnalyzer.getInstance(myProject);
    HighlightVisitor[] highlightVisitors = getHighlightVisitors();
    final HighlightVisitor[] filteredVisitors = filterVisitors(highlightVisitors, myFile);
    final List<PsiElement> inside = new ArrayList<PsiElement>();
    final List<PsiElement> outside = new ArrayList<PsiElement>();
    try {
      Divider.divideInsideAndOutside(
          myFile,
          myStartOffset,
          myEndOffset,
          myPriorityRange,
          inside,
          outside,
          HighlightLevelUtil.AnalysisLevel.HIGHLIGHT,
          false);

      setProgressLimit((long) (inside.size() + outside.size()));

      final boolean forceHighlightParents = forceHighlightParents();

      if (!isDumbMode()) {
        highlightTodos(
            myFile,
            myDocument.getCharsSequence(),
            myStartOffset,
            myEndOffset,
            progress,
            myPriorityRange,
            gotHighlights,
            outsideResult);
      }

      collectHighlights(
          inside,
          new Runnable() {
            @Override
            public void run() {
              // 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>();
              final Set<PsiFile> injected = new THashSet<PsiFile>();
              getInjectedPsiFiles(inside, outside, progress, injected);
              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 (myPriorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) {
                  gotHighlights.add(info);
                } else {
                  // nonconditionally apply injected results regardless whether they are in
                  // myStartOffset,myEndOffset
                  injectionsOutside.add(info);
                }
              }

              if (outsideResult.isEmpty() && injectionsOutside.isEmpty()) {
                return; // apply only result (by default apply command) and only within inside
              }

              final ProperTextRange priorityIntersection =
                  myPriorityRange.intersection(new TextRange(myStartOffset, myEndOffset));
              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();
                gotHighlights.addAll(outsideResult);
                final long modificationStamp = myDocument.getModificationStamp();
                UIUtil.invokeLaterIfNeeded(
                    new Runnable() {
                      @Override
                      public void run() {
                        if (myProject.isDisposed()
                            || modificationStamp != myDocument.getModificationStamp()) return;
                        MarkupModel markupModel =
                            DocumentMarkupModel.forDocument(myDocument, myProject, true);

                        UpdateHighlightersUtil.setHighlightersInRange(
                            myProject,
                            myDocument,
                            priorityIntersection,
                            getColorsScheme(),
                            toApplyInside,
                            (MarkupModelEx) markupModel,
                            Pass.UPDATE_ALL);
                        if (myEditor != null) {
                          new ShowAutoImportPass(myProject, myFile, myEditor)
                              .applyInformationToEditor();
                        }
                      }
                    });
              }

              myApplyCommand =
                  new Runnable() {
                    @Override
                    public void run() {
                      ProperTextRange range = new ProperTextRange(myStartOffset, myEndOffset);

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

                      UpdateHighlightersUtil.setHighlightersOutsideRange(
                          myProject,
                          myDocument,
                          toApply,
                          getColorsScheme(),
                          myStartOffset,
                          myEndOffset,
                          myPriorityRange,
                          Pass.UPDATE_ALL);
                    }
                  };
            }
          },
          outside,
          progress,
          filteredVisitors,
          gotHighlights,
          forceHighlightParents);

      if (myUpdateAll) {
        ((DaemonCodeAnalyzerImpl) daemonCodeAnalyzer)
            .getFileStatusMap()
            .setErrorFoundFlag(myDocument, myErrorFound);
      }
    } finally {
      incVisitorUsageCount(-1);
    }
    myHighlights.addAll(gotHighlights);
  }

  private void getInjectedPsiFiles(
      @NotNull final List<PsiElement> elements1,
      @NotNull final List<PsiElement> elements2,
      @NotNull final ProgressIndicator progress,
      @NotNull final Set<PsiFile> outInjected) {
    List<DocumentWindow> injected = InjectedLanguageUtil.getCachedInjectedDocuments(myFile);
    Collection<PsiElement> hosts =
        new THashSet<PsiElement>(elements1.size() + elements2.size() + injected.size());

    // rehighlight all injected PSI regardless the range,
    // since change in one place can lead to invalidation of injected PSI in (completely) other
    // place.
    for (DocumentWindow documentRange : injected) {
      progress.checkCanceled();
      if (!documentRange.isValid()) continue;
      PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(documentRange);
      if (file == null) continue;
      PsiElement context = file.getContext();
      if (context != null
          && context.isValid()
          && !file.getProject().isDisposed()
          && (myUpdateAll
              || new ProperTextRange(myStartOffset, myEndOffset)
                  .intersects(context.getTextRange()))) {
        hosts.add(context);
      }
    }
    hosts.addAll(elements1);
    hosts.addAll(elements2);

    final PsiLanguageInjectionHost.InjectedPsiVisitor visitor =
        new PsiLanguageInjectionHost.InjectedPsiVisitor() {
          @Override
          public void visit(
              @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
            synchronized (outInjected) {
              outInjected.add(injectedPsi);
            }
          }
        };
    if (!JobUtil.invokeConcurrentlyUnderProgress(
        new ArrayList<PsiElement>(hosts),
        progress,
        false,
        new Processor<PsiElement>() {
          @Override
          public boolean process(PsiElement element) {
            progress.checkCanceled();
            InjectedLanguageUtil.enumerate(element, myFile, false, visitor);
            return true;
          }
        })) throw new ProcessCanceledException();
  }

  // returns false if canceled
  private boolean addInjectedPsiHighlights(
      @NotNull final Set<PsiFile> injectedFiles,
      @NotNull final ProgressIndicator progress,
      @NotNull final Collection<HighlightInfo> outInfos) {
    if (injectedFiles.isEmpty()) return true;
    final InjectedLanguageManager injectedLanguageManager =
        InjectedLanguageManager.getInstance(myProject);
    final TextAttributes injectedAttributes =
        myGlobalScheme.getAttributes(EditorColors.INJECTED_LANGUAGE_FRAGMENT);

    return JobUtil.invokeConcurrentlyUnderProgress(
        new ArrayList<PsiFile>(injectedFiles),
        progress,
        myFailFastOnAcquireReadAction,
        new Processor<PsiFile>() {
          @Override
          public boolean process(final PsiFile injectedPsi) {
            DocumentWindow documentWindow =
                (DocumentWindow)
                    PsiDocumentManager.getInstance(myProject).getCachedDocument(injectedPsi);
            if (documentWindow == null) return true;
            Place places = InjectedLanguageUtil.getShreds(injectedPsi);
            for (PsiLanguageInjectionHost.Shred place : places) {
              TextRange textRange =
                  place.getRangeInsideHost().shiftRight(place.host.getTextRange().getStartOffset());
              if (textRange.isEmpty()) continue;
              String desc =
                  injectedPsi.getLanguage().getDisplayName() + ": " + injectedPsi.getText();
              HighlightInfo info =
                  HighlightInfo.createHighlightInfo(
                      HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT,
                      textRange,
                      null,
                      desc,
                      injectedAttributes);
              info.fromInjection = true;
              outInfos.add(info);
            }

            HighlightInfoHolder holder = createInfoHolder(injectedPsi);
            runHighlightVisitorsForInjected(injectedPsi, holder, progress);
            for (int i = 0; i < holder.size(); i++) {
              HighlightInfo info = holder.get(i);
              final int startOffset = documentWindow.injectedToHost(info.startOffset);
              final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset);
              addPatchedInfos(
                  info,
                  injectedPsi,
                  documentWindow,
                  injectedLanguageManager,
                  fixedTextRange,
                  outInfos);
            }
            holder.clear();
            highlightInjectedSyntax(injectedPsi, holder);
            for (int i = 0; i < holder.size(); i++) {
              HighlightInfo info = holder.get(i);
              final int startOffset = info.startOffset;
              final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset);
              if (fixedTextRange == null) {
                info.fromInjection = true;
                outInfos.add(info);
              } else {
                HighlightInfo patched =
                    new HighlightInfo(
                        info.forcedTextAttributes,
                        info.forcedTextAttributesKey,
                        info.type,
                        fixedTextRange.getStartOffset(),
                        fixedTextRange.getEndOffset(),
                        info.description,
                        info.toolTip,
                        info.type.getSeverity(null),
                        info.isAfterEndOfLine,
                        null,
                        false);
                patched.fromInjection = true;
                outInfos.add(patched);
              }
            }

            if (!isDumbMode()) {
              List<HighlightInfo> todos = new ArrayList<HighlightInfo>();
              highlightTodos(
                  injectedPsi,
                  injectedPsi.getText(),
                  0,
                  injectedPsi.getTextLength(),
                  progress,
                  myPriorityRange,
                  todos,
                  todos);
              for (HighlightInfo info : todos) {
                addPatchedInfos(
                    info, injectedPsi, documentWindow, injectedLanguageManager, null, outInfos);
              }
            }
            return true;
          }
        });
  }

  private static TextRange getFixedTextRange(
      @NotNull DocumentWindow documentWindow, int startOffset) {
    final TextRange fixedTextRange;
    TextRange textRange = documentWindow.getHostRange(startOffset);
    if (textRange == null) {
      // todo[cdr] check this fix. prefix/suffix code annotation case
      textRange = findNearestTextRange(documentWindow, startOffset);
      final boolean isBefore = startOffset < textRange.getStartOffset();
      fixedTextRange =
          new ProperTextRange(
              isBefore ? textRange.getStartOffset() - 1 : textRange.getEndOffset(),
              isBefore ? textRange.getStartOffset() : textRange.getEndOffset() + 1);
    } else {
      fixedTextRange = null;
    }
    return fixedTextRange;
  }

  private static void addPatchedInfos(
      @NotNull HighlightInfo info,
      @NotNull PsiFile injectedPsi,
      @NotNull DocumentWindow documentWindow,
      @NotNull InjectedLanguageManager injectedLanguageManager,
      @Nullable TextRange fixedTextRange,
      @NotNull Collection<HighlightInfo> out) {
    ProperTextRange textRange = new ProperTextRange(info.startOffset, info.endOffset);
    List<TextRange> editables =
        injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, textRange);
    for (TextRange editable : editables) {
      TextRange hostRange =
          fixedTextRange == null ? documentWindow.injectedToHost(editable) : fixedTextRange;

      boolean isAfterEndOfLine = info.isAfterEndOfLine;
      if (isAfterEndOfLine) {
        // convert injected afterEndOfLine to either host' afterEndOfLine or not-afterEndOfLine
        // highlight of the injected fragment boundary
        int hostEndOffset = hostRange.getEndOffset();
        int lineNumber = documentWindow.getDelegate().getLineNumber(hostEndOffset);
        int hostLineEndOffset = documentWindow.getDelegate().getLineEndOffset(lineNumber);
        if (hostEndOffset < hostLineEndOffset) {
          // convert to non-afterEndOfLine
          isAfterEndOfLine = false;
          hostRange = new ProperTextRange(hostRange.getStartOffset(), hostEndOffset + 1);
        }
      }

      HighlightInfo patched =
          new HighlightInfo(
              info.forcedTextAttributes,
              info.forcedTextAttributesKey,
              info.type,
              hostRange.getStartOffset(),
              hostRange.getEndOffset(),
              info.description,
              info.toolTip,
              info.type.getSeverity(null),
              isAfterEndOfLine,
              null,
              false);
      patched.setHint(info.hasHint());
      patched.setGutterIconRenderer(info.getGutterIconRenderer());

      if (info.quickFixActionRanges != null) {
        for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair :
            info.quickFixActionRanges) {
          TextRange quickfixTextRange = pair.getSecond();
          List<TextRange> editableQF =
              injectedLanguageManager.intersectWithAllEditableFragments(
                  injectedPsi, quickfixTextRange);
          for (TextRange editableRange : editableQF) {
            HighlightInfo.IntentionActionDescriptor descriptor = pair.getFirst();
            if (patched.quickFixActionRanges == null)
              patched.quickFixActionRanges =
                  new ArrayList<Pair<HighlightInfo.IntentionActionDescriptor, TextRange>>();
            TextRange hostEditableRange = documentWindow.injectedToHost(editableRange);
            patched.quickFixActionRanges.add(Pair.create(descriptor, hostEditableRange));
          }
        }
      }
      patched.fromInjection = true;
      out.add(patched);
    }
  }

  // finds the first nearest text range
  private static TextRange findNearestTextRange(
      final DocumentWindow documentWindow, final int startOffset) {
    TextRange textRange = null;
    for (RangeMarker marker : documentWindow.getHostRanges()) {
      TextRange curRange = ProperTextRange.create(marker);
      if (curRange.getStartOffset() > startOffset && textRange != null) break;
      textRange = curRange;
    }
    assert textRange != null;
    return textRange;
  }

  private void runHighlightVisitorsForInjected(
      @NotNull PsiFile injectedPsi,
      @NotNull final HighlightInfoHolder holder,
      @NotNull final ProgressIndicator progress) {
    HighlightVisitor[] visitors = getHighlightVisitors();
    try {
      HighlightVisitor[] filtered = filterVisitors(visitors, injectedPsi);
      final List<PsiElement> elements =
          CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength());
      for (final HighlightVisitor visitor : filtered) {
        visitor.analyze(
            injectedPsi,
            true,
            holder,
            new Runnable() {
              @Override
              public void run() {
                for (PsiElement element : elements) {
                  progress.checkCanceled();
                  visitor.visit(element);
                }
              }
            });
      }
    } finally {
      incVisitorUsageCount(-1);
    }
  }

  private void highlightInjectedSyntax(final PsiFile injectedPsi, HighlightInfoHolder holder) {
    List<Trinity<IElementType, PsiLanguageInjectionHost, TextRange>> tokens =
        InjectedLanguageUtil.getHighlightTokens(injectedPsi);
    if (tokens == null) return;

    final Language injectedLanguage = injectedPsi.getLanguage();
    Project project = injectedPsi.getProject();
    SyntaxHighlighter syntaxHighlighter =
        SyntaxHighlighterFactory.getSyntaxHighlighter(
            injectedLanguage, project, injectedPsi.getVirtualFile());
    final TextAttributes defaultAttrs = myGlobalScheme.getAttributes(HighlighterColors.TEXT);

    for (Trinity<IElementType, PsiLanguageInjectionHost, TextRange> token : tokens) {
      ProgressManager.checkCanceled();
      IElementType tokenType = token.getFirst();
      PsiLanguageInjectionHost injectionHost = token.getSecond();
      TextRange textRange = token.getThird();
      TextAttributesKey[] keys = syntaxHighlighter.getTokenHighlights(tokenType);
      if (textRange.getLength() == 0) continue;

      TextRange annRange = textRange.shiftRight(injectionHost.getTextRange().getStartOffset());
      // force attribute colors to override host' ones
      TextAttributes attributes = null;
      for (TextAttributesKey key : keys) {
        TextAttributes attrs2 = myGlobalScheme.getAttributes(key);
        if (attrs2 != null) {
          attributes = attributes == null ? attrs2 : TextAttributes.merge(attributes, attrs2);
        }
      }
      TextAttributes forcedAttributes;
      if (attributes == null || attributes.isEmpty() || attributes.equals(defaultAttrs)) {
        forcedAttributes = TextAttributes.ERASE_MARKER;
      } else {
        Color back =
            attributes.getBackgroundColor() == null
                ? myGlobalScheme.getDefaultBackground()
                : attributes.getBackgroundColor();
        Color fore =
            attributes.getForegroundColor() == null
                ? myGlobalScheme.getDefaultForeground()
                : attributes.getForegroundColor();
        forcedAttributes =
            new TextAttributes(
                fore,
                back,
                attributes.getEffectColor(),
                attributes.getEffectType(),
                attributes.getFontType());
      }

      HighlightInfo info =
          HighlightInfo.createHighlightInfo(
              HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT, annRange, null, null, forcedAttributes);
      holder.add(info);
    }
  }

  private boolean isWholeFileHighlighting() {
    return myUpdateAll && myStartOffset == 0 && myEndOffset == myDocument.getTextLength();
  }

  @Override
  protected void applyInformationWithProgress() {
    myFile.putUserData(HAS_ERROR_ELEMENT, myHasErrorElement);

    myApplyCommand.run();

    if (myUpdateAll) {
      reportErrorsToWolf();
    }
  }

  @Override
  @NotNull
  public List<HighlightInfo> getInfos() {
    return new ArrayList<HighlightInfo>(myHighlights);
  }

  private void collectHighlights(
      @NotNull final List<PsiElement> elements1,
      @NotNull final Runnable after1,
      @NotNull final List<PsiElement> elements2,
      @NotNull final ProgressIndicator progress,
      @NotNull final HighlightVisitor[] visitors,
      @NotNull final Set<HighlightInfo> gotHighlights,
      final boolean forceHighlightParents) {
    final Set<PsiElement> skipParentsSet = new THashSet<PsiElement>();

    // TODO - add color scheme to holder
    final HighlightInfoHolder holder = createInfoHolder(myFile);

    final int chunkSize =
        Math.max(1, (elements1.size() + elements2.size()) / 100); // one percent precision is enough

    final Runnable action =
        new Runnable() {
          @Override
          public void run() {
            //noinspection unchecked
            boolean failed = false;
            for (List<PsiElement> elements : new List[] {elements1, elements2}) {
              int nextLimit = chunkSize;
              for (int i = 0; i < elements.size(); i++) {
                PsiElement element = elements.get(i);
                progress.checkCanceled();

                if (element != myFile
                    && !skipParentsSet.isEmpty()
                    && element.getFirstChild() != null
                    && skipParentsSet.contains(element)) {
                  skipParentsSet.add(element.getParent());
                  continue;
                }

                if (element instanceof PsiErrorElement) {
                  myHasErrorElement = true;
                }
                holder.clear();

                for (final HighlightVisitor visitor : visitors) {
                  try {
                    visitor.visit(element);
                  } catch (ProcessCanceledException e) {
                    throw e;
                  } catch (IndexNotReadyException e) {
                    throw e;
                  } catch (WolfTheProblemSolverImpl.HaveGotErrorException e) {
                    throw e;
                  } catch (Exception e) {
                    if (!failed) {
                      LOG.error(e);
                    }
                    failed = true;
                  }
                }

                if (i == nextLimit) {
                  advanceProgress(chunkSize);
                  nextLimit = i + chunkSize;
                }

                //noinspection ForLoopReplaceableByForEach
                for (int j = 0; j < holder.size(); j++) {
                  final HighlightInfo info = holder.get(j);
                  assert info != null;
                  // have to filter out already obtained highlights
                  if (!gotHighlights.add(info)) continue;
                  boolean isError = info.getSeverity() == HighlightSeverity.ERROR;
                  if (isError) {
                    if (!forceHighlightParents) {
                      skipParentsSet.add(element.getParent());
                    }
                    myErrorFound = true;
                  }
                  myTransferToEDTQueue.offer(Pair.create(info, progress));
                }
              }
              advanceProgress(elements.size() - (nextLimit - chunkSize));
              if (elements == elements1) after1.run();
            }
          }
        };

    analyzeByVisitors(progress, visitors, holder, 0, action);
  }

  // private void collectHighlights(@NotNull final List<PsiElement> elements1,
  //                               @NotNull final Runnable after1,
  //                               @NotNull final List<PsiElement> elements2,
  //                               @NotNull final ProgressIndicator progress,
  //                               @NotNull final HighlightVisitor[] visitors,
  //                               @NotNull final Set<HighlightInfo> gotHighlights,
  //                               final boolean forceHighlightParents) {
  //  final HighlightInfoHolder holder = createInfoHolder(myFile);
  //
  //  final int chunkSize = Math.max(1, (elements1.size()+elements2.size()) / 100); // one percent
  // precision is enough
  //
  //  final Runnable action = new Runnable() {
  //    public void run() {
  //      //noinspection unchecked
  //      boolean failed = false;
  //      PsiNodeTask task = createPsiTask(myFile);
  //      JobSchedulerImpl.submitTask(task);
  //
  //      for (List<PsiElement> elements : new List[]{elements1, elements2}) {
  //        int nextLimit = chunkSize;
  //        for (int i = 0; i < elements.size(); i++) {
  //          PsiElement element = elements.get(i);
  //          progress.checkCanceled();
  //
  //          if (element != myFile && !skipParentsSet.isEmpty() && element.getFirstChild() != null
  // && skipParentsSet.contains(element)) {
  //            skipParentsSet.add(element.getParent());
  //            continue;
  //          }
  //
  //          if (element instanceof PsiErrorElement) {
  //            myHasErrorElement = true;
  //          }
  //          kjlhkjh
  //          if (i == nextLimit) {
  //            advanceProgress(chunkSize);
  //            nextLimit = i + chunkSize;
  //          }
  //
  //        }
  //        advanceProgress(elements.size() - (nextLimit-chunkSize));
  //        if (elements == elements1) after1.run();
  //      }
  //    }
  //
  //    private PsiNodeTask createPsiTask(@NotNull PsiElement root) {
  //      return new PsiNodeTask(root) {
  //        @Override
  //        public void onEnter(@NotNull PsiElement element) {
  //          if (element instanceof PsiErrorElement) {
  //            myHasErrorElement = true;
  //          }
  //          super.onEnter(element);
  //        }
  //
  //        @Override
  //        protected PsiNodeTask forkNode(@NotNull PsiElement child) {
  //          return createPsiTask(child);
  //        }
  //
  //        @Override
  //        protected boolean highlight(PsiElement element) {
  //          holder.clear();
  //
  //          for (final HighlightVisitor visitor : visitors) {
  //            try {
  //              visitor.visit(element);
  //            }
  //            catch (ProcessCanceledException e) {
  //              throw e;
  //            }
  //            catch (IndexNotReadyException e) {
  //              throw e;
  //            }
  //            catch (WolfTheProblemSolverImpl.HaveGotErrorException e) {
  //              throw e;
  //            }
  //            catch (Exception e) {
  //                LOG.error(e);
  //            }
  //          }
  //
  //          //noinspection ForLoopReplaceableByForEach
  //          for (int j = 0; j < holder.size(); j++) {
  //            final HighlightInfo info = holder.get(j);
  //            assert info != null;
  //            // have to filter out already obtained highlights
  //            if (!gotHighlights.add(info)) continue;
  //            boolean isError = info.getSeverity() == HighlightSeverity.ERROR;
  //            if (isError) {
  //              if (!forceHighlightParents) {
  //                skipParentsSet.add(element.getParent());
  //              }
  //              myErrorFound = true;
  //            }
  //            myTransferToEDTQueue.offer(Pair.create(info, progress));
  //          }
  //          return true;
  //        }
  //      };
  //    }
  //  };
  //
  //  analyzeByVisitors(progress, visitors, holder, 0, action);
  // }

  private final Map<TextRange, RangeMarker> ranges2markersCache =
      new THashMap<TextRange, RangeMarker>();
  private final TransferToEDTQueue<Pair<HighlightInfo, ProgressIndicator>> myTransferToEDTQueue =
      new TransferToEDTQueue<Pair<HighlightInfo, ProgressIndicator>>(
          "Apply highlighting results",
          new Processor<Pair<HighlightInfo, ProgressIndicator>>() {
            @Override
            public boolean process(Pair<HighlightInfo, ProgressIndicator> pair) {
              ApplicationManager.getApplication().assertIsDispatchThread();
              ProgressIndicator indicator = pair.getSecond();
              if (indicator.isCanceled()) {
                return false;
              }
              HighlightInfo info = pair.getFirst();
              final EditorColorsScheme colorsScheme = getColorsScheme();
              UpdateHighlightersUtil.addHighlighterToEditorIncrementally(
                  myProject,
                  myDocument,
                  myFile,
                  myStartOffset,
                  myEndOffset,
                  info,
                  colorsScheme,
                  Pass.UPDATE_ALL,
                  ranges2markersCache);

              return true;
            }
          },
          myProject.getDisposed(),
          200);

  private void analyzeByVisitors(
      @NotNull final ProgressIndicator progress,
      @NotNull final HighlightVisitor[] visitors,
      @NotNull final HighlightInfoHolder holder,
      final int i,
      @NotNull final Runnable action) {
    if (i == visitors.length) {
      action.run();
    } else {
      if (!visitors[i].analyze(
          myFile,
          myUpdateAll,
          holder,
          new Runnable() {
            @Override
            public void run() {
              analyzeByVisitors(progress, visitors, holder, i + 1, action);
            }
          })) {
        cancelAndRestartDaemonLater(progress, myProject, this);
      }
    }
  }

  private static HighlightVisitor[] filterVisitors(
      HighlightVisitor[] highlightVisitors, final PsiFile file) {
    final List<HighlightVisitor> visitors =
        new ArrayList<HighlightVisitor>(highlightVisitors.length);
    List<HighlightVisitor> list = Arrays.asList(highlightVisitors);
    for (HighlightVisitor visitor :
        DumbService.getInstance(file.getProject()).filterByDumbAwareness(list)) {
      if (visitor.suitableForFile(file)) visitors.add(visitor);
    }
    LOG.assertTrue(!visitors.isEmpty(), list);

    HighlightVisitor[] visitorArray = visitors.toArray(new HighlightVisitor[visitors.size()]);
    Arrays.sort(visitorArray, VISITOR_ORDER_COMPARATOR);
    return visitorArray;
  }

  static Void cancelAndRestartDaemonLater(
      ProgressIndicator progress, final Project project, TextEditorHighlightingPass pass) {
    PassExecutorService.log(progress, pass, "Cancel and restart");
    progress.cancel();
    ApplicationManager.getApplication()
        .invokeLater(
            new Runnable() {
              @Override
              public void run() {
                try {
                  Thread.sleep(new Random().nextInt(100));
                } catch (InterruptedException e) {
                  LOG.error(e);
                }
                DaemonCodeAnalyzer.getInstance(project).restart();
              }
            },
            project.getDisposed());
    throw new ProcessCanceledException();
  }

  private boolean forceHighlightParents() {
    boolean forceHighlightParents = false;
    for (HighlightRangeExtension extension :
        Extensions.getExtensions(HighlightRangeExtension.EP_NAME)) {
      if (extension.isForceHighlightParents(myFile)) {
        forceHighlightParents = true;
        break;
      }
    }
    return forceHighlightParents;
  }

  protected HighlightInfoHolder createInfoHolder(final PsiFile file) {
    final HighlightInfoFilter[] filters =
        ApplicationManager.getApplication().getExtensions(HighlightInfoFilter.EXTENSION_POINT_NAME);
    return new HighlightInfoHolder(file, getColorsScheme(), filters);
  }

  private static void highlightTodos(
      @NotNull PsiFile file,
      @NotNull CharSequence text,
      int startOffset,
      int endOffset,
      @NotNull ProgressIndicator progress,
      @NotNull ProperTextRange priorityRange,
      @NotNull Collection<HighlightInfo> result,
      @NotNull Collection<HighlightInfo> outsideResult) {
    PsiSearchHelper helper = PsiSearchHelper.SERVICE.getInstance(file.getProject());
    TodoItem[] todoItems = helper.findTodoItems(file, startOffset, endOffset);
    if (todoItems.length == 0) return;

    for (TodoItem todoItem : todoItems) {
      progress.checkCanceled();
      TextRange range = todoItem.getTextRange();
      String description =
          text.subSequence(range.getStartOffset(), range.getEndOffset()).toString();
      TextAttributes attributes = todoItem.getPattern().getAttributes().getTextAttributes();
      HighlightInfo info =
          HighlightInfo.createHighlightInfo(
              HighlightInfoType.TODO, range, description, description, attributes);
      assert info != null;
      if (priorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) {
        result.add(info);
      } else {
        outsideResult.add(info);
      }
    }
  }

  private void reportErrorsToWolf() {
    if (!myFile.getViewProvider().isPhysical()) return; // e.g. errors in evaluate expression
    Project project = myFile.getProject();
    if (!PsiManager.getInstance(project).isInProject(myFile))
      return; // do not report problems in libraries
    VirtualFile file = myFile.getVirtualFile();
    if (file == null) return;

    List<Problem> problems = convertToProblems(getInfos(), file, myHasErrorElement);
    WolfTheProblemSolver wolf = WolfTheProblemSolver.getInstance(project);

    boolean hasErrors = DaemonCodeAnalyzerImpl.hasErrors(project, getDocument());
    if (!hasErrors || isWholeFileHighlighting()) {
      wolf.reportProblems(file, problems);
    } else {
      wolf.weHaveGotProblems(file, problems);
    }
  }

  @Override
  public double getProgress() {
    // do not show progress of visible highlighters update
    return myUpdateAll ? super.getProgress() : -1;
  }

  private static List<Problem> convertToProblems(
      final Collection<HighlightInfo> infos,
      final VirtualFile file,
      final boolean hasErrorElement) {
    List<Problem> problems = new SmartList<Problem>();
    for (HighlightInfo info : infos) {
      if (info.getSeverity() == HighlightSeverity.ERROR) {
        Problem problem = new ProblemImpl(file, info, hasErrorElement);
        problems.add(problem);
      }
    }
    return problems;
  }

  @Override
  public String toString() {
    return super.toString()
        + " updateAll="
        + myUpdateAll
        + " range=("
        + myStartOffset
        + ","
        + myEndOffset
        + ")";
  }

  public void setFailFastOnAcquireReadAction(boolean failFastOnAcquireReadAction) {
    myFailFastOnAcquireReadAction = failFastOnAcquireReadAction;
  }
}
Beispiel #4
0
 public static void setAlternativeSource(VirtualFile source, VirtualFile dest) {
   ALTERNATIVE_SOURCE_KEY.set(source, dest);
   ALTERNATIVE_SOURCE_KEY.set(dest, null);
 }
public class JavaCompletionUtil {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.completion.JavaCompletionUtil");
  public static final Key<PairFunction<PsiExpression, CompletionParameters, PsiType>>
      DYNAMIC_TYPE_EVALUATOR = Key.create("DYNAMIC_TYPE_EVALUATOR");

  private static final Key<PsiType> QUALIFIER_TYPE_ATTR =
      Key.create("qualifierType"); // SmartPsiElementPointer to PsiType of "qualifier"
  public static final OffsetKey LPAREN_OFFSET = OffsetKey.create("lparen");
  public static final OffsetKey RPAREN_OFFSET = OffsetKey.create("rparen");
  public static final OffsetKey ARG_LIST_END_OFFSET = OffsetKey.create("argListEnd");
  static final NullableLazyKey<ExpectedTypeInfo[], CompletionLocation> EXPECTED_TYPES =
      NullableLazyKey.create(
          "expectedTypes",
          new NullableFunction<CompletionLocation, ExpectedTypeInfo[]>() {
            @Override
            @Nullable
            public ExpectedTypeInfo[] fun(final CompletionLocation location) {
              if (PsiJavaPatterns.psiElement()
                  .beforeLeaf(PsiJavaPatterns.psiElement().withText("."))
                  .accepts(location.getCompletionParameters().getPosition())) {
                return ExpectedTypeInfo.EMPTY_ARRAY;
              }

              return JavaSmartCompletionContributor.getExpectedTypes(
                  location.getCompletionParameters());
            }
          });
  private static final ElementPattern<PsiElement> LEFT_PAREN =
      psiElement(JavaTokenType.LPARENTH)
          .andOr(
              psiElement().withParent(PsiExpressionList.class),
              psiElement().afterLeaf(".", PsiKeyword.NEW));

  public static final Key<Boolean> SUPER_METHOD_PARAMETERS = Key.create("SUPER_METHOD_PARAMETERS");

  @Nullable
  public static Set<PsiType> getExpectedTypes(final CompletionParameters parameters) {
    final PsiExpression expr =
        PsiTreeUtil.getContextOfType(parameters.getPosition(), PsiExpression.class, true);
    if (expr != null) {
      final Set<PsiType> set = new THashSet<PsiType>();
      for (final ExpectedTypeInfo expectedInfo :
          JavaSmartCompletionContributor.getExpectedTypes(parameters)) {
        set.add(expectedInfo.getType());
      }
      return set;
    }
    return null;
  }

  private static final Key<List<SmartPsiElementPointer<PsiMethod>>> ALL_METHODS_ATTRIBUTE =
      Key.create("allMethods");

  public static PsiType getQualifierType(LookupItem item) {
    return item.getUserData(QUALIFIER_TYPE_ATTR);
  }

  public static void completeVariableNameForRefactoring(
      Project project,
      Set<LookupElement> set,
      String prefix,
      PsiType varType,
      VariableKind varKind) {
    final CamelHumpMatcher camelHumpMatcher = new CamelHumpMatcher(prefix);
    JavaMemberNameCompletionContributor.completeVariableNameForRefactoring(
        project, set, camelHumpMatcher, varType, varKind, true, false);
  }

  public static void putAllMethods(LookupElement item, List<PsiMethod> methods) {
    item.putUserData(
        ALL_METHODS_ATTRIBUTE,
        ContainerUtil.map(
            methods,
            new Function<PsiMethod, SmartPsiElementPointer<PsiMethod>>() {
              @Override
              public SmartPsiElementPointer<PsiMethod> fun(PsiMethod method) {
                return SmartPointerManager.getInstance(method.getProject())
                    .createSmartPsiElementPointer(method);
              }
            }));
  }

  public static List<PsiMethod> getAllMethods(LookupElement item) {
    List<SmartPsiElementPointer<PsiMethod>> pointers = item.getUserData(ALL_METHODS_ATTRIBUTE);
    if (pointers == null) return null;

    return ContainerUtil.mapNotNull(
        pointers,
        new Function<SmartPsiElementPointer<PsiMethod>, PsiMethod>() {
          @Override
          public PsiMethod fun(SmartPsiElementPointer<PsiMethod> pointer) {
            return pointer.getElement();
          }
        });
  }

  public static String[] completeVariableNameForRefactoring(
      JavaCodeStyleManager codeStyleManager,
      @Nullable final PsiType varType,
      final VariableKind varKind,
      SuggestedNameInfo suggestedNameInfo) {
    return JavaMemberNameCompletionContributor.completeVariableNameForRefactoring(
        codeStyleManager,
        new CamelHumpMatcher(""),
        varType,
        varKind,
        suggestedNameInfo,
        true,
        false);
  }

  public static boolean isInExcludedPackage(
      @NotNull final PsiMember member, boolean allowInstanceInnerClasses) {
    final String name = PsiUtil.getMemberQualifiedName(member);
    if (name == null) return false;

    if (!member.hasModifierProperty(PsiModifier.STATIC)) {
      if (member instanceof PsiMethod || member instanceof PsiField) {
        return false;
      }
      if (allowInstanceInnerClasses
          && member instanceof PsiClass
          && member.getContainingClass() != null) {
        return false;
      }
    }

    return ProjectCodeInsightSettings.getSettings(member.getProject()).isExcluded(name);
  }

  @SuppressWarnings({"unchecked"})
  @NotNull
  public static <T extends PsiType> T originalize(@NotNull T type) {
    if (!type.isValid()) {
      return type;
    }

    T result =
        new PsiTypeMapper() {
          private final Set<PsiClassType> myVisited = ContainerUtil.newIdentityTroveSet();

          @Override
          public PsiType visitClassType(final PsiClassType classType) {
            if (!myVisited.add(classType)) return classType;

            final PsiClassType.ClassResolveResult classResolveResult = classType.resolveGenerics();
            final PsiClass psiClass = classResolveResult.getElement();
            final PsiSubstitutor substitutor = classResolveResult.getSubstitutor();
            if (psiClass == null) return classType;

            return new PsiImmediateClassType(
                CompletionUtil.getOriginalOrSelf(psiClass), originalizeSubstitutor(substitutor));
          }

          private PsiSubstitutor originalizeSubstitutor(final PsiSubstitutor substitutor) {
            PsiSubstitutor originalSubstitutor = PsiSubstitutor.EMPTY;
            for (final Map.Entry<PsiTypeParameter, PsiType> entry :
                substitutor.getSubstitutionMap().entrySet()) {
              final PsiType value = entry.getValue();
              originalSubstitutor =
                  originalSubstitutor.put(
                      CompletionUtil.getOriginalOrSelf(entry.getKey()),
                      value == null ? null : mapType(value));
            }
            return originalSubstitutor;
          }

          @Override
          public PsiType visitType(PsiType type) {
            return type;
          }
        }.mapType(type);
    if (result == null) {
      throw new AssertionError("Null result for type " + type + " of class " + type.getClass());
    }
    return result;
  }

  public static void initOffsets(final PsiFile file, final OffsetMap offsetMap) {
    int offset =
        Math.max(
            offsetMap.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET),
            offsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET));

    PsiElement element = file.findElementAt(offset);
    if (element instanceof PsiWhiteSpace
        && (!element.textContains('\n')
            || CodeStyleSettingsManager.getSettings(file.getProject())
                .getCommonSettings(JavaLanguage.INSTANCE)
                .METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE)) {
      element = file.findElementAt(element.getTextRange().getEndOffset());
    }
    if (element == null) return;

    if (LEFT_PAREN.accepts(element)) {
      offsetMap.addOffset(LPAREN_OFFSET, element.getTextRange().getStartOffset());
      PsiElement list = element.getParent();
      PsiElement last = list.getLastChild();
      if (last instanceof PsiJavaToken
          && ((PsiJavaToken) last).getTokenType() == JavaTokenType.RPARENTH) {
        offsetMap.addOffset(RPAREN_OFFSET, last.getTextRange().getStartOffset());
      }

      offsetMap.addOffset(ARG_LIST_END_OFFSET, list.getTextRange().getEndOffset());
    }
  }

  public static void resetParensInfo(final OffsetMap offsetMap) {
    offsetMap.removeOffset(LPAREN_OFFSET);
    offsetMap.removeOffset(RPAREN_OFFSET);
    offsetMap.removeOffset(ARG_LIST_END_OFFSET);
    offsetMap.removeOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET);
  }

  @Nullable
  public static List<? extends PsiElement> getAllPsiElements(final LookupElement item) {
    List<PsiMethod> allMethods = getAllMethods(item);
    if (allMethods != null) return allMethods;
    if (item.getObject() instanceof PsiElement)
      return Collections.singletonList((PsiElement) item.getObject());
    return null;
  }

  @Nullable
  public static PsiType getLookupElementType(final LookupElement element) {
    TypedLookupItem typed = element.as(TypedLookupItem.CLASS_CONDITION_KEY);
    return typed != null ? typed.getType() : null;
  }

  @Nullable
  public static PsiType getQualifiedMemberReferenceType(
      @Nullable PsiType qualifierType, @NotNull final PsiMember member) {
    final Ref<PsiSubstitutor> subst = Ref.create(PsiSubstitutor.EMPTY);
    class MyProcessor extends BaseScopeProcessor implements NameHint, ElementClassHint {
      @Override
      public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
        if (element == member) {
          subst.set(state.get(PsiSubstitutor.KEY));
        }
        return true;
      }

      @Override
      public String getName(@NotNull ResolveState state) {
        return member.getName();
      }

      @Override
      public boolean shouldProcess(DeclarationKind kind) {
        return member instanceof PsiEnumConstant
            ? kind == DeclarationKind.ENUM_CONST
            : member instanceof PsiField
                ? kind == DeclarationKind.FIELD
                : kind == DeclarationKind.METHOD;
      }

      @Override
      public <T> T getHint(@NotNull Key<T> hintKey) {
        return hintKey == NameHint.KEY || hintKey == ElementClassHint.KEY ? (T) this : null;
      }
    }

    PsiScopesUtil.processTypeDeclarations(qualifierType, member, new MyProcessor());

    PsiType rawType =
        member instanceof PsiField
            ? ((PsiField) member).getType()
            : member instanceof PsiMethod
                ? ((PsiMethod) member).getReturnType()
                : JavaPsiFacade.getElementFactory(member.getProject())
                    .createType((PsiClass) member);
    return subst.get().substitute(rawType);
  }

  public static Set<LookupElement> processJavaReference(
      PsiElement element,
      PsiJavaReference javaReference,
      ElementFilter elementFilter,
      JavaCompletionProcessor.Options options,
      final PrefixMatcher matcher,
      CompletionParameters parameters) {
    final Set<LookupElement> set = new LinkedHashSet<LookupElement>();
    final Condition<String> nameCondition =
        new Condition<String>() {
          @Override
          public boolean value(String s) {
            return matcher.prefixMatches(s);
          }
        };

    PsiMethodCallExpression call =
        PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class);
    boolean checkInitialized =
        parameters.getInvocationCount() <= 1
            && call != null
            && PsiKeyword.SUPER.equals(call.getMethodExpression().getText());

    final JavaCompletionProcessor processor =
        new JavaCompletionProcessor(
            element, elementFilter, options.withInitialized(checkInitialized), nameCondition);
    final PsiType plainQualifier = processor.getQualifierType();
    PsiType qualifierType = plainQualifier;

    PsiType runtimeQualifier = getQualifierCastType(javaReference, parameters);
    if (runtimeQualifier != null) {
      PsiType composite =
          qualifierType == null
              ? runtimeQualifier
              : PsiIntersectionType.createIntersection(qualifierType, runtimeQualifier);
      PsiElement ctx = createContextWithXxxVariable(element, composite);
      javaReference =
          (PsiReferenceExpression)
              JavaPsiFacade.getElementFactory(element.getProject())
                  .createExpressionFromText("xxx.xxx", ctx);
      qualifierType = runtimeQualifier;
      processor.setQualifierType(qualifierType);
    }

    javaReference.processVariants(processor);

    final PsiTypeLookupItem castItem =
        runtimeQualifier == null
            ? null
            : PsiTypeLookupItem.createLookupItem(
                runtimeQualifier, (PsiReferenceExpression) javaReference);

    final boolean pkgContext = inSomePackage(element);

    final Set<PsiMember> mentioned = new THashSet<PsiMember>();
    for (CompletionElement completionElement : processor.getResults()) {
      for (LookupElement item : createLookupElements(completionElement, javaReference)) {
        item.putUserData(QUALIFIER_TYPE_ATTR, qualifierType);
        final Object o = item.getObject();
        if (o instanceof PsiClass && !isSourceLevelAccessible(element, (PsiClass) o, pkgContext)) {
          continue;
        }
        if (o instanceof PsiMember) {
          if (isInExcludedPackage((PsiMember) o, true)) {
            continue;
          }
          mentioned.add(CompletionUtil.getOriginalOrSelf((PsiMember) o));
        }
        set.add(
            highlightIfNeeded(
                qualifierType,
                castQualifier(item, castItem, plainQualifier, processor),
                o,
                element));
      }
    }

    if (javaReference instanceof PsiJavaCodeReferenceElement
        && !((PsiJavaCodeReferenceElement) javaReference).isQualified()) {
      final StaticMemberProcessor memberProcessor = new JavaStaticMemberProcessor(parameters);
      memberProcessor.processMembersOfRegisteredClasses(
          matcher,
          new PairConsumer<PsiMember, PsiClass>() {
            @Override
            public void consume(PsiMember member, PsiClass psiClass) {
              if (!mentioned.contains(member)
                  && processor.satisfies(member, ResolveState.initial())) {
                set.add(memberProcessor.createLookupElement(member, psiClass, true));
              }
            }
          });
    }

    return set;
  }

  @Nullable
  private static PsiType getQualifierCastType(
      PsiJavaReference javaReference, CompletionParameters parameters) {
    if (javaReference instanceof PsiReferenceExpression) {
      final PsiReferenceExpression refExpr = (PsiReferenceExpression) javaReference;
      final PsiExpression qualifier = refExpr.getQualifierExpression();
      if (qualifier != null) {
        final Project project = qualifier.getProject();
        PsiType type = null;
        final PairFunction<PsiExpression, CompletionParameters, PsiType> evaluator =
            refExpr.getContainingFile().getCopyableUserData(DYNAMIC_TYPE_EVALUATOR);
        if (evaluator != null) {
          type = evaluator.fun(qualifier, parameters);
        }
        if (type == null) {
          type = GuessManager.getInstance(project).getControlFlowExpressionType(qualifier);
        }
        return type;
      }
    }
    return null;
  }

  @NotNull
  private static LookupElement castQualifier(
      @NotNull LookupElement item,
      @Nullable final PsiTypeLookupItem castTypeItem,
      @Nullable PsiType plainQualifier,
      JavaCompletionProcessor processor) {
    if (castTypeItem == null) {
      return item;
    }
    if (plainQualifier != null) {
      Object o = item.getObject();
      if (o instanceof PsiMethod) {
        PsiType castType = castTypeItem.getType();
        if (plainQualifier instanceof PsiClassType && castType instanceof PsiClassType) {
          PsiMethod method = (PsiMethod) o;
          PsiClassType.ClassResolveResult plainResult =
              ((PsiClassType) plainQualifier).resolveGenerics();
          PsiClass plainClass = plainResult.getElement();
          if (plainClass != null && plainClass.findMethodBySignature(method, true) != null) {
            PsiClass castClass = ((PsiClassType) castType).resolveGenerics().getElement();

            if (castClass == null || !castClass.isInheritor(plainClass, true)) {
              return item;
            }

            PsiSubstitutor plainSub = plainResult.getSubstitutor();
            PsiSubstitutor castSub =
                TypeConversionUtil.getSuperClassSubstitutor(plainClass, (PsiClassType) castType);
            PsiType returnType = method.getReturnType();
            if (method.getSignature(plainSub).equals(method.getSignature(castSub))) {
              PsiType typeAfterCast = toRaw(castSub.substitute(returnType));
              PsiType typeDeclared = toRaw(plainSub.substitute(returnType));
              if (typeAfterCast != null
                  && typeDeclared != null
                  && typeAfterCast.isAssignableFrom(typeDeclared)
                  && processor.isAccessible(plainClass.findMethodBySignature(method, true))) {
                return item;
              }
            }
          }
        }
      } else if (containsMember(plainQualifier, o)) {
        return item;
      }
    }

    return LookupElementDecorator.withInsertHandler(
        item,
        new InsertHandlerDecorator<LookupElement>() {
          @Override
          public void handleInsert(
              InsertionContext context, LookupElementDecorator<LookupElement> item) {
            final Document document = context.getEditor().getDocument();
            context.commitDocument();
            final PsiFile file = context.getFile();
            final PsiJavaCodeReferenceElement ref =
                PsiTreeUtil.findElementOfClassAtOffset(
                    file, context.getStartOffset(), PsiJavaCodeReferenceElement.class, false);
            if (ref != null) {
              final PsiElement qualifier = ref.getQualifier();
              if (qualifier != null) {
                final CommonCodeStyleSettings settings = context.getCodeStyleSettings();

                final String parenSpace = settings.SPACE_WITHIN_PARENTHESES ? " " : "";
                document.insertString(qualifier.getTextRange().getEndOffset(), parenSpace + ")");

                final String spaceWithin = settings.SPACE_WITHIN_CAST_PARENTHESES ? " " : "";
                final String prefix = "(" + parenSpace + "(" + spaceWithin;
                final String spaceAfter = settings.SPACE_AFTER_TYPE_CAST ? " " : "";
                final int exprStart = qualifier.getTextRange().getStartOffset();
                document.insertString(exprStart, prefix + spaceWithin + ")" + spaceAfter);

                CompletionUtil.emulateInsertion(context, exprStart + prefix.length(), castTypeItem);
                PsiDocumentManager.getInstance(file.getProject())
                    .doPostponedOperationsAndUnblockDocument(document);
                context.getEditor().getCaretModel().moveToOffset(context.getTailOffset());
              }
            }

            item.getDelegate().handleInsert(context);
          }
        });
  }

  @Nullable
  private static PsiType toRaw(@Nullable PsiType type) {
    return type instanceof PsiClassType ? ((PsiClassType) type).rawType() : type;
  }

  @NotNull
  public static LookupElement highlightIfNeeded(
      @Nullable PsiType qualifierType,
      @NotNull LookupElement item,
      @NotNull Object object,
      @NotNull PsiElement place) {
    if (shouldMarkRed(object, place)) {
      return PrioritizedLookupElement.withExplicitProximity(
          LookupElementDecorator.withRenderer(
              item,
              new LookupElementRenderer<LookupElementDecorator<LookupElement>>() {
                @Override
                public void renderElement(
                    LookupElementDecorator<LookupElement> element,
                    LookupElementPresentation presentation) {
                  element.getDelegate().renderElement(presentation);
                  presentation.setItemTextForeground(JBColor.RED);
                }
              }),
          -1);
    }
    if (containsMember(qualifierType, object)) {
      LookupElementRenderer<LookupElementDecorator<LookupElement>> boldRenderer =
          new LookupElementRenderer<LookupElementDecorator<LookupElement>>() {
            @Override
            public void renderElement(
                LookupElementDecorator<LookupElement> element,
                LookupElementPresentation presentation) {
              element.getDelegate().renderElement(presentation);
              presentation.setItemTextBold(true);
            }
          };
      return PrioritizedLookupElement.withExplicitProximity(
          LookupElementDecorator.withRenderer(item, boldRenderer), 1);
    }
    return item;
  }

  private static boolean shouldMarkRed(@NotNull Object object, @NotNull PsiElement place) {
    if (!(object instanceof PsiMember)) return false;
    if (Java15APIUsageInspectionBase.isForbiddenApiUsage(
        (PsiMember) object, PsiUtil.getLanguageLevel(place))) return true;

    if (object instanceof PsiEnumConstant) {
      return findConstantsUsedInSwitch(place)
          .contains(CompletionUtil.getOriginalOrSelf((PsiEnumConstant) object));
    }
    return false;
  }

  public static boolean containsMember(@Nullable PsiType qualifierType, @NotNull Object object) {
    if (qualifierType instanceof PsiArrayType
        && object instanceof PsiMember) { // length and clone()
      PsiFile file = ((PsiMember) object).getContainingFile();
      if (file == null || file.getVirtualFile() == null) { // yes, they're a bit dummy
        return true;
      }
    } else if (qualifierType instanceof PsiClassType) {
      PsiClass qualifierClass = ((PsiClassType) qualifierType).resolve();
      if (qualifierClass == null) return false;
      if (object instanceof PsiMethod
          && qualifierClass.findMethodBySignature((PsiMethod) object, false) != null) {
        return true;
      }
      if (object instanceof PsiMember) {
        return qualifierClass.equals(((PsiMember) object).getContainingClass());
      }
    }
    return false;
  }

  private static List<? extends LookupElement> createLookupElements(
      CompletionElement completionElement, PsiJavaReference reference) {
    Object completion = completionElement.getElement();
    assert !(completion instanceof LookupElement);

    if (reference instanceof PsiJavaCodeReferenceElement) {
      if (completion instanceof PsiMethod
          && ((PsiJavaCodeReferenceElement) reference).getParent()
              instanceof PsiImportStaticStatement) {
        return Collections.singletonList(
            JavaLookupElementBuilder.forMethod((PsiMethod) completion, PsiSubstitutor.EMPTY));
      }

      if (completion instanceof PsiClass) {
        return JavaClassNameCompletionContributor.createClassLookupItems(
            (PsiClass) completion,
            JavaClassNameCompletionContributor.AFTER_NEW.accepts(reference),
            JavaClassNameInsertHandler.JAVA_CLASS_INSERT_HANDLER,
            Conditions.<PsiClass>alwaysTrue());
      }
    }

    if (reference instanceof PsiMethodReferenceExpression
        && completion instanceof PsiMethod
        && ((PsiMethod) completion).isConstructor()) {
      return Collections.singletonList(
          JavaLookupElementBuilder.forMethod(
              (PsiMethod) completion, "new", PsiSubstitutor.EMPTY, null));
    }

    LookupElement _ret = LookupItemUtil.objectToLookupItem(completion);
    if (_ret instanceof LookupItem) {
      final PsiSubstitutor substitutor = completionElement.getSubstitutor();
      if (substitutor != null) {
        ((LookupItem<?>) _ret).setAttribute(LookupItem.SUBSTITUTOR, substitutor);
      }
    }
    return Collections.singletonList(_ret);
  }

  public static boolean hasAccessibleConstructor(PsiType type) {
    if (type instanceof PsiArrayType) return true;

    final PsiClass psiClass = PsiUtil.resolveClassInType(type);
    if (psiClass == null || psiClass.isEnum() || psiClass.isAnnotationType()) return false;

    if (!(psiClass instanceof PsiCompiledElement)) return true;

    final PsiMethod[] methods = psiClass.getConstructors();
    if (methods.length == 0) return true;

    for (final PsiMethod method : methods) {
      if (!method.hasModifierProperty(PsiModifier.PRIVATE)) return true;
    }
    return false;
  }

  public static LookupItem qualify(final LookupItem ret) {
    return ret.forceQualify();
  }

  public static Set<String> getAllLookupStrings(@NotNull PsiMember member) {
    Set<String> allLookupStrings = ContainerUtil.newLinkedHashSet();
    String name = member.getName();
    allLookupStrings.add(name);
    PsiClass containingClass = member.getContainingClass();
    while (containingClass != null) {
      final String className = containingClass.getName();
      if (className == null) {
        break;
      }
      name = className + "." + name;
      allLookupStrings.add(name);
      final PsiElement parent = containingClass.getParent();
      if (!(parent instanceof PsiClass)) {
        break;
      }
      containingClass = (PsiClass) parent;
    }
    return allLookupStrings;
  }

  public static LookupItem setShowFQN(final LookupItem ret) {
    ret.setAttribute(
        JavaPsiClassReferenceElement.PACKAGE_NAME,
        PsiFormatUtil.getPackageDisplayName((PsiClass) ret.getObject()));
    return ret;
  }

  public static boolean mayHaveSideEffects(@Nullable final PsiElement element) {
    return element instanceof PsiExpression
        && SideEffectChecker.mayHaveSideEffects((PsiExpression) element);
  }

  public static void insertClassReference(
      @NotNull PsiClass psiClass, @NotNull PsiFile file, int offset) {
    insertClassReference(psiClass, file, offset, offset);
  }

  public static int insertClassReference(
      PsiClass psiClass, PsiFile file, int startOffset, int endOffset) {
    final Project project = file.getProject();
    PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
    documentManager.commitAllDocuments();

    final PsiManager manager = file.getManager();

    final Document document =
        FileDocumentManager.getInstance().getDocument(file.getViewProvider().getVirtualFile());

    final PsiReference reference = file.findReferenceAt(startOffset);
    if (reference != null) {
      final PsiElement resolved = reference.resolve();
      if (resolved instanceof PsiClass) {
        if (((PsiClass) resolved).getQualifiedName() == null
            || manager.areElementsEquivalent(psiClass, resolved)) {
          return endOffset;
        }
      }
    }

    String name = psiClass.getName();
    if (name == null) {
      return endOffset;
    }

    assert document != null;
    document.replaceString(startOffset, endOffset, name);

    int newEndOffset = startOffset + name.length();
    final RangeMarker toDelete = insertTemporary(newEndOffset, document, " ");

    documentManager.commitAllDocuments();

    PsiElement element = file.findElementAt(startOffset);
    if (element instanceof PsiIdentifier) {
      PsiElement parent = element.getParent();
      if (parent instanceof PsiJavaCodeReferenceElement
          && !((PsiJavaCodeReferenceElement) parent).isQualified()
          && !(parent.getParent() instanceof PsiPackageStatement)) {
        PsiJavaCodeReferenceElement ref = (PsiJavaCodeReferenceElement) parent;

        if (psiClass.isValid()
            && !psiClass.getManager().areElementsEquivalent(psiClass, resolveReference(ref))) {
          final boolean staticImport = ref instanceof PsiImportStaticReferenceElement;
          PsiElement newElement;
          try {
            newElement =
                staticImport
                    ? ((PsiImportStaticReferenceElement) ref).bindToTargetClass(psiClass)
                    : ref.bindToElement(psiClass);
          } catch (IncorrectOperationException e) {
            return endOffset; // can happen if fqn contains reserved words, for example
          }

          final RangeMarker rangeMarker = document.createRangeMarker(newElement.getTextRange());
          documentManager.doPostponedOperationsAndUnblockDocument(document);
          documentManager.commitDocument(document);

          newElement =
              CodeInsightUtilCore.findElementInRange(
                  file,
                  rangeMarker.getStartOffset(),
                  rangeMarker.getEndOffset(),
                  PsiJavaCodeReferenceElement.class,
                  JavaLanguage.INSTANCE);
          rangeMarker.dispose();
          if (newElement != null) {
            newEndOffset = newElement.getTextRange().getEndOffset();
            if (!(newElement instanceof PsiReferenceExpression)) {
              PsiReferenceParameterList parameterList =
                  ((PsiJavaCodeReferenceElement) newElement).getParameterList();
              if (parameterList != null) {
                newEndOffset = parameterList.getTextRange().getStartOffset();
              }
            }

            if (!staticImport
                && !psiClass
                    .getManager()
                    .areElementsEquivalent(psiClass, resolveReference((PsiReference) newElement))
                && !PsiUtil.isInnerClass(psiClass)) {
              final String qName = psiClass.getQualifiedName();
              if (qName != null) {
                document.replaceString(
                    newElement.getTextRange().getStartOffset(), newEndOffset, qName);
                newEndOffset = newElement.getTextRange().getStartOffset() + qName.length();
              }
            }
          }
        }
      }
    }

    if (toDelete.isValid()) {
      document.deleteString(toDelete.getStartOffset(), toDelete.getEndOffset());
    }

    return newEndOffset;
  }

  @Nullable
  static PsiElement resolveReference(final PsiReference psiReference) {
    if (psiReference instanceof PsiPolyVariantReference) {
      final ResolveResult[] results = ((PsiPolyVariantReference) psiReference).multiResolve(true);
      if (results.length == 1) return results[0].getElement();
    }
    return psiReference.resolve();
  }

  public static RangeMarker insertTemporary(
      final int endOffset, final Document document, final String temporary) {
    final CharSequence chars = document.getCharsSequence();
    final int length = chars.length();
    final RangeMarker toDelete;
    if (endOffset < length && Character.isJavaIdentifierPart(chars.charAt(endOffset))) {
      document.insertString(endOffset, temporary);
      toDelete = document.createRangeMarker(endOffset, endOffset + 1);
    } else if (endOffset >= length) {
      toDelete = document.createRangeMarker(length, length);
    } else {
      toDelete = document.createRangeMarker(endOffset, endOffset);
    }
    toDelete.setGreedyToLeft(true);
    toDelete.setGreedyToRight(true);
    return toDelete;
  }

  public static void insertParentheses(
      final InsertionContext context,
      final LookupElement item,
      boolean overloadsMatter,
      boolean hasParams) {
    insertParentheses(context, item, overloadsMatter, hasParams, false);
  }

  public static void insertParentheses(
      final InsertionContext context,
      final LookupElement item,
      boolean overloadsMatter,
      boolean hasParams,
      final boolean forceClosingParenthesis) {
    final Editor editor = context.getEditor();
    final char completionChar = context.getCompletionChar();
    final PsiFile file = context.getFile();

    final TailType tailType =
        completionChar == '('
            ? TailType.NONE
            : completionChar == ':'
                ? TailType.COND_EXPR_COLON
                : LookupItem.handleCompletionChar(context.getEditor(), item, completionChar);
    final boolean hasTail = tailType != TailType.NONE && tailType != TailType.UNKNOWN;
    final boolean smart = completionChar == Lookup.COMPLETE_STATEMENT_SELECT_CHAR;

    if (completionChar == '('
        || completionChar == '.'
        || completionChar == ','
        || completionChar == ';'
        || completionChar == ':'
        || completionChar == ' ') {
      context.setAddCompletionChar(false);
    }

    if (hasTail) {
      hasParams = false;
    }
    final boolean needRightParenth =
        forceClosingParenthesis
            || !smart
                && (CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET
                    || !hasParams && completionChar != '(');

    context.commitDocument();

    final CommonCodeStyleSettings styleSettings = context.getCodeStyleSettings();
    final PsiElement elementAt = file.findElementAt(context.getStartOffset());
    if (elementAt == null || !(elementAt.getParent() instanceof PsiMethodReferenceExpression)) {
      ParenthesesInsertHandler.getInstance(
              hasParams,
              styleSettings.SPACE_BEFORE_METHOD_CALL_PARENTHESES,
              styleSettings.SPACE_WITHIN_METHOD_CALL_PARENTHESES && hasParams,
              needRightParenth,
              styleSettings.METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE)
          .handleInsert(context, item);
    }

    if (hasParams) {
      // Invoke parameters popup
      AutoPopupController.getInstance(file.getProject())
          .autoPopupParameterInfo(editor, overloadsMatter ? null : (PsiElement) item.getObject());
    }

    if (smart || !needRightParenth || !insertTail(context, item, tailType, hasTail)) {
      return;
    }

    if (completionChar == '.') {
      AutoPopupController.getInstance(file.getProject())
          .autoPopupMemberLookup(context.getEditor(), null);
    } else if (completionChar == ',') {
      AutoPopupController.getInstance(file.getProject())
          .autoPopupParameterInfo(context.getEditor(), null);
    }
  }

  public static boolean insertTail(
      InsertionContext context, LookupElement item, TailType tailType, boolean hasTail) {
    TailType toInsert = tailType;
    LookupItem<?> lookupItem = item.as(LookupItem.CLASS_CONDITION_KEY);
    if (lookupItem == null
        || lookupItem.getAttribute(LookupItem.TAIL_TYPE_ATTR) != TailType.UNKNOWN) {
      if (!hasTail
          && item.getObject() instanceof PsiMethod
          && ((PsiMethod) item.getObject()).getReturnType() == PsiType.VOID) {
        PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments();
        if (psiElement()
            .beforeLeaf(psiElement().withText("."))
            .accepts(context.getFile().findElementAt(context.getTailOffset() - 1))) {
          return false;
        }

        boolean insertAdditionalSemicolon = true;
        final PsiReferenceExpression referenceExpression =
            PsiTreeUtil.getTopmostParentOfType(
                context.getFile().findElementAt(context.getStartOffset()),
                PsiReferenceExpression.class);
        if (referenceExpression instanceof PsiMethodReferenceExpression
            && LambdaHighlightingUtil.insertSemicolon(referenceExpression.getParent())) {
          insertAdditionalSemicolon = false;
        } else if (referenceExpression != null) {
          PsiElement parent = referenceExpression.getParent();
          if (parent instanceof PsiMethodCallExpression) {
            parent = parent.getParent();
          }
          if (parent instanceof PsiLambdaExpression
              && !LambdaHighlightingUtil.insertSemicolonAfter((PsiLambdaExpression) parent)) {
            insertAdditionalSemicolon = false;
          }
        }
        if (insertAdditionalSemicolon) {
          toInsert = TailType.SEMICOLON;
        }
      }
    }
    toInsert.processTail(context.getEditor(), context.getTailOffset());
    return true;
  }

  // need to shorten references in type argument list
  public static void shortenReference(final PsiFile file, final int offset)
      throws IncorrectOperationException {
    Project project = file.getProject();
    final PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
    Document document = manager.getDocument(file);
    if (document == null) {
      PsiUtilCore.ensureValid(file);
      LOG.error("No document for " + file);
      return;
    }

    manager.commitDocument(document);
    final PsiReference ref = file.findReferenceAt(offset);
    if (ref != null) {
      PsiElement element = ref.getElement();
      if (element != null) {
        JavaCodeStyleManager.getInstance(project).shortenClassReferences(element);
        PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document);
      }
    }
  }

  public static boolean inSomePackage(PsiElement context) {
    PsiFile contextFile = context.getContainingFile();
    return contextFile instanceof PsiClassOwner
        && StringUtil.isNotEmpty(((PsiClassOwner) contextFile).getPackageName());
  }

  public static boolean isSourceLevelAccessible(
      PsiElement context, PsiClass psiClass, final boolean pkgContext) {
    if (!JavaPsiFacade.getInstance(psiClass.getProject())
        .getResolveHelper()
        .isAccessible(psiClass, context, null)) {
      return false;
    }

    if (pkgContext) {
      PsiClass topLevel = PsiUtil.getTopLevelClass(psiClass);
      if (topLevel != null) {
        String fqName = topLevel.getQualifiedName();
        if (fqName != null && StringUtil.isEmpty(StringUtil.getPackageName(fqName))) {
          return false;
        }
      }
    }

    return true;
  }

  public static boolean promptTypeArgs(InsertionContext context, int offset) {
    if (offset < 0) {
      return false;
    }

    OffsetKey key = context.trackOffset(offset, false);
    PostprocessReformattingAspect.getInstance(context.getProject()).doPostponedFormatting();
    offset = context.getOffset(key);
    if (offset < 0) {
      return false;
    }

    String open = escapeXmlIfNeeded(context, "<");
    context.getDocument().insertString(offset, open);
    context.getEditor().getCaretModel().moveToOffset(offset + open.length());
    context.getDocument().insertString(offset + open.length(), escapeXmlIfNeeded(context, ">"));
    context.setAddCompletionChar(false);
    return true;
  }

  public static FakePsiElement createContextWithXxxVariable(
      final PsiElement place, final PsiType varType) {
    return new FakePsiElement() {
      @Override
      public boolean processDeclarations(
          @NotNull PsiScopeProcessor processor,
          @NotNull ResolveState state,
          PsiElement lastParent,
          @NotNull PsiElement place) {
        return processor.execute(
            new LightVariableBuilder("xxx", varType, place), ResolveState.initial());
      }

      @Override
      public PsiElement getParent() {
        return place;
      }
    };
  }

  public static String escapeXmlIfNeeded(InsertionContext context, String generics) {
    if (context.getFile().getViewProvider().getBaseLanguage() == StdLanguages.JSPX) {
      return StringUtil.escapeXml(generics);
    }
    return generics;
  }
}
public abstract class PsiDocumentManagerBase extends PsiDocumentManager
    implements DocumentListener {
  static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl");
  private static final Key<Document> HARD_REF_TO_DOCUMENT =
      Key.create("HARD_REFERENCE_TO_DOCUMENT");
  private static final Key<PsiFile> HARD_REF_TO_PSI = Key.create("HARD_REFERENCE_TO_PSI");
  private static final Key<List<Runnable>> ACTION_AFTER_COMMIT = Key.create("ACTION_AFTER_COMMIT");

  protected final Project myProject;
  private final PsiManager myPsiManager;
  private final DocumentCommitProcessor myDocumentCommitProcessor;
  protected final Set<Document> myUncommittedDocuments = ContainerUtil.newConcurrentSet();
  private final Map<Document, Pair<CharSequence, Long>> myLastCommittedTexts =
      ContainerUtil.newConcurrentMap();
  protected boolean myStopTrackingDocuments;
  protected boolean myPerformBackgroundCommit = true;

  private volatile boolean myIsCommitInProgress;
  private final PsiToDocumentSynchronizer mySynchronizer;

  private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  protected PsiDocumentManagerBase(
      @NotNull final Project project,
      @NotNull PsiManager psiManager,
      @NotNull MessageBus bus,
      @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) {
    myProject = project;
    myPsiManager = psiManager;
    myDocumentCommitProcessor = documentCommitProcessor;
    mySynchronizer = new PsiToDocumentSynchronizer(this, bus);
    myPsiManager.addPsiTreeChangeListener(mySynchronizer);
    bus.connect()
        .subscribe(
            PsiDocumentTransactionListener.TOPIC,
            new PsiDocumentTransactionListener() {
              @Override
              public void transactionStarted(@NotNull Document document, @NotNull PsiFile file) {
                myUncommittedDocuments.remove(document);
              }

              @Override
              public void transactionCompleted(@NotNull Document document, @NotNull PsiFile file) {}
            });
  }

  @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, @Nullable 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
  FileViewProvider getCachedViewProvider(@NotNull Document document) {
    final VirtualFile virtualFile = getVirtualFile(document);
    if (virtualFile == null) return null;
    return getCachedViewProvider(virtualFile);
  }

  private FileViewProvider getCachedViewProvider(@NotNull VirtualFile virtualFile) {
    return ((PsiManagerEx) myPsiManager).getFileManager().findCachedViewProvider(virtualFile);
  }

  private static VirtualFile getVirtualFile(@NotNull Document document) {
    final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !virtualFile.isValid()) return null;
    return virtualFile;
  }

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

  @Nullable
  private PsiFile getPsiFile(@NotNull 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) {
        PsiUtilCore.ensureValid(file);
        cachePsi(document, file);
      }
      return document;
    }

    FileViewProvider viewProvider = file.getViewProvider();
    if (!viewProvider.isEventSystemEnabled()) return null;

    document = FileDocumentManager.getInstance().getDocument(viewProvider.getVirtualFile());
    if (document != null) {
      if (document.getTextLength() != file.getTextLength()) {
        String message =
            "Document/PSI mismatch: "
                + file
                + " ("
                + file.getClass()
                + "); physical="
                + viewProvider.isPhysical();
        if (document.getTextLength() + file.getTextLength() < 8096) {
          message +=
              "\n=== document ===\n" + document.getText() + "\n=== PSI ===\n" + file.getText();
        }
        throw new AssertionError(message);
      }

      if (!viewProvider.isPhysical()) {
        PsiUtilCore.ensureValid(file);
        cachePsi(document, file);
        file.putUserData(HARD_REF_TO_DOCUMENT, document);
      }
    }

    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();
      if (!hasUncommitedDocuments()) {
        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);
    }
  }

  // public for Upsource
  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)) {
        InjectedLanguageManager.getInstance(myProject).startRunInjectors(document, synchronously);
      }
      // run after commit actions outside write action
      runAfterCommitActions(document);
      if (DebugUtil.DO_EXPENSIVE_CHECKS && !ApplicationInfoImpl.isInPerformanceTest()) {
        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 : finishRunnable + " in " + finishProcessors;
          }
          if (!success) {
            break;
          }
        }
        if (success) {
          myLastCommittedTexts.remove(document);
          viewProvider.contentsSynchronized();
        }
      } else {
        handleCommitWithoutPsi(document);
      }
    } 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
                        + "'");
              }
            }
          });
    }
  }

  private void doCommit(@NotNull final Document document) {
    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)) return;

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

  @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 reparseFiles(@NotNull Collection<VirtualFile> files, boolean includeOpenFiles) {
    FileContentUtilCore.reparseFiles(files);
  }

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

      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) {
        try {
          Runnable action = actionsWhenAllDocumentsAreCommitted.remove(key);
          myDocumentCommitProcessor.log(
              "Running after commit runnable: ", null, false, key, action);
          action.run();
        } catch (Throwable e) {
          LOG.error(e);
        }
      }
    }
  }

  @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) {}

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

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

  @Override
  @NotNull
  public CharSequence getLastCommittedText(@NotNull Document document) {
    Pair<CharSequence, Long> pair = myLastCommittedTexts.get(document);
    return pair != null ? pair.first : document.getImmutableCharSequence();
  }

  @Override
  public long getLastCommittedStamp(@NotNull Document document) {
    Pair<CharSequence, Long> pair = myLastCommittedTexts.get(document);
    return pair != null ? pair.second : document.getModificationStamp();
  }

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

  boolean isInUncommittedSet(@NotNull Document document) {
    if (document instanceof DocumentWindow)
      return isInUncommittedSet(((DocumentWindow) document).getDelegate());
    return myUncommittedDocuments.contains(document);
  }

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

  @Override
  public boolean isCommitted(@NotNull Document document) {
    if (document instanceof DocumentWindow)
      return isCommitted(((DocumentWindow) document).getDelegate());
    if (getSynchronizer().isInSynchronization(document)) return true;
    return !((DocumentEx) document).isInEventsHandling() && !isInUncommittedSet(document);
  }

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

  @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);
  }

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

  @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);
    }
  }

  void handleCommitWithoutPsi(@NotNull Document document) {
    final Pair<CharSequence, Long> prevPair = myLastCommittedTexts.remove(document);
    if (prevPair == null) {
      return;
    }

    if (!myProject.isInitialized() || myProject.isDisposed()) {
      return;
    }

    myUncommittedDocuments.remove(document);

    VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
    if (virtualFile == null || !FileIndexFacade.getInstance(myProject).isInContent(virtualFile)) {
      return;
    }

    final PsiFile psiFile = getPsiFile(document);
    if (psiFile == null) {
      return;
    }

    // we can end up outside write action here if the document has forUseInNonAWTThread=true
    ApplicationManager.getApplication()
        .runWriteAction(
            new ExternalChangeAction() {
              @Override
              public void run() {
                psiFile.getViewProvider().beforeContentsSynchronized();
                synchronized (PsiLock.LOCK) {
                  final int oldLength = prevPair.first.length();
                  PsiManagerImpl manager = (PsiManagerImpl) psiFile.getManager();
                  BlockSupportImpl.sendBeforeChildrenChangeEvent(manager, psiFile, true);
                  BlockSupportImpl.sendBeforeChildrenChangeEvent(manager, psiFile, false);
                  if (psiFile instanceof PsiFileImpl) {
                    ((PsiFileImpl) psiFile).onContentReload();
                  }
                  BlockSupportImpl.sendAfterChildrenChangedEvent(
                      manager, psiFile, oldLength, false);
                  BlockSupportImpl.sendAfterChildrenChangedEvent(manager, psiFile, oldLength, true);
                }
                psiFile.getViewProvider().contentsSynchronized();
              }
            });
  }

  private boolean isRelevant(@NotNull VirtualFile virtualFile) {
    return !virtualFile.getFileType().isBinary() && !myProject.isDisposed();
  }

  public static boolean checkConsistency(@NotNull PsiFile psiFile, @NotNull 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() {
    myLastCommittedTexts.clear();
    myUncommittedDocuments.clear();
    mySynchronizer.cleanupForNextTest();
  }

  @TestOnly
  public void disableBackgroundCommit(@NotNull Disposable parentDisposable) {
    assert myPerformBackgroundCommit;
    myPerformBackgroundCommit = false;
    Disposer.register(
        parentDisposable,
        new Disposable() {
          @Override
          public void dispose() {
            myPerformBackgroundCommit = true;
          }
        });
  }

  @NotNull
  public PsiToDocumentSynchronizer getSynchronizer() {
    return mySynchronizer;
  }
}
class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.folding.impl.DocumentFoldingInfo");
  private static final Key<FoldingInfo> FOLDING_INFO_KEY = Key.create("FOLDING_INFO");

  @NotNull private final Project myProject;
  private final VirtualFile myFile;

  private static class SerializedPsiElement {
    private final String mySerializedElement;
    private final FoldingInfo myFoldingInfo;

    public SerializedPsiElement(@NotNull String serialized, @NotNull FoldingInfo foldingInfo) {
      mySerializedElement = serialized;
      myFoldingInfo = foldingInfo;
    }
  }

  @NotNull
  private final List<SmartPsiElementPointer<PsiElement>> myPsiElements =
      ContainerUtil.createLockFreeCopyOnWriteList();

  @NotNull
  private final List<SerializedPsiElement> mySerializedElements =
      ContainerUtil.createLockFreeCopyOnWriteList();

  @NotNull
  private final List<RangeMarker> myRangeMarkers = ContainerUtil.createLockFreeCopyOnWriteList();

  private static final String DEFAULT_PLACEHOLDER = "...";
  @NonNls private static final String ELEMENT_TAG = "element";
  @NonNls private static final String SIGNATURE_ATT = "signature";
  @NonNls private static final String EXPANDED_ATT = "expanded";
  @NonNls private static final String MARKER_TAG = "marker";
  @NonNls private static final String DATE_ATT = "date";
  @NonNls private static final String PLACEHOLDER_ATT = "placeholder";

  DocumentFoldingInfo(@NotNull Project project, @NotNull Document document) {
    myProject = project;
    myFile = FileDocumentManager.getInstance().getFile(document);
  }

  void loadFromEditor(@NotNull Editor editor) {
    assertDispatchThread();
    LOG.assertTrue(!editor.isDisposed());
    clear();

    PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
    documentManager.commitDocument(editor.getDocument());
    PsiFile file = documentManager.getPsiFile(editor.getDocument());

    SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject);
    EditorFoldingInfo info = EditorFoldingInfo.get(editor);
    FoldRegion[] foldRegions = editor.getFoldingModel().getAllFoldRegions();
    for (FoldRegion region : foldRegions) {
      if (!region.isValid()) continue;
      PsiElement element = info.getPsiElement(region);
      boolean expanded = region.isExpanded();
      boolean collapseByDefault =
          element != null
              && FoldingPolicy.isCollapseByDefault(element)
              && !FoldingUtil.caretInsideRange(editor, TextRange.create(region));
      if (collapseByDefault == expanded || element == null) {
        FoldingInfo fi = new FoldingInfo(region.getPlaceholderText(), expanded);
        if (element != null) {
          myPsiElements.add(smartPointerManager.createSmartPsiElementPointer(element, file));
          element.putUserData(FOLDING_INFO_KEY, fi);
        } else if (region.isValid()) {
          myRangeMarkers.add(region);
          region.putUserData(FOLDING_INFO_KEY, fi);
        }
      }
    }
  }

  private static void assertDispatchThread() {
    ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
  }

  @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);
    }
  }

  @NotNull
  private static Map<PsiElement, FoldingDescriptor> buildRanges(
      @NotNull Editor editor, @NotNull PsiFile psiFile) {
    final FoldingBuilder foldingBuilder =
        LanguageFolding.INSTANCE.forLanguage(psiFile.getLanguage());
    final ASTNode node = psiFile.getNode();
    if (node == null) return Collections.emptyMap();
    final FoldingDescriptor[] descriptors =
        LanguageFolding.buildFoldingDescriptors(
            foldingBuilder, psiFile, editor.getDocument(), true);
    Map<PsiElement, FoldingDescriptor> ranges = new HashMap<PsiElement, FoldingDescriptor>();
    for (FoldingDescriptor descriptor : descriptors) {
      final ASTNode ast = descriptor.getElement();
      final PsiElement psi = ast.getPsi();
      if (psi != null) {
        ranges.put(psi, descriptor);
      }
    }
    return ranges;
  }

  void clear() {
    myPsiElements.clear();
    for (RangeMarker marker : myRangeMarkers) {
      if (!(marker instanceof FoldRegion)) marker.dispose();
    }
    myRangeMarkers.clear();
    mySerializedElements.clear();
  }

  @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 readExternal(final Element element) {
    ApplicationManager.getApplication()
        .runReadAction(
            new Runnable() {
              @Override
              public void run() {
                clear();

                if (!myFile.isValid()) return;

                final Document document = FileDocumentManager.getInstance().getDocument(myFile);
                if (document == null) return;

                PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
                if (psiFile == null || !psiFile.getViewProvider().isPhysical()) return;

                String date = null;
                boolean canRestoreElement =
                    !DumbService.getInstance(myProject).isDumb()
                        || FoldingUpdate.supportsDumbModeFolding(psiFile);
                for (final Object o : element.getChildren()) {
                  Element e = (Element) o;
                  Boolean expanded = Boolean.valueOf(e.getAttributeValue(EXPANDED_ATT));
                  if (ELEMENT_TAG.equals(e.getName())) {
                    String signature = e.getAttributeValue(SIGNATURE_ATT);
                    if (signature == null) {
                      continue;
                    }
                    FoldingInfo fi = new FoldingInfo(DEFAULT_PLACEHOLDER, expanded);
                    if (canRestoreElement) {
                      PsiElement restoredElement =
                          FoldingPolicy.restoreBySignature(psiFile, signature);
                      if (restoredElement != null && restoredElement.isValid()) {
                        myPsiElements.add(
                            SmartPointerManager.getInstance(myProject)
                                .createSmartPsiElementPointer(restoredElement));
                        restoredElement.putUserData(FOLDING_INFO_KEY, fi);
                      }
                    } else {
                      // Postponed initialization
                      mySerializedElements.add(new SerializedPsiElement(signature, fi));
                    }
                  } else if (MARKER_TAG.equals(e.getName())) {
                    if (date == null) {
                      date = getTimeStamp();
                    }
                    if (date.isEmpty()) continue;

                    if (!date.equals(e.getAttributeValue(DATE_ATT))
                        || FileDocumentManager.getInstance().isDocumentUnsaved(document)) continue;
                    StringTokenizer tokenizer =
                        new StringTokenizer(e.getAttributeValue(SIGNATURE_ATT), ":");
                    try {
                      int start = Integer.valueOf(tokenizer.nextToken()).intValue();
                      int end = Integer.valueOf(tokenizer.nextToken()).intValue();
                      if (start < 0 || end >= document.getTextLength() || start > end) continue;
                      RangeMarker marker = document.createRangeMarker(start, end);
                      myRangeMarkers.add(marker);
                      String placeHolderText = e.getAttributeValue(PLACEHOLDER_ATT);
                      if (placeHolderText == null) placeHolderText = DEFAULT_PLACEHOLDER;
                      FoldingInfo fi = new FoldingInfo(placeHolderText, expanded);
                      marker.putUserData(FOLDING_INFO_KEY, fi);
                    } catch (NoSuchElementException exc) {
                      LOG.error(exc);
                    }
                  } else {
                    throw new IllegalStateException("unknown tag: " + e.getName());
                  }
                }
              }
            });
  }

  private String getTimeStamp() {
    if (!myFile.isValid()) return "";
    return Long.toString(myFile.getTimeStamp());
  }

  @Override
  public int hashCode() {
    int result = myProject.hashCode();
    result = 31 * result + (myFile != null ? myFile.hashCode() : 0);
    result = 31 * result + myPsiElements.hashCode();
    result = 31 * result + myRangeMarkers.hashCode();
    result = 31 * result + mySerializedElements.hashCode();
    return result;
  }

  @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 static class FoldingInfo {
    private final String placeHolder;
    private final boolean expanded;

    private FoldingInfo(@NotNull String placeHolder, boolean expanded) {
      this.placeHolder = placeHolder;
      this.expanded = expanded;
    }

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

      FoldingInfo info = (FoldingInfo) o;

      return expanded == info.expanded && placeHolder.equals(info.placeHolder);
    }

    @Override
    public int hashCode() {
      int result = placeHolder.hashCode();
      result = 31 * result + (expanded ? 1 : 0);
      return result;
    }

    public boolean getExpanded() {
      return expanded;
    }
  }
}
public class FindUsagesManager implements JDOMExternalizable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.find.findParameterUsages.FindUsagesManager");

  private enum FileSearchScope {
    FROM_START,
    FROM_END,
    AFTER_CARET,
    BEFORE_CARET
  }

  private static final Key<String> KEY_START_USAGE_AGAIN = Key.create("KEY_START_USAGE_AGAIN");
  @NonNls private static final String VALUE_START_USAGE_AGAIN = "START_AGAIN";
  private final Project myProject;
  private final com.intellij.usages.UsageViewManager myAnotherManager;
  private boolean myToOpenInNewTab = false;

  public static class SearchData {
    public SmartPsiElementPointer[] myElements = null;
    public FindUsagesOptions myOptions = null;

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

      final SearchData that = (SearchData) o;

      return Arrays.equals(myElements, that.myElements)
          && (myOptions != null ? myOptions.equals(that.myOptions) : that.myOptions == null);
    }

    public int hashCode() {
      return myElements != null ? Arrays.hashCode(myElements) : 0;
    }
  }

  private SearchData myLastSearchInFileData = new SearchData();
  private final List<SearchData> myFindUsagesHistory =
      ContainerUtil.createLockFreeCopyOnWriteList();

  public FindUsagesManager(
      @NotNull Project project, @NotNull com.intellij.usages.UsageViewManager anotherManager) {
    myProject = project;
    myAnotherManager = anotherManager;
  }

  public boolean canFindUsages(@NotNull final PsiElement element) {
    for (FindUsagesHandlerFactory factory :
        Extensions.getExtensions(FindUsagesHandlerFactory.EP_NAME, myProject)) {
      try {
        if (factory.canFindUsages(element)) {
          return true;
        }
      } catch (IndexNotReadyException e) {
        throw e;
      } catch (Exception e) {
        LOG.error(e);
      }
    }
    return false;
  }

  public void clearFindingNextUsageInFile() {
    myLastSearchInFileData.myOptions = null;
    myLastSearchInFileData.myElements = null;
  }

  public boolean findNextUsageInFile(FileEditor editor) {
    return findUsageInFile(editor, FileSearchScope.AFTER_CARET);
  }

  public boolean findPreviousUsageInFile(FileEditor editor) {
    return findUsageInFile(editor, FileSearchScope.BEFORE_CARET);
  }

  @Override
  public void readExternal(Element element) throws InvalidDataException {
    myToOpenInNewTab = JDOMExternalizer.readBoolean(element, "OPEN_NEW_TAB");
  }

  @Override
  public void writeExternal(Element element) throws WriteExternalException {
    JDOMExternalizer.write(element, "OPEN_NEW_TAB", myToOpenInNewTab);
  }

  private boolean findUsageInFile(@NotNull FileEditor editor, @NotNull FileSearchScope direction) {
    PsiElement[] elements = restorePsiElements(myLastSearchInFileData, true);
    if (elements == null) return false;
    if (elements.length == 0) return true; // all elements have invalidated

    UsageInfoToUsageConverter.TargetElementsDescriptor descriptor =
        new UsageInfoToUsageConverter.TargetElementsDescriptor(elements);

    // todo
    TextEditor textEditor = (TextEditor) editor;
    Document document = textEditor.getEditor().getDocument();
    PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
    if (psiFile == null) return false;

    final FindUsagesHandler handler = getFindUsagesHandler(elements[0], false);
    if (handler == null) return false;
    findUsagesInEditor(
        descriptor, handler, psiFile, direction, myLastSearchInFileData.myOptions, textEditor);
    return true;
  }

  // returns null if cannot find, empty Pair if all elements have been changed
  @Nullable
  private PsiElement[] restorePsiElements(SearchData searchData, final boolean showErrorMessage) {
    if (searchData == null) return null;
    SmartPsiElementPointer[] lastSearchElements = searchData.myElements;
    if (lastSearchElements == null) return null;
    List<PsiElement> elements = new ArrayList<PsiElement>();
    for (SmartPsiElementPointer pointer : lastSearchElements) {
      PsiElement element = pointer.getElement();
      if (element != null) elements.add(element);
    }
    if (elements.isEmpty() && showErrorMessage) {
      Messages.showMessageDialog(
          myProject,
          FindBundle.message("find.searched.elements.have.been.changed.error"),
          FindBundle.message("cannot.search.for.usages.title"),
          Messages.getInformationIcon());
      // SCR #10022
      // clearFindingNextUsageInFile();
      return PsiElement.EMPTY_ARRAY;
    }

    return PsiUtilCore.toPsiElementArray(elements);
  }

  private void initLastSearchElement(
      final FindUsagesOptions findUsagesOptions,
      UsageInfoToUsageConverter.TargetElementsDescriptor descriptor) {
    myLastSearchInFileData = createSearchData(descriptor.getAllElements(), findUsagesOptions);
  }

  private SearchData createSearchData(
      @NotNull List<? extends PsiElement> psiElements, final FindUsagesOptions findUsagesOptions) {
    SearchData data = new SearchData();

    data.myElements = new SmartPsiElementPointer[psiElements.size()];
    int idx = 0;
    for (PsiElement psiElement : psiElements) {
      data.myElements[idx++] =
          SmartPointerManager.getInstance(myProject).createSmartPsiElementPointer(psiElement);
    }
    data.myOptions = findUsagesOptions;
    return data;
  }

  @Nullable
  public FindUsagesHandler getFindUsagesHandler(
      PsiElement element, final boolean forHighlightUsages) {
    for (FindUsagesHandlerFactory factory :
        Extensions.getExtensions(FindUsagesHandlerFactory.EP_NAME, myProject)) {
      if (factory.canFindUsages(element)) {
        final FindUsagesHandler handler =
            factory.createFindUsagesHandler(element, forHighlightUsages);
        if (handler == FindUsagesHandler.NULL_HANDLER) return null;
        if (handler != null) {
          return handler;
        }
      }
    }
    return null;
  }

  @Nullable
  public FindUsagesHandler getNewFindUsagesHandler(
      @NotNull PsiElement element, final boolean forHighlightUsages) {
    for (FindUsagesHandlerFactory factory :
        Extensions.getExtensions(FindUsagesHandlerFactory.EP_NAME, myProject)) {
      if (factory.canFindUsages(element)) {
        Class<? extends FindUsagesHandlerFactory> aClass = factory.getClass();
        FindUsagesHandlerFactory copy =
            (FindUsagesHandlerFactory)
                new ConstructorInjectionComponentAdapter(aClass.getName(), aClass)
                    .getComponentInstance(myProject.getPicoContainer());
        final FindUsagesHandler handler = copy.createFindUsagesHandler(element, forHighlightUsages);
        if (handler == FindUsagesHandler.NULL_HANDLER) return null;
        if (handler != null) {
          return handler;
        }
      }
    }
    return null;
  }

  public void findUsages(
      @NotNull PsiElement psiElement,
      final PsiFile scopeFile,
      final FileEditor editor,
      boolean showDialog) {
    doShowDialogAndStartFind(psiElement, scopeFile, editor, showDialog, true);
  }

  private void doShowDialogAndStartFind(
      @NotNull PsiElement psiElement,
      PsiFile scopeFile,
      FileEditor editor,
      boolean showDialog,
      boolean useMaximalScope) {
    FindUsagesHandler handler = getNewFindUsagesHandler(psiElement, false);
    if (handler == null) return;

    boolean singleFile = scopeFile != null;
    AbstractFindUsagesDialog dialog =
        handler.getFindUsagesDialog(singleFile, shouldOpenInNewTab(), mustOpenInNewTab());
    if (showDialog) {
      dialog.show();
      if (!dialog.isOK()) return;
    } else {
      dialog.close(DialogWrapper.OK_EXIT_CODE);
    }

    setOpenInNewTab(dialog.isShowInSeparateWindow());

    FindUsagesOptions findUsagesOptions = dialog.calcFindUsagesOptions();
    if (!showDialog && useMaximalScope) {
      findUsagesOptions.searchScope = getMaximalScope(handler);
    }

    clearFindingNextUsageInFile();
    LOG.assertTrue(handler.getPsiElement().isValid());
    PsiElement[] primaryElements = handler.getPrimaryElements();
    checkNotNull(primaryElements, handler, "getPrimaryElements()");
    PsiElement[] secondaryElements = handler.getSecondaryElements();
    checkNotNull(secondaryElements, handler, "getSecondaryElements()");
    UsageInfoToUsageConverter.TargetElementsDescriptor descriptor =
        new UsageInfoToUsageConverter.TargetElementsDescriptor(primaryElements, secondaryElements);
    if (singleFile) {
      findUsagesOptions = findUsagesOptions.clone();
      editor.putUserData(KEY_START_USAGE_AGAIN, null);
      findUsagesInEditor(
          descriptor, handler, scopeFile, FileSearchScope.FROM_START, findUsagesOptions, editor);
    } else {
      findUsages(
          descriptor,
          handler,
          dialog.isSkipResultsWhenOneUsage(),
          dialog.isShowInSeparateWindow(),
          findUsagesOptions);
    }
  }

  public void showSettingsAndFindUsages(@NotNull NavigationItem[] targets) {
    UsageTarget[] usageTargets = (UsageTarget[]) targets;
    PsiElement[] elements = getPsiElements(usageTargets);
    if (elements.length == 0) return;
    PsiElement psiElement = elements[0];
    doShowDialogAndStartFind(psiElement, null, null, true, false);
  }

  private static void checkNotNull(
      @NotNull PsiElement[] primaryElements,
      @NotNull FindUsagesHandler handler,
      @NonNls @NotNull String methodName) {
    for (PsiElement element : primaryElements) {
      if (element == null) {
        LOG.error(
            handler
                + "."
                + methodName
                + " has returned array with null elements: "
                + Arrays.asList(primaryElements));
      }
    }
  }

  public boolean isUsed(@NotNull PsiElement element, @NotNull FindUsagesOptions findUsagesOptions) {
    FindUsagesHandler handler = getFindUsagesHandler(element, true);
    if (handler == null) return false;
    UsageInfoToUsageConverter.TargetElementsDescriptor descriptor =
        new UsageInfoToUsageConverter.TargetElementsDescriptor(element);
    UsageSearcher usageSearcher = createUsageSearcher(descriptor, handler, findUsagesOptions, null);
    final AtomicBoolean used = new AtomicBoolean();
    usageSearcher.generate(
        new Processor<Usage>() {
          @Override
          public boolean process(final Usage usage) {
            used.set(true);
            return false;
          }
        });
    return used.get();
  }

  @NotNull
  public static ProgressIndicator startProcessUsages(
      @NotNull FindUsagesHandler handler,
      @NotNull UsageInfoToUsageConverter.TargetElementsDescriptor descriptor,
      @NotNull final Processor<Usage> processor,
      @NotNull FindUsagesOptions findUsagesOptions,
      @NotNull final Runnable onComplete) {
    final UsageSearcher usageSearcher =
        createUsageSearcher(descriptor, handler, findUsagesOptions, null);

    final ProgressIndicatorBase indicator = new ProgressIndicatorBase();
    ApplicationManager.getApplication()
        .executeOnPooledThread(
            new Runnable() {
              @Override
              public void run() {
                try {
                  ProgressManager.getInstance()
                      .runProcess(
                          new Runnable() {
                            @Override
                            public void run() {
                              usageSearcher.generate(processor);
                            }
                          },
                          indicator);
                } finally {
                  onComplete.run();
                }
              }
            });

    return indicator;
  }

  @NotNull
  public UsageViewPresentation createPresentation(
      @NotNull FindUsagesHandler handler, @NotNull FindUsagesOptions findUsagesOptions) {
    PsiElement element = handler.getPsiElement();
    LOG.assertTrue(element.isValid());
    return createPresentation(element, findUsagesOptions, myToOpenInNewTab);
  }

  private void setOpenInNewTab(final boolean toOpenInNewTab) {
    if (!mustOpenInNewTab()) {
      myToOpenInNewTab = toOpenInNewTab;
    }
  }

  private boolean shouldOpenInNewTab() {
    return mustOpenInNewTab() || myToOpenInNewTab;
  }

  private boolean mustOpenInNewTab() {
    Content selectedContent = UsageViewManager.getInstance(myProject).getSelectedContent(true);
    return selectedContent != null && selectedContent.isPinned();
  }

  private static UsageSearcher createUsageSearcher(
      @NotNull final UsageInfoToUsageConverter.TargetElementsDescriptor descriptor,
      @NotNull final FindUsagesHandler handler,
      @NotNull FindUsagesOptions _options,
      final PsiFile scopeFile) {
    final FindUsagesOptions options = _options.clone();
    return new UsageSearcher() {
      @Override
      public void generate(@NotNull final Processor<Usage> processor) {
        if (scopeFile != null) {
          options.searchScope = new LocalSearchScope(scopeFile);
        }
        final Processor<UsageInfo> usageInfoProcessor =
            new CommonProcessors.UniqueProcessor<UsageInfo>(
                new Processor<UsageInfo>() {
                  @Override
                  public boolean process(UsageInfo usageInfo) {
                    return processor.process(
                        UsageInfoToUsageConverter.convert(descriptor, usageInfo));
                  }
                });
        final List<? extends PsiElement> elements =
            ApplicationManager.getApplication()
                .runReadAction(
                    new Computable<List<? extends PsiElement>>() {
                      @Override
                      public List<? extends PsiElement> compute() {
                        return descriptor.getAllElements();
                      }
                    });

        options.fastTrack = new SearchRequestCollector(new SearchSession());

        try {
          for (final PsiElement element : elements) {
            ApplicationManager.getApplication()
                .runReadAction(
                    new Runnable() {
                      @Override
                      public void run() {
                        LOG.assertTrue(element.isValid());
                      }
                    });
            handler.processElementUsages(element, usageInfoProcessor, options);
            for (CustomUsageSearcher searcher :
                Extensions.getExtensions(CustomUsageSearcher.EP_NAME)) {
              try {
                searcher.processElementUsages(element, processor, options);
              } catch (IndexNotReadyException e) {
                DumbService.getInstance(element.getProject())
                    .showDumbModeNotification("Find usages is not available during indexing");
              } catch (Exception e) {
                LOG.error(e);
              }
            }
          }

          Project project =
              ApplicationManager.getApplication()
                  .runReadAction(
                      new Computable<Project>() {
                        @Override
                        public Project compute() {
                          return scopeFile != null
                              ? scopeFile.getProject()
                              : !elements.isEmpty()
                                  ? elements.get(0).getProject()
                                  : handler.getProject();
                        }
                      });
          PsiSearchHelper.SERVICE
              .getInstance(project)
              .processRequests(
                  options.fastTrack,
                  new ReadActionProcessor<PsiReference>() {
                    @Override
                    public boolean processInReadAction(final PsiReference ref) {
                      return !ref.getElement().isValid()
                          || usageInfoProcessor.process(new UsageInfo(ref));
                    }
                  });
        } finally {
          options.fastTrack = null;
        }
      }
    };
  }

  private static PsiElement2UsageTargetAdapter[] convertToUsageTargets(
      final List<? extends PsiElement> elementsToSearch) {
    final ArrayList<PsiElement2UsageTargetAdapter> targets =
        new ArrayList<PsiElement2UsageTargetAdapter>(elementsToSearch.size());
    for (PsiElement element : elementsToSearch) {
      convertToUsageTarget(targets, element);
    }
    return targets.toArray(new PsiElement2UsageTargetAdapter[targets.size()]);
  }

  private void findUsages(
      @NotNull final UsageInfoToUsageConverter.TargetElementsDescriptor descriptor,
      @NotNull final FindUsagesHandler handler,
      final boolean toSkipUsagePanelWhenOneUsage,
      final boolean toOpenInNewTab,
      @NotNull final FindUsagesOptions findUsagesOptions) {
    List<? extends PsiElement> elements = descriptor.getAllElements();
    if (elements.isEmpty()) {
      throw new AssertionError(handler + " " + findUsagesOptions);
    }
    final UsageTarget[] targets = convertToUsageTargets(elements);
    myAnotherManager.searchAndShowUsages(
        targets,
        new Factory<UsageSearcher>() {
          @Override
          public UsageSearcher create() {
            return createUsageSearcher(descriptor, handler, findUsagesOptions, null);
          }
        },
        !toSkipUsagePanelWhenOneUsage,
        true,
        createPresentation(elements.get(0), findUsagesOptions, toOpenInNewTab),
        null);
    addToHistory(elements, findUsagesOptions);
  }

  @NotNull
  private static UsageViewPresentation createPresentation(
      @NotNull PsiElement psiElement,
      @NotNull FindUsagesOptions findUsagesOptions,
      boolean toOpenInNewTab) {
    UsageViewPresentation presentation = new UsageViewPresentation();
    String scopeString =
        findUsagesOptions.searchScope == null
            ? null
            : findUsagesOptions.searchScope.getDisplayName();
    presentation.setScopeText(scopeString);
    String usagesString = generateUsagesString(findUsagesOptions);
    presentation.setUsagesString(usagesString);
    String title =
        scopeString == null
            ? FindBundle.message(
                "find.usages.of.element.panel.title",
                usagesString,
                UsageViewUtil.getLongName(psiElement))
            : FindBundle.message(
                "find.usages.of.element.in.scope.panel.title",
                usagesString,
                UsageViewUtil.getLongName(psiElement),
                scopeString);
    presentation.setTabText(title);
    presentation.setTabName(
        FindBundle.message(
            "find.usages.of.element.tab.name",
            usagesString,
            UsageViewUtil.getShortName(psiElement)));
    presentation.setTargetsNodeText(StringUtil.capitalize(UsageViewUtil.getType(psiElement)));
    presentation.setOpenInNewTab(toOpenInNewTab);
    return presentation;
  }

  private void findUsagesInEditor(
      @NotNull UsageInfoToUsageConverter.TargetElementsDescriptor descriptor,
      @NotNull FindUsagesHandler handler,
      @NotNull PsiFile scopeFile,
      @NotNull FileSearchScope direction,
      @NotNull final FindUsagesOptions findUsagesOptions,
      @NotNull FileEditor fileEditor) {
    initLastSearchElement(findUsagesOptions, descriptor);

    clearStatusBar();

    final FileEditorLocation currentLocation = fileEditor.getCurrentLocation();

    final UsageSearcher usageSearcher =
        createUsageSearcher(descriptor, handler, findUsagesOptions, scopeFile);
    AtomicBoolean usagesWereFound = new AtomicBoolean();

    Usage fUsage =
        findSiblingUsage(usageSearcher, direction, currentLocation, usagesWereFound, fileEditor);

    if (fUsage != null) {
      fUsage.navigate(true);
      fUsage.selectInEditor();
    } else if (!usagesWereFound.get()) {
      String message =
          getNoUsagesFoundMessage(descriptor.getPrimaryElements()[0])
              + " in "
              + scopeFile.getName();
      showHintOrStatusBarMessage(message, fileEditor);
    } else {
      fileEditor.putUserData(KEY_START_USAGE_AGAIN, VALUE_START_USAGE_AGAIN);
      showHintOrStatusBarMessage(
          getSearchAgainMessage(descriptor.getPrimaryElements()[0], direction), fileEditor);
    }
  }

  private static String getNoUsagesFoundMessage(PsiElement psiElement) {
    String elementType = UsageViewUtil.getType(psiElement);
    String elementName = UsageViewUtil.getShortName(psiElement);
    return FindBundle.message(
        "find.usages.of.element_type.element_name.not.found.message", elementType, elementName);
  }

  private void clearStatusBar() {
    StatusBar.Info.set("", myProject);
  }

  private static String getSearchAgainMessage(PsiElement element, final FileSearchScope direction) {
    String message = getNoUsagesFoundMessage(element);
    if (direction == FileSearchScope.AFTER_CARET) {
      AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_NEXT);
      String shortcutsText = KeymapUtil.getFirstKeyboardShortcutText(action);
      if (shortcutsText.isEmpty()) {
        message = FindBundle.message("find.search.again.from.top.action.message", message);
      } else {
        message =
            FindBundle.message("find.search.again.from.top.hotkey.message", message, shortcutsText);
      }
    } else {
      String shortcutsText =
          KeymapUtil.getFirstKeyboardShortcutText(
              ActionManager.getInstance().getAction(IdeActions.ACTION_FIND_PREVIOUS));
      if (shortcutsText.isEmpty()) {
        message = FindBundle.message("find.search.again.from.bottom.action.message", message);
      } else {
        message =
            FindBundle.message(
                "find.search.again.from.bottom.hotkey.message", message, shortcutsText);
      }
    }
    return message;
  }

  private void showHintOrStatusBarMessage(String message, FileEditor fileEditor) {
    if (fileEditor instanceof TextEditor) {
      TextEditor textEditor = (TextEditor) fileEditor;
      showEditorHint(message, textEditor.getEditor());
    } else {
      StatusBar.Info.set(message, myProject);
    }
  }

  private static Usage findSiblingUsage(
      @NotNull final UsageSearcher usageSearcher,
      @NotNull FileSearchScope dir,
      final FileEditorLocation currentLocation,
      @NotNull final AtomicBoolean usagesWereFound,
      @NotNull FileEditor fileEditor) {
    if (fileEditor.getUserData(KEY_START_USAGE_AGAIN) != null) {
      dir =
          dir == FileSearchScope.AFTER_CARET
              ? FileSearchScope.FROM_START
              : FileSearchScope.FROM_END;
    }

    final FileSearchScope direction = dir;

    final AtomicReference<Usage> foundUsage = new AtomicReference<Usage>();
    usageSearcher.generate(
        new Processor<Usage>() {
          @Override
          public boolean process(Usage usage) {
            usagesWereFound.set(true);
            if (direction == FileSearchScope.FROM_START) {
              foundUsage.compareAndSet(null, usage);
              return false;
            }
            if (direction == FileSearchScope.FROM_END) {
              foundUsage.set(usage);
            } else if (direction == FileSearchScope.AFTER_CARET) {
              if (Comparing.compare(usage.getLocation(), currentLocation) > 0) {
                foundUsage.set(usage);
                return false;
              }
            } else if (direction == FileSearchScope.BEFORE_CARET) {
              if (Comparing.compare(usage.getLocation(), currentLocation) >= 0) {
                return false;
              }
              while (true) {
                Usage found = foundUsage.get();
                if (found == null) {
                  if (foundUsage.compareAndSet(null, usage)) break;
                } else {
                  if (Comparing.compare(found.getLocation(), usage.getLocation()) < 0
                      && foundUsage.compareAndSet(found, usage)) break;
                }
              }
            }

            return true;
          }
        });

    fileEditor.putUserData(KEY_START_USAGE_AGAIN, null);

    return foundUsage.get();
  }

  private static void convertToUsageTarget(
      @NotNull List<PsiElement2UsageTargetAdapter> targets, @NotNull PsiElement elementToSearch) {
    if (elementToSearch instanceof NavigationItem) {
      targets.add(new PsiElement2UsageTargetAdapter(elementToSearch));
    } else {
      throw new IllegalArgumentException(
          "Wrong usage target:" + elementToSearch + "; " + elementToSearch.getClass());
    }
  }

  private static String generateUsagesString(final FindUsagesOptions selectedOptions) {
    return selectedOptions.generateUsagesString();
  }

  private static void showEditorHint(String message, final Editor editor) {
    JComponent component = HintUtil.createInformationLabel(message);
    final LightweightHint hint = new LightweightHint(component);
    HintManagerImpl.getInstanceImpl()
        .showEditorHint(
            hint,
            editor,
            HintManager.UNDER,
            HintManager.HIDE_BY_ANY_KEY
                | HintManager.HIDE_BY_TEXT_CHANGE
                | HintManager.HIDE_BY_SCROLLING,
            0,
            false);
  }

  public static String getHelpID(PsiElement element) {
    return LanguageFindUsages.INSTANCE.forLanguage(element.getLanguage()).getHelpId(element);
  }

  private void addToHistory(
      final List<? extends PsiElement> elements, final FindUsagesOptions findUsagesOptions) {
    SearchData data = createSearchData(elements, findUsagesOptions);
    myFindUsagesHistory.remove(data);
    myFindUsagesHistory.add(data);

    // todo configure history depth limit
    if (myFindUsagesHistory.size() > 15) {
      myFindUsagesHistory.remove(0);
    }
  }

  public void rerunAndRecallFromHistory(@NotNull SearchData searchData) {
    myFindUsagesHistory.remove(searchData);
    PsiElement[] elements = restorePsiElements(searchData, true);
    if (elements == null || elements.length == 0) return;
    UsageInfoToUsageConverter.TargetElementsDescriptor descriptor =
        new UsageInfoToUsageConverter.TargetElementsDescriptor(elements);
    final FindUsagesHandler handler = getFindUsagesHandler(elements[0], false);
    if (handler == null) return;
    findUsages(descriptor, handler, false, false, searchData.myOptions);
  }

  // most recent entry is at the end of the list
  public List<SearchData> getFindUsageHistory() {
    removeInvalidElementsFromHistory();
    return Collections.unmodifiableList(myFindUsagesHistory);
  }

  private void removeInvalidElementsFromHistory() {
    for (SearchData data : myFindUsagesHistory) {
      PsiElement[] elements = restorePsiElements(data, false);
      if (elements == null || elements.length == 0) myFindUsagesHistory.remove(data);
    }
  }

  @NotNull
  private static PsiElement[] getPsiElements(@NotNull UsageTarget[] targets) {
    List<PsiElement> result = new ArrayList<PsiElement>();
    for (UsageTarget target : targets) {
      if (target instanceof PsiElementUsageTarget) {
        PsiElement element = ((PsiElementUsageTarget) target).getElement();
        if (element != null) {
          result.add(element);
        }
      }
    }
    return PsiUtilCore.toPsiElementArray(result);
  }

  @NotNull
  public static GlobalSearchScope getMaximalScope(@NotNull FindUsagesHandler handler) {
    PsiElement element = handler.getPsiElement();
    Project project = element.getProject();
    PsiFile file = element.getContainingFile();
    if (file != null
        && ProjectFileIndex.SERVICE
            .getInstance(project)
            .isInContent(file.getViewProvider().getVirtualFile())) {
      return GlobalSearchScope.projectScope(project);
    }
    return GlobalSearchScope.allScope(project);
  }
}
/** @author cdr */
public class InjectedLanguageUtil {
  static final Key<
          List<Trinity<IElementType, SmartPsiElementPointer<PsiLanguageInjectionHost>, TextRange>>>
      HIGHLIGHT_TOKENS = Key.create("HIGHLIGHT_TOKENS");
  public static Key<Boolean> FRANKENSTEIN_INJECTION =
      Key.create("FRANKENSTEIN_INJECTION"); // meaning: injected file text is probably incorrect

  public static void forceInjectionOnElement(@NotNull PsiElement host) {
    enumerate(
        host,
        new PsiLanguageInjectionHost.InjectedPsiVisitor() {
          @Override
          public void visit(
              @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {}
        });
  }

  @NotNull
  static PsiElement loadTree(@NotNull PsiElement host, @NotNull PsiFile containingFile) {
    if (containingFile instanceof DummyHolder) {
      PsiElement context = containingFile.getContext();
      if (context != null) {
        PsiFile topFile = context.getContainingFile();
        topFile.getNode(); // load tree
        TextRange textRange =
            host.getTextRange().shiftRight(context.getTextRange().getStartOffset());

        PsiElement inLoadedTree =
            PsiTreeUtil.findElementOfClassAtRange(
                topFile, textRange.getStartOffset(), textRange.getEndOffset(), host.getClass());
        if (inLoadedTree != null) {
          host = inLoadedTree;
        }
      }
    }
    return host;
  }

  public static List<
          Trinity<IElementType, SmartPsiElementPointer<PsiLanguageInjectionHost>, TextRange>>
      getHighlightTokens(@NotNull PsiFile file) {
    return file.getUserData(HIGHLIGHT_TOKENS);
  }

  public static Place getShreds(@NotNull PsiFile injectedFile) {
    FileViewProvider viewProvider = injectedFile.getViewProvider();
    return getShreds(viewProvider);
  }

  public static Place getShreds(@NotNull FileViewProvider viewProvider) {
    if (!(viewProvider instanceof InjectedFileViewProvider)) return null;
    InjectedFileViewProvider myFileViewProvider = (InjectedFileViewProvider) viewProvider;
    return ((DocumentWindowImpl) myFileViewProvider.getDocument()).getShreds();
  }

  public static void enumerate(
      @NotNull PsiElement host, @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) {
    PsiFile containingFile = host.getContainingFile();
    enumerate(host, containingFile, true, visitor);
  }

  public static void enumerate(
      @NotNull PsiElement host,
      @NotNull PsiFile containingFile,
      boolean probeUp,
      @NotNull PsiLanguageInjectionHost.InjectedPsiVisitor visitor) {
    // do not inject into nonphysical files except during completion
    if (!containingFile.isPhysical() && containingFile.getOriginalFile() == containingFile) {
      final PsiElement context =
          InjectedLanguageManager.getInstance(containingFile.getProject())
              .getInjectionHost(containingFile);
      if (context == null) return;

      final PsiFile file = context.getContainingFile();
      if (file == null || !file.isPhysical() && file.getOriginalFile() == file) return;
    }

    if (containingFile.getViewProvider() instanceof InjectedFileViewProvider)
      return; // no injection inside injection

    PsiElement inTree = loadTree(host, containingFile);
    if (inTree != host) {
      host = inTree;
      containingFile = host.getContainingFile();
    }

    MultiHostRegistrarImpl registrar = probeElementsUp(host, containingFile, probeUp);
    if (registrar == null) {
      return;
    }
    List<Pair<Place, PsiFile>> places = registrar.getResult();
    for (Pair<Place, PsiFile> pair : places) {
      PsiFile injectedPsi = pair.second;
      visitor.visit(injectedPsi, pair.first);
    }
  }

  public static Editor getEditorForInjectedLanguageNoCommit(
      @Nullable Editor editor, @Nullable PsiFile file) {
    if (editor == null || file == null || editor instanceof EditorWindow) return editor;

    int offset = editor.getCaretModel().getOffset();
    return getEditorForInjectedLanguageNoCommit(editor, file, offset);
  }

  public static Editor getEditorForInjectedLanguageNoCommit(
      @Nullable Editor editor, @Nullable PsiFile file, final int offset) {
    if (editor == null || file == null || editor instanceof EditorWindow) return editor;
    PsiFile injectedFile = findInjectedPsiNoCommit(file, offset);
    return getInjectedEditorForInjectedFile(editor, injectedFile);
  }

  @NotNull
  public static Editor getInjectedEditorForInjectedFile(
      @NotNull Editor hostEditor, @Nullable final PsiFile injectedFile) {
    if (injectedFile == null || hostEditor instanceof EditorWindow || hostEditor.isDisposed())
      return hostEditor;
    Project project = hostEditor.getProject();
    if (project == null) project = injectedFile.getProject();
    Document document = PsiDocumentManager.getInstance(project).getDocument(injectedFile);
    if (!(document instanceof DocumentWindowImpl)) return hostEditor;
    DocumentWindowImpl documentWindow = (DocumentWindowImpl) document;
    SelectionModel selectionModel = hostEditor.getSelectionModel();
    if (selectionModel.hasSelection()) {
      int selstart = selectionModel.getSelectionStart();
      int selend = selectionModel.getSelectionEnd();
      if (!documentWindow.containsRange(selstart, selend)) {
        // selection spreads out the injected editor range
        return hostEditor;
      }
    }
    if (!documentWindow.isValid())
      return hostEditor; // since the moment we got hold of injectedFile and this moment call,
                         // document may have been dirtied
    return EditorWindow.create(documentWindow, (EditorImpl) hostEditor, injectedFile);
  }

  @Nullable
  public static PsiFile findInjectedPsiNoCommit(@NotNull PsiFile host, int offset) {
    PsiElement injected = findInjectedElementNoCommit(host, offset);
    return injected == null ? null : injected.getContainingFile();
  }

  // consider injected elements
  public static PsiElement findElementAtNoCommit(@NotNull PsiFile file, int offset) {
    FileViewProvider viewProvider = file.getViewProvider();
    Trinity<PsiElement, PsiElement, Language> result = null;
    if (!(viewProvider instanceof InjectedFileViewProvider)) {
      PsiDocumentManager documentManager = PsiDocumentManager.getInstance(file.getProject());
      result = tryOffset(file, offset, documentManager);
      PsiElement injected = result.first;
      if (injected != null) {
        return injected;
      }
    }
    Language baseLanguage = viewProvider.getBaseLanguage();
    if (result != null && baseLanguage == result.third) {
      return result.second; // already queried
    }
    return viewProvider.findElementAt(offset, baseLanguage);
  }

  private static final InjectedPsiCachedValueProvider INJECTED_PSI_PROVIDER =
      new InjectedPsiCachedValueProvider();
  private static final Key<ParameterizedCachedValue<MultiHostRegistrarImpl, PsiElement>>
      INJECTED_PSI = Key.create("INJECTED_PSI");

  private static MultiHostRegistrarImpl probeElementsUp(
      @NotNull PsiElement element, @NotNull PsiFile hostPsiFile, boolean probeUp) {
    PsiManager psiManager = hostPsiFile.getManager();
    final Project project = psiManager.getProject();
    InjectedLanguageManagerImpl injectedManager =
        InjectedLanguageManagerImpl.getInstanceImpl(project);
    if (injectedManager == null) {
      return null; // for tests
    }
    MultiHostRegistrarImpl registrar = null;
    PsiElement current = element;
    nextParent:
    while (current != null && current != hostPsiFile) {
      ProgressManager.checkCanceled();
      if ("EL".equals(current.getLanguage().getID())) break;
      ParameterizedCachedValue<MultiHostRegistrarImpl, PsiElement> data =
          current.getUserData(INJECTED_PSI);
      if (data == null) {
        registrar =
            InjectedPsiCachedValueProvider.doCompute(
                current, injectedManager, project, hostPsiFile);
      } else {
        registrar = data.getValue(current);
      }

      current = current.getParent(); // cache no injection for current

      if (registrar != null) {
        List<Pair<Place, PsiFile>> places = registrar.getResult();
        // check that injections found intersect with queried element
        TextRange elementRange = element.getTextRange();
        for (Pair<Place, PsiFile> pair : places) {
          Place place = pair.first;
          for (PsiLanguageInjectionHost.Shred shred : place) {
            if (shred.getHost().getTextRange().intersects(elementRange)) {
              if (place.isValid()) break nextParent;
            }
          }
        }
      }
      if (!probeUp) {
        break;
      }
    }

    if (probeUp) {
      // cache only if we walked all parents
      for (PsiElement e = element;
          e != current && e != null && e != hostPsiFile;
          e = e.getParent()) {
        ProgressManager.checkCanceled();
        if (registrar == null) {
          e.putUserData(INJECTED_PSI, null);
        } else {
          ParameterizedCachedValue<MultiHostRegistrarImpl, PsiElement> cachedValue =
              CachedValuesManager.getManager(project)
                  .createParameterizedCachedValue(INJECTED_PSI_PROVIDER, false);

          CachedValueProvider.Result<MultiHostRegistrarImpl> result =
              CachedValueProvider.Result.create(
                  registrar, PsiModificationTracker.MODIFICATION_COUNT, registrar);
          ((PsiParameterizedCachedValue<MultiHostRegistrarImpl, PsiElement>) cachedValue)
              .setValue(result);

          e.putUserData(INJECTED_PSI, cachedValue);
        }
      }
    }
    return registrar;
  }

  public static PsiElement findInjectedElementNoCommit(
      @NotNull PsiFile hostFile, final int offset) {
    if (hostFile instanceof PsiCompiledElement) return null;
    Project project = hostFile.getProject();
    if (InjectedLanguageManager.getInstance(project).isInjectedFragment(hostFile)) return null;
    final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
    Trinity<PsiElement, PsiElement, Language> result = tryOffset(hostFile, offset, documentManager);
    PsiElement injected = result.first;
    return injected;
  }

  // returns (injected psi, leaf element at the offset, language of the leaf element)
  // since findElementAt() is expensive, we trying to reuse its result
  @NotNull
  private static Trinity<PsiElement, PsiElement, Language> tryOffset(
      @NotNull PsiFile hostFile, final int offset, @NotNull PsiDocumentManager documentManager) {
    FileViewProvider provider = hostFile.getViewProvider();
    Language leafLanguage = null;
    PsiElement leafElement = null;
    for (Language language : provider.getLanguages()) {
      PsiElement element = provider.findElementAt(offset, language);
      if (element != null) {
        if (leafLanguage == null) {
          leafLanguage = language;
          leafElement = element;
        }
        PsiElement injected = findInside(element, hostFile, offset, documentManager);
        if (injected != null) return Trinity.create(injected, element, language);
      }
      // maybe we are at the border between two psi elements, then try to find injection at the end
      // of the left element
      if (offset != 0 && (element == null || element.getTextRange().getStartOffset() == offset)) {
        PsiElement leftElement = provider.findElementAt(offset - 1, language);
        if (leftElement != null && leftElement.getTextRange().getEndOffset() == offset) {
          PsiElement injected = findInside(leftElement, hostFile, offset, documentManager);
          if (injected != null) return Trinity.create(injected, element, language);
        }
      }
    }

    return Trinity.create(null, leafElement, leafLanguage);
  }

  private static PsiElement findInside(
      @NotNull PsiElement element,
      @NotNull PsiFile hostFile,
      final int hostOffset,
      @NotNull final PsiDocumentManager documentManager) {
    final Ref<PsiElement> out = new Ref<PsiElement>();
    enumerate(
        element,
        hostFile,
        true,
        new PsiLanguageInjectionHost.InjectedPsiVisitor() {
          @Override
          public void visit(
              @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
            for (PsiLanguageInjectionHost.Shred place : places) {
              TextRange hostRange = place.getHost().getTextRange();
              if (hostRange.cutOut(place.getRangeInsideHost()).grown(1).contains(hostOffset)) {
                DocumentWindowImpl document =
                    (DocumentWindowImpl) documentManager.getCachedDocument(injectedPsi);
                if (document == null) return;
                int injectedOffset = document.hostToInjected(hostOffset);
                PsiElement injElement = injectedPsi.findElementAt(injectedOffset);
                out.set(injElement == null ? injectedPsi : injElement);
              }
            }
          }
        });
    return out.get();
  }

  private static final Key<List<DocumentWindow>> INJECTED_DOCS_KEY =
      Key.create("INJECTED_DOCS_KEY");

  @NotNull
  public static List<DocumentWindow> getCachedInjectedDocuments(@NotNull PsiFile hostPsiFile) {
    // modification of cachedInjectedDocuments must be under PsiLock only
    List<DocumentWindow> injected = hostPsiFile.getUserData(INJECTED_DOCS_KEY);
    if (injected == null) {
      injected =
          ((UserDataHolderEx) hostPsiFile)
              .putUserDataIfAbsent(
                  INJECTED_DOCS_KEY, ContainerUtil.<DocumentWindow>createEmptyCOWList());
    }
    return injected;
  }

  public static void clearCachedInjectedFragmentsForFile(@NotNull PsiFile file) {
    file.putUserData(INJECTED_DOCS_KEY, null);
  }

  public static void clearCaches(
      @NotNull PsiFile injected, @NotNull DocumentWindowImpl documentWindow) {
    VirtualFileWindowImpl virtualFile = (VirtualFileWindowImpl) injected.getVirtualFile();
    PsiManagerEx psiManagerEx = (PsiManagerEx) injected.getManager();
    if (psiManagerEx.getProject().isDisposed()) return;
    psiManagerEx.getFileManager().setViewProvider(virtualFile, null);
    PsiElement context =
        InjectedLanguageManager.getInstance(injected.getProject()).getInjectionHost(injected);
    PsiFile hostFile;
    if (context != null) {
      hostFile = context.getContainingFile();
    } else {
      VirtualFile delegate = virtualFile.getDelegate();
      hostFile = delegate.isValid() ? psiManagerEx.findFile(delegate) : null;
    }
    if (hostFile != null) {
      // modification of cachedInjectedDocuments must be under PsiLock
      synchronized (PsiLock.LOCK) {
        List<DocumentWindow> cachedInjectedDocuments = getCachedInjectedDocuments(hostFile);
        for (int i = cachedInjectedDocuments.size() - 1; i >= 0; i--) {
          DocumentWindow cachedInjectedDocument = cachedInjectedDocuments.get(i);
          if (cachedInjectedDocument == documentWindow) {
            cachedInjectedDocuments.remove(i);
          }
        }
      }
    }
  }

  public static Editor openEditorFor(@NotNull PsiFile file, @NotNull Project project) {
    Document document = PsiDocumentManager.getInstance(project).getDocument(file);
    // may return editor injected in current selection in the host editor, not for the file passed
    // as argument
    VirtualFile virtualFile = file.getVirtualFile();
    if (virtualFile == null) {
      return null;
    }
    if (virtualFile instanceof VirtualFileWindow) {
      virtualFile = ((VirtualFileWindow) virtualFile).getDelegate();
    }
    Editor editor =
        FileEditorManager.getInstance(project)
            .openTextEditor(new OpenFileDescriptor(project, virtualFile, -1), false);
    if (editor == null || editor instanceof EditorWindow || editor.isDisposed()) return editor;
    if (document instanceof DocumentWindowImpl) {
      return EditorWindow.create((DocumentWindowImpl) document, (EditorImpl) editor, file);
    }
    return editor;
  }

  public static PsiFile getTopLevelFile(@NotNull PsiElement element) {
    PsiFile containingFile = element.getContainingFile();
    if (containingFile == null) return null;
    Document document =
        PsiDocumentManager.getInstance(element.getProject()).getCachedDocument(containingFile);
    if (document instanceof DocumentWindow) {
      PsiElement host =
          InjectedLanguageManager.getInstance(containingFile.getProject())
              .getInjectionHost(containingFile);
      if (host != null) containingFile = host.getContainingFile();
    }
    return containingFile;
  }

  @NotNull
  public static Editor getTopLevelEditor(@NotNull Editor editor) {
    return editor instanceof EditorWindow ? ((EditorWindow) editor).getDelegate() : editor;
  }

  public static boolean isInInjectedLanguagePrefixSuffix(@NotNull final PsiElement element) {
    PsiFile injectedFile = element.getContainingFile();
    if (injectedFile == null) return false;
    Project project = injectedFile.getProject();
    InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(project);
    if (!languageManager.isInjectedFragment(injectedFile)) return false;
    TextRange elementRange = element.getTextRange();
    List<TextRange> editables =
        languageManager.intersectWithAllEditableFragments(injectedFile, elementRange);
    int combinedEdiablesLength = 0;
    for (TextRange editable : editables) {
      combinedEdiablesLength += editable.getLength();
    }

    return combinedEdiablesLength != elementRange.getLength();
  }

  public static boolean isSelectionIsAboutToOverflowInjectedFragment(
      @NotNull EditorWindow injectedEditor) {
    int selStart = injectedEditor.getSelectionModel().getSelectionStart();
    int selEnd = injectedEditor.getSelectionModel().getSelectionEnd();

    DocumentWindow document = injectedEditor.getDocument();

    boolean isStartOverflows = selStart == 0;
    if (!isStartOverflows) {
      int hostPrev = document.injectedToHost(selStart - 1);
      isStartOverflows = document.hostToInjected(hostPrev) == selStart;
    }

    boolean isEndOverflows = selEnd == document.getTextLength();
    if (!isEndOverflows) {
      int hostNext = document.injectedToHost(selEnd + 1);
      isEndOverflows = document.hostToInjected(hostNext) == selEnd;
    }

    return isStartOverflows && isEndOverflows;
  }

  public static boolean hasInjections(@NotNull PsiLanguageInjectionHost host) {
    if (!host.isPhysical()) return false;
    final Ref<Boolean> result = Ref.create(false);
    enumerate(
        host,
        new PsiLanguageInjectionHost.InjectedPsiVisitor() {
          @Override
          public void visit(
              @NotNull final PsiFile injectedPsi,
              @NotNull final List<PsiLanguageInjectionHost.Shred> places) {
            result.set(true);
          }
        });
    return result.get().booleanValue();
  }

  public static String getUnescapedText(
      PsiFile file,
      @Nullable final PsiElement startElement,
      @Nullable final PsiElement endElement) {
    final InjectedLanguageManager manager = InjectedLanguageManager.getInstance(file.getProject());
    if (manager.getInjectionHost(file) == null) {
      return file.getText()
          .substring(
              startElement == null ? 0 : startElement.getTextRange().getStartOffset(),
              endElement == null
                  ? file.getTextLength()
                  : endElement.getTextRange().getStartOffset());
    }
    final StringBuilder sb = new StringBuilder();
    file.accept(
        new PsiRecursiveElementWalkingVisitor() {

          Boolean myState = startElement == null ? Boolean.TRUE : null;

          @Override
          public void visitElement(PsiElement element) {
            if (element == startElement) myState = Boolean.TRUE;
            if (element == endElement) myState = Boolean.FALSE;
            if (Boolean.FALSE == myState) return;
            if (Boolean.TRUE == myState && element.getFirstChild() == null) {
              sb.append(getUnescapedLeafText(element, false));
            } else {
              super.visitElement(element);
            }
          }
        });
    return sb.toString();
  }

  @Nullable
  public static String getUnescapedLeafText(PsiElement element, boolean strict) {
    String unescaped = element.getCopyableUserData(LeafPatcher.UNESCAPED_TEXT);
    if (unescaped != null) {
      return unescaped;
    }
    if (!strict && element.getFirstChild() == null) {
      return element.getText();
    }
    return null;
  }

  @Nullable
  public static DocumentWindow getDocumentWindow(@NotNull PsiElement element) {
    PsiFile file = element.getContainingFile();
    if (file == null) return null;
    VirtualFile virtualFile = file.getVirtualFile();
    if (virtualFile instanceof VirtualFileWindow)
      return ((VirtualFileWindow) virtualFile).getDocumentWindow();
    return null;
  }

  public static boolean isInjectableLanguage(Language language) {
    return LanguageUtil.isInjectableLanguage(language);
  }
}
/** @author Gregory Shrago */
public class QuickEditHandler extends DocumentAdapter implements Disposable {
  private final Project myProject;
  private final QuickEditAction myAction;

  private final Editor myEditor;
  private final Document myOrigDocument;

  private final Document myNewDocument;
  private final PsiFile myNewFile;
  private final LightVirtualFile myNewVirtualFile;

  private final long myOrigCreationStamp;
  private EditorWindow mySplittedWindow;
  private boolean myCommittingToOriginal;

  private final PsiFile myInjectedFile;
  private final List<Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer>> myMarkers =
      ContainerUtil.newLinkedList();

  @Nullable private final RangeMarker myAltFullRange;
  private static final Key<String> REPLACEMENT_KEY = Key.create("REPLACEMENT_KEY");

  QuickEditHandler(
      Project project,
      @NotNull PsiFile injectedFile,
      final PsiFile origFile,
      Editor editor,
      QuickEditAction action) {
    myProject = project;
    myEditor = editor;
    myAction = action;
    myOrigDocument = editor.getDocument();
    Place shreds = InjectedLanguageUtil.getShreds(injectedFile);
    FileType fileType = injectedFile.getFileType();
    Language language = injectedFile.getLanguage();
    PsiLanguageInjectionHost.Shred firstShred = ContainerUtil.getFirstItem(shreds);

    PsiFileFactory factory = PsiFileFactory.getInstance(project);
    String text = InjectedLanguageManager.getInstance(project).getUnescapedText(injectedFile);
    String newFileName =
        StringUtil.notNullize(language.getDisplayName(), "Injected")
            + " Fragment "
            + "("
            + origFile.getName()
            + ":"
            + firstShred.getHost().getTextRange().getStartOffset()
            + ")"
            + "."
            + fileType.getDefaultExtension();

    // preserve \r\n as it is done in MultiHostRegistrarImpl
    myNewFile = factory.createFileFromText(newFileName, language, text, true, false);
    myNewVirtualFile = ObjectUtils.assertNotNull((LightVirtualFile) myNewFile.getVirtualFile());
    myNewVirtualFile.setOriginalFile(origFile.getVirtualFile());

    assert myNewFile != null : "PSI file is null";
    assert myNewFile.getTextLength() == myNewVirtualFile.getContent().length()
        : "PSI / Virtual file text mismatch";

    myNewVirtualFile.setOriginalFile(origFile.getVirtualFile());
    // suppress possible errors as in injected mode
    myNewFile.putUserData(
        InjectedLanguageUtil.FRANKENSTEIN_INJECTION,
        injectedFile.getUserData(InjectedLanguageUtil.FRANKENSTEIN_INJECTION));
    myNewFile.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, shreds.getHostPointer());
    myNewDocument = PsiDocumentManager.getInstance(project).getDocument(myNewFile);
    assert myNewDocument != null;
    EditorActionManager.getInstance()
        .setReadonlyFragmentModificationHandler(myNewDocument, new MyQuietHandler());
    myOrigCreationStamp =
        myOrigDocument.getModificationStamp(); // store creation stamp for UNDO tracking
    myOrigDocument.addDocumentListener(this, this);
    myNewDocument.addDocumentListener(this, this);
    EditorFactory editorFactory = ObjectUtils.assertNotNull(EditorFactory.getInstance());
    // not FileEditorManager listener because of RegExp checker and alike
    editorFactory.addEditorFactoryListener(
        new EditorFactoryAdapter() {
          int useCount;

          @Override
          public void editorCreated(@NotNull EditorFactoryEvent event) {
            if (event.getEditor().getDocument() != myNewDocument) return;
            useCount++;
          }

          @Override
          public void editorReleased(@NotNull EditorFactoryEvent event) {
            if (event.getEditor().getDocument() != myNewDocument) return;
            if (--useCount > 0) return;
            if (Boolean.TRUE.equals(
                myNewVirtualFile.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN))) return;

            Disposer.dispose(QuickEditHandler.this);
          }
        },
        this);

    if ("JAVA".equals(firstShred.getHost().getLanguage().getID())) {
      PsiLanguageInjectionHost.Shred lastShred = ContainerUtil.getLastItem(shreds);
      myAltFullRange =
          myOrigDocument.createRangeMarker(
              firstShred.getHostRangeMarker().getStartOffset(),
              lastShred.getHostRangeMarker().getEndOffset());
      myAltFullRange.setGreedyToLeft(true);
      myAltFullRange.setGreedyToRight(true);

      initGuardedBlocks(shreds);
      myInjectedFile = null;
    } else {
      initMarkers(shreds);
      myAltFullRange = null;
      myInjectedFile = injectedFile;
    }
  }

  public boolean isValid() {
    boolean valid =
        myNewVirtualFile.isValid()
            && (myAltFullRange == null && myInjectedFile.isValid()
                || myAltFullRange != null && myAltFullRange.isValid());
    if (valid) {
      for (Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> t : myMarkers) {
        if (!t.first.isValid() || !t.second.isValid() || t.third.getElement() == null) {
          valid = false;
          break;
        }
      }
    }
    return valid;
  }

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

  @Override
  public void documentChanged(DocumentEvent e) {
    UndoManager undoManager = UndoManager.getInstance(myProject);
    boolean undoOrRedo = undoManager.isUndoInProgress() || undoManager.isRedoInProgress();
    if (undoOrRedo) {
      // allow undo/redo up until 'creation stamp' back in time
      // and check it after action is completed
      if (e.getDocument() == myOrigDocument) {
        //noinspection SSBasedInspection
        SwingUtilities.invokeLater(
            () -> {
              if (myOrigCreationStamp > myOrigDocument.getModificationStamp()) {
                closeEditor();
              }
            });
      }
    } else if (e.getDocument() == myNewDocument) {
      commitToOriginal(e);
      if (!isValid()) {
        ApplicationManager.getApplication()
            .invokeLater(() -> closeEditor(), myProject.getDisposed());
      }
    } else if (e.getDocument() == myOrigDocument) {
      if (myCommittingToOriginal || myAltFullRange != null && myAltFullRange.isValid()) return;
      ApplicationManager.getApplication().invokeLater(() -> closeEditor(), myProject.getDisposed());
    }
  }

  private void closeEditor() {
    boolean unsplit = false;
    if (mySplittedWindow != null && !mySplittedWindow.isDisposed()) {
      final EditorWithProviderComposite[] editors = mySplittedWindow.getEditors();
      if (editors.length == 1 && Comparing.equal(editors[0].getFile(), myNewVirtualFile)) {
        unsplit = true;
      }
    }
    FileEditorManager.getInstance(myProject).closeFile(myNewVirtualFile);
    if (unsplit) {
      for (EditorWindow editorWindow : mySplittedWindow.findSiblings()) {
        editorWindow.unsplit(true);
      }
    }
  }

  public void initMarkers(Place shreds) {
    SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject);
    int curOffset = -1;
    for (PsiLanguageInjectionHost.Shred shred : shreds) {
      final RangeMarker rangeMarker =
          myNewDocument.createRangeMarker(
              shred.getRange().getStartOffset() + shred.getPrefix().length(),
              shred.getRange().getEndOffset() - shred.getSuffix().length());
      final TextRange rangeInsideHost = shred.getRangeInsideHost();
      PsiLanguageInjectionHost host = shred.getHost();
      RangeMarker origMarker =
          myOrigDocument.createRangeMarker(
              rangeInsideHost.shiftRight(host.getTextRange().getStartOffset()));
      SmartPsiElementPointer<PsiLanguageInjectionHost> elementPointer =
          smartPointerManager.createSmartPsiElementPointer(host);
      Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> markers =
          Trinity.<RangeMarker, RangeMarker, SmartPsiElementPointer>create(
              origMarker, rangeMarker, elementPointer);
      myMarkers.add(markers);

      origMarker.setGreedyToRight(true);
      rangeMarker.setGreedyToRight(true);
      if (origMarker.getStartOffset() > curOffset) {
        origMarker.setGreedyToLeft(true);
        rangeMarker.setGreedyToLeft(true);
      }
      curOffset = origMarker.getEndOffset();
    }
    initGuardedBlocks(shreds);
  }

  private void initGuardedBlocks(Place shreds) {
    int origOffset = -1;
    int curOffset = 0;
    for (PsiLanguageInjectionHost.Shred shred : shreds) {
      Segment hostRangeMarker = shred.getHostRangeMarker();
      int start = shred.getRange().getStartOffset() + shred.getPrefix().length();
      int end = shred.getRange().getEndOffset() - shred.getSuffix().length();
      if (curOffset < start) {
        RangeMarker guard = myNewDocument.createGuardedBlock(curOffset, start);
        if (curOffset == 0 && shred == shreds.get(0)) guard.setGreedyToLeft(true);
        String padding =
            origOffset < 0
                ? ""
                : myOrigDocument.getText().substring(origOffset, hostRangeMarker.getStartOffset());
        guard.putUserData(REPLACEMENT_KEY, fixQuotes(padding));
      }
      curOffset = end;
      origOffset = hostRangeMarker.getEndOffset();
    }
    if (curOffset < myNewDocument.getTextLength()) {
      RangeMarker guard =
          myNewDocument.createGuardedBlock(curOffset, myNewDocument.getTextLength());
      guard.setGreedyToRight(true);
      guard.putUserData(REPLACEMENT_KEY, "");
    }
  }

  private void commitToOriginal(final DocumentEvent e) {
    VirtualFile origVirtualFile = PsiUtilCore.getVirtualFile(myNewFile.getContext());
    myCommittingToOriginal = true;
    try {
      if (origVirtualFile == null
          || !ReadonlyStatusHandler.getInstance(myProject)
              .ensureFilesWritable(origVirtualFile)
              .hasReadonlyFiles()) {
        PostprocessReformattingAspect.getInstance(myProject)
            .disablePostprocessFormattingInside(
                () -> {
                  if (myAltFullRange != null) {
                    altCommitToOriginal(e);
                    return;
                  }
                  commitToOriginalInner();
                });
        PsiDocumentManager.getInstance(myProject)
            .doPostponedOperationsAndUnblockDocument(myOrigDocument);
      }
    } finally {
      myCommittingToOriginal = false;
    }
  }

  private void commitToOriginalInner() {
    final String text = myNewDocument.getText();
    final Map<
            PsiLanguageInjectionHost,
            Set<Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer>>>
        map =
            ContainerUtil.classify(
                myMarkers.iterator(),
                new Convertor<
                    Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer>,
                    PsiLanguageInjectionHost>() {
                  @Override
                  public PsiLanguageInjectionHost convert(
                      final Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> o) {
                    final PsiElement element = o.third.getElement();
                    return (PsiLanguageInjectionHost) element;
                  }
                });
    PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
    documentManager.commitDocument(myOrigDocument); // commit here and after each manipulator update
    int localInsideFileCursor = 0;
    for (PsiLanguageInjectionHost host : map.keySet()) {
      if (host == null) continue;
      String hostText = host.getText();
      ProperTextRange insideHost = null;
      StringBuilder sb = new StringBuilder();
      for (Trinity<RangeMarker, RangeMarker, SmartPsiElementPointer> entry : map.get(host)) {
        RangeMarker origMarker = entry.first; // check for validity?
        int hostOffset = host.getTextRange().getStartOffset();
        ProperTextRange localInsideHost =
            new ProperTextRange(
                origMarker.getStartOffset() - hostOffset, origMarker.getEndOffset() - hostOffset);
        RangeMarker rangeMarker = entry.second;
        ProperTextRange localInsideFile =
            new ProperTextRange(
                Math.max(localInsideFileCursor, rangeMarker.getStartOffset()),
                rangeMarker.getEndOffset());
        if (insideHost != null) {
          // append unchanged inter-markers fragment
          sb.append(
              hostText.substring(insideHost.getEndOffset(), localInsideHost.getStartOffset()));
        }
        sb.append(
            localInsideFile.getEndOffset() <= text.length() && !localInsideFile.isEmpty()
                ? localInsideFile.substring(text)
                : "");
        localInsideFileCursor = localInsideFile.getEndOffset();
        insideHost = insideHost == null ? localInsideHost : insideHost.union(localInsideHost);
      }
      assert insideHost != null;
      ElementManipulators.getManipulator(host).handleContentChange(host, insideHost, sb.toString());
      documentManager.commitDocument(myOrigDocument);
    }
  }

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

  private static String fixQuotes(String padding) {
    if (padding.isEmpty()) return padding;
    if (padding.startsWith("'")) padding = '\"' + padding.substring(1);
    if (padding.endsWith("'")) padding = padding.substring(0, padding.length() - 1) + "\"";
    return padding;
  }

  private static void fixDocumentQuotes(Document doc, int offset) {
    if (doc.getCharsSequence().charAt(offset) == '\'') {
      doc.replaceString(offset, offset + 1, "\"");
    }
  }

  @Override
  public void dispose() {
    // noop
  }

  @TestOnly
  public PsiFile getNewFile() {
    return myNewFile;
  }

  public boolean changesRange(TextRange range) {
    if (myAltFullRange != null) {
      return range.intersects(myAltFullRange.getStartOffset(), myAltFullRange.getEndOffset());
    } else if (!myMarkers.isEmpty()) {
      TextRange hostRange =
          TextRange.create(
              myMarkers.get(0).first.getStartOffset(),
              myMarkers.get(myMarkers.size() - 1).first.getEndOffset());
      return range.intersects(hostRange);
    }
    return false;
  }

  private static class MyQuietHandler implements ReadonlyFragmentModificationHandler {
    @Override
    public void handle(final ReadOnlyFragmentModificationException e) {
      // nothing
    }
  }
}
/** @author cdr */
public class InjectedLanguageManagerImpl extends InjectedLanguageManager implements Disposable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl");
  private final Project myProject;
  private final DumbService myDumbService;
  private final AtomicReference<MultiHostInjector> myPsiManagerRegisteredInjectorsAdapter =
      new AtomicReference<MultiHostInjector>();
  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);
    multiPoint.addExtensionPointListener(
        new ExtensionPointListener<MultiHostInjector>() {
          @Override
          public void extensionAdded(
              @NotNull MultiHostInjector injector, @Nullable PluginDescriptor pluginDescriptor) {
            registerMultiHostInjector(injector);
          }

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

          @Override
          public void extensionRemoved(
              @NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) {
            psiManagerInjectorsChanged();
          }
        };
    final ExtensionPoint<LanguageInjector> psiManagerPoint =
        Extensions.getRootArea().getExtensionPoint(LanguageInjector.EXTENSION_POINT_NAME);
    psiManagerPoint.addExtensionPointListener(myListener, this);
    myProgress = new DaemonProgressIndicator();
    project
        .getMessageBus()
        .connect(this)
        .subscribe(
            DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC,
            new DaemonCodeAnalyzer.DaemonListener() {
              @Override
              public void daemonFinished() {}

              @Override
              public void daemonCancelEventOccurred() {
                myProgress.cancel();
              }
            });
  }

  @Override
  public void dispose() {}

  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 CopyOnWriteArrayList<DocumentWindow> injected =
        (CopyOnWriteArrayList<DocumentWindow>)
            InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile);
    if (injected.isEmpty()) return;

    if (myProgress.isCanceled()) {
      myProgress = new DaemonProgressIndicator();
    }

    final Processor<DocumentWindow> commitProcessor =
        new Processor<DocumentWindow>() {
          @Override
          public boolean process(DocumentWindow 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

            Segment[] ranges = documentWindow.getHostRanges();
            Segment rangeMarker = ranges.length > 0 ? ranges[0] : null;
            PsiElement element =
                rangeMarker == null
                    ? null
                    : hostPsiFile.findElementAt(rangeMarker.getStartOffset());
            if (element == null) {
              synchronized (PsiLock.LOCK) {
                injected.remove(documentWindow);
              }
              return true;
            }
            final DocumentWindow[] stillInjectedDocument = {null};
            // it is here where the reparse happens and old file contents replaced
            InjectedLanguageUtil.enumerate(
                element,
                hostPsiFile,
                true,
                new PsiLanguageInjectionHost.InjectedPsiVisitor() {
                  @Override
                  public void visit(
                      @NotNull PsiFile injectedPsi,
                      @NotNull List<PsiLanguageInjectionHost.Shred> places) {
                    stillInjectedDocument[0] =
                        (DocumentWindow) injectedPsi.getViewProvider().getDocument();
                    PsiDocumentManagerImpl.checkConsistency(injectedPsi, stillInjectedDocument[0]);
                  }
                });
            synchronized (PsiLock.LOCK) {
              if (stillInjectedDocument[0] == null) {
                injected.remove(documentWindow);
              } else if (stillInjectedDocument[0] != documentWindow) {
                injected.remove(documentWindow);
                injected.addIfAbsent(stillInjectedDocument[0]);
              }
            }

            return true;
          }
        };
    final Runnable commitInjectionsRunnable =
        new Runnable() {
          @Override
          public void run() {
            if (myProgress.isCanceled()) return;
            JobLauncher.getInstance()
                .invokeConcurrentlyUnderProgress(
                    new ArrayList<DocumentWindow>(injected),
                    myProgress,
                    !synchronously,
                    commitProcessor);
          }
        };

    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<DocumentWindow>(injected), commitProcessor);
      } else {
        commitInjectionsRunnable.run();
      }
    } else {
      JobLauncher.getInstance()
          .submitToJobThread(
              Job.DEFAULT_PRIORITY,
              new Runnable() {
                @Override
                public void run() {
                  ApplicationManagerEx.getApplicationEx()
                      .tryRunReadAction(commitInjectionsRunnable);
                }
              });
    }
  }

  public void psiManagerInjectorsChanged() {
    LanguageInjector[] extensions = Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME);
    if (extensions.length == 0) {
      MultiHostInjector prev = myPsiManagerRegisteredInjectorsAdapter.getAndSet(null);
      if (prev != null) {
        unregisterMultiHostInjector(prev);
      }
    } else {
      PsiManagerRegisteredInjectorsAdapter adapter = new PsiManagerRegisteredInjectorsAdapter();
      if (myPsiManagerRegisteredInjectorsAdapter.compareAndSet(null, adapter)) {
        registerMultiHostInjector(adapter);
      }
    }
  }

  @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) {
    ProperTextRange.assertProperRange(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);
  }

  private final ConcurrentMap<Class, MultiHostInjector[]> injectors =
      new ConcurrentHashMap<Class, MultiHostInjector[]>();
  private final ClassMapCachingNulls<MultiHostInjector> cachedInjectors =
      new ClassMapCachingNulls<MultiHostInjector>(injectors, new MultiHostInjector[0]);

  @Override
  public void registerMultiHostInjector(@NotNull MultiHostInjector injector) {
    for (Class<? extends PsiElement> place : injector.elementsToInjectIn()) {
      LOG.assertTrue(place != null, injector);
      while (true) {
        MultiHostInjector[] injectors = this.injectors.get(place);
        if (injectors == null) {
          if (this.injectors.putIfAbsent(place, new MultiHostInjector[] {injector}) == null) break;
        } else {
          MultiHostInjector[] newInfos = ArrayUtil.append(injectors, injector);
          if (this.injectors.replace(place, injectors, newInfos)) break;
        }
      }
    }
    cachedInjectors.clearCache();
  }

  @Override
  public boolean unregisterMultiHostInjector(@NotNull MultiHostInjector injector) {
    boolean removed = false;
    Iterator<Map.Entry<Class, MultiHostInjector[]>> iterator = injectors.entrySet().iterator();
    while (iterator.hasNext()) {
      Map.Entry<Class, MultiHostInjector[]> entry = iterator.next();
      MultiHostInjector[] infos = entry.getValue();
      int i = ArrayUtil.find(infos, injector);
      if (i != -1) {
        MultiHostInjector[] newInfos = ArrayUtil.remove(infos, i);
        if (newInfos.length == 0) {
          iterator.remove();
        } else {
          injectors.put(entry.getKey(), newInfos);
        }
        removed = true;
      }
    }
    cachedInjectors.clearCache();
    return removed;
  }

  static final Key<String> UNESCAPED_TEXT = Key.create("INJECTED_UNESCAPED_TEXT");

  @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 unescaped = element.getCopyableUserData(UNESCAPED_TEXT);
            if (unescaped != null) {
              text.append(unescaped);
              return;
            }
            if (element.getFirstChild() == null) {
              text.append(element.getText());
              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<TextRange>();
            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.<TextRange>emptyList()
        : count == 1 ? Collections.singletonList((TextRange) result) : (List<TextRange>) result;
  }

  @Override
  public boolean isInjectedFragment(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);
  }

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

  @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.injectors);
  }

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

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

  public void processInPlaceInjectorsFor(
      @NotNull PsiElement element, @NotNull InjProcessor processor) {
    MultiHostInjector[] infos = cachedInjectors.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<Pair<PsiElement, TextRange>>();
    InjectedLanguageUtil.enumerate(
        inTree,
        new PsiLanguageInjectionHost.InjectedPsiVisitor() {
          @Override
          public void visit(
              @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
            for (PsiLanguageInjectionHost.Shred place : places) {
              if (place.getHost() == inTree) {
                result.add(
                    new Pair<PsiElement, TextRange>(injectedPsi, place.getRangeInsideHost()));
              }
            }
          }
        });
    return result.isEmpty() ? null : result;
  }

  private static class PsiManagerRegisteredInjectorsAdapter implements MultiHostInjector {
    @Override
    public void getLanguagesToInject(
        @NotNull final MultiHostRegistrar injectionPlacesRegistrar, @NotNull PsiElement context) {
      final PsiLanguageInjectionHost host = (PsiLanguageInjectionHost) context;
      InjectedLanguagePlaces placesRegistrar =
          new InjectedLanguagePlaces() {
            @Override
            public void addPlace(
                @NotNull Language language,
                @NotNull TextRange rangeInsideHost,
                @NonNls @Nullable String prefix,
                @NonNls @Nullable String suffix) {
              ProperTextRange.assertProperRange(rangeInsideHost);
              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 Arrays.asList(PsiLanguageInjectionHost.class);
    }
  }
}
public class PostprocessReformattingAspect implements PomModelAspect {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformattingAspect");
  private final Project myProject;
  private final PsiManager myPsiManager;
  private final TreeAspect myTreeAspect;
  private final Map<FileViewProvider, List<ASTNode>> myReformatElements =
      new HashMap<FileViewProvider, List<ASTNode>>();
  private volatile int myDisabledCounter = 0;
  private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>();
  private final AtomicInteger myPostponedCounter = new AtomicInteger();
  private static final Key<Throwable> REFORMAT_ORIGINATOR = Key.create("REFORMAT_ORIGINATOR");
  private static final boolean STORE_REFORMAT_ORIGINATOR_STACKTRACE =
      ApplicationManager.getApplication().isInternal();

  public PostprocessReformattingAspect(
      Project project,
      PsiManager psiManager,
      TreeAspect treeAspect,
      final CommandProcessor processor) {
    myProject = project;
    myPsiManager = psiManager;
    myTreeAspect = treeAspect;
    PomManager.getModel(psiManager.getProject())
        .registerAspect(
            PostprocessReformattingAspect.class,
            this,
            Collections.singleton((PomModelAspect) treeAspect));

    ApplicationListener applicationListener =
        new ApplicationAdapter() {
          @Override
          public void writeActionStarted(final Object action) {
            if (processor != null) {
              final Project project = processor.getCurrentCommandProject();
              if (project == myProject) {
                incrementPostponedCounter();
              }
            }
          }

          @Override
          public void writeActionFinished(final Object action) {
            if (processor != null) {
              final Project project = processor.getCurrentCommandProject();
              if (project == myProject) {
                decrementPostponedCounter();
              }
            }
          }
        };
    ApplicationManager.getApplication().addApplicationListener(applicationListener, project);
  }

  public void disablePostprocessFormattingInside(@NotNull final Runnable runnable) {
    disablePostprocessFormattingInside(
        new NullableComputable<Object>() {
          @Override
          public Object compute() {
            runnable.run();
            return null;
          }
        });
  }

  public <T> T disablePostprocessFormattingInside(@NotNull Computable<T> computable) {
    try {
      myDisabledCounter++;
      return computable.compute();
    } finally {
      myDisabledCounter--;
      LOG.assertTrue(myDisabledCounter > 0 || !isDisabled());
    }
  }

  public void postponeFormattingInside(@NotNull final Runnable runnable) {
    postponeFormattingInside(
        new NullableComputable<Object>() {
          @Override
          public Object compute() {
            runnable.run();
            return null;
          }
        });
  }

  public <T> T postponeFormattingInside(@NotNull Computable<T> computable) {
    Application application = ApplicationManager.getApplication();
    application.assertIsDispatchThread();
    try {
      incrementPostponedCounter();
      return computable.compute();
    } finally {
      decrementPostponedCounter();
    }
  }

  private void incrementPostponedCounter() {
    myPostponedCounter.incrementAndGet();
  }

  private void decrementPostponedCounter() {
    Application application = ApplicationManager.getApplication();
    application.assertIsDispatchThread();
    if (myPostponedCounter.decrementAndGet() == 0) {
      if (application.isWriteAccessAllowed()) {
        doPostponedFormatting();
      } else {
        application.runWriteAction(
            new Runnable() {
              @Override
              public void run() {
                doPostponedFormatting();
              }
            });
      }
    }
  }

  private static void atomic(@NotNull Runnable r) {
    ProgressManager.getInstance().executeNonCancelableSection(r);
  }

  @Override
  public void update(@NotNull final PomModelEvent event) {
    atomic(
        new Runnable() {
          @Override
          public void run() {
            if (isDisabled()
                || myPostponedCounter.get() == 0
                    && !ApplicationManager.getApplication().isUnitTestMode()) return;
            final TreeChangeEvent changeSet = (TreeChangeEvent) event.getChangeSet(myTreeAspect);
            if (changeSet == null) return;
            final PsiElement psiElement = changeSet.getRootElement().getPsi();
            if (psiElement == null) return;
            PsiFile containingFile =
                InjectedLanguageManager.getInstance(psiElement.getProject())
                    .getTopLevelFile(psiElement);
            final FileViewProvider viewProvider = containingFile.getViewProvider();

            if (!viewProvider.isEventSystemEnabled()) return;
            myUpdatedProviders.add(viewProvider);
            for (final ASTNode node : changeSet.getChangedElements()) {
              final TreeChange treeChange = changeSet.getChangesByElement(node);
              for (final ASTNode affectedChild : treeChange.getAffectedChildren()) {
                final ChangeInfo childChange = treeChange.getChangeByChild(affectedChild);
                switch (childChange.getChangeType()) {
                  case ChangeInfo.ADD:
                  case ChangeInfo.REPLACE:
                    postponeFormatting(viewProvider, affectedChild);
                    break;
                  case ChangeInfo.CONTENTS_CHANGED:
                    if (!CodeEditUtil.isNodeGenerated(affectedChild)) {
                      ((TreeElement) affectedChild)
                          .acceptTree(
                              new RecursiveTreeElementWalkingVisitor() {
                                @Override
                                protected void visitNode(TreeElement element) {
                                  if (CodeEditUtil.isNodeGenerated(element)
                                      && CodeEditUtil.isSuspendedNodesReformattingAllowed()) {
                                    postponeFormatting(viewProvider, element);
                                    return;
                                  }
                                  super.visitNode(element);
                                }
                              });
                    }
                    break;
                }
              }
            }
          }
        });
  }

  public void doPostponedFormatting() {
    atomic(
        new Runnable() {
          @Override
          public void run() {
            if (isDisabled()) return;
            try {
              FileViewProvider[] viewProviders =
                  myUpdatedProviders.toArray(new FileViewProvider[myUpdatedProviders.size()]);
              for (final FileViewProvider viewProvider : viewProviders) {
                doPostponedFormatting(viewProvider);
              }
            } catch (Exception e) {
              LOG.error(e);
            } finally {
              LOG.assertTrue(myReformatElements.isEmpty(), myReformatElements);
            }
          }
        });
  }

  public void postponedFormatting(@NotNull FileViewProvider viewProvider) {
    postponedFormattingImpl(viewProvider, true);
  }

  public void doPostponedFormatting(@NotNull FileViewProvider viewProvider) {
    postponedFormattingImpl(viewProvider, false);
  }

  private void postponedFormattingImpl(
      @NotNull final FileViewProvider viewProvider, final boolean check) {
    atomic(
        new Runnable() {
          @Override
          public void run() {
            if (isDisabled() || check && !myUpdatedProviders.contains(viewProvider)) return;

            try {
              disablePostprocessFormattingInside(
                  new Runnable() {
                    @Override
                    public void run() {
                      doPostponedFormattingInner(viewProvider);
                    }
                  });
            } finally {
              myUpdatedProviders.remove(viewProvider);
              myReformatElements.remove(viewProvider);
              viewProvider.putUserData(REFORMAT_ORIGINATOR, null);
            }
          }
        });
  }

  public boolean isViewProviderLocked(@NotNull FileViewProvider fileViewProvider) {
    return myReformatElements.containsKey(fileViewProvider);
  }

  public void beforeDocumentChanged(@NotNull FileViewProvider viewProvider) {
    if (isViewProviderLocked(viewProvider)) {
      Throwable cause = viewProvider.getUserData(REFORMAT_ORIGINATOR);
      @NonNls
      String message =
          "Document is locked by write PSI operations. "
              + "Use PsiDocumentManager.doPostponedOperationsAndUnblockDocument() to commit PSI changes to the document."
              + (cause == null ? "" : " See cause stacktrace for the reason to lock.");
      throw cause == null ? new RuntimeException(message) : new RuntimeException(message, cause);
    }
    postponedFormatting(viewProvider);
  }

  public static PostprocessReformattingAspect getInstance(Project project) {
    return project.getComponent(PostprocessReformattingAspect.class);
  }

  private void postponeFormatting(@NotNull FileViewProvider viewProvider, @NotNull ASTNode child) {
    if (!CodeEditUtil.isNodeGenerated(child) && child.getElementType() != TokenType.WHITE_SPACE) {
      final int oldIndent = CodeEditUtil.getOldIndentation(child);
      LOG.assertTrue(
          oldIndent >= 0,
          "for not generated items old indentation must be defined: element="
              + child
              + ", text="
              + child.getText());
    }
    List<ASTNode> list = myReformatElements.get(viewProvider);
    if (list == null) {
      list = new ArrayList<ASTNode>();
      myReformatElements.put(viewProvider, list);
      if (STORE_REFORMAT_ORIGINATOR_STACKTRACE) {
        viewProvider.putUserData(REFORMAT_ORIGINATOR, new Throwable());
      }
    }
    list.add(child);
  }

  private void doPostponedFormattingInner(@NotNull FileViewProvider key) {
    final List<ASTNode> astNodes = myReformatElements.remove(key);
    final Document document = key.getDocument();
    // Sort ranges by end offsets so that we won't need any offset adjustment after reformat or
    // reindent
    if (document == null) return;

    final VirtualFile virtualFile = key.getVirtualFile();
    if (!virtualFile.isValid()) return;

    final TreeSet<PostprocessFormattingTask> postProcessTasks =
        new TreeSet<PostprocessFormattingTask>();
    Collection<Disposable> toDispose = ContainerUtilRt.newArrayList();
    try {
      // process all roots in viewProvider to find marked for reformat before elements and create
      // appropriate range markers
      handleReformatMarkers(key, postProcessTasks);
      toDispose.addAll(postProcessTasks);

      // then we create ranges by changed nodes. One per node. There ranges can intersect. Ranges
      // are sorted by end offset.
      if (astNodes != null) createActionsMap(astNodes, key, postProcessTasks);

      if (Boolean.getBoolean("check.psi.is.valid")
          && ApplicationManager.getApplication().isUnitTestMode()) {
        checkPsiIsCorrect(key);
      }

      while (!postProcessTasks.isEmpty()) {
        // now we have to normalize actions so that they not intersect and ordered in most
        // appropriate way
        // (free reformatting -> reindent -> formatting under reindent)
        final List<PostponedAction> normalizedActions =
            normalizeAndReorderPostponedActions(postProcessTasks, document);
        toDispose.addAll(normalizedActions);

        // only in following loop real changes in document are made
        for (final PostponedAction normalizedAction : normalizedActions) {
          CodeStyleSettings settings =
              CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
          boolean old = settings.ENABLE_JAVADOC_FORMATTING;
          settings.ENABLE_JAVADOC_FORMATTING = false;
          try {
            normalizedAction.execute(key);
          } finally {
            settings.ENABLE_JAVADOC_FORMATTING = old;
          }
        }
      }
    } finally {
      for (Disposable disposable : toDispose) {
        //noinspection SSBasedInspection
        disposable.dispose();
      }
    }
  }

  private void checkPsiIsCorrect(@NotNull FileViewProvider key) {
    PsiFile actualPsi = key.getPsi(key.getBaseLanguage());

    PsiTreeDebugBuilder treeDebugBuilder =
        new PsiTreeDebugBuilder().setShowErrorElements(false).setShowWhiteSpaces(false);

    String actualPsiTree = treeDebugBuilder.psiToString(actualPsi);

    String fileName = key.getVirtualFile().getName();
    PsiFile psi =
        PsiFileFactory.getInstance(myProject)
            .createFileFromText(
                fileName,
                FileTypeManager.getInstance().getFileTypeByFileName(fileName),
                actualPsi.getNode().getText(),
                LocalTimeCounter.currentTime(),
                false);

    if (actualPsi.getClass().equals(psi.getClass())) {
      String expectedPsi = treeDebugBuilder.psiToString(psi);

      if (!expectedPsi.equals(actualPsiTree)) {
        myReformatElements.clear();
        assert expectedPsi.equals(actualPsiTree)
            : "Refactored psi should be the same as result of parsing";
      }
    }
  }

  @NotNull
  private List<PostponedAction> normalizeAndReorderPostponedActions(
      @NotNull Set<PostprocessFormattingTask> rangesToProcess, @NotNull Document document) {
    final List<PostprocessFormattingTask> freeFormattingActions =
        new ArrayList<PostprocessFormattingTask>();
    final List<ReindentTask> indentActions = new ArrayList<ReindentTask>();

    PostprocessFormattingTask accumulatedTask = null;
    Iterator<PostprocessFormattingTask> iterator = rangesToProcess.iterator();
    while (iterator.hasNext()) {
      final PostprocessFormattingTask currentTask = iterator.next();
      if (accumulatedTask == null) {
        accumulatedTask = currentTask;
        iterator.remove();
      } else if (accumulatedTask.getStartOffset() > currentTask.getEndOffset()
          || accumulatedTask.getStartOffset() == currentTask.getEndOffset()
              && !canStickActionsTogether(accumulatedTask, currentTask)) {
        // action can be pushed
        if (accumulatedTask instanceof ReindentTask) {
          indentActions.add((ReindentTask) accumulatedTask);
        } else {
          freeFormattingActions.add(accumulatedTask);
        }

        accumulatedTask = currentTask;
        iterator.remove();
      } else if (accumulatedTask instanceof ReformatTask && currentTask instanceof ReindentTask) {
        // split accumulated reformat range into two
        if (accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
          final RangeMarker endOfRange =
              document.createRangeMarker(
                  accumulatedTask.getStartOffset(), currentTask.getStartOffset());
          // add heading reformat part
          rangesToProcess.add(new ReformatTask(endOfRange));
          // and manage heading whitespace because formatter does not edit it in previous action
          iterator = rangesToProcess.iterator();
          //noinspection StatementWithEmptyBody
          while (iterator.next().getRange() != currentTask.getRange()) ;
        }
        final RangeMarker rangeToProcess =
            document.createRangeMarker(currentTask.getEndOffset(), accumulatedTask.getEndOffset());
        freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(rangeToProcess));
        accumulatedTask = currentTask;
        iterator.remove();
      } else {
        if (!(accumulatedTask instanceof ReindentTask)) {
          iterator.remove();

          boolean withLeadingWhitespace =
              accumulatedTask instanceof ReformatWithHeadingWhitespaceTask;
          if (accumulatedTask instanceof ReformatTask
              && currentTask instanceof ReformatWithHeadingWhitespaceTask
              && accumulatedTask.getStartOffset() == currentTask.getStartOffset()) {
            withLeadingWhitespace = true;
          } else if (accumulatedTask instanceof ReformatWithHeadingWhitespaceTask
              && currentTask instanceof ReformatTask
              && accumulatedTask.getStartOffset() < currentTask.getStartOffset()) {
            withLeadingWhitespace = false;
          }
          int newStart = Math.min(accumulatedTask.getStartOffset(), currentTask.getStartOffset());
          int newEnd = Math.max(accumulatedTask.getEndOffset(), currentTask.getEndOffset());
          RangeMarker rangeMarker;

          if (accumulatedTask.getStartOffset() == newStart
              && accumulatedTask.getEndOffset() == newEnd) {
            rangeMarker = accumulatedTask.getRange();
          } else if (currentTask.getStartOffset() == newStart
              && currentTask.getEndOffset() == newEnd) {
            rangeMarker = currentTask.getRange();
          } else {
            rangeMarker = document.createRangeMarker(newStart, newEnd);
          }

          if (withLeadingWhitespace) {
            accumulatedTask = new ReformatWithHeadingWhitespaceTask(rangeMarker);
          } else {
            accumulatedTask = new ReformatTask(rangeMarker);
          }
        } else if (currentTask instanceof ReindentTask) {
          iterator.remove();
        } // TODO[ik]: need to be fixed to correctly process indent inside indent
      }
    }
    if (accumulatedTask != null) {
      if (accumulatedTask instanceof ReindentTask) {
        indentActions.add((ReindentTask) accumulatedTask);
      } else {
        freeFormattingActions.add(accumulatedTask);
      }
    }

    final List<PostponedAction> result = new ArrayList<PostponedAction>();
    Collections.reverse(freeFormattingActions);
    Collections.reverse(indentActions);

    if (!freeFormattingActions.isEmpty()) {
      FormatTextRanges ranges = new FormatTextRanges();
      for (PostprocessFormattingTask action : freeFormattingActions) {
        TextRange range = TextRange.create(action);
        ranges.add(range, action instanceof ReformatWithHeadingWhitespaceTask);
      }
      result.add(new ReformatRangesAction(ranges));
    }

    if (!indentActions.isEmpty()) {
      ReindentRangesAction reindentRangesAction = new ReindentRangesAction();
      for (ReindentTask action : indentActions) {
        reindentRangesAction.add(action.getRange(), action.getOldIndent());
      }
      result.add(reindentRangesAction);
    }

    return result;
  }

  private static boolean canStickActionsTogether(
      final PostprocessFormattingTask currentTask, final PostprocessFormattingTask nextTask) {
    // empty reformat markers can't be stuck together with any action
    if (nextTask instanceof ReformatWithHeadingWhitespaceTask
        && nextTask.getStartOffset() == nextTask.getEndOffset()) return false;
    if (currentTask instanceof ReformatWithHeadingWhitespaceTask
        && currentTask.getStartOffset() == currentTask.getEndOffset()) {
      return false;
    }
    // reindent actions can't be be stuck at all
    return !(currentTask instanceof ReindentTask);
  }

  private static void createActionsMap(
      @NotNull List<ASTNode> astNodes,
      @NotNull FileViewProvider provider,
      @NotNull final TreeSet<PostprocessFormattingTask> rangesToProcess) {
    final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes);
    final Document document = provider.getDocument();
    if (document == null) {
      return;
    }
    for (final ASTNode node : astNodes) {
      nodesToProcess.remove(node);
      final FileElement fileElement = TreeUtil.getFileElement((TreeElement) node);
      if (fileElement == null || ((PsiFile) fileElement.getPsi()).getViewProvider() != provider)
        continue;
      final boolean isGenerated = CodeEditUtil.isNodeGenerated(node);

      ((TreeElement) node)
          .acceptTree(
              new RecursiveTreeElementVisitor() {
                boolean inGeneratedContext = !isGenerated;

                @Override
                protected boolean visitNode(TreeElement element) {
                  if (nodesToProcess.contains(element)) return false;

                  final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element);
                  CodeEditUtil.setNodeGenerated(element, false);
                  if (currentNodeGenerated && !inGeneratedContext) {
                    rangesToProcess.add(
                        new ReformatTask(document.createRangeMarker(element.getTextRange())));
                    inGeneratedContext = true;
                  }
                  if (!currentNodeGenerated && inGeneratedContext) {
                    if (element.getElementType() == TokenType.WHITE_SPACE) return false;
                    final int oldIndent = CodeEditUtil.getOldIndentation(element);
                    CodeEditUtil.setOldIndentation(element, -1);
                    LOG.assertTrue(
                        oldIndent >= 0,
                        "for not generated items old indentation must be defined: element "
                            + element);
                    for (TextRange indentRange : getEnabledRanges(element.getPsi())) {
                      rangesToProcess.add(
                          new ReindentTask(document.createRangeMarker(indentRange), oldIndent));
                    }
                    inGeneratedContext = false;
                  }
                  return true;
                }

                private Iterable<TextRange> getEnabledRanges(@NotNull PsiElement element) {
                  List<TextRange> disabledRanges = new ArrayList<TextRange>();
                  for (DisabledIndentRangesProvider rangesProvider :
                      DisabledIndentRangesProvider.EP_NAME.getExtensions()) {
                    Collection<TextRange> providedDisabledRanges =
                        rangesProvider.getDisabledIndentRanges(element);
                    if (providedDisabledRanges != null) {
                      disabledRanges.addAll(providedDisabledRanges);
                    }
                  }
                  return TextRangeUtil.excludeRanges(element.getTextRange(), disabledRanges);
                }

                @Override
                public void visitComposite(CompositeElement composite) {
                  boolean oldGeneratedContext = inGeneratedContext;
                  super.visitComposite(composite);
                  inGeneratedContext = oldGeneratedContext;
                }

                @Override
                public void visitLeaf(LeafElement leaf) {
                  boolean oldGeneratedContext = inGeneratedContext;
                  super.visitLeaf(leaf);
                  inGeneratedContext = oldGeneratedContext;
                }
              });
    }
  }

  private static void handleReformatMarkers(
      @NotNull final FileViewProvider key,
      @NotNull final Set<PostprocessFormattingTask> rangesToProcess) {
    final Document document = key.getDocument();
    if (document == null) {
      return;
    }
    for (final FileElement fileElement : ((SingleRootFileViewProvider) key).getKnownTreeRoots()) {
      fileElement.acceptTree(
          new RecursiveTreeElementWalkingVisitor() {
            @Override
            protected void visitNode(TreeElement element) {
              if (CodeEditUtil.isMarkedToReformatBefore(element)) {
                CodeEditUtil.markToReformatBefore(element, false);
                rangesToProcess.add(
                    new ReformatWithHeadingWhitespaceTask(
                        document.createRangeMarker(
                            element.getStartOffset(), element.getStartOffset())));
              } else if (CodeEditUtil.isMarkedToReformat(element)) {
                CodeEditUtil.markToReformat(element, false);
                rangesToProcess.add(
                    new ReformatWithHeadingWhitespaceTask(
                        document.createRangeMarker(
                            element.getStartOffset(),
                            element.getStartOffset() + element.getTextLength())));
              }
              super.visitNode(element);
            }
          });
    }
  }

  private static void adjustIndentationInRange(
      @NotNull PsiFile file,
      @NotNull Document document,
      @NotNull TextRange[] indents,
      final int indentAdjustment) {
    final CharSequence charsSequence = document.getCharsSequence();
    for (final TextRange indent : indents) {
      final String oldIndentStr =
          charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString();
      final int oldIndent =
          IndentHelperImpl.getIndent(file.getProject(), file.getFileType(), oldIndentStr, true);
      final String newIndentStr =
          IndentHelperImpl.fillIndent(
              file.getProject(), file.getFileType(), Math.max(oldIndent + indentAdjustment, 0));
      document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr);
    }
  }

  private static int getNewIndent(@NotNull PsiFile psiFile, final int firstWhitespace) {
    final Document document = psiFile.getViewProvider().getDocument();
    assert document != null;
    final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace));
    int endOffset = startOffset;
    final CharSequence charsSequence = document.getCharsSequence();
    //noinspection StatementWithEmptyBody
    while (Character.isWhitespace(charsSequence.charAt(endOffset++))) ;
    final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString();
    return IndentHelperImpl.getIndent(
        psiFile.getProject(), psiFile.getFileType(), newIndentStr, true);
  }

  public boolean isDisabled() {
    return myDisabledCounter > 0;
  }

  @NotNull
  private CodeFormatterFacade getFormatterFacade(@NotNull FileViewProvider viewProvider) {
    final CodeStyleSettings styleSettings =
        CodeStyleSettingsManager.getSettings(myPsiManager.getProject());
    final PsiDocumentManager documentManager =
        PsiDocumentManager.getInstance(myPsiManager.getProject());
    final Document document = viewProvider.getDocument();
    assert document != null;
    final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings);

    documentManager.commitDocument(document);
    return codeFormatter;
  }

  private abstract static class PostprocessFormattingTask
      implements Comparable<PostprocessFormattingTask>, Segment, Disposable {
    @NotNull private final RangeMarker myRange;

    public PostprocessFormattingTask(@NotNull RangeMarker rangeMarker) {
      myRange = rangeMarker;
    }

    @Override
    public int compareTo(@NotNull PostprocessFormattingTask o) {
      RangeMarker o1 = myRange;
      RangeMarker o2 = o.myRange;
      if (o1.equals(o2)) return 0;
      final int diff = o2.getEndOffset() - o1.getEndOffset();
      if (diff == 0) {
        if (o1.getStartOffset() == o2.getStartOffset()) return 0;
        if (o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first
        if (o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first
        return o1.getStartOffset() - o2.getStartOffset();
      }
      return diff;
    }

    @NotNull
    public RangeMarker getRange() {
      return myRange;
    }

    @Override
    public int getStartOffset() {
      return myRange.getStartOffset();
    }

    @Override
    public int getEndOffset() {
      return myRange.getEndOffset();
    }

    @Override
    public void dispose() {
      if (myRange.isValid()) {
        myRange.dispose();
      }
    }
  }

  private static class ReformatTask extends PostprocessFormattingTask {
    public ReformatTask(@NotNull RangeMarker rangeMarker) {
      super(rangeMarker);
    }
  }

  private static class ReformatWithHeadingWhitespaceTask extends PostprocessFormattingTask {
    public ReformatWithHeadingWhitespaceTask(@NotNull RangeMarker rangeMarker) {
      super(rangeMarker);
    }
  }

  private static class ReindentTask extends PostprocessFormattingTask {
    private final int myOldIndent;

    public ReindentTask(@NotNull RangeMarker rangeMarker, int oldIndent) {
      super(rangeMarker);
      myOldIndent = oldIndent;
    }

    public int getOldIndent() {
      return myOldIndent;
    }
  }

  private interface PostponedAction extends Disposable {
    void execute(@NotNull FileViewProvider viewProvider);
  }

  private class ReformatRangesAction implements PostponedAction {
    private final FormatTextRanges myRanges;

    public ReformatRangesAction(@NotNull FormatTextRanges ranges) {
      myRanges = ranges;
    }

    @Override
    public void execute(@NotNull FileViewProvider viewProvider) {
      final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider);
      codeFormatter.processText(
          viewProvider.getPsi(viewProvider.getBaseLanguage()), myRanges.ensureNonEmpty(), false);
    }

    @Override
    public void dispose() {}
  }

  private static class ReindentRangesAction implements PostponedAction {
    private final List<Pair<Integer, RangeMarker>> myRangesToReindent =
        new ArrayList<Pair<Integer, RangeMarker>>();

    public void add(@NotNull RangeMarker rangeMarker, int oldIndent) {
      myRangesToReindent.add(new Pair<Integer, RangeMarker>(oldIndent, rangeMarker));
    }

    @Override
    public void execute(@NotNull FileViewProvider viewProvider) {
      final Document document = viewProvider.getDocument();
      assert document != null;
      final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage());
      for (Pair<Integer, RangeMarker> integerRangeMarkerPair : myRangesToReindent) {
        RangeMarker marker = integerRangeMarkerPair.second;
        final CharSequence charsSequence =
            document.getCharsSequence().subSequence(marker.getStartOffset(), marker.getEndOffset());
        final int oldIndent = integerRangeMarkerPair.first;
        final TextRange[] whitespaces =
            CharArrayUtil.getIndents(charsSequence, marker.getStartOffset());
        final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent;
        if (indentAdjustment != 0)
          adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment);
      }
    }

    @Override
    public void dispose() {
      for (Pair<Integer, RangeMarker> pair : myRangesToReindent) {
        RangeMarker marker = pair.second;
        if (marker.isValid()) {
          marker.dispose();
        }
      }
    }
  }

  @TestOnly
  public void clear() {
    myReformatElements.clear();
  }
}
/** @author ik Date: 24.10.2003 */
public class PsiClassImplUtil {
  private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiClassImplUtil");
  private static final Key<ParameterizedCachedValue<MembersMap, PsiClass>> MAP_IN_CLASS_KEY =
      Key.create("MAP_KEY");

  private PsiClassImplUtil() {}

  public static void cacheEverything(PsiClass aClass) {
    getValues(aClass).getValue(aClass);
  }

  @NotNull
  public static PsiField[] getAllFields(@NotNull PsiClass aClass) {
    List<PsiField> map = getAllByMap(aClass, MemberType.FIELD);
    return map.toArray(new PsiField[map.size()]);
  }

  @NotNull
  public static PsiMethod[] getAllMethods(@NotNull PsiClass aClass) {
    List<PsiMethod> methods = getAllByMap(aClass, MemberType.METHOD);
    return methods.toArray(new PsiMethod[methods.size()]);
  }

  @NotNull
  public static PsiClass[] getAllInnerClasses(@NotNull PsiClass aClass) {
    List<PsiClass> classes = getAllByMap(aClass, MemberType.CLASS);
    return classes.toArray(new PsiClass[classes.size()]);
  }

  @Nullable
  public static PsiField findFieldByName(
      @NotNull PsiClass aClass, String name, boolean checkBases) {
    List<PsiMember> byMap = findByMap(aClass, name, checkBases, MemberType.FIELD);
    return byMap.isEmpty() ? null : (PsiField) byMap.get(0);
  }

  @NotNull
  public static PsiMethod[] findMethodsByName(
      @NotNull PsiClass aClass, String name, boolean checkBases) {
    List<PsiMember> methods = findByMap(aClass, name, checkBases, MemberType.METHOD);
    //noinspection SuspiciousToArrayCall
    return methods.toArray(new PsiMethod[methods.size()]);
  }

  @Nullable
  public static PsiMethod findMethodBySignature(
      @NotNull PsiClass aClass, @NotNull PsiMethod patternMethod, final boolean checkBases) {
    final List<PsiMethod> result = findMethodsBySignature(aClass, patternMethod, checkBases, true);
    return result.isEmpty() ? null : result.get(0);
  }

  // ----------------------------- findMethodsBySignature -----------------------------------

  @NotNull
  public static PsiMethod[] findMethodsBySignature(
      @NotNull PsiClass aClass, @NotNull PsiMethod patternMethod, final boolean checkBases) {
    List<PsiMethod> methods = findMethodsBySignature(aClass, patternMethod, checkBases, false);
    return methods.toArray(new PsiMethod[methods.size()]);
  }

  @NotNull
  private static List<PsiMethod> findMethodsBySignature(
      @NotNull PsiClass aClass,
      @NotNull PsiMethod patternMethod,
      boolean checkBases,
      boolean stopOnFirst) {
    final PsiMethod[] methodsByName = aClass.findMethodsByName(patternMethod.getName(), checkBases);
    if (methodsByName.length == 0) return Collections.emptyList();
    final List<PsiMethod> methods = new SmartList<PsiMethod>();
    final MethodSignature patternSignature = patternMethod.getSignature(PsiSubstitutor.EMPTY);
    for (final PsiMethod method : methodsByName) {
      final PsiClass superClass = method.getContainingClass();
      final PsiSubstitutor substitutor;
      if (checkBases && !aClass.equals(superClass)) {
        substitutor =
            TypeConversionUtil.getSuperClassSubstitutor(superClass, aClass, PsiSubstitutor.EMPTY);
      } else {
        substitutor = PsiSubstitutor.EMPTY;
      }
      final MethodSignature signature = method.getSignature(substitutor);
      if (signature.equals(patternSignature)) {
        methods.add(method);
        if (stopOnFirst) {
          break;
        }
      }
    }
    return methods;
  }

  // ----------------------------------------------------------------------------------------

  @Nullable
  public static PsiClass findInnerByName(
      @NotNull PsiClass aClass, String name, boolean checkBases) {
    List<PsiMember> byMap = findByMap(aClass, name, checkBases, MemberType.CLASS);
    return byMap.isEmpty() ? null : (PsiClass) byMap.get(0);
  }

  @NotNull
  private static List<PsiMember> findByMap(
      @NotNull PsiClass aClass, String name, boolean checkBases, @NotNull MemberType type) {
    if (name == null) return Collections.emptyList();

    if (checkBases) {
      Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allMethodsMap = getMap(aClass, type);
      List<Pair<PsiMember, PsiSubstitutor>> list = allMethodsMap.get(name);
      if (list == null) return Collections.emptyList();
      List<PsiMember> ret = new ArrayList<PsiMember>(list.size());
      for (final Pair<PsiMember, PsiSubstitutor> info : list) {
        ret.add(info.getFirst());
      }

      return ret;
    } else {
      PsiMember[] members = null;
      switch (type) {
        case METHOD:
          members = aClass.getMethods();
          break;
        case CLASS:
          members = aClass.getInnerClasses();
          break;
        case FIELD:
          members = aClass.getFields();
          break;
      }

      List<PsiMember> list = new ArrayList<PsiMember>();
      for (PsiMember member : members) {
        if (name.equals(member.getName())) {
          list.add(member);
        }
      }
      return list;
    }
  }

  @NotNull
  public static <T extends PsiMember> List<Pair<T, PsiSubstitutor>> getAllWithSubstitutorsByMap(
      @NotNull PsiClass aClass, @NotNull MemberType type) {
    Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allMap = getMap(aClass, type);
    //noinspection unchecked
    return (List) allMap.get(ALL);
  }

  @NotNull
  private static <T extends PsiMember> List<T> getAllByMap(
      @NotNull PsiClass aClass, @NotNull MemberType type) {
    List<Pair<T, PsiSubstitutor>> pairs = getAllWithSubstitutorsByMap(aClass, type);

    final List<T> ret = new ArrayList<T>(pairs.size());
    //noinspection ForLoopReplaceableByForEach
    for (int i = 0; i < pairs.size(); i++) {
      Pair<T, PsiSubstitutor> pair = pairs.get(i);
      T t = pair.getFirst();
      LOG.assertTrue(t != null, aClass);
      ret.add(t);
    }
    return ret;
  }

  @NonNls private static final String ALL = "Intellij-IDEA-ALL";

  public enum MemberType {
    CLASS,
    FIELD,
    METHOD
  }

  @NotNull
  private static MembersMap buildAllMaps(@NotNull PsiClass psiClass) {
    final List<Pair<PsiMember, PsiSubstitutor>> classes =
        new ArrayList<Pair<PsiMember, PsiSubstitutor>>();
    final List<Pair<PsiMember, PsiSubstitutor>> fields =
        new ArrayList<Pair<PsiMember, PsiSubstitutor>>();
    final List<Pair<PsiMember, PsiSubstitutor>> methods =
        new ArrayList<Pair<PsiMember, PsiSubstitutor>>();

    FilterScopeProcessor<MethodCandidateInfo> processor =
        new FilterScopeProcessor<MethodCandidateInfo>(
            new OrFilter(
                ElementClassFilter.METHOD, ElementClassFilter.FIELD, ElementClassFilter.CLASS)) {
          @Override
          protected void add(PsiElement element, PsiSubstitutor substitutor) {
            if (element instanceof PsiMethod) {
              methods.add(Pair.create((PsiMember) element, substitutor));
            } else if (element instanceof PsiField) {
              fields.add(Pair.create((PsiMember) element, substitutor));
            } else if (element instanceof PsiClass) {
              classes.add(Pair.create((PsiMember) element, substitutor));
            }
          }
        };
    processDeclarationsInClassNotCached(
        psiClass,
        processor,
        ResolveState.initial(),
        null,
        null,
        psiClass,
        false,
        PsiUtil.getLanguageLevel(psiClass));

    MembersMap result = new MembersMap(MemberType.class);
    result.put(MemberType.CLASS, generateMapByList(classes));
    result.put(MemberType.METHOD, generateMapByList(methods));
    result.put(MemberType.FIELD, generateMapByList(fields));
    return result;
  }

  @NotNull
  private static Map<String, List<Pair<PsiMember, PsiSubstitutor>>> generateMapByList(
      @NotNull final List<Pair<PsiMember, PsiSubstitutor>> list) {
    Map<String, List<Pair<PsiMember, PsiSubstitutor>>> map =
        new THashMap<String, List<Pair<PsiMember, PsiSubstitutor>>>();
    map.put(ALL, list);
    for (final Pair<PsiMember, PsiSubstitutor> info : list) {
      PsiMember element = info.getFirst();
      String currentName = element.getName();
      List<Pair<PsiMember, PsiSubstitutor>> listByName = map.get(currentName);
      if (listByName == null) {
        listByName = new ArrayList<Pair<PsiMember, PsiSubstitutor>>(1);
        map.put(currentName, listByName);
      }
      listByName.add(info);
    }
    return map;
  }

  private static Map<String, List<Pair<PsiMember, PsiSubstitutor>>> getMap(
      @NotNull PsiClass aClass, @NotNull MemberType type) {
    ParameterizedCachedValue<MembersMap, PsiClass> value = getValues(aClass);
    return value.getValue(aClass).get(type);
  }

  @NotNull
  private static ParameterizedCachedValue<MembersMap, PsiClass> getValues(
      @NotNull PsiClass aClass) {
    ParameterizedCachedValue<MembersMap, PsiClass> value = aClass.getUserData(MAP_IN_CLASS_KEY);
    if (value == null) {
      value =
          CachedValuesManager.getManager(aClass.getProject())
              .createParameterizedCachedValue(ByNameCachedValueProvider.INSTANCE, false);
      // Do not cache for nonphysical elements
      if (aClass.isPhysical()) {
        value = ((UserDataHolderEx) aClass).putUserDataIfAbsent(MAP_IN_CLASS_KEY, value);
      }
    }
    return value;
  }

  private static class ClassIconRequest {
    @NotNull private final PsiClass psiClass;
    private final int flags;
    private final Icon symbolIcon;

    private ClassIconRequest(@NotNull PsiClass psiClass, int flags, Icon symbolIcon) {
      this.psiClass = psiClass;
      this.flags = flags;
      this.symbolIcon = symbolIcon;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof ClassIconRequest)) return false;

      ClassIconRequest that = (ClassIconRequest) o;

      return flags == that.flags && psiClass.equals(that.psiClass);
    }

    @Override
    public int hashCode() {
      int result = psiClass.hashCode();
      result = 31 * result + flags;
      return result;
    }
  }

  private static final Function<ClassIconRequest, Icon> FULL_ICON_EVALUATOR =
      new NullableFunction<ClassIconRequest, Icon>() {
        @Override
        public Icon fun(ClassIconRequest r) {
          if (!r.psiClass.isValid() || r.psiClass.getProject().isDisposed()) return null;

          final boolean isLocked =
              (r.flags & Iconable.ICON_FLAG_READ_STATUS) != 0 && !r.psiClass.isWritable();
          Icon symbolIcon =
              r.symbolIcon != null
                  ? r.symbolIcon
                  : ElementPresentationUtil.getClassIconOfKind(
                      r.psiClass, ElementPresentationUtil.getClassKind(r.psiClass));
          RowIcon baseIcon =
              ElementPresentationUtil.createLayeredIcon(symbolIcon, r.psiClass, isLocked);
          return ElementPresentationUtil.addVisibilityIcon(r.psiClass, r.flags, baseIcon);
        }
      };

  public static Icon getClassIcon(final int flags, @NotNull PsiClass aClass) {
    return getClassIcon(flags, aClass, null);
  }

  public static Icon getClassIcon(int flags, @NotNull PsiClass aClass, @Nullable Icon symbolIcon) {
    Icon base = Iconable.LastComputedIcon.get(aClass, flags);
    if (base == null) {
      if (symbolIcon == null) {
        symbolIcon =
            ElementPresentationUtil.getClassIconOfKind(
                aClass, ElementPresentationUtil.getBasicClassKind(aClass));
      }
      RowIcon baseIcon = ElementBase.createLayeredIcon(aClass, symbolIcon, 0);
      base = ElementPresentationUtil.addVisibilityIcon(aClass, flags, baseIcon);
    }

    return IconDeferrer.getInstance()
        .defer(base, new ClassIconRequest(aClass, flags, symbolIcon), FULL_ICON_EVALUATOR);
  }

  @NotNull
  public static SearchScope getClassUseScope(@NotNull PsiClass aClass) {
    if (aClass instanceof PsiAnonymousClass) {
      return new LocalSearchScope(aClass);
    }
    final GlobalSearchScope maximalUseScope = ResolveScopeManager.getElementUseScope(aClass);
    PsiFile file = aClass.getContainingFile();
    if (PsiImplUtil.isInServerPage(file)) return maximalUseScope;
    final PsiClass containingClass = aClass.getContainingClass();
    if (aClass.hasModifierProperty(PsiModifier.PUBLIC)
        || aClass.hasModifierProperty(PsiModifier.PROTECTED)) {
      return containingClass == null ? maximalUseScope : containingClass.getUseScope();
    } else if (aClass.hasModifierProperty(PsiModifier.PRIVATE)
        || aClass instanceof PsiTypeParameter) {
      PsiClass topClass = PsiUtil.getTopLevelClass(aClass);
      return new LocalSearchScope(topClass == null ? aClass.getContainingFile() : topClass);
    } else {
      PsiPackage aPackage = null;
      if (file instanceof PsiJavaFile) {
        aPackage =
            JavaPsiFacade.getInstance(aClass.getProject())
                .findPackage(((PsiJavaFile) file).getPackageName());
      }

      if (aPackage == null) {
        PsiDirectory dir = file.getContainingDirectory();
        if (dir != null) {
          aPackage = JavaDirectoryService.getInstance().getPackage(dir);
        }
      }

      if (aPackage != null) {
        SearchScope scope = PackageScope.packageScope(aPackage, false);
        scope = scope.intersectWith(maximalUseScope);
        return scope;
      }

      return new LocalSearchScope(file);
    }
  }

  public static boolean isMainOrPremainMethod(@NotNull PsiMethod method) {
    if (!PsiType.VOID.equals(method.getReturnType())) return false;
    String name = method.getName();
    if (!("main".equals(name) || "premain".equals(name))) return false;

    PsiElementFactory factory = JavaPsiFacade.getInstance(method.getProject()).getElementFactory();
    MethodSignature signature = method.getSignature(PsiSubstitutor.EMPTY);
    try {
      MethodSignature main = createSignatureFromText(factory, "void main(String[] args);");
      if (MethodSignatureUtil.areSignaturesEqual(signature, main)) return true;
      MethodSignature premain =
          createSignatureFromText(
              factory, "void premain(String args, java.lang.instrument.Instrumentation i);");
      if (MethodSignatureUtil.areSignaturesEqual(signature, premain)) return true;
    } catch (IncorrectOperationException e) {
      LOG.error(e);
    }

    return false;
  }

  @NotNull
  private static MethodSignature createSignatureFromText(
      @NotNull PsiElementFactory factory, @NotNull String text) {
    return factory.createMethodFromText(text, null).getSignature(PsiSubstitutor.EMPTY);
  }

  private static class MembersMap
      extends EnumMap<MemberType, Map<String, List<Pair<PsiMember, PsiSubstitutor>>>> {
    public MembersMap(@NotNull Class<MemberType> keyType) {
      super(keyType);
    }
  }

  private static class ByNameCachedValueProvider
      implements ParameterizedCachedValueProvider<MembersMap, PsiClass> {
    private static final ByNameCachedValueProvider INSTANCE = new ByNameCachedValueProvider();

    @Override
    public CachedValueProvider.Result<MembersMap> compute(@NotNull PsiClass myClass) {
      MembersMap map = buildAllMaps(myClass);
      return new CachedValueProvider.Result<MembersMap>(
          map, PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT);
    }
  }

  public static boolean processDeclarationsInClass(
      @NotNull PsiClass aClass,
      @NotNull final PsiScopeProcessor processor,
      @NotNull ResolveState state,
      @Nullable Set<PsiClass> visited,
      PsiElement last,
      @NotNull PsiElement place,
      boolean isRaw) {
    if (last instanceof PsiTypeParameterList || last instanceof PsiModifierList) {
      return true; // TypeParameterList and ModifierList do not see our declarations
    }
    if (visited != null && visited.contains(aClass)) return true;

    PsiSubstitutor substitutor = state.get(PsiSubstitutor.KEY);
    isRaw = isRaw || PsiUtil.isRawSubstitutor(aClass, substitutor);

    ParameterizedCachedValue<MembersMap, PsiClass> cache =
        getValues(aClass); // aClass.getUserData(MAP_IN_CLASS_KEY);
    boolean upToDate = cache.hasUpToDateValue();
    LanguageLevel languageLevel = PsiUtil.getLanguageLevel(place);
    if (
    /*true || */ upToDate) {
      final NameHint nameHint = processor.getHint(NameHint.KEY);
      if (nameHint != null) {
        String name = nameHint.getName(state);
        return processCachedMembersByName(
            aClass,
            processor,
            state,
            visited,
            last,
            place,
            isRaw,
            substitutor,
            cache.getValue(aClass),
            name,
            languageLevel);
      }
    }
    return processDeclarationsInClassNotCached(
        aClass, processor, state, visited, last, place, isRaw, languageLevel);
  }

  private static boolean processCachedMembersByName(
      @NotNull PsiClass aClass,
      @NotNull PsiScopeProcessor processor,
      @NotNull ResolveState state,
      @Nullable Set<PsiClass> visited,
      PsiElement last,
      @NotNull PsiElement place,
      boolean isRaw,
      @NotNull PsiSubstitutor substitutor,
      @NotNull MembersMap value,
      String name,
      @NotNull LanguageLevel languageLevel) {
    final ElementClassHint classHint = processor.getHint(ElementClassHint.KEY);

    PsiElementFactory factory = JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory();

    if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.FIELD)) {
      final PsiField fieldByName = aClass.findFieldByName(name, false);
      if (fieldByName != null) {
        processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass);
        if (!processor.execute(fieldByName, state)) return false;
      } else {
        final Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allFieldsMap =
            value.get(MemberType.FIELD);

        final List<Pair<PsiMember, PsiSubstitutor>> list = allFieldsMap.get(name);
        if (list != null) {
          for (final Pair<PsiMember, PsiSubstitutor> candidate : list) {
            PsiMember candidateField = candidate.getFirst();
            PsiSubstitutor finalSubstitutor =
                obtainFinalSubstitutor(
                    candidateField.getContainingClass(),
                    candidate.getSecond(),
                    aClass,
                    substitutor,
                    factory,
                    languageLevel);

            processor.handleEvent(
                PsiScopeProcessor.Event.SET_DECLARATION_HOLDER,
                candidateField.getContainingClass());
            if (!processor.execute(candidateField, state.put(PsiSubstitutor.KEY, finalSubstitutor)))
              return false;
          }
        }
      }
    }
    if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.CLASS)) {
      if (last != null && last.getParent() == aClass) {
        if (last instanceof PsiClass) {
          if (!processor.execute(last, state)) return false;
        }
        // Parameters
        final PsiTypeParameterList list = aClass.getTypeParameterList();
        if (list != null && !list.processDeclarations(processor, state, last, place)) return false;
      }
      if (!(last instanceof PsiReferenceList)) {
        final PsiClass classByName = aClass.findInnerClassByName(name, false);
        if (classByName != null) {
          processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass);
          if (!processor.execute(classByName, state)) return false;
        } else {
          Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allClassesMap =
              value.get(MemberType.CLASS);

          List<Pair<PsiMember, PsiSubstitutor>> list = allClassesMap.get(name);
          if (list != null) {
            for (final Pair<PsiMember, PsiSubstitutor> candidate : list) {
              PsiMember inner = candidate.getFirst();
              PsiClass containingClass = inner.getContainingClass();
              if (containingClass != null) {
                PsiSubstitutor finalSubstitutor =
                    obtainFinalSubstitutor(
                        containingClass,
                        candidate.getSecond(),
                        aClass,
                        substitutor,
                        factory,
                        languageLevel);
                processor.handleEvent(
                    PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, containingClass);
                if (!processor.execute(inner, state.put(PsiSubstitutor.KEY, finalSubstitutor)))
                  return false;
              }
            }
          }
        }
      }
    }
    if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.METHOD)) {
      if (processor instanceof MethodResolverProcessor) {
        final MethodResolverProcessor methodResolverProcessor = (MethodResolverProcessor) processor;
        if (methodResolverProcessor.isConstructor()) {
          final PsiMethod[] constructors = aClass.getConstructors();
          methodResolverProcessor.handleEvent(
              PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass);
          for (PsiMethod constructor : constructors) {
            if (!methodResolverProcessor.execute(constructor, state)) return false;
          }
          return true;
        }
      }
      Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allMethodsMap =
          value.get(MemberType.METHOD);
      List<Pair<PsiMember, PsiSubstitutor>> list = allMethodsMap.get(name);
      if (list != null) {
        for (final Pair<PsiMember, PsiSubstitutor> candidate : list) {
          ProgressIndicatorProvider.checkCanceled();
          PsiMethod candidateMethod = (PsiMethod) candidate.getFirst();
          if (processor instanceof MethodResolverProcessor) {
            if (candidateMethod.isConstructor()
                != ((MethodResolverProcessor) processor).isConstructor()) continue;
          }
          final PsiClass containingClass = candidateMethod.getContainingClass();
          if (visited != null && visited.contains(candidateMethod.getContainingClass())) {
            continue;
          }

          PsiSubstitutor finalSubstitutor =
              obtainFinalSubstitutor(
                  containingClass,
                  candidate.getSecond(),
                  aClass,
                  substitutor,
                  factory,
                  languageLevel);
          finalSubstitutor = checkRaw(isRaw, factory, candidateMethod, finalSubstitutor);
          processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, containingClass);
          if (!processor.execute(candidateMethod, state.put(PsiSubstitutor.KEY, finalSubstitutor)))
            return false;
        }

        if (visited != null) {
          for (Pair<PsiMember, PsiSubstitutor> aList : list) {
            visited.add(aList.getFirst().getContainingClass());
          }
        }
      }
    }
    return true;
  }

  private static PsiSubstitutor checkRaw(
      boolean isRaw,
      @NotNull PsiElementFactory factory,
      @NotNull PsiMethod candidateMethod,
      @NotNull PsiSubstitutor substitutor) {
    if (isRaw
        && !candidateMethod.hasModifierProperty(
            PsiModifier.STATIC)) { // static methods are not erased due to raw overriding
      PsiTypeParameter[] methodTypeParameters = candidateMethod.getTypeParameters();
      substitutor = factory.createRawSubstitutor(substitutor, methodTypeParameters);
    }
    return substitutor;
  }

  public static PsiSubstitutor obtainFinalSubstitutor(
      @NotNull PsiClass candidateClass,
      @NotNull PsiSubstitutor candidateSubstitutor,
      @NotNull PsiClass aClass,
      @NotNull PsiSubstitutor substitutor,
      @NotNull PsiElementFactory elementFactory,
      @NotNull LanguageLevel languageLevel) {
    if (PsiUtil.isRawSubstitutor(aClass, substitutor)) {
      return elementFactory.createRawSubstitutor(candidateClass);
    }
    final PsiType containingType =
        elementFactory.createType(candidateClass, candidateSubstitutor, languageLevel);
    PsiType type = substitutor.substitute(containingType);
    if (!(type instanceof PsiClassType)) return candidateSubstitutor;
    return ((PsiClassType) type).resolveGenerics().getSubstitutor();
  }

  private static boolean processDeclarationsInClassNotCached(
      @NotNull PsiClass aClass,
      @NotNull PsiScopeProcessor processor,
      @NotNull ResolveState state,
      @Nullable Set<PsiClass> visited,
      PsiElement last,
      @NotNull PsiElement place,
      boolean isRaw,
      @NotNull LanguageLevel languageLevel) {
    if (visited == null) visited = new THashSet<PsiClass>();
    if (!visited.add(aClass)) return true;
    processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass);
    final ElementClassHint classHint = processor.getHint(ElementClassHint.KEY);
    final NameHint nameHint = processor.getHint(NameHint.KEY);

    if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.FIELD)) {
      if (nameHint != null) {
        final PsiField fieldByName = aClass.findFieldByName(nameHint.getName(state), false);
        if (fieldByName != null && !processor.execute(fieldByName, state)) return false;
      } else {
        final PsiField[] fields = aClass.getFields();
        for (final PsiField field : fields) {
          if (!processor.execute(field, state)) return false;
        }
      }
    }

    PsiElementFactory factory = JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory();

    if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.METHOD)) {
      PsiSubstitutor baseSubstitutor = state.get(PsiSubstitutor.KEY);
      final PsiMethod[] methods =
          nameHint != null
              ? aClass.findMethodsByName(nameHint.getName(state), false)
              : aClass.getMethods();
      for (final PsiMethod method : methods) {
        PsiSubstitutor finalSubstitutor = checkRaw(isRaw, factory, method, baseSubstitutor);
        ResolveState methodState =
            finalSubstitutor == baseSubstitutor
                ? state
                : state.put(PsiSubstitutor.KEY, finalSubstitutor);
        if (!processor.execute(method, methodState)) return false;
      }
    }

    if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.CLASS)) {
      if (last != null && last.getParent() == aClass) {
        // Parameters
        final PsiTypeParameterList list = aClass.getTypeParameterList();
        if (list != null
            && !list.processDeclarations(processor, ResolveState.initial(), last, place))
          return false;
      }

      if (!(last instanceof PsiReferenceList) && !(last instanceof PsiModifierList)) {
        // Inners
        if (nameHint != null) {
          final PsiClass inner = aClass.findInnerClassByName(nameHint.getName(state), false);
          if (inner != null) {
            if (!processor.execute(inner, state)) return false;
          }
        } else {
          final PsiClass[] inners = aClass.getInnerClasses();
          for (final PsiClass inner : inners) {
            if (!processor.execute(inner, state)) return false;
          }
        }
      }
    }

    return last instanceof PsiReferenceList
        || processSuperTypes(
            aClass, processor, visited, last, place, state, isRaw, factory, languageLevel);
  }

  private static boolean processSuperTypes(
      @NotNull PsiClass aClass,
      @NotNull PsiScopeProcessor processor,
      @Nullable Set<PsiClass> visited,
      PsiElement last,
      @NotNull PsiElement place,
      @NotNull ResolveState state,
      boolean isRaw,
      @NotNull PsiElementFactory factory,
      @NotNull LanguageLevel languageLevel) {
    boolean resolved = false;
    for (final PsiClassType superType : aClass.getSuperTypes()) {
      final PsiClassType.ClassResolveResult superTypeResolveResult = superType.resolveGenerics();
      PsiClass superClass = superTypeResolveResult.getElement();
      if (superClass == null) continue;
      PsiSubstitutor finalSubstitutor =
          obtainFinalSubstitutor(
              superClass,
              superTypeResolveResult.getSubstitutor(),
              aClass,
              state.get(PsiSubstitutor.KEY),
              factory,
              languageLevel);
      if (aClass instanceof PsiTypeParameter
          && PsiUtil.isRawSubstitutor(superClass, finalSubstitutor)) {
        finalSubstitutor = PsiSubstitutor.EMPTY;
      }
      if (!processDeclarationsInClass(
          superClass,
          processor,
          state.put(PsiSubstitutor.KEY, finalSubstitutor),
          visited,
          last,
          place,
          isRaw)) {
        resolved = true;
      }
    }
    return !resolved;
  }

  @Nullable
  public static PsiClass getSuperClass(@NotNull PsiClass psiClass) {
    PsiManager manager = psiClass.getManager();
    GlobalSearchScope resolveScope = psiClass.getResolveScope();

    final JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject());
    if (psiClass.isInterface()) {
      return facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope);
    }
    if (psiClass.isEnum()) {
      return facade.findClass(CommonClassNames.JAVA_LANG_ENUM, resolveScope);
    }

    if (psiClass instanceof PsiAnonymousClass) {
      PsiClassType baseClassReference = ((PsiAnonymousClass) psiClass).getBaseClassType();
      PsiClass baseClass = baseClassReference.resolve();
      if (baseClass == null || baseClass.isInterface())
        return facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope);
      return baseClass;
    }

    if (CommonClassNames.JAVA_LANG_OBJECT.equals(psiClass.getQualifiedName())) return null;

    final PsiClassType[] referenceElements = psiClass.getExtendsListTypes();

    if (referenceElements.length == 0)
      return facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope);

    PsiClass psiResoved = referenceElements[0].resolve();
    return psiResoved == null
        ? facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope)
        : psiResoved;
  }

  @NotNull
  public static PsiClass[] getSupers(@NotNull PsiClass psiClass) {
    final PsiClass[] supers = getSupersInner(psiClass);
    for (final PsiClass aSuper : supers) {
      LOG.assertTrue(aSuper != null);
    }
    return supers;
  }

  @NotNull
  private static PsiClass[] getSupersInner(@NotNull PsiClass psiClass) {
    PsiClassType[] extendsListTypes = psiClass.getExtendsListTypes();
    PsiClassType[] implementsListTypes = psiClass.getImplementsListTypes();

    if (psiClass.isInterface()) {
      return resolveClassReferenceList(
          extendsListTypes, psiClass.getManager(), psiClass.getResolveScope(), true);
    }

    if (psiClass instanceof PsiAnonymousClass) {
      PsiAnonymousClass psiAnonymousClass = (PsiAnonymousClass) psiClass;
      PsiClassType baseClassReference = psiAnonymousClass.getBaseClassType();
      PsiClass baseClass = baseClassReference.resolve();
      if (baseClass != null) {
        if (baseClass.isInterface()) {
          PsiClass objectClass =
              JavaPsiFacade.getInstance(psiClass.getProject())
                  .findClass(CommonClassNames.JAVA_LANG_OBJECT, psiClass.getResolveScope());
          return objectClass != null
              ? new PsiClass[] {objectClass, baseClass}
              : new PsiClass[] {baseClass};
        }
        return new PsiClass[] {baseClass};
      }

      PsiClass objectClass =
          JavaPsiFacade.getInstance(psiClass.getProject())
              .findClass(CommonClassNames.JAVA_LANG_OBJECT, psiClass.getResolveScope());
      return objectClass != null ? new PsiClass[] {objectClass} : PsiClass.EMPTY_ARRAY;
    }
    if (psiClass instanceof PsiTypeParameter) {
      if (extendsListTypes.length == 0) {
        final PsiClass objectClass =
            JavaPsiFacade.getInstance(psiClass.getProject())
                .findClass(CommonClassNames.JAVA_LANG_OBJECT, psiClass.getResolveScope());
        return objectClass != null ? new PsiClass[] {objectClass} : PsiClass.EMPTY_ARRAY;
      }
      return resolveClassReferenceList(
          extendsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false);
    }

    PsiClass[] interfaces =
        resolveClassReferenceList(
            implementsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false);

    PsiClass superClass = getSuperClass(psiClass);
    if (superClass == null) return interfaces;
    PsiClass[] types = new PsiClass[interfaces.length + 1];
    types[0] = superClass;
    System.arraycopy(interfaces, 0, types, 1, interfaces.length);

    return types;
  }

  @NotNull
  public static PsiClassType[] getSuperTypes(@NotNull PsiClass psiClass) {
    if (psiClass instanceof PsiAnonymousClass) {
      PsiClassType baseClassType = ((PsiAnonymousClass) psiClass).getBaseClassType();
      PsiClass baseClass = baseClassType.resolve();
      if (baseClass == null || !baseClass.isInterface()) {
        return new PsiClassType[] {baseClassType};
      } else {
        PsiClassType objectType =
            PsiType.getJavaLangObject(psiClass.getManager(), psiClass.getResolveScope());
        return new PsiClassType[] {objectType, baseClassType};
      }
    }

    PsiClassType[] extendsTypes = psiClass.getExtendsListTypes();
    PsiClassType[] implementsTypes = psiClass.getImplementsListTypes();
    boolean hasExtends = extendsTypes.length != 0;
    int extendsListLength = extendsTypes.length + (hasExtends ? 0 : 1);
    PsiClassType[] result = new PsiClassType[extendsListLength + implementsTypes.length];

    System.arraycopy(extendsTypes, 0, result, 0, extendsTypes.length);
    if (!hasExtends) {
      if (CommonClassNames.JAVA_LANG_OBJECT.equals(psiClass.getQualifiedName())) {
        return PsiClassType.EMPTY_ARRAY;
      }
      PsiManager manager = psiClass.getManager();
      PsiClassType objectType = PsiType.getJavaLangObject(manager, psiClass.getResolveScope());
      result[0] = objectType;
    }
    System.arraycopy(implementsTypes, 0, result, extendsListLength, implementsTypes.length);
    for (int i = 0; i < result.length; i++) {
      PsiClassType type = result[i];
      result[i] = (PsiClassType) PsiUtil.captureToplevelWildcards(type, psiClass);
    }
    return result;
  }

  @NotNull
  private static PsiClassType getAnnotationSuperType(
      @NotNull PsiClass psiClass, @NotNull PsiElementFactory factory) {
    return factory.createTypeByFQClassName(
        "java.lang.annotation.Annotation", psiClass.getResolveScope());
  }

  private static PsiClassType getEnumSuperType(
      @NotNull PsiClass psiClass, @NotNull PsiElementFactory factory) {
    PsiClassType superType;
    final PsiManager manager = psiClass.getManager();
    final PsiClass enumClass =
        JavaPsiFacade.getInstance(manager.getProject())
            .findClass("java.lang.Enum", psiClass.getResolveScope());
    if (enumClass == null) {
      try {
        superType = (PsiClassType) factory.createTypeFromText("java.lang.Enum", null);
      } catch (IncorrectOperationException e) {
        superType = null;
      }
    } else {
      final PsiTypeParameter[] typeParameters = enumClass.getTypeParameters();
      PsiSubstitutor substitutor = PsiSubstitutor.EMPTY;
      if (typeParameters.length == 1) {
        substitutor = substitutor.put(typeParameters[0], factory.createType(psiClass));
      }
      superType = new PsiImmediateClassType(enumClass, substitutor);
    }
    return superType;
  }

  @NotNull
  public static PsiClass[] getInterfaces(@NotNull PsiTypeParameter typeParameter) {
    final PsiClassType[] referencedTypes = typeParameter.getExtendsListTypes();
    if (referencedTypes.length == 0) {
      return PsiClass.EMPTY_ARRAY;
    }
    final List<PsiClass> result = new ArrayList<PsiClass>(referencedTypes.length);
    for (PsiClassType referencedType : referencedTypes) {
      final PsiClass psiClass = referencedType.resolve();
      if (psiClass != null && psiClass.isInterface()) {
        result.add(psiClass);
      }
    }
    return result.toArray(new PsiClass[result.size()]);
  }

  @NotNull
  public static PsiClass[] getInterfaces(@NotNull PsiClass psiClass) {
    if (psiClass.isInterface()) {
      final PsiClassType[] extendsListTypes = psiClass.getExtendsListTypes();
      return resolveClassReferenceList(
          extendsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false);
    }

    if (psiClass instanceof PsiAnonymousClass) {
      PsiClassType baseClassReference = ((PsiAnonymousClass) psiClass).getBaseClassType();
      PsiClass baseClass = baseClassReference.resolve();
      return baseClass != null && baseClass.isInterface()
          ? new PsiClass[] {baseClass}
          : PsiClass.EMPTY_ARRAY;
    }

    final PsiClassType[] implementsListTypes = psiClass.getImplementsListTypes();
    return resolveClassReferenceList(
        implementsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false);
  }

  @NotNull
  private static PsiClass[] resolveClassReferenceList(
      @NotNull PsiClassType[] listOfTypes,
      @NotNull PsiManager manager,
      @NotNull GlobalSearchScope resolveScope,
      boolean includeObject) {
    PsiClass objectClass =
        JavaPsiFacade.getInstance(manager.getProject())
            .findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope);
    if (objectClass == null) includeObject = false;
    if (listOfTypes.length == 0) {
      if (includeObject) return new PsiClass[] {objectClass};
      return PsiClass.EMPTY_ARRAY;
    }

    int referenceCount = listOfTypes.length;
    if (includeObject) referenceCount++;

    PsiClass[] resolved = new PsiClass[referenceCount];
    int resolvedCount = 0;

    if (includeObject) resolved[resolvedCount++] = objectClass;
    for (PsiClassType reference : listOfTypes) {
      PsiClass refResolved = reference.resolve();
      if (refResolved != null) resolved[resolvedCount++] = refResolved;
    }

    if (resolvedCount < referenceCount) {
      PsiClass[] shorter = new PsiClass[resolvedCount];
      System.arraycopy(resolved, 0, shorter, 0, resolvedCount);
      resolved = shorter;
    }

    return resolved;
  }

  @NotNull
  public static List<Pair<PsiMethod, PsiSubstitutor>> findMethodsAndTheirSubstitutorsByName(
      @NotNull PsiClass psiClass, String name, boolean checkBases) {
    if (!checkBases) {
      final PsiMethod[] methodsByName = psiClass.findMethodsByName(name, false);
      final List<Pair<PsiMethod, PsiSubstitutor>> ret =
          new ArrayList<Pair<PsiMethod, PsiSubstitutor>>(methodsByName.length);
      for (final PsiMethod method : methodsByName) {
        ret.add(new Pair<PsiMethod, PsiSubstitutor>(method, PsiSubstitutor.EMPTY));
      }
      return ret;
    }
    Map<String, List<Pair<PsiMember, PsiSubstitutor>>> map = getMap(psiClass, MemberType.METHOD);
    @SuppressWarnings("unchecked")
    List<Pair<PsiMethod, PsiSubstitutor>> list = (List) map.get(name);
    return list == null
        ? Collections.<Pair<PsiMethod, PsiSubstitutor>>emptyList()
        : Collections.unmodifiableList(list);
  }

  @NotNull
  public static PsiClassType[] getExtendsListTypes(@NotNull PsiClass psiClass) {
    if (psiClass.isEnum()) {
      PsiClassType enumSuperType =
          getEnumSuperType(
              psiClass, JavaPsiFacade.getInstance(psiClass.getProject()).getElementFactory());
      return enumSuperType == null ? PsiClassType.EMPTY_ARRAY : new PsiClassType[] {enumSuperType};
    }
    if (psiClass.isAnnotationType()) {
      return new PsiClassType[] {
        getAnnotationSuperType(
            psiClass, JavaPsiFacade.getInstance(psiClass.getProject()).getElementFactory())
      };
    }
    final PsiReferenceList extendsList = psiClass.getExtendsList();
    if (extendsList != null) {
      return extendsList.getReferencedTypes();
    }
    return PsiClassType.EMPTY_ARRAY;
  }

  @NotNull
  public static PsiClassType[] getImplementsListTypes(@NotNull PsiClass psiClass) {
    final PsiReferenceList extendsList = psiClass.getImplementsList();
    if (extendsList != null) {
      return extendsList.getReferencedTypes();
    }
    return PsiClassType.EMPTY_ARRAY;
  }

  public static boolean isClassEquivalentTo(@NotNull PsiClass aClass, PsiElement another) {
    if (aClass == another) return true;
    if (!(another instanceof PsiClass)) return false;
    String name1 = aClass.getName();
    if (name1 == null) return false;
    if (!another.isValid()) return false;
    String name2 = ((PsiClass) another).getName();
    if (name2 == null) return false;
    if (name1.hashCode() != name2.hashCode()) return false;
    if (!name1.equals(name2)) return false;
    String qName1 = aClass.getQualifiedName();
    String qName2 = ((PsiClass) another).getQualifiedName();
    if (qName1 == null || qName2 == null) {
      //noinspection StringEquality
      if (qName1 != qName2) return false;

      if (aClass instanceof PsiTypeParameter && another instanceof PsiTypeParameter) {
        PsiTypeParameter p1 = (PsiTypeParameter) aClass;
        PsiTypeParameter p2 = (PsiTypeParameter) another;

        return p1.getIndex() == p2.getIndex()
            && aClass.getManager().areElementsEquivalent(p1.getOwner(), p2.getOwner());
      } else {
        return false;
      }
    }
    if (qName1.hashCode() != qName2.hashCode() || !qName1.equals(qName2)) {
      return false;
    }

    if (originalElement(aClass).equals(originalElement((PsiClass) another))) {
      return true;
    }

    final PsiFile file1 = aClass.getContainingFile().getOriginalFile();
    final PsiFile file2 = another.getContainingFile().getOriginalFile();

    // see com.intellij.openapi.vcs.changes.PsiChangeTracker
    // see com.intellij.psi.impl.PsiFileFactoryImpl#createFileFromText(CharSequence,PsiFile)
    final PsiFile original1 = file1.getUserData(PsiFileFactory.ORIGINAL_FILE);
    final PsiFile original2 = file2.getUserData(PsiFileFactory.ORIGINAL_FILE);
    if (original1 == original2 && original1 != null
        || original1 == file2
        || original2 == file1
        || file1 == file2) {
      return compareClassSeqNumber(aClass, (PsiClass) another);
    }

    final FileIndexFacade fileIndex =
        ServiceManager.getService(file1.getProject(), FileIndexFacade.class);
    final VirtualFile vfile1 = file1.getViewProvider().getVirtualFile();
    final VirtualFile vfile2 = file2.getViewProvider().getVirtualFile();
    boolean lib1 = fileIndex.isInLibraryClasses(vfile1);
    boolean lib2 = fileIndex.isInLibraryClasses(vfile2);

    return (fileIndex.isInSource(vfile1) || lib1) && (fileIndex.isInSource(vfile2) || lib2);
  }

  private static boolean compareClassSeqNumber(
      @NotNull PsiClass aClass, @NotNull PsiClass another) {
    // there may be several classes in one file, they must not be equal
    int index1 = getSeqNumber(aClass);
    if (index1 == -1) return true;
    int index2 = getSeqNumber(another);
    return index1 == index2;
  }

  private static int getSeqNumber(@NotNull PsiClass aClass) {
    // sequence number of this class among its parent' child classes named the same
    PsiElement parent = aClass.getParent();
    if (parent == null) return -1;
    int seqNo = 0;
    for (PsiElement child : parent.getChildren()) {
      if (child == aClass) return seqNo;
      if (child instanceof PsiClass
          && Comparing.strEqual(aClass.getName(), ((PsiClass) child).getName())) {
        seqNo++;
      }
    }
    return -1;
  }

  @NotNull
  private static PsiElement originalElement(@NotNull PsiClass aClass) {
    final PsiElement originalElement = aClass.getOriginalElement();
    ASTNode node = originalElement.getNode();
    if (node != null) {
      final PsiCompiledElement compiled = node.getUserData(ClsElementImpl.COMPILED_ELEMENT);
      if (compiled != null) {
        return compiled;
      }
    }
    return originalElement;
  }

  public static boolean isFieldEquivalentTo(@NotNull PsiField field, PsiElement another) {
    if (!(another instanceof PsiField)) return false;
    String name1 = field.getName();
    if (name1 == null) return false;
    if (!another.isValid()) return false;

    String name2 = ((PsiField) another).getName();
    if (!name1.equals(name2)) return false;
    PsiClass aClass1 = field.getContainingClass();
    PsiClass aClass2 = ((PsiField) another).getContainingClass();
    return aClass1 != null
        && aClass2 != null
        && field.getManager().areElementsEquivalent(aClass1, aClass2);
  }

  public static boolean isMethodEquivalentTo(@NotNull PsiMethod method1, PsiElement another) {
    if (method1 == another) return true;
    if (!(another instanceof PsiMethod)) return false;
    PsiMethod method2 = (PsiMethod) another;
    if (!another.isValid()) return false;
    if (!method1.getName().equals(method2.getName())) return false;
    PsiClass aClass1 = method1.getContainingClass();
    PsiClass aClass2 = method2.getContainingClass();
    PsiManager manager = method1.getManager();
    if (!(aClass1 != null && aClass2 != null && manager.areElementsEquivalent(aClass1, aClass2)))
      return false;

    PsiParameter[] parameters1 = method1.getParameterList().getParameters();
    PsiParameter[] parameters2 = method2.getParameterList().getParameters();
    if (parameters1.length != parameters2.length) return false;
    for (int i = 0; i < parameters1.length; i++) {
      PsiParameter parameter1 = parameters1[i];
      PsiParameter parameter2 = parameters2[i];
      PsiType type1 = parameter1.getType();
      PsiType type2 = parameter2.getType();
      if (!compareParamTypes(manager, type1, type2)) return false;
    }
    return true;
  }

  private static boolean compareParamTypes(
      @NotNull PsiManager manager, @NotNull PsiType type1, @NotNull PsiType type2) {
    if (type1 instanceof PsiArrayType) {
      return type2 instanceof PsiArrayType
          && compareParamTypes(
              manager,
              ((PsiArrayType) type1).getComponentType(),
              ((PsiArrayType) type2).getComponentType());
    }

    if (!(type1 instanceof PsiClassType) || !(type2 instanceof PsiClassType)) {
      return type1.equals(type2);
    }

    PsiClass class1 = ((PsiClassType) type1).resolve();
    PsiClass class2 = ((PsiClassType) type2).resolve();

    if (class1 instanceof PsiTypeParameter && class2 instanceof PsiTypeParameter) {
      return Comparing.equal(class1.getName(), class2.getName())
          && ((PsiTypeParameter) class1).getIndex() == ((PsiTypeParameter) class2).getIndex();
    }

    return manager.areElementsEquivalent(class1, class2);
  }
}