Пример #1
0
/** @author max */
public class FileContextUtil {
  public static final Key<SmartPsiElementPointer> INJECTED_IN_ELEMENT = Key.create("injectedIn");
  public static final Key<PsiFile> CONTAINING_FILE_KEY = Key.create("CONTAINING_FILE_KEY");

  private FileContextUtil() {}

  @Nullable
  public static PsiElement getFileContext(PsiFile file) {
    SmartPsiElementPointer pointer = file.getUserData(INJECTED_IN_ELEMENT);
    return pointer == null ? null : pointer.getElement();
  }

  @Nullable
  public static PsiFile getContextFile(@NotNull PsiElement element) {
    if (!element.isValid()) return null;
    PsiFile file = element.getContainingFile();
    if (file == null) return null;
    PsiElement context = file.getContext();
    if (context == null) {
      return file;
    } else {
      return getContextFile(context);
    }
  }
}
public abstract class RefJavaManager implements RefManagerExtension<RefJavaManager> {
  @NonNls public static final String CLASS = "class";
  @NonNls public static final String METHOD = "method";
  @NonNls public static final String IMPLICIT_CONSTRUCTOR = "implicit.constructor";
  @NonNls public static final String FIELD = "field";
  @NonNls public static final String PARAMETER = "parameter";
  @NonNls public static final String JAVA_MODULE = "java.module";
  // used in OfflineProjectDescriptor
  @NonNls public static final String PACKAGE = "package";
  public static final Key<RefJavaManager> MANAGER = Key.create("RefJavaManager");

  public abstract RefImplicitConstructor getImplicitConstructor(String classFQName);

  /**
   * Creates (if necessary) and returns the reference graph node for the package with the specified
   * name.
   *
   * @param packageName the name of the package for which the reference graph node is requested.
   * @return the node for the package.
   */
  public abstract RefPackage getPackage(String packageName);

  /**
   * Creates (if necessary) and returns the reference graph node for the specified PSI parameter.
   *
   * @param param the parameter for which the reference graph node is requested.
   * @param index the index of the parameter in its parameter list.
   * @return the node for the element, or null if the element is not valid or does not have a
   *     corresponding reference graph node type (is not a field, method, class or file).
   */
  public abstract RefParameter getParameterReference(PsiParameter param, int index);

  public abstract RefPackage getDefaultPackage();

  public abstract PsiMethod getAppMainPattern();

  public abstract PsiMethod getAppPremainPattern();

  public abstract PsiMethod getAppAgentmainPattern();

  public abstract PsiClass getApplet();

  public abstract PsiClass getServlet();

  public abstract EntryPointsManager getEntryPointsManager();

  @NotNull
  @Override
  public Language getLanguage() {
    return JavaLanguage.INSTANCE;
  }

  @NotNull
  @Override
  public Key<RefJavaManager> getID() {
    return MANAGER;
  }
}
/**
 * @author Dmitry Avdeev
 * @author peter
 */
public class PerIndexDocumentVersionMap {
  private static final int INVALID_STAMP =
      -1; // 0 isn't acceptable as Document has 0 stamp when loaded from unchanged file
  private volatile int mapVersion;

  private static class IdVersionInfo {
    private final ID<?, ?> id;
    private int mapVersion;
    private long docVersion;

    private IdVersionInfo(@NotNull ID<?, ?> id, long docVersion, int mapVersion) {
      this.docVersion = docVersion;
      this.mapVersion = mapVersion;
      this.id = id;
    }
  }

  private static final Key<List<IdVersionInfo>> KEY = Key.create("UnsavedDocIdVersionInfo");

  public long getAndSet(@NotNull Document document, @NotNull ID<?, ?> indexId, long value) {
    List<IdVersionInfo> list = document.getUserData(KEY);
    if (list == null) {
      list = ((UserDataHolderEx) document).putUserDataIfAbsent(KEY, new ArrayList<IdVersionInfo>());
    }

    synchronized (list) {
      for (IdVersionInfo info : list) {
        if (info.id == indexId) {
          long old = info.docVersion;
          if (info.mapVersion != mapVersion) {
            old = INVALID_STAMP;
            info.mapVersion = mapVersion;
          }
          info.docVersion = value;
          return old;
        }
      }
      list.add(new IdVersionInfo(indexId, value, mapVersion));
      return INVALID_STAMP;
    }
  }

  public void clear() {
    mapVersion++;
  }
}
Пример #4
0
public interface IGosuMethod
    extends IGosuMembersDeclaration,
        PsiMethod,
        IGosuNamedElement,
        IGosuMember,
        IGosuParametersOwner,
        IGosuTypeParameterListOwner {
  Key<Boolean> BUILDER_METHOD = Key.create("BUILDER_METHOD");

  IGosuMethod[] EMPTY_ARRAY = new IGosuMethod[0];

  ArrayFactory<IGosuMethod> ARRAY_FACTORY =
      new ArrayFactory<IGosuMethod>() {
        @NotNull
        public IGosuMethod[] create(int count) {
          return count == 0 ? EMPTY_ARRAY : new IGosuMethod[count];
        }
      };

  IGosuTypeElement getReturnTypeElementGosu();

  /** @return the static return type, which will appear in the compiled Gosu class */
  @Nullable
  PsiType getReturnType();

  @Nullable
  IGosuTypeElement setReturnType(PsiType newReturnType);

  @NotNull
  String getName();

  @NotNull
  IGosuParameterList getParameterList();

  @NotNull
  IGosuModifierList getModifierList();

  String[] getNamedParametersArray();

  boolean isForProperty();

  boolean isForPropertySetter();

  boolean isForPropertyGetter();
}
public abstract class HTMLJavaHTMLComposer implements HTMLComposerExtension<HTMLJavaHTMLComposer> {
  public static final Key<HTMLJavaHTMLComposer> COMPOSER = Key.create("HTMLJavaComposer");

  public abstract void appendClassOrInterface(
      StringBuffer buf, RefClass refClass, boolean capitalizeFirstLetter);

  public static String getClassOrInterface(RefClass refClass, boolean capitalizeFirstLetter) {
    if (refClass.isInterface()) {
      return capitalizeFirstLetter
          ? InspectionsBundle.message("inspection.export.results.capitalized.interface")
          : InspectionsBundle.message("inspection.export.results.interface");
    } else if (refClass.isAbstract()) {
      return capitalizeFirstLetter
          ? InspectionsBundle.message("inspection.export.results.capitalized.abstract.class")
          : InspectionsBundle.message("inspection.export.results.abstract.class");
    } else {
      return capitalizeFirstLetter
          ? InspectionsBundle.message("inspection.export.results.capitalized.class")
          : InspectionsBundle.message("inspection.export.results.class");
    }
  }

  public abstract void appendClassExtendsImplements(StringBuffer buf, RefClass refClass);

  public abstract void appendDerivedClasses(StringBuffer buf, RefClass refClass);

  public abstract void appendLibraryMethods(StringBuffer buf, RefClass refClass);

  public abstract void appendSuperMethods(StringBuffer buf, RefMethod refMethod);

  public abstract void appendDerivedMethods(StringBuffer buf, RefMethod refMethod);

  public abstract void appendTypeReferences(StringBuffer buf, RefClass refClass);

  public Key<HTMLJavaHTMLComposer> getID() {
    return COMPOSER;
  }

  public Language getLanguage() {
    return StdLanguages.JAVA;
  }
}
  private static final class CompileStatistics {
    private static final Key<CompileStatistics> KEY = Key.create("_Compile_Statistics_");
    private int myClassesCount;
    private int myFilesCount;

    public int getClassesCount() {
      return myClassesCount;
    }

    public int incClassesCount() {
      return ++myClassesCount;
    }

    public int getFilesCount() {
      return myFilesCount;
    }

    public int incFilesCount() {
      return ++myFilesCount;
    }
  }
/** @author Eugene.Kudelevsky */
public class AndroidDependenciesCache {
  private static final Key<SoftReference<AndroidDependenciesCache>> KEY =
      Key.create("ANDROID_DEPENDENCIES_CACHE");
  private final Module myModule;

  private final Ref<List<WeakReference<AndroidModuleExtension>>> myAllDependencies = Ref.create();
  private final Ref<List<WeakReference<AndroidModuleExtension>>> myAllLibraryDependencies =
      Ref.create();

  private AndroidDependenciesCache(@NotNull Module module) {
    myModule = module;

    module
        .getProject()
        .getMessageBus()
        .connect(module)
        .subscribe(
            ProjectTopics.PROJECT_ROOTS,
            new ModuleRootAdapter() {
              @Override
              public void rootsChanged(ModuleRootEvent event) {
                dropCache();
              }
            });
  }

  private synchronized void dropCache() {
    myAllDependencies.set(null);
    myAllLibraryDependencies.set(null);
  }

  @NotNull
  public static AndroidDependenciesCache getInstance(@NotNull Module module) {
    AndroidDependenciesCache cache = SoftReference.dereference(module.getUserData(KEY));

    if (cache == null) {
      cache = new AndroidDependenciesCache(module);
      module.putUserData(KEY, new SoftReference<AndroidDependenciesCache>(cache));
    }
    return cache;
  }

  @NotNull
  public synchronized List<AndroidModuleExtension> getAllAndroidDependencies(
      boolean androidLibrariesOnly) {
    return getAllAndroidDependencies(
        myModule, androidLibrariesOnly, getListRef(androidLibrariesOnly));
  }

  @NotNull
  private Ref<List<WeakReference<AndroidModuleExtension>>> getListRef(
      boolean androidLibrariesOnly) {
    return androidLibrariesOnly ? myAllLibraryDependencies : myAllDependencies;
  }

  @NotNull
  private static List<AndroidModuleExtension> getAllAndroidDependencies(
      @NotNull Module module,
      boolean androidLibrariesOnly,
      Ref<List<WeakReference<AndroidModuleExtension>>> listRef) {
    List<WeakReference<AndroidModuleExtension>> refs = listRef.get();

    if (refs == null) {
      final List<AndroidModuleExtension> facets = new ArrayList<AndroidModuleExtension>();
      collectAllAndroidDependencies(
          module, androidLibrariesOnly, facets, new HashSet<AndroidModuleExtension>());

      refs =
          ContainerUtil.map(
              ContainerUtil.reverse(facets),
              new Function<AndroidModuleExtension, WeakReference<AndroidModuleExtension>>() {
                @Override
                public WeakReference<AndroidModuleExtension> fun(AndroidModuleExtension facet) {
                  return new WeakReference<AndroidModuleExtension>(facet);
                }
              });
      listRef.set(refs);
    }
    return dereference(refs);
  }

  @NotNull
  private static List<AndroidModuleExtension> dereference(
      @NotNull List<WeakReference<AndroidModuleExtension>> refs) {
    return ContainerUtil.mapNotNull(
        refs,
        new Function<WeakReference<AndroidModuleExtension>, AndroidModuleExtension>() {
          @Override
          public AndroidModuleExtension fun(WeakReference<AndroidModuleExtension> ref) {
            return ref.get();
          }
        });
  }

  private static void collectAllAndroidDependencies(
      Module module,
      boolean androidLibrariesOnly,
      List<AndroidModuleExtension> result,
      Set<AndroidModuleExtension> visited) {
    final OrderEntry[] entries = ModuleRootManager.getInstance(module).getOrderEntries();
    // loop in the inverse order to resolve dependencies on the libraries, so that if a library
    // is required by two higher level libraries it can be inserted in the correct place

    for (int i = entries.length - 1; i >= 0; i--) {
      final OrderEntry orderEntry = entries[i];
      if (orderEntry instanceof ModuleOrderEntry) {
        final ModuleOrderEntry moduleOrderEntry = (ModuleOrderEntry) orderEntry;

        if (moduleOrderEntry.getScope() == DependencyScope.COMPILE) {
          final Module depModule = moduleOrderEntry.getModule();

          if (depModule != null) {
            final AndroidModuleExtension depFacet =
                ModuleUtilCore.getExtension(depModule, AndroidModuleExtension.class);

            if (depFacet != null
                && (!androidLibrariesOnly || depFacet.isLibraryProject())
                && visited.add(depFacet)) {
              final List<WeakReference<AndroidModuleExtension>> cachedDepDeps =
                  getInstance(depModule).getListRef(androidLibrariesOnly).get();

              if (cachedDepDeps != null) {
                final List<AndroidModuleExtension> depDeps = dereference(cachedDepDeps);

                for (AndroidModuleExtension depDepFacet : depDeps) {
                  if (visited.add(depDepFacet)) {
                    result.add(depDepFacet);
                  }
                }
              } else {
                collectAllAndroidDependencies(depModule, androidLibrariesOnly, result, visited);
              }
              result.add(depFacet);
            }
          }
        }
      }
    }
  }
}
/** @author gregsh */
public class GeneratedParserUtilBase {

  private static final Logger LOG =
      Logger.getInstance("org.intellij.grammar.parser.GeneratedParserUtilBase");

  private static final int MAX_RECURSION_LEVEL = 1000;
  private static final int MAX_VARIANTS_SIZE = 10000;
  private static final int MAX_VARIANTS_TO_DISPLAY = 50;

  private static final int INITIAL_VARIANTS_SIZE = 1000;
  private static final int VARIANTS_POOL_SIZE = 10000;
  private static final int FRAMES_POOL_SIZE = 500;

  public static final IElementType DUMMY_BLOCK = new DummyBlockElementType();

  public interface Parser {
    boolean parse(PsiBuilder builder, int level);
  }

  public static final Parser TOKEN_ADVANCER =
      new Parser() {
        @Override
        public boolean parse(PsiBuilder builder, int level) {
          if (builder.eof()) return false;
          builder.advanceLexer();
          return true;
        }
      };

  public static final Parser TRUE_CONDITION =
      new Parser() {
        @Override
        public boolean parse(PsiBuilder builder, int level) {
          return true;
        }
      };

  public static boolean eof(PsiBuilder builder, int level) {
    return builder.eof();
  }

  public static int current_position_(PsiBuilder builder) {
    return builder.rawTokenIndex();
  }

  public static boolean recursion_guard_(PsiBuilder builder, int level, String funcName) {
    if (level > MAX_RECURSION_LEVEL) {
      builder.error(
          "Maximum recursion level (" + MAX_RECURSION_LEVEL + ") reached in '" + funcName + "'");
      return false;
    }
    return true;
  }

  public static boolean empty_element_parsed_guard_(PsiBuilder builder, String funcName, int pos) {
    if (pos == current_position_(builder)) {
      builder.error(
          "Empty element parsed in '" + funcName + "' at offset " + builder.getCurrentOffset());
      return false;
    }
    return true;
  }

  public static boolean invalid_left_marker_guard_(
      PsiBuilder builder, PsiBuilder.Marker marker, String funcName) {
    // builder.error("Invalid left marker encountered in " + funcName_ +" at offset " +
    // builder.getCurrentOffset());
    boolean goodMarker =
        marker != null; // && ((LighterASTNode)marker).getTokenType() != TokenType.ERROR_ELEMENT;
    if (!goodMarker) return false;
    ErrorState state = ErrorState.get(builder);

    return !state.frameStack.isEmpty();
  }

  public static TokenSet create_token_set_(IElementType... tokenTypes) {
    return TokenSet.create(tokenTypes);
  }

  public static boolean leftMarkerIs(PsiBuilder builder, IElementType type) {
    LighterASTNode marker = builder.getLatestDoneMarker();
    return marker != null && marker.getTokenType() == type;
  }

  private static boolean consumeTokens(
      PsiBuilder builder, boolean smart, int pin, IElementType... tokens) {
    ErrorState state = ErrorState.get(builder);
    if (state.completionState != null && state.predicateSign) {
      addCompletionVariant(builder, state.completionState, tokens);
    }
    // suppress single token completion
    CompletionState completionState = state.completionState;
    state.completionState = null;
    boolean result = true;
    boolean pinned = false;
    for (int i = 0, tokensLength = tokens.length; i < tokensLength; i++) {
      if (pin > 0 && i == pin) pinned = result;
      if (result || pinned) {
        boolean fast = smart && i == 0;
        if (!(fast ? consumeTokenFast(builder, tokens[i]) : consumeToken(builder, tokens[i]))) {
          result = false;
          if (pin < 0 || pinned) report_error_(builder, state, false);
        }
      }
    }
    state.completionState = completionState;
    return pinned || result;
  }

  public static boolean consumeTokens(PsiBuilder builder, int pin, IElementType... token) {
    return consumeTokens(builder, false, pin, token);
  }

  public static boolean consumeTokensSmart(PsiBuilder builder, int pin, IElementType... token) {
    return consumeTokens(builder, true, pin, token);
  }

  public static boolean parseTokens(PsiBuilder builder, int pin, IElementType... tokens) {
    return parseTokens(builder, false, pin, tokens);
  }

  public static boolean parseTokensSmart(PsiBuilder builder, int pin, IElementType... tokens) {
    return parseTokens(builder, true, pin, tokens);
  }

  public static boolean parseTokens(
      PsiBuilder builder, boolean smart, int pin, IElementType... tokens) {
    PsiBuilder.Marker marker = builder.mark();
    boolean result = consumeTokens(builder, smart, pin, tokens);
    if (!result) {
      marker.rollbackTo();
    } else {
      marker.drop();
    }
    return result;
  }

  public static boolean consumeTokenSmart(PsiBuilder builder, IElementType token) {
    addCompletionVariantSmart(builder, token);
    return consumeTokenFast(builder, token);
  }

  public static boolean consumeTokenSmart(PsiBuilder builder, String token) {
    addCompletionVariantSmart(builder, token);
    return consumeTokenFast(builder, token);
  }

  public static boolean consumeToken(PsiBuilder builder, IElementType token) {
    addVariantSmart(builder, token, true);
    if (nextTokenIsFast(builder, token)) {
      builder.advanceLexer();
      return true;
    }
    return false;
  }

  public static boolean consumeTokenFast(PsiBuilder builder, IElementType token) {
    if (nextTokenIsFast(builder, token)) {
      builder.advanceLexer();
      return true;
    }
    return false;
  }

  public static boolean consumeToken(PsiBuilder builder, String text) {
    return consumeToken(builder, text, ErrorState.get(builder).caseSensitive);
  }

  public static boolean consumeToken(PsiBuilder builder, String text, boolean caseSensitive) {
    addVariantSmart(builder, text, true);
    int count = nextTokenIsFast(builder, text, caseSensitive);
    if (count > 0) {
      while (count-- > 0) builder.advanceLexer();
      return true;
    }
    return false;
  }

  public static boolean consumeTokenFast(PsiBuilder builder, String text) {
    int count = nextTokenIsFast(builder, text, ErrorState.get(builder).caseSensitive);
    if (count > 0) {
      while (count-- > 0) builder.advanceLexer();
      return true;
    }
    return false;
  }

  public static boolean nextTokenIsFast(PsiBuilder builder, IElementType token) {
    return builder.getTokenType() == token;
  }

  public static boolean nextTokenIsFast(PsiBuilder builder, IElementType... tokens) {
    IElementType tokenType = builder.getTokenType();
    for (IElementType token : tokens) {
      if (token == tokenType) return true;
    }
    return false;
  }

  public static boolean nextTokenIs(PsiBuilder builder, String frameName, IElementType... tokens) {
    ErrorState state = ErrorState.get(builder);
    if (state.completionState != null) return true;
    boolean track = !state.suppressErrors && state.predicateCount < 2 && state.predicateSign;
    if (!track) return nextTokenIsFast(builder, tokens);
    IElementType tokenType = builder.getTokenType();
    if (StringUtil.isNotEmpty(frameName)) {
      addVariantInner(state, builder.rawTokenIndex(), frameName);
    } else {
      for (IElementType token : tokens) {
        addVariant(builder, state, token);
      }
    }
    if (tokenType == null) return false;
    for (IElementType token : tokens) {
      if (tokenType == token) return true;
    }
    return false;
  }

  public static boolean nextTokenIs(PsiBuilder builder, IElementType token) {
    if (!addVariantSmart(builder, token, false)) return true;
    return nextTokenIsFast(builder, token);
  }

  public static boolean nextTokenIs(PsiBuilder builder, String tokenText) {
    if (!addVariantSmart(builder, tokenText, false)) return true;
    return nextTokenIsFast(builder, tokenText, ErrorState.get(builder).caseSensitive) > 0;
  }

  public static boolean nextTokenIsFast(PsiBuilder builder, String tokenText) {
    return nextTokenIsFast(builder, tokenText, ErrorState.get(builder).caseSensitive) > 0;
  }

  public static int nextTokenIsFast(PsiBuilder builder, String tokenText, boolean caseSensitive) {
    CharSequence sequence = builder.getOriginalText();
    int offset = builder.getCurrentOffset();
    int endOffset = offset + tokenText.length();
    CharSequence subSequence = sequence.subSequence(offset, Math.min(endOffset, sequence.length()));

    if (!Comparing.equal(subSequence, tokenText, caseSensitive)) return 0;

    int count = 0;
    while (true) {
      int nextOffset = builder.rawTokenTypeStart(++count);
      if (nextOffset > endOffset) {
        return -count;
      } else if (nextOffset == endOffset) {
        break;
      }
    }
    return count;
  }

  private static void addCompletionVariantSmart(PsiBuilder builder, Object token) {
    ErrorState state = ErrorState.get(builder);
    CompletionState completionState = state.completionState;
    if (completionState != null && state.predicateSign) {
      addCompletionVariant(builder, completionState, token);
    }
  }

  private static boolean addVariantSmart(PsiBuilder builder, Object token, boolean force) {
    ErrorState state = ErrorState.get(builder);
    // skip FIRST check in completion mode
    if (state.completionState != null && !force) return false;
    builder.eof();
    if (!state.suppressErrors && state.predicateCount < 2) {
      addVariant(builder, state, token);
    }
    return true;
  }

  public static void addVariant(PsiBuilder builder, String text) {
    addVariant(builder, ErrorState.get(builder), text);
  }

  private static void addVariant(PsiBuilder builder, ErrorState state, Object o) {
    builder.eof(); // skip whitespaces
    addVariantInner(state, builder.rawTokenIndex(), o);

    CompletionState completionState = state.completionState;
    if (completionState != null && state.predicateSign) {
      addCompletionVariant(builder, completionState, o);
    }
  }

  private static void addVariantInner(ErrorState state, int pos, Object o) {
    Variant variant = state.VARIANTS.alloc().init(pos, o);
    if (state.predicateSign) {
      state.variants.add(variant);
      if (state.lastExpectedVariantPos < variant.position) {
        state.lastExpectedVariantPos = variant.position;
      }
    } else {
      state.unexpected.add(variant);
    }
  }

  private static void addCompletionVariant(
      @NotNull PsiBuilder builder, @NotNull CompletionState completionState, Object o) {
    int offset = builder.getCurrentOffset();
    if (!builder.eof() && offset == builder.rawTokenTypeStart(1))
      return; // suppress for zero-length tokens

    boolean add = false;
    int diff = completionState.offset - offset;
    String text = completionState.convertItem(o);
    int length = text == null ? 0 : text.length();
    if (length == 0) return;
    if (diff == 0) {
      add = true;
    } else if (diff > 0 && diff <= length) {
      CharSequence fragment = builder.getOriginalText().subSequence(offset, completionState.offset);
      add = completionState.prefixMatches(fragment.toString(), text);
    } else if (diff < 0) {
      for (int i = -1; ; i--) {
        IElementType type = builder.rawLookup(i);
        int tokenStart = builder.rawTokenTypeStart(i);
        if (isWhitespaceOrComment(builder, type)) {
          diff = completionState.offset - tokenStart;
        } else if (type != null && tokenStart < completionState.offset) {
          CharSequence fragment =
              builder.getOriginalText().subSequence(tokenStart, completionState.offset);
          if (completionState.prefixMatches(fragment.toString(), text)) {
            diff = completionState.offset - tokenStart;
          }
          break;
        } else break;
      }
      add = diff >= 0 && diff < length;
    }
    add =
        add
            && length > 1
            && !(text.charAt(0) == '<' && text.charAt(length - 1) == '>')
            && !(text.charAt(0) == '\'' && text.charAt(length - 1) == '\'' && length < 5);
    if (add) {
      completionState.addItem(builder, text);
    }
  }

  public static boolean isWhitespaceOrComment(
      @NotNull PsiBuilder builder, @Nullable IElementType type) {
    return ((PsiBuilderImpl) ((Builder) builder).getDelegate()).whitespaceOrComment(type);
  }

  // here's the new section API for compact parsers & less IntelliJ platform API exposure
  public static final int _NONE_ = 0x0;
  public static final int _COLLAPSE_ = 0x1;
  public static final int _LEFT_ = 0x2;
  public static final int _LEFT_INNER_ = 0x4;
  public static final int _AND_ = 0x8;
  public static final int _NOT_ = 0x10;

  // simple enter/exit methods pair that doesn't require frame object
  public static PsiBuilder.Marker enter_section_(PsiBuilder builder) {
    return builder.mark();
  }

  public static void exit_section_(
      PsiBuilder builder,
      PsiBuilder.Marker marker,
      @Nullable IElementType elementType,
      boolean result) {
    close_marker_impl_(ErrorState.get(builder).frameStack.peekLast(), marker, elementType, result);
  }

  // complex enter/exit methods pair with frame object
  public static PsiBuilder.Marker enter_section_(
      PsiBuilder builder, int level, int modifiers, @Nullable String frameName) {
    PsiBuilder.Marker marker = builder.mark();
    enter_section_impl_(builder, level, modifiers, frameName);
    return marker;
  }

  private static void enter_section_impl_(
      PsiBuilder builder, int level, int modifiers, @Nullable String frameName) {
    ErrorState state = ErrorState.get(builder);
    Frame frame = state.FRAMES.alloc().init(builder, state, level, modifiers, frameName);
    Frame prevFrame = state.frameStack.peekLast();
    if (prevFrame != null && prevFrame.errorReportedAt > frame.position) {
      // report error for previous unsuccessful frame
      reportError(builder, state, frame, null, true, false);
    }
    if (((frame.modifiers & _LEFT_) | (frame.modifiers & _LEFT_INNER_)) != 0) {
      PsiBuilder.Marker left = (PsiBuilder.Marker) builder.getLatestDoneMarker();
      if (invalid_left_marker_guard_(builder, left, frameName)) {
        frame.leftMarker = left;
      }
    }
    state.frameStack.add(frame);
    if ((modifiers & _AND_) != 0) {
      if (state.predicateCount == 0 && !state.predicateSign) {
        throw new AssertionError("Incorrect false predicate sign");
      }
      state.predicateCount++;
    } else if ((modifiers & _NOT_) != 0) {
      if (state.predicateCount == 0) {
        state.predicateSign = false;
      } else {
        state.predicateSign = !state.predicateSign;
      }
      state.predicateCount++;
    }
  }

  public static void exit_section_(
      PsiBuilder builder,
      int level,
      PsiBuilder.Marker marker,
      @Nullable IElementType elementType,
      boolean result,
      boolean pinned,
      @Nullable Parser eatMore) {
    ErrorState state = ErrorState.get(builder);

    Frame frame = state.frameStack.pollLast();
    if (frame == null || level != frame.level) {
      LOG.error("Unbalanced error section: got " + frame + ", expected level " + level);
      if (frame != null) state.FRAMES.recycle(frame);
      close_marker_impl_(frame, marker, elementType, result);
      return;
    }

    if (((frame.modifiers & _AND_) | (frame.modifiers & _NOT_)) != 0) {
      close_marker_impl_(frame, marker, null, false);
      state.predicateCount--;
      if ((frame.modifiers & _NOT_) != 0) state.predicateSign = !state.predicateSign;
      state.FRAMES.recycle(frame);
      return;
    }
    close_frame_impl_(state, frame, builder, marker, elementType, result, pinned);
    exit_section_impl_(state, frame, builder, elementType, result, pinned, eatMore);
    state.FRAMES.recycle(frame);
  }

  private static void exit_section_impl_(
      ErrorState state,
      Frame frame,
      PsiBuilder builder,
      @Nullable IElementType elementType,
      boolean result,
      boolean pinned,
      @Nullable Parser eatMore) {
    int initialPos = builder.rawTokenIndex();
    boolean willFail = !result && !pinned;
    if (willFail
        && initialPos == frame.position
        && state.lastExpectedVariantPos == frame.position
        && frame.name != null
        && state.variants.size() - frame.variantCount > 1) {
      state.clearVariants(true, frame.variantCount);
      addVariantInner(state, initialPos, frame.name);
    }
    int lastErrorPos = getLastVariantPos(state, initialPos);
    if (!state.suppressErrors && eatMore != null) {
      state.suppressErrors = true;
      final boolean eatMoreFlagOnce = !builder.eof() && eatMore.parse(builder, frame.level + 1);
      boolean eatMoreFlag =
          eatMoreFlagOnce
              || !result && frame.position == initialPos && lastErrorPos > frame.position;

      PsiBuilderImpl.ProductionMarker latestDoneMarker =
          (pinned || result) && (state.altMode || elementType != null) && eatMoreFlagOnce
              ? (PsiBuilderImpl.ProductionMarker) builder.getLatestDoneMarker()
              : null;
      PsiBuilder.Marker extensionMarker = null;
      IElementType extensionTokenType = null;
      // whitespace prefix makes the very first frame offset bigger than marker start offset which
      // is always 0
      if (latestDoneMarker != null
          && frame.position >= latestDoneMarker.getStartIndex()
          && frame.position <= latestDoneMarker.getEndIndex()) {
        extensionMarker = ((PsiBuilder.Marker) latestDoneMarker).precede();
        extensionTokenType = latestDoneMarker.getTokenType();
        ((PsiBuilder.Marker) latestDoneMarker).drop();
      }
      // advance to the last error pos
      // skip tokens until lastErrorPos. parseAsTree might look better here...
      int parenCount = 0;
      while ((eatMoreFlag || parenCount > 0) && builder.rawTokenIndex() < lastErrorPos) {
        if (state.braces != null) {
          if (builder.getTokenType() == state.braces[0].getLeftBraceType()) parenCount++;
          else if (builder.getTokenType() == state.braces[0].getRightBraceType()) parenCount--;
        }
        builder.advanceLexer();
        eatMoreFlag = eatMore.parse(builder, frame.level + 1);
      }
      boolean errorReported =
          frame.errorReportedAt == initialPos || !result && frame.errorReportedAt >= frame.position;
      if (errorReported) {
        if (eatMoreFlag) {
          builder.advanceLexer();
          parseAsTree(state, builder, frame.level + 1, DUMMY_BLOCK, true, TOKEN_ADVANCER, eatMore);
        }
      } else if (eatMoreFlag) {
        errorReported = reportError(builder, state, frame, null, true, true);
        parseAsTree(state, builder, frame.level + 1, DUMMY_BLOCK, true, TOKEN_ADVANCER, eatMore);
      } else if (eatMoreFlagOnce
          || (!result && frame.position != builder.rawTokenIndex())
          || frame.errorReportedAt > initialPos) {
        errorReported = reportError(builder, state, frame, null, true, false);
      }
      if (extensionMarker != null) {
        extensionMarker.done(extensionTokenType);
      }
      state.suppressErrors = false;
      if (errorReported || result) {
        state.clearVariants(true, 0);
        state.clearVariants(false, 0);
        state.lastExpectedVariantPos = -1;
      }
    } else if (!result && pinned && frame.errorReportedAt < 0) {
      // do not report if there are errors beyond current position
      if (lastErrorPos == initialPos) {
        // do not force, inner recoverRoot might have skipped some tokens
        reportError(builder, state, frame, elementType, false, false);
      } else if (lastErrorPos > initialPos) {
        // set error pos here as if it is reported for future reference
        frame.errorReportedAt = lastErrorPos;
      }
    }
    // propagate errorReportedAt up the stack to avoid duplicate reporting
    Frame prevFrame = willFail && eatMore == null ? null : state.frameStack.peekLast();
    if (prevFrame != null && prevFrame.errorReportedAt < frame.errorReportedAt) {
      prevFrame.errorReportedAt = frame.errorReportedAt;
    }
  }

  private static void close_frame_impl_(
      ErrorState state,
      Frame frame,
      PsiBuilder builder,
      PsiBuilder.Marker marker,
      IElementType elementType,
      boolean result,
      boolean pinned) {
    if (elementType != null && marker != null) {
      if ((frame.modifiers & _COLLAPSE_) != 0) {
        PsiBuilderImpl.ProductionMarker last =
            result || pinned
                ? (PsiBuilderImpl.ProductionMarker) builder.getLatestDoneMarker()
                : null;
        if (last != null
            && last.getStartIndex() == frame.position
            && state.typeExtends(last.getTokenType(), elementType)) {
          IElementType resultType = last.getTokenType();
          ((PsiBuilder.Marker) last).drop();
          marker.done(resultType);
          return;
        }
      }
      if (result || pinned) {
        if ((frame.modifiers & _LEFT_INNER_) != 0 && frame.leftMarker != null) {
          marker.done(elementType);
          frame.leftMarker.precede().done(((LighterASTNode) frame.leftMarker).getTokenType());
          frame.leftMarker.drop();
        } else if ((frame.modifiers & _LEFT_) != 0 && frame.leftMarker != null) {
          marker.drop();
          frame.leftMarker.precede().done(elementType);
        } else {
          if (frame.level == 0) builder.eof(); // skip whitespaces
          marker.done(elementType);
        }
      } else {
        close_marker_impl_(frame, marker, null, false);
      }
    } else if (result || pinned) {
      if (marker != null) marker.drop();
      if ((frame.modifiers & _LEFT_INNER_) != 0 && frame.leftMarker != null) {
        frame.leftMarker.precede().done(((LighterASTNode) frame.leftMarker).getTokenType());
        frame.leftMarker.drop();
      }
    } else {
      close_marker_impl_(frame, marker, null, false);
    }
  }

  private static void close_marker_impl_(
      Frame frame, PsiBuilder.Marker marker, IElementType elementType, boolean result) {
    if (marker == null) return;
    if (result) {
      if (elementType != null) {
        marker.done(elementType);
      } else {
        marker.drop();
      }
    } else {
      if (frame != null) {
        int position = ((PsiBuilderImpl.ProductionMarker) marker).getStartIndex();
        if (frame.errorReportedAt > position) {
          frame.errorReportedAt = frame.errorReportedAtPrev;
        }
      }
      marker.rollbackTo();
    }
  }

  public static boolean report_error_(PsiBuilder builder, boolean result) {
    if (!result) report_error_(builder, ErrorState.get(builder), false);
    return result;
  }

  public static void report_error_(PsiBuilder builder, ErrorState state, boolean advance) {
    Frame frame = state.frameStack.isEmpty() ? null : state.frameStack.getLast();
    if (frame == null) {
      LOG.error("unbalanced enter/exit section call: got null");
      return;
    }
    int position = builder.rawTokenIndex();
    if (frame.errorReportedAt < position && getLastVariantPos(state, position + 1) <= position) {
      reportError(builder, state, frame, null, true, advance);
    }
  }

  private static int getLastVariantPos(ErrorState state, int defValue) {
    return state.lastExpectedVariantPos < 0 ? defValue : state.lastExpectedVariantPos;
  }

  private static boolean reportError(
      PsiBuilder builder,
      ErrorState state,
      Frame frame,
      IElementType elementType,
      boolean force,
      boolean advance) {
    String expectedText = state.getExpectedText(builder);
    boolean notEmpty = StringUtil.isNotEmpty(expectedText);
    if (force || notEmpty || advance) {
      String gotText =
          builder.eof()
              ? "unexpected end of file"
              : notEmpty
                  ? "got '" + builder.getTokenText() + "'"
                  : "'" + builder.getTokenText() + "' unexpected";
      String message = expectedText + gotText;
      if (advance) {
        PsiBuilder.Marker mark = builder.mark();
        builder.advanceLexer();
        mark.error(message);
      } else if (!force) {
        PsiBuilder.Marker extensionMarker = null;
        IElementType extensionTokenType = null;
        PsiBuilderImpl.ProductionMarker latestDoneMarker =
            elementType == null
                ? null
                : (PsiBuilderImpl.ProductionMarker) builder.getLatestDoneMarker();
        if (latestDoneMarker != null
            && frame.position >= latestDoneMarker.getStartIndex()
            && frame.position <= latestDoneMarker.getEndIndex()) {
          extensionMarker = ((PsiBuilder.Marker) latestDoneMarker).precede();
          extensionTokenType = latestDoneMarker.getTokenType();
          ((PsiBuilder.Marker) latestDoneMarker).drop();
        }
        builder.error(message);
        if (extensionMarker != null) extensionMarker.done(extensionTokenType);
      } else {
        builder.error(message);
      }
      builder.eof(); // skip whitespaces
      frame.errorReportedAt = builder.rawTokenIndex();
      return true;
    }
    return false;
  }

  public static final Key<CompletionState> COMPLETION_STATE_KEY =
      Key.create("COMPLETION_STATE_KEY");

  public static class CompletionState implements Function<Object, String> {
    public final int offset;
    public final Collection<String> items = ContainerUtil.newTroveSet();

    public CompletionState(int offset_) {
      offset = offset_;
    }

    @Nullable
    public String convertItem(Object o) {
      return o instanceof Object[] ? StringUtil.join((Object[]) o, this, " ") : o.toString();
    }

    @Override
    public String fun(Object o) {
      return o.toString();
    }

    public void addItem(@NotNull PsiBuilder builder, @NotNull String text) {
      items.add(text);
    }

    public boolean prefixMatches(@NotNull String prefix, @NotNull String variant) {
      boolean matches =
          new CamelHumpMatcher(prefix, false).prefixMatches(variant.replace(' ', '_'));
      if (matches && StringUtil.isWhiteSpace(prefix.charAt(prefix.length() - 1))) {
        return StringUtil.startsWithIgnoreCase(variant, prefix);
      }
      return matches;
    }
  }

  public static class Builder extends PsiBuilderAdapter {
    public final ErrorState state;
    public final PsiParser parser;

    public Builder(PsiBuilder builder, ErrorState state_, PsiParser parser_) {
      super(builder);
      state = state_;
      parser = parser_;
    }

    public Lexer getLexer() {
      return ((PsiBuilderImpl) myDelegate).getLexer();
    }
  }

  public static PsiBuilder adapt_builder_(IElementType root, PsiBuilder builder, PsiParser parser) {
    return adapt_builder_(root, builder, parser, null);
  }

  public static PsiBuilder adapt_builder_(
      IElementType root, PsiBuilder builder, PsiParser parser, TokenSet[] extendsSets) {
    ErrorState state = new ErrorState();
    ErrorState.initState(state, builder, root, extendsSets);
    return new Builder(builder, state, parser);
  }

  public static class ErrorState {
    TokenSet[] extendsSets;
    public PairProcessor<IElementType, IElementType> altExtendsChecker;

    int predicateCount;
    boolean predicateSign = true;
    boolean suppressErrors;
    public final LinkedList<Frame> frameStack = new LinkedList<Frame>();
    public CompletionState completionState;

    private boolean caseSensitive;
    public BracePair[] braces;
    public boolean altMode;

    int lastExpectedVariantPos = -1;
    MyList<Variant> variants = new MyList<Variant>(INITIAL_VARIANTS_SIZE);
    MyList<Variant> unexpected = new MyList<Variant>(INITIAL_VARIANTS_SIZE / 10);

    final LimitedPool<Variant> VARIANTS =
        new LimitedPool<Variant>(
            VARIANTS_POOL_SIZE,
            new LimitedPool.ObjectFactory<Variant>() {
              @NotNull
              @Override
              public Variant create() {
                return new Variant();
              }

              @Override
              public void cleanup(@NotNull final Variant o) {}
            });
    final LimitedPool<Frame> FRAMES =
        new LimitedPool<Frame>(
            FRAMES_POOL_SIZE,
            new LimitedPool.ObjectFactory<Frame>() {
              @NotNull
              @Override
              public Frame create() {
                return new Frame();
              }

              @Override
              public void cleanup(@NotNull final Frame o) {}
            });

    public static ErrorState get(PsiBuilder builder) {
      return ((Builder) builder).state;
    }

    public static void initState(
        ErrorState state, PsiBuilder builder, IElementType root, TokenSet[] extendsSets) {
      state.extendsSets = extendsSets;
      PsiFile file = builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY);
      state.completionState = file == null ? null : file.getUserData(COMPLETION_STATE_KEY);
      Language language = file == null ? root.getLanguage() : file.getLanguage();
      state.caseSensitive = language.isCaseSensitive();
      PairedBraceMatcher matcher = LanguageBraceMatching.INSTANCE.forLanguage(language);
      state.braces = matcher == null ? null : matcher.getPairs();
      if (state.braces != null && state.braces.length == 0) state.braces = null;
    }

    public String getExpectedText(PsiBuilder builder) {
      int position = builder.rawTokenIndex();
      StringBuilder sb = new StringBuilder();
      if (addExpected(sb, position, true)) {
        sb.append(" expected, ");
      }
      return sb.toString();
    }

    private boolean addExpected(StringBuilder sb, int position, boolean expected) {
      MyList<Variant> list = expected ? variants : unexpected;
      String[] strings = new String[list.size()];
      long[] hashes = new long[strings.length];
      Arrays.fill(strings, "");
      int count = 0;
      loop:
      for (Variant variant : list) {
        if (position == variant.position) {
          String text = variant.object.toString();
          long hash = StringHash.calc(text);
          for (int i = 0; i < count; i++) {
            if (hashes[i] == hash) continue loop;
          }
          hashes[count] = hash;
          strings[count] = text;
          count++;
        }
      }
      Arrays.sort(strings);
      count = 0;
      for (String s : strings) {
        if (s.length() == 0) continue;
        if (count++ > 0) {
          if (count > MAX_VARIANTS_TO_DISPLAY) {
            sb.append(" and ...");
            break;
          } else {
            sb.append(", ");
          }
        }
        char c = s.charAt(0);
        String displayText = c == '<' || StringUtil.isJavaIdentifierStart(c) ? s : '\'' + s + '\'';
        sb.append(displayText);
      }
      if (count > 1 && count < MAX_VARIANTS_TO_DISPLAY) {
        int idx = sb.lastIndexOf(", ");
        sb.replace(idx, idx + 1, " or");
      }
      return count > 0;
    }

    public int getVariantsSize() {
      return variants.size();
    }

    public void clearVariants(boolean expected, int start) {
      MyList<Variant> list = expected ? variants : unexpected;
      if (start < 0 || start >= list.size()) return;
      for (int i = start, len = list.size(); i < len; i++) {
        VARIANTS.recycle(list.get(i));
      }
      list.setSize(start);
      if (expected) {
        lastExpectedVariantPos = -1;
        for (Variant variant : list) {
          if (lastExpectedVariantPos < variant.position) lastExpectedVariantPos = variant.position;
        }
      }
    }

    boolean typeExtends(IElementType child, IElementType parent) {
      if (child == parent) return true;
      if (extendsSets != null) {
        for (TokenSet set : extendsSets) {
          if (set.contains(child) && set.contains(parent)) return true;
        }
      }
      return altExtendsChecker != null && altExtendsChecker.process(child, parent);
    }
  }

  public static class Frame {
    public int offset;
    public int position;
    public int level;
    public int modifiers;
    public String name;
    public int variantCount;
    public int errorReportedAt;
    public int errorReportedAtPrev;
    public PsiBuilder.Marker leftMarker;

    public Frame() {}

    public Frame init(
        PsiBuilder builder, ErrorState state, int level_, int modifiers_, String name_) {
      offset = builder.getCurrentOffset();
      position = builder.rawTokenIndex();
      level = level_;
      modifiers = modifiers_;
      name = name_;
      variantCount = state.variants.size();
      errorReportedAt = -1;

      Frame prev = state.frameStack.peekLast();
      errorReportedAtPrev = prev == null ? -1 : prev.errorReportedAt;
      leftMarker = null;
      return this;
    }

    @Override
    public String toString() {
      String mod =
          modifiers == _NONE_
              ? "_NONE_, "
              : ((modifiers & _COLLAPSE_) != 0 ? "_CAN_COLLAPSE_, " : "")
                  + ((modifiers & _LEFT_) != 0 ? "_LEFT_, " : "")
                  + ((modifiers & _LEFT_INNER_) != 0 ? "_LEFT_INNER_, " : "")
                  + ((modifiers & _AND_) != 0 ? "_AND_, " : "")
                  + ((modifiers & _NOT_) != 0 ? "_NOT_, " : "");
      return String.format(
          "{%s:%s:%d, %d, %s%s}", offset, position, level, errorReportedAt, mod, name);
    }
  }

  private static class Variant {
    int position;
    Object object;

    public Variant init(int pos, Object o) {
      position = pos;
      object = o;
      return this;
    }

    @Override
    public String toString() {
      return "<" + position + ", " + object + ">";
    }

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

      Variant variant = (Variant) o;

      if (position != variant.position) return false;
      if (!this.object.equals(variant.object)) return false;

      return true;
    }

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

  private static final int MAX_CHILDREN_IN_TREE = 10;

  public static boolean parseAsTree(
      ErrorState state,
      final PsiBuilder builder,
      int level,
      final IElementType chunkType,
      boolean checkBraces,
      final Parser parser,
      final Parser eatMoreCondition) {
    final LinkedList<Pair<PsiBuilder.Marker, PsiBuilder.Marker>> parenList =
        new LinkedList<Pair<PsiBuilder.Marker, PsiBuilder.Marker>>();
    final LinkedList<Pair<PsiBuilder.Marker, Integer>> siblingList =
        new LinkedList<Pair<PsiBuilder.Marker, Integer>>();
    PsiBuilder.Marker marker = null;

    final Runnable checkSiblingsRunnable =
        new Runnable() {
          @Override
          public void run() {
            main:
            while (!siblingList.isEmpty()) {
              final Pair<PsiBuilder.Marker, PsiBuilder.Marker> parenPair = parenList.peek();
              final int rating = siblingList.getFirst().second;
              int count = 0;
              for (Pair<PsiBuilder.Marker, Integer> pair : siblingList) {
                if (pair.second != rating || parenPair != null && pair.first == parenPair.second)
                  break main;
                if (++count >= MAX_CHILDREN_IN_TREE) {
                  PsiBuilder.Marker parentMarker = pair.first.precede();
                  parentMarker.setCustomEdgeTokenBinders(
                      WhitespacesBinders.GREEDY_LEFT_BINDER, null);
                  while (count-- > 0) {
                    siblingList.removeFirst();
                  }
                  parentMarker.done(chunkType);
                  siblingList.addFirst(Pair.create(parentMarker, rating + 1));
                  continue main;
                }
              }
              break;
            }
          }
        };
    boolean checkParens = state.braces != null && checkBraces;
    int totalCount = 0;
    int tokenCount = 0;
    if (checkParens) {
      int tokenIdx = -1;
      while (builder.rawLookup(tokenIdx) == TokenType.WHITE_SPACE) tokenIdx--;
      LighterASTNode doneMarker =
          builder.rawLookup(tokenIdx) == state.braces[0].getLeftBraceType()
              ? builder.getLatestDoneMarker()
              : null;
      if (doneMarker != null
          && doneMarker.getStartOffset() == builder.rawTokenTypeStart(tokenIdx)
          && doneMarker.getTokenType() == TokenType.ERROR_ELEMENT) {
        parenList.add(
            Pair.create(((PsiBuilder.Marker) doneMarker).precede(), (PsiBuilder.Marker) null));
      }
    }
    while (true) {
      final IElementType tokenType = builder.getTokenType();
      if (checkParens
          && (tokenType == state.braces[0].getLeftBraceType()
              || tokenType == state.braces[0].getRightBraceType() && !parenList.isEmpty())) {
        if (marker != null) {
          marker.done(chunkType);
          siblingList.addFirst(Pair.create(marker, 1));
          marker = null;
          tokenCount = 0;
        }
        if (tokenType == state.braces[0].getLeftBraceType()) {
          final Pair<PsiBuilder.Marker, Integer> prev = siblingList.peek();
          parenList.addFirst(Pair.create(builder.mark(), prev == null ? null : prev.first));
        }
        checkSiblingsRunnable.run();
        builder.advanceLexer();
        if (tokenType == state.braces[0].getRightBraceType()) {
          final Pair<PsiBuilder.Marker, PsiBuilder.Marker> pair = parenList.removeFirst();
          pair.first.done(chunkType);
          // drop all markers inside parens
          while (!siblingList.isEmpty() && siblingList.getFirst().first != pair.second) {
            siblingList.removeFirst();
          }
          siblingList.addFirst(Pair.create(pair.first, 1));
          checkSiblingsRunnable.run();
        }
      } else {
        if (marker == null) {
          marker = builder.mark();
          marker.setCustomEdgeTokenBinders(WhitespacesBinders.GREEDY_LEFT_BINDER, null);
        }
        final boolean result =
            (!parenList.isEmpty() || eatMoreCondition.parse(builder, level + 1))
                && parser.parse(builder, level + 1);
        if (result) {
          tokenCount++;
          totalCount++;
        }
        if (!result) {
          break;
        }
      }

      if (tokenCount >= MAX_CHILDREN_IN_TREE && marker != null) {
        marker.done(chunkType);
        siblingList.addFirst(Pair.create(marker, 1));
        checkSiblingsRunnable.run();
        marker = null;
        tokenCount = 0;
      }
    }
    if (marker != null) {
      marker.drop();
    }
    for (Pair<PsiBuilder.Marker, PsiBuilder.Marker> pair : parenList) {
      pair.first.drop();
    }
    return totalCount != 0;
  }

  private static class DummyBlockElementType extends IElementType implements ICompositeElementType {
    DummyBlockElementType() {
      super("DUMMY_BLOCK", Language.ANY);
    }

    @NotNull
    @Override
    public ASTNode createCompositeNode() {
      return new DummyBlock();
    }
  }

  public static class DummyBlock extends CompositePsiElement {
    DummyBlock() {
      super(DUMMY_BLOCK);
    }

    @NotNull
    @Override
    public PsiReference[] getReferences() {
      return PsiReference.EMPTY_ARRAY;
    }

    @NotNull
    @Override
    public Language getLanguage() {
      return getParent().getLanguage();
    }
  }

  private static class MyList<E> extends ArrayList<E> {
    MyList(int initialCapacity) {
      super(initialCapacity);
    }

    protected void setSize(int fromIndex) {
      removeRange(fromIndex, size());
    }

    @Override
    public boolean add(E e) {
      int size = size();
      if (size >= MAX_VARIANTS_SIZE) {
        removeRange(MAX_VARIANTS_SIZE / 4, size - MAX_VARIANTS_SIZE / 4);
      }
      return super.add(e);
    }
  }
}
Пример #9
0
/** @author peter */
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public abstract class UsefulTestCase extends TestCase {
  public static final boolean IS_UNDER_TEAMCITY = System.getenv("TEAMCITY_VERSION") != null;

  public static final String IDEA_MARKER_CLASS =
      "com.intellij.openapi.components.impl.stores.IdeaProjectStoreImpl";
  public static final String TEMP_DIR_MARKER = "unitTest_";

  protected static boolean OVERWRITE_TESTDATA = false;

  private static final String DEFAULT_SETTINGS_EXTERNALIZED;
  private static final Random RNG = new SecureRandom();
  private static final String ORIGINAL_TEMP_DIR = FileUtil.getTempDirectory();

  protected final Disposable myTestRootDisposable =
      new Disposable() {
        @Override
        public void dispose() {}

        @Override
        public String toString() {
          String testName = getTestName(false);
          return UsefulTestCase.this.getClass()
              + (StringUtil.isEmpty(testName) ? "" : ".test" + testName);
        }
      };

  protected static String ourPathToKeep = null;

  private CodeStyleSettings myOldCodeStyleSettings;
  private String myTempDir;

  protected static final Key<String> CREATION_PLACE = Key.create("CREATION_PLACE");

  static {
    // Radar #5755208: Command line Java applications need a way to launch without a Dock icon.
    System.setProperty("apple.awt.UIElement", "true");

    try {
      CodeInsightSettings defaultSettings = new CodeInsightSettings();
      Element oldS = new Element("temp");
      defaultSettings.writeExternal(oldS);
      DEFAULT_SETTINGS_EXTERNALIZED = JDOMUtil.writeElement(oldS, "\n");
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  protected boolean shouldContainTempFiles() {
    return true;
  }

  @Override
  protected void setUp() throws Exception {
    super.setUp();

    if (shouldContainTempFiles()) {
      String testName = getTestName(true);
      if (StringUtil.isEmptyOrSpaces(testName)) testName = "";
      testName = new File(testName).getName(); // in case the test name contains file separators
      myTempDir =
          FileUtil.toSystemDependentName(
              ORIGINAL_TEMP_DIR + "/" + TEMP_DIR_MARKER + testName + "_" + RNG.nextInt(1000));
      FileUtil.resetCanonicalTempPathCache(myTempDir);
    }
    //noinspection AssignmentToStaticFieldFromInstanceMethod
    DocumentImpl.CHECK_DOCUMENT_CONSISTENCY = !isPerformanceTest();
  }

  @Override
  protected void tearDown() throws Exception {
    try {
      Disposer.dispose(myTestRootDisposable);
      cleanupSwingDataStructures();
      cleanupDeleteOnExitHookList();
    } finally {
      if (shouldContainTempFiles()) {
        FileUtil.resetCanonicalTempPathCache(ORIGINAL_TEMP_DIR);
        if (ourPathToKeep != null && FileUtil.isAncestor(myTempDir, ourPathToKeep, false)) {
          File[] files = new File(myTempDir).listFiles();
          if (files != null) {
            for (File file : files) {
              if (!FileUtil.pathsEqual(file.getPath(), ourPathToKeep)) {
                FileUtil.delete(file);
              }
            }
          }
        } else {
          FileUtil.delete(new File(myTempDir));
        }
      }
    }

    UIUtil.removeLeakingAppleListeners();
    super.tearDown();
  }

  private static final Set<String> DELETE_ON_EXIT_HOOK_DOT_FILES;
  private static final Class DELETE_ON_EXIT_HOOK_CLASS;

  static {
    Class<?> aClass = null;
    Set<String> files = null;
    try {
      aClass = Class.forName("java.io.DeleteOnExitHook");
      files = ReflectionUtil.getField(aClass, null, Set.class, "files");
    } catch (Exception ignored) {
    }
    DELETE_ON_EXIT_HOOK_CLASS = aClass;
    DELETE_ON_EXIT_HOOK_DOT_FILES = files;
  }

  public static void cleanupDeleteOnExitHookList()
      throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    // try to reduce file set retained by java.io.DeleteOnExitHook
    List<String> list;
    synchronized (DELETE_ON_EXIT_HOOK_CLASS) {
      if (DELETE_ON_EXIT_HOOK_DOT_FILES.isEmpty()) return;
      list = new ArrayList<String>(DELETE_ON_EXIT_HOOK_DOT_FILES);
    }
    for (int i = list.size() - 1; i >= 0; i--) {
      String path = list.get(i);
      if (FileSystemUtil.getAttributes(path) == null || new File(path).delete()) {
        synchronized (DELETE_ON_EXIT_HOOK_CLASS) {
          DELETE_ON_EXIT_HOOK_DOT_FILES.remove(path);
        }
      }
    }
  }

  private static void cleanupSwingDataStructures() throws Exception {
    Class<?> aClass = Class.forName("javax.swing.KeyboardManager");

    Method get = aClass.getMethod("getCurrentManager");
    get.setAccessible(true);
    Object manager = get.invoke(null);
    {
      Field mapF = aClass.getDeclaredField("componentKeyStrokeMap");
      mapF.setAccessible(true);
      Object map = mapF.get(manager);
      ((Map) map).clear();
    }
    {
      Field mapF = aClass.getDeclaredField("containerMap");
      mapF.setAccessible(true);
      Object map = mapF.get(manager);
      ((Map) map).clear();
    }
  }

  protected CompositeException checkForSettingsDamage() throws Exception {
    Application app = ApplicationManager.getApplication();
    if (isPerformanceTest() || app == null || app instanceof MockApplication) {
      return new CompositeException();
    }

    CodeStyleSettings oldCodeStyleSettings = myOldCodeStyleSettings;
    myOldCodeStyleSettings = null;

    return doCheckForSettingsDamage(oldCodeStyleSettings, getCurrentCodeStyleSettings());
  }

  public static CompositeException doCheckForSettingsDamage(
      @NotNull CodeStyleSettings oldCodeStyleSettings,
      @NotNull CodeStyleSettings currentCodeStyleSettings)
      throws Exception {
    CompositeException result = new CompositeException();
    final CodeInsightSettings settings = CodeInsightSettings.getInstance();
    try {
      Element newS = new Element("temp");
      settings.writeExternal(newS);
      Assert.assertEquals(
          "Code insight settings damaged",
          DEFAULT_SETTINGS_EXTERNALIZED,
          JDOMUtil.writeElement(newS, "\n"));
    } catch (AssertionError error) {
      CodeInsightSettings clean = new CodeInsightSettings();
      Element temp = new Element("temp");
      clean.writeExternal(temp);
      settings.loadState(temp);
      result.add(error);
    }

    currentCodeStyleSettings.getIndentOptions(StdFileTypes.JAVA);
    try {
      checkSettingsEqual(
          oldCodeStyleSettings, currentCodeStyleSettings, "Code style settings damaged");
    } catch (AssertionError e) {
      result.add(e);
    } finally {
      currentCodeStyleSettings.clearCodeStyleSettings();
    }

    try {
      InplaceRefactoring.checkCleared();
    } catch (AssertionError e) {
      result.add(e);
    }
    try {
      StartMarkAction.checkCleared();
    } catch (AssertionError e) {
      result.add(e);
    }

    return result;
  }

  protected void storeSettings() {
    if (!isPerformanceTest() && ApplicationManager.getApplication() != null) {
      myOldCodeStyleSettings = getCurrentCodeStyleSettings().clone();
      myOldCodeStyleSettings.getIndentOptions(StdFileTypes.JAVA);
    }
  }

  protected CodeStyleSettings getCurrentCodeStyleSettings() {
    if (CodeStyleSchemes.getInstance().getCurrentScheme() == null) return new CodeStyleSettings();
    return CodeStyleSettingsManager.getInstance().getCurrentSettings();
  }

  public Disposable getTestRootDisposable() {
    return myTestRootDisposable;
  }

  @Override
  protected void runTest() throws Throwable {
    final Throwable[] throwables = new Throwable[1];

    Runnable runnable =
        new Runnable() {
          @Override
          public void run() {
            try {
              UsefulTestCase.super.runTest();
            } catch (InvocationTargetException e) {
              e.fillInStackTrace();
              throwables[0] = e.getTargetException();
            } catch (IllegalAccessException e) {
              e.fillInStackTrace();
              throwables[0] = e;
            } catch (Throwable e) {
              throwables[0] = e;
            }
          }
        };

    invokeTestRunnable(runnable);

    if (throwables[0] != null) {
      throw throwables[0];
    }
  }

  protected boolean shouldRunTest() {
    return PlatformTestUtil.canRunTest(getClass());
  }

  public static void edt(Runnable r) {
    UIUtil.invokeAndWaitIfNeeded(r);
  }

  protected void invokeTestRunnable(@NotNull Runnable runnable) throws Exception {
    UIUtil.invokeAndWaitIfNeeded(runnable);
    // runnable.run();
  }

  protected void defaultRunBare() throws Throwable {
    super.runBare();
  }

  @Override
  public void runBare() throws Throwable {
    if (!shouldRunTest()) return;

    if (runInDispatchThread()) {
      final Throwable[] exception = {null};
      UIUtil.invokeAndWaitIfNeeded(
          new Runnable() {
            @Override
            public void run() {
              try {
                defaultRunBare();
              } catch (Throwable tearingDown) {
                if (exception[0] == null) exception[0] = tearingDown;
              }
            }
          });
      if (exception[0] != null) throw exception[0];
    } else {
      defaultRunBare();
    }
  }

  protected boolean runInDispatchThread() {
    return true;
  }

  @NonNls
  public static String toString(Iterable<?> collection) {
    if (!collection.iterator().hasNext()) {
      return "<empty>";
    }

    final StringBuilder builder = new StringBuilder();
    for (final Object o : collection) {
      if (o instanceof THashSet) {
        builder.append(new TreeSet<Object>((THashSet) o));
      } else {
        builder.append(o);
      }
      builder.append("\n");
    }
    return builder.toString();
  }

  public static <T> void assertOrderedEquals(T[] actual, T... expected) {
    assertOrderedEquals(Arrays.asList(actual), expected);
  }

  public static <T> void assertOrderedEquals(Iterable<T> actual, T... expected) {
    assertOrderedEquals(null, actual, expected);
  }

  public static void assertOrderedEquals(@NotNull byte[] actual, @NotNull byte[] expected) {
    assertEquals(actual.length, expected.length);
    for (int i = 0; i < actual.length; i++) {
      byte a = actual[i];
      byte e = expected[i];
      assertEquals("not equals at index: " + i, e, a);
    }
  }

  public static <T> void assertOrderedEquals(
      final String errorMsg, @NotNull Iterable<T> actual, @NotNull T... expected) {
    Assert.assertNotNull(actual);
    Assert.assertNotNull(expected);
    assertOrderedEquals(errorMsg, actual, Arrays.asList(expected));
  }

  public static <T> void assertOrderedEquals(
      final Iterable<? extends T> actual, final Collection<? extends T> expected) {
    assertOrderedEquals(null, actual, expected);
  }

  public static <T> void assertOrderedEquals(
      final String erroMsg,
      final Iterable<? extends T> actual,
      final Collection<? extends T> expected) {
    ArrayList<T> list = new ArrayList<T>();
    for (T t : actual) {
      list.add(t);
    }
    if (!list.equals(new ArrayList<T>(expected))) {
      String expectedString = toString(expected);
      String actualString = toString(actual);
      Assert.assertEquals(erroMsg, expectedString, actualString);
      Assert.fail(
          "Warning! 'toString' does not reflect the difference.\nExpected: "
              + expectedString
              + "\nActual: "
              + actualString);
    }
  }

  public static <T> void assertOrderedCollection(T[] collection, @NotNull Consumer<T>... checkers) {
    Assert.assertNotNull(collection);
    assertOrderedCollection(Arrays.asList(collection), checkers);
  }

  public static <T> void assertSameElements(T[] collection, T... expected) {
    assertSameElements(Arrays.asList(collection), expected);
  }

  public static <T> void assertSameElements(Collection<? extends T> collection, T... expected) {
    assertSameElements(collection, Arrays.asList(expected));
  }

  public static <T> void assertSameElements(
      Collection<? extends T> collection, Collection<T> expected) {
    assertSameElements(null, collection, expected);
  }

  public static <T> void assertSameElements(
      String message, Collection<? extends T> collection, Collection<T> expected) {
    assertNotNull(collection);
    assertNotNull(expected);
    if (collection.size() != expected.size()
        || !new HashSet<T>(expected).equals(new HashSet<T>(collection))) {
      Assert.assertEquals(message, toString(expected, "\n"), toString(collection, "\n"));
      Assert.assertEquals(message, new HashSet<T>(expected), new HashSet<T>(collection));
    }
  }

  public <T> void assertContainsOrdered(Collection<? extends T> collection, T... expected) {
    assertContainsOrdered(collection, Arrays.asList(expected));
  }

  public <T> void assertContainsOrdered(
      Collection<? extends T> collection, Collection<T> expected) {
    ArrayList<T> copy = new ArrayList<T>(collection);
    copy.retainAll(expected);
    assertOrderedEquals(toString(collection), copy, expected);
  }

  public <T> void assertContainsElements(Collection<? extends T> collection, T... expected) {
    assertContainsElements(collection, Arrays.asList(expected));
  }

  public <T> void assertContainsElements(
      Collection<? extends T> collection, Collection<T> expected) {
    ArrayList<T> copy = new ArrayList<T>(collection);
    copy.retainAll(expected);
    assertSameElements(toString(collection), copy, expected);
  }

  public static String toString(Object[] collection, String separator) {
    return toString(Arrays.asList(collection), separator);
  }

  public <T> void assertDoesntContain(Collection<? extends T> collection, T... notExpected) {
    assertDoesntContain(collection, Arrays.asList(notExpected));
  }

  public <T> void assertDoesntContain(
      Collection<? extends T> collection, Collection<T> notExpected) {
    ArrayList<T> expected = new ArrayList<T>(collection);
    expected.removeAll(notExpected);
    assertSameElements(collection, expected);
  }

  public static String toString(Collection<?> collection, String separator) {
    List<String> list =
        ContainerUtil.map2List(
            collection,
            new Function<Object, String>() {
              @Override
              public String fun(final Object o) {
                return String.valueOf(o);
              }
            });
    Collections.sort(list);
    StringBuilder builder = new StringBuilder();
    boolean flag = false;
    for (final String o : list) {
      if (flag) {
        builder.append(separator);
      }
      builder.append(o);
      flag = true;
    }
    return builder.toString();
  }

  public static <T> void assertOrderedCollection(
      Collection<? extends T> collection, Consumer<T>... checkers) {
    Assert.assertNotNull(collection);
    if (collection.size() != checkers.length) {
      Assert.fail(toString(collection));
    }
    int i = 0;
    for (final T actual : collection) {
      try {
        checkers[i].consume(actual);
      } catch (AssertionFailedError e) {
        System.out.println(i + ": " + actual);
        throw e;
      }
      i++;
    }
  }

  public static <T> void assertUnorderedCollection(T[] collection, Consumer<T>... checkers) {
    assertUnorderedCollection(Arrays.asList(collection), checkers);
  }

  public static <T> void assertUnorderedCollection(
      Collection<? extends T> collection, Consumer<T>... checkers) {
    Assert.assertNotNull(collection);
    if (collection.size() != checkers.length) {
      Assert.fail(toString(collection));
    }
    Set<Consumer<T>> checkerSet = new HashSet<Consumer<T>>(Arrays.asList(checkers));
    int i = 0;
    Throwable lastError = null;
    for (final T actual : collection) {
      boolean flag = true;
      for (final Consumer<T> condition : checkerSet) {
        Throwable error = accepts(condition, actual);
        if (error == null) {
          checkerSet.remove(condition);
          flag = false;
          break;
        } else {
          lastError = error;
        }
      }
      if (flag) {
        lastError.printStackTrace();
        Assert.fail("Incorrect element(" + i + "): " + actual);
      }
      i++;
    }
  }

  private static <T> Throwable accepts(final Consumer<T> condition, final T actual) {
    try {
      condition.consume(actual);
      return null;
    } catch (Throwable e) {
      return e;
    }
  }

  public static <T> T assertInstanceOf(Object o, Class<T> aClass) {
    Assert.assertNotNull("Expected instance of: " + aClass.getName() + " actual: " + null, o);
    Assert.assertTrue(
        "Expected instance of: " + aClass.getName() + " actual: " + o.getClass().getName(),
        aClass.isInstance(o));
    @SuppressWarnings("unchecked")
    T t = (T) o;
    return t;
  }

  public static <T> T assertOneElement(Collection<T> collection) {
    Assert.assertNotNull(collection);
    Assert.assertEquals(toString(collection), 1, collection.size());
    return collection.iterator().next();
  }

  public static <T> T assertOneElement(T[] ts) {
    Assert.assertNotNull(ts);
    Assert.assertEquals(Arrays.asList(ts).toString(), 1, ts.length);
    return ts[0];
  }

  public static <T> void assertOneOf(T value, T... values) {
    boolean found = false;
    for (T v : values) {
      if (value == v || value != null && value.equals(v)) {
        found = true;
      }
    }
    Assert.assertTrue(value + " should be equal to one of " + Arrays.toString(values), found);
  }

  public static void printThreadDump() {
    PerformanceWatcher.dumpThreadsToConsole("Thread dump:");
  }

  public static void assertEmpty(final Object[] array) {
    assertOrderedEquals(array);
  }

  public static void assertNotEmpty(final Collection<?> collection) {
    if (collection == null) return;
    assertTrue(!collection.isEmpty());
  }

  public static void assertEmpty(final Collection<?> collection) {
    assertEmpty(collection.toString(), collection);
  }

  public static void assertNullOrEmpty(final Collection<?> collection) {
    if (collection == null) return;
    assertEmpty(null, collection);
  }

  public static void assertEmpty(final String s) {
    assertTrue(s, StringUtil.isEmpty(s));
  }

  public static <T> void assertEmpty(final String errorMsg, final Collection<T> collection) {
    assertOrderedEquals(errorMsg, collection);
  }

  public static void assertSize(int expectedSize, final Object[] array) {
    assertEquals(toString(Arrays.asList(array)), expectedSize, array.length);
  }

  public static void assertSize(int expectedSize, final Collection<?> c) {
    assertEquals(toString(c), expectedSize, c.size());
  }

  protected <T extends Disposable> T disposeOnTearDown(final T disposable) {
    Disposer.register(myTestRootDisposable, disposable);
    return disposable;
  }

  public static void assertSameLines(String expected, String actual) {
    String expectedText = StringUtil.convertLineSeparators(expected.trim());
    String actualText = StringUtil.convertLineSeparators(actual.trim());
    Assert.assertEquals(expectedText, actualText);
  }

  public static void assertExists(File file) {
    assertTrue("File should exist " + file, file.exists());
  }

  public static void assertDoesntExist(File file) {
    assertFalse("File should not exist " + file, file.exists());
  }

  protected String getTestName(boolean lowercaseFirstLetter) {
    String name = getName();
    return getTestName(name, lowercaseFirstLetter);
  }

  public static String getTestName(String name, boolean lowercaseFirstLetter) {
    if (name == null) {
      return "";
    }
    name = StringUtil.trimStart(name, "test");
    if (StringUtil.isEmpty(name)) {
      return "";
    }
    return lowercaseFirstLetter(name, lowercaseFirstLetter);
  }

  public static String lowercaseFirstLetter(String name, boolean lowercaseFirstLetter) {
    if (lowercaseFirstLetter && !isAllUppercaseName(name)) {
      name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
    }
    return name;
  }

  public static boolean isAllUppercaseName(String name) {
    int uppercaseChars = 0;
    for (int i = 0; i < name.length(); i++) {
      if (Character.isLowerCase(name.charAt(i))) {
        return false;
      }
      if (Character.isUpperCase(name.charAt(i))) {
        uppercaseChars++;
      }
    }
    return uppercaseChars >= 3;
  }

  protected String getTestDirectoryName() {
    final String testName = getTestName(true);
    return testName.replaceAll("_.*", "");
  }

  protected static void assertSameLinesWithFile(String filePath, String actualText) {
    String fileText;
    try {
      if (OVERWRITE_TESTDATA) {
        VfsTestUtil.overwriteTestData(filePath, actualText);
        System.out.println("File " + filePath + " created.");
      }
      fileText = FileUtil.loadFile(new File(filePath), CharsetToolkit.UTF8);
    } catch (FileNotFoundException e) {
      VfsTestUtil.overwriteTestData(filePath, actualText);
      throw new AssertionFailedError("No output text found. File " + filePath + " created.");
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    String expected = StringUtil.convertLineSeparators(fileText.trim());
    String actual = StringUtil.convertLineSeparators(actualText.trim());
    if (!Comparing.equal(expected, actual)) {
      throw new FileComparisonFailure(null, expected, actual, filePath);
    }
  }

  public static void clearFields(final Object test) throws IllegalAccessException {
    Class aClass = test.getClass();
    while (aClass != null) {
      clearDeclaredFields(test, aClass);
      aClass = aClass.getSuperclass();
    }
  }

  public static void clearDeclaredFields(Object test, Class aClass) throws IllegalAccessException {
    if (aClass == null) return;
    for (final Field field : aClass.getDeclaredFields()) {
      @NonNls final String name = field.getDeclaringClass().getName();
      if (!name.startsWith("junit.framework.") && !name.startsWith("com.intellij.testFramework.")) {
        final int modifiers = field.getModifiers();
        if ((modifiers & Modifier.FINAL) == 0
            && (modifiers & Modifier.STATIC) == 0
            && !field.getType().isPrimitive()) {
          field.setAccessible(true);
          field.set(test, null);
        }
      }
    }
  }

  @SuppressWarnings("deprecation")
  protected static void checkSettingsEqual(
      JDOMExternalizable expected, JDOMExternalizable settings, String message) throws Exception {
    if (expected == null || settings == null) return;

    Element oldS = new Element("temp");
    expected.writeExternal(oldS);
    Element newS = new Element("temp");
    settings.writeExternal(newS);

    String newString = JDOMUtil.writeElement(newS, "\n");
    String oldString = JDOMUtil.writeElement(oldS, "\n");
    Assert.assertEquals(message, oldString, newString);
  }

  public boolean isPerformanceTest() {
    String name = getName();
    return name != null && name.contains("Performance")
        || getClass().getName().contains("Performance");
  }

  public static void doPostponedFormatting(final Project project) {
    DocumentUtil.writeInRunUndoTransparentAction(
        new Runnable() {
          @Override
          public void run() {
            PsiDocumentManager.getInstance(project).commitAllDocuments();
            PostprocessReformattingAspect.getInstance(project).doPostponedFormatting();
          }
        });
  }

  protected static void checkAllTimersAreDisposed() {
    try {
      Class<?> aClass = Class.forName("javax.swing.TimerQueue");

      Method inst = aClass.getDeclaredMethod("sharedInstance");
      inst.setAccessible(true);
      Object queue = inst.invoke(null);
      Field field = aClass.getDeclaredField("firstTimer");
      field.setAccessible(true);
      Object firstTimer = field.get(queue);
      if (firstTimer != null) {
        try {
          fail("Not disposed Timer: " + firstTimer.toString() + "; queue:" + queue);
        } finally {
          field.set(queue, null);
        }
      }
    } catch (Throwable e) {
      // Ignore
    }
  }

  /**
   * Checks that code block throw corresponding exception.
   *
   * @param exceptionCase Block annotated with some exception type
   * @throws Throwable
   */
  protected void assertException(final AbstractExceptionCase exceptionCase) throws Throwable {
    assertException(exceptionCase, null);
  }

  /**
   * Checks that code block throw corresponding exception with expected error msg. If expected error
   * message is null it will not be checked.
   *
   * @param exceptionCase Block annotated with some exception type
   * @param expectedErrorMsg expected error messge
   * @throws Throwable
   */
  protected void assertException(
      final AbstractExceptionCase exceptionCase, @Nullable final String expectedErrorMsg)
      throws Throwable {
    assertExceptionOccurred(true, exceptionCase, expectedErrorMsg);
  }

  /**
   * Checks that code block doesn't throw corresponding exception.
   *
   * @param exceptionCase Block annotated with some exception type
   * @throws Throwable
   */
  protected void assertNoException(final AbstractExceptionCase exceptionCase) throws Throwable {
    assertExceptionOccurred(false, exceptionCase, null);
  }

  protected void assertNoThrowable(final Runnable closure) {
    String throwableName = null;
    try {
      closure.run();
    } catch (Throwable thr) {
      throwableName = thr.getClass().getName();
    }
    assertNull(throwableName);
  }

  private static void assertExceptionOccurred(
      boolean shouldOccur, AbstractExceptionCase exceptionCase, String expectedErrorMsg)
      throws Throwable {
    boolean wasThrown = false;
    try {
      exceptionCase.tryClosure();
    } catch (Throwable e) {
      if (shouldOccur) {
        wasThrown = true;
        final String errorMessage = exceptionCase.getAssertionErrorMessage();
        assertEquals(errorMessage, exceptionCase.getExpectedExceptionClass(), e.getClass());
        if (expectedErrorMsg != null) {
          assertEquals("Compare error messages", expectedErrorMsg, e.getMessage());
        }
      } else if (exceptionCase.getExpectedExceptionClass().equals(e.getClass())) {
        wasThrown = true;

        System.out.println("");
        e.printStackTrace(System.out);

        fail("Exception isn't expected here. Exception message: " + e.getMessage());
      } else {
        throw e;
      }
    } finally {
      if (shouldOccur && !wasThrown) {
        fail(exceptionCase.getAssertionErrorMessage());
      }
    }
  }

  protected boolean annotatedWith(@NotNull Class annotationClass) {
    Class<?> aClass = getClass();
    String methodName = "test" + getTestName(false);
    boolean methodChecked = false;
    while (aClass != null && aClass != Object.class) {
      if (aClass.getAnnotation(annotationClass) != null) return true;
      if (!methodChecked) {
        try {
          Method method = aClass.getDeclaredMethod(methodName);
          if (method.getAnnotation(annotationClass) != null) return true;
          methodChecked = true;
        } catch (NoSuchMethodException ignored) {
        }
      }
      aClass = aClass.getSuperclass();
    }
    return false;
  }

  protected String getHomePath() {
    return PathManager.getHomePath().replace(File.separatorChar, '/');
  }

  protected static boolean isInHeadlessEnvironment() {
    return GraphicsEnvironment.isHeadless();
  }

  public static void refreshRecursively(@NotNull VirtualFile file) {
    VfsUtilCore.visitChildrenRecursively(
        file,
        new VirtualFileVisitor() {
          @Override
          public boolean visitFile(@NotNull VirtualFile file) {
            file.getChildren();
            return true;
          }
        });
    file.refresh(false, true);
  }

  @NotNull
  public static Test filteredSuite(@RegExp String regexp, @NotNull Test test) {
    final Pattern pattern = Pattern.compile(regexp);
    final TestSuite testSuite = new TestSuite();
    new Processor<Test>() {

      @Override
      public boolean process(Test test) {
        if (test instanceof TestSuite) {
          for (int i = 0, len = ((TestSuite) test).testCount(); i < len; i++) {
            process(((TestSuite) test).testAt(i));
          }
        } else if (pattern.matcher(test.toString()).find()) {
          testSuite.addTest(test);
        }
        return false;
      }
    }.process(test);
    return testSuite;
  }
}
public abstract class PsiDocumentManagerBase extends PsiDocumentManager
    implements ProjectComponent, DocumentListener {
  protected static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl");
  protected static final Key<PsiFile> HARD_REF_TO_PSI = new Key<PsiFile>("HARD_REFERENCE_TO_PSI");
  protected static final Key<List<Runnable>> ACTION_AFTER_COMMIT =
      Key.create("ACTION_AFTER_COMMIT");

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

  protected volatile boolean myIsCommitInProgress;
  protected final PsiToDocumentSynchronizer mySynchronizer;

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

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

  @Override
  public void projectOpened() {}

  @Override
  public void projectClosed() {}

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

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {}

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

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

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

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

    fireFileCreated(document, psiFile);

    return psiFile;
  }

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

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

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

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

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

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

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

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

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

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

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

    return document;
  }

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

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

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

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

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

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

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

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

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

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

    if (ok[0]) {
      // otherwise changes maybe not synced to the document yet, and injectors will crash
      if (!mySynchronizer.isDocumentAffectedByTransactions(document)) {
        final InjectedLanguageManager injectedLanguageManager =
            InjectedLanguageManager.getInstance(myProject);
        if (injectedLanguageManager != null)
          injectedLanguageManager.startRunInjectors(document, synchronously);
      }
      // run after commit actions outside write action
      runAfterCommitActions(document);
      if (DebugUtil.DO_EXPENSIVE_CHECKS) {
        checkAllElementsValid(document, reason);
      }
    }
    return ok[0];
  }

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

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

    return success;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (commitNecessary) {
      myUncommittedDocuments.add(document);
      if (forceCommit) {
        commitDocument(document);
      } else if (!((DocumentEx) document).isInBulkUpdate()) {
        myDocumentCommitProcessor.commitAsynchronously(myProject, document, event);
      }
    }
  }

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

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

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

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

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

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

  public PsiToDocumentSynchronizer getSynchronizer() {
    return mySynchronizer;
  }
}
Пример #11
0
public class MavenDomUtil {

  private static final Key<Pair<Long, Set<VirtualFile>>> FILTERED_RESOURCES_ROOTS_KEY =
      Key.create("MavenDomUtil.FILTERED_RESOURCES_ROOTS");

  // see http://maven.apache.org/settings.html
  private static final Set<String> SUBTAGS_IN_SETTINGS_FILE =
      ContainerUtil.newHashSet(
          "localRepository",
          "interactiveMode",
          "usePluginRegistry",
          "offline",
          "pluginGroups",
          "servers",
          "mirrors",
          "proxies",
          "profiles",
          "activeProfiles");

  public static boolean isMavenFile(PsiFile file) {
    return isProjectFile(file) || isProfilesFile(file) || isSettingsFile(file);
  }

  public static boolean isProjectFile(PsiFile file) {
    if (!(file instanceof XmlFile)) return false;

    String name = file.getName();
    return name.equals(MavenConstants.POM_XML)
        || name.endsWith(".pom")
        || name.equals(MavenConstants.SUPER_POM_XML);
  }

  public static boolean isProfilesFile(PsiFile file) {
    if (!(file instanceof XmlFile)) return false;

    return MavenConstants.PROFILES_XML.equals(file.getName());
  }

  public static boolean isSettingsFile(PsiFile file) {
    if (!(file instanceof XmlFile)) return false;

    XmlTag rootTag = ((XmlFile) file).getRootTag();
    if (rootTag == null || !"settings".equals(rootTag.getName())) return false;

    String xmlns = rootTag.getAttributeValue("xmlns");
    if (xmlns != null) {
      return xmlns.contains("maven");
    }

    boolean hasTag = false;

    for (PsiElement e = rootTag.getFirstChild(); e != null; e = e.getNextSibling()) {
      if (e instanceof XmlTag) {
        if (SUBTAGS_IN_SETTINGS_FILE.contains(((XmlTag) e).getName())) return true;
        hasTag = true;
      }
    }

    return !hasTag;
  }

  public static boolean isMavenFile(PsiElement element) {
    return isMavenFile(element.getContainingFile());
  }

  @Nullable
  public static Module findContainingMavenizedModule(@NotNull PsiFile psiFile) {
    VirtualFile file = psiFile.getVirtualFile();
    if (file == null) return null;

    Project project = psiFile.getProject();

    MavenProjectsManager manager = MavenProjectsManager.getInstance(project);
    if (!manager.isMavenizedProject()) return null;

    ProjectFileIndex index = ProjectRootManager.getInstance(project).getFileIndex();

    Module module = index.getModuleForFile(file);
    if (module == null || !manager.isMavenizedModule(module)) return null;
    return module;
  }

  public static boolean isMavenProperty(PsiElement target) {
    XmlTag tag = PsiTreeUtil.getParentOfType(target, XmlTag.class, false);
    if (tag == null) return false;
    return DomUtil.findDomElement(tag, MavenDomProperties.class) != null;
  }

  public static String calcRelativePath(VirtualFile parent, VirtualFile child) {
    String result = FileUtil.getRelativePath(parent.getPath(), child.getPath(), '/');
    if (result == null) {
      MavenLog.LOG.warn(
          "cannot calculate relative path for\nparent: " + parent + "\nchild: " + child);
      result = child.getPath();
    }
    return FileUtil.toSystemIndependentName(result);
  }

  public static MavenDomParent updateMavenParent(
      MavenDomProjectModel mavenModel, MavenProject parentProject) {
    MavenDomParent result = mavenModel.getMavenParent();

    VirtualFile pomFile = DomUtil.getFile(mavenModel).getVirtualFile();
    Project project = mavenModel.getXmlElement().getProject();

    MavenId parentId = parentProject.getMavenId();
    result.getGroupId().setStringValue(parentId.getGroupId());
    result.getArtifactId().setStringValue(parentId.getArtifactId());
    result.getVersion().setStringValue(parentId.getVersion());

    if (!Comparing.equal(pomFile.getParent().getParent(), parentProject.getDirectoryFile())) {
      result
          .getRelativePath()
          .setValue(PsiManager.getInstance(project).findFile(parentProject.getFile()));
    }

    return result;
  }

  public static <T> T getImmediateParent(ConvertContext context, Class<T> clazz) {
    DomElement parentElement = context.getInvocationElement().getParent();
    return clazz.isInstance(parentElement) ? (T) parentElement : null;
  }

  @Nullable
  public static VirtualFile getVirtualFile(@NotNull DomElement element) {
    PsiFile psiFile = DomUtil.getFile(element);
    return getVirtualFile(psiFile);
  }

  @Nullable
  public static VirtualFile getVirtualFile(@NotNull PsiElement element) {
    PsiFile psiFile = element.getContainingFile();
    return getVirtualFile(psiFile);
  }

  @Nullable
  private static VirtualFile getVirtualFile(PsiFile psiFile) {
    if (psiFile == null) return null;
    psiFile = psiFile.getOriginalFile();
    return psiFile.getVirtualFile();
  }

  @Nullable
  public static MavenProject findProject(@NotNull MavenDomProjectModel projectDom) {
    XmlElement element = projectDom.getXmlElement();
    if (element == null) return null;

    VirtualFile file = getVirtualFile(element);
    if (file == null) return null;
    MavenProjectsManager manager = MavenProjectsManager.getInstance(element.getProject());
    return manager.findProject(file);
  }

  @Nullable
  public static MavenProject findContainingProject(@NotNull DomElement element) {
    PsiElement psi = element.getXmlElement();
    return psi == null ? null : findContainingProject(psi);
  }

  @Nullable
  public static MavenProject findContainingProject(@NotNull PsiElement element) {
    VirtualFile file = getVirtualFile(element);
    if (file == null) return null;
    MavenProjectsManager manager = MavenProjectsManager.getInstance(element.getProject());
    return manager.findContainingProject(file);
  }

  @Nullable
  public static MavenDomProjectModel getMavenDomProjectModel(
      @NotNull Project project, @NotNull VirtualFile file) {
    return getMavenDomModel(project, file, MavenDomProjectModel.class);
  }

  @Nullable
  public static MavenDomProfiles getMavenDomProfilesModel(
      @NotNull Project project, @NotNull VirtualFile file) {
    MavenDomProfilesModel model = getMavenDomModel(project, file, MavenDomProfilesModel.class);
    if (model != null) return model.getProfiles();
    return getMavenDomModel(project, file, MavenDomProfiles.class); // try old-style model
  }

  @Nullable
  public static <T extends MavenDomElement> T getMavenDomModel(
      @NotNull Project project, @NotNull VirtualFile file, @NotNull Class<T> clazz) {
    if (!file.isValid()) return null;
    PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
    if (psiFile == null) return null;
    return getMavenDomModel(psiFile, clazz);
  }

  @Nullable
  public static <T extends MavenDomElement> T getMavenDomModel(
      @NotNull PsiFile file, @NotNull Class<T> clazz) {
    DomFileElement<T> fileElement = getMavenDomFile(file, clazz);
    return fileElement == null ? null : fileElement.getRootElement();
  }

  @Nullable
  private static <T extends MavenDomElement> DomFileElement<T> getMavenDomFile(
      @NotNull PsiFile file, @NotNull Class<T> clazz) {
    if (!(file instanceof XmlFile)) return null;
    return DomManager.getDomManager(file.getProject()).getFileElement((XmlFile) file, clazz);
  }

  @Nullable
  public static XmlTag findTag(@NotNull DomElement domElement, @NotNull String path) {
    List<String> elements = StringUtil.split(path, ".");
    if (elements.isEmpty()) return null;

    Pair<String, Integer> nameAndIndex = translateTagName(elements.get(0));
    String name = nameAndIndex.first;
    Integer index = nameAndIndex.second;

    XmlTag result = domElement.getXmlTag();
    if (result == null || !name.equals(result.getName())) return null;
    result = getIndexedTag(result, index);

    for (String each : elements.subList(1, elements.size())) {
      nameAndIndex = translateTagName(each);
      name = nameAndIndex.first;
      index = nameAndIndex.second;

      result = result.findFirstSubTag(name);
      if (result == null) return null;
      result = getIndexedTag(result, index);
    }
    return result;
  }

  private static final Pattern XML_TAG_NAME_PATTERN = Pattern.compile("(\\S*)\\[(\\d*)\\]\\z");

  private static Pair<String, Integer> translateTagName(String text) {
    String tagName = text.trim();
    Integer index = null;

    Matcher matcher = XML_TAG_NAME_PATTERN.matcher(tagName);
    if (matcher.find()) {
      tagName = matcher.group(1);
      try {
        index = Integer.parseInt(matcher.group(2));
      } catch (NumberFormatException e) {
        return null;
      }
    }

    return Pair.create(tagName, index);
  }

  private static XmlTag getIndexedTag(XmlTag parent, Integer index) {
    if (index == null) return parent;

    XmlTag[] children = parent.getSubTags();
    if (index < 0 || index >= children.length) return null;
    return children[index];
  }

  @Nullable
  public static PropertiesFile getPropertiesFile(
      @NotNull Project project, @NotNull VirtualFile file) {
    PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
    if (!(psiFile instanceof PropertiesFile)) return null;
    return (PropertiesFile) psiFile;
  }

  @Nullable
  public static IProperty findProperty(
      @NotNull Project project, @NotNull VirtualFile file, @NotNull String propName) {
    PropertiesFile propertiesFile = getPropertiesFile(project, file);
    return propertiesFile == null ? null : propertiesFile.findPropertyByKey(propName);
  }

  @Nullable
  public static PsiElement findPropertyValue(
      @NotNull Project project, @NotNull VirtualFile file, @NotNull String propName) {
    IProperty prop = findProperty(project, file, propName);
    return prop == null
        ? null
        : prop.getPsiElement().getFirstChild().getNextSibling().getNextSibling();
  }

  private static Set<VirtualFile> getFilteredResourcesRoots(@NotNull MavenProject mavenProject) {
    Pair<Long, Set<VirtualFile>> cachedValue =
        mavenProject.getCachedValue(FILTERED_RESOURCES_ROOTS_KEY);

    if (cachedValue == null
        || cachedValue.first != VirtualFileManager.getInstance().getModificationCount()) {
      Set<VirtualFile> set = null;

      for (MavenResource resource :
          ContainerUtil.concat(mavenProject.getResources(), mavenProject.getTestResources())) {
        if (!resource.isFiltered()) continue;

        VirtualFile resourceDir =
            LocalFileSystem.getInstance().findFileByPath(resource.getDirectory());
        if (resourceDir == null) continue;

        if (set == null) {
          set = new HashSet<VirtualFile>();
        }

        set.add(resourceDir);
      }

      if (set == null) {
        set = Collections.emptySet();
      }

      cachedValue = Pair.create(VirtualFileManager.getInstance().getModificationCount(), set);
      mavenProject.putCachedValue(FILTERED_RESOURCES_ROOTS_KEY, cachedValue);
    }

    return cachedValue.second;
  }

  public static boolean isFilteredResourceFile(PsiElement element) {
    PsiFile psiFile = element.getContainingFile();
    VirtualFile file = getVirtualFile(psiFile);
    if (file == null) return false;

    MavenProjectsManager manager = MavenProjectsManager.getInstance(psiFile.getProject());
    MavenProject mavenProject = manager.findContainingProject(file);
    if (mavenProject == null) return false;

    Set<VirtualFile> filteredRoots = getFilteredResourcesRoots(mavenProject);

    if (!filteredRoots.isEmpty()) {
      for (VirtualFile f = file.getParent(); f != null; f = f.getParent()) {
        if (filteredRoots.contains(f)) {
          return true;
        }
      }
    }

    return false;
  }

  public static List<DomFileElement<MavenDomProjectModel>> collectProjectModels(Project p) {
    return DomService.getInstance()
        .getFileElements(MavenDomProjectModel.class, p, GlobalSearchScope.projectScope(p));
  }

  public static MavenId describe(PsiFile psiFile) {
    MavenDomProjectModel model = getMavenDomModel(psiFile, MavenDomProjectModel.class);

    String groupId = model.getGroupId().getStringValue();
    String artifactId = model.getArtifactId().getStringValue();
    String version = model.getVersion().getStringValue();

    if (groupId == null) {
      groupId = model.getMavenParent().getGroupId().getStringValue();
    }

    if (version == null) {
      version = model.getMavenParent().getVersion().getStringValue();
    }

    return new MavenId(groupId, artifactId, version);
  }

  @NotNull
  public static MavenDomDependency createDomDependency(
      MavenDomProjectModel model, @Nullable Editor editor, @NotNull final MavenId id) {
    return createDomDependency(model.getDependencies(), editor, id);
  }

  @NotNull
  public static MavenDomDependency createDomDependency(
      MavenDomDependencies dependencies, @Nullable Editor editor, @NotNull final MavenId id) {
    MavenDomDependency dep = createDomDependency(dependencies, editor);

    dep.getGroupId().setStringValue(id.getGroupId());
    dep.getArtifactId().setStringValue(id.getArtifactId());
    dep.getVersion().setStringValue(id.getVersion());

    return dep;
  }

  @NotNull
  public static MavenDomDependency createDomDependency(
      @NotNull MavenDomProjectModel model, @Nullable Editor editor) {
    return createDomDependency(model.getDependencies(), editor);
  }

  @NotNull
  public static MavenDomDependency createDomDependency(
      @NotNull MavenDomDependencies dependencies, @Nullable Editor editor) {
    int index = getCollectionIndex(dependencies, editor);
    if (index >= 0) {
      DomCollectionChildDescription childDescription =
          dependencies.getGenericInfo().getCollectionChildDescription("dependency");
      if (childDescription != null) {
        DomElement element = childDescription.addValue(dependencies, index);
        if (element instanceof MavenDomDependency) {
          return (MavenDomDependency) element;
        }
      }
    }
    return dependencies.addDependency();
  }

  public static int getCollectionIndex(
      @NotNull final MavenDomDependencies dependencies, @Nullable final Editor editor) {
    if (editor != null) {
      int offset = editor.getCaretModel().getOffset();

      List<MavenDomDependency> dependencyList = dependencies.getDependencies();

      for (int i = 0; i < dependencyList.size(); i++) {
        MavenDomDependency dependency = dependencyList.get(i);
        XmlElement xmlElement = dependency.getXmlElement();

        if (xmlElement != null && xmlElement.getTextRange().getStartOffset() >= offset) {
          return i;
        }
      }
    }
    return -1;
  }
}
Пример #12
0
/** @author peter */
@Deprecated // TODO [VISTALL] rewrit it using ModuleExtensions
public abstract class MvcFramework {
  protected static final ExtensionPointName<MvcFramework> EP_NAME =
      ExtensionPointName.create("org.intellij.groovy.mvc.framework");

  private static final Logger LOG =
      Logger.getInstance("#org.jetbrains.plugins.groovy.mvc.MvcFramework");
  public static final Key<Boolean> CREATE_APP_STRUCTURE = Key.create("CREATE_MVC_APP_STRUCTURE");
  public static final Key<Boolean> UPGRADE = Key.create("UPGRADE");
  @NonNls public static final String GROOVY_STARTER_CONF = "/conf/groovy-starter.conf";
  @NonNls public static final String XMX_JVM_PARAMETER = "-Xmx";

  public abstract boolean hasSupport(@NotNull Module module);

  public boolean isAuxModule(@NotNull Module module) {
    return isCommonPluginsModule(module) || isGlobalPluginModule(module);
  }

  public boolean hasFrameworkJar(@NotNull Module module) {
    GlobalSearchScope scope =
        GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);
    return JavaPsiFacade.getInstance(module.getProject()).findClass(getSomeFrameworkClass(), scope)
        != null;
  }

  public boolean isCommonPluginsModule(@NotNull Module module) {
    return module.getName().endsWith(getCommonPluginSuffix());
  }

  public List<Module> reorderModulesForMvcView(List<Module> modules) {
    return modules;
  }

  public abstract String getApplicationDirectoryName();

  public void syncSdkAndLibrariesInPluginsModule(@NotNull Module module) {
    final Module pluginsModule = findCommonPluginsModule(module);
    if (pluginsModule != null) {
      MvcModuleStructureUtil.syncAuxModuleSdk(module, pluginsModule, this);
    }
  }

  public boolean isInteractiveConsoleSupported(@NotNull Module module) {
    return false;
  }

  public void runInteractiveConsole(@NotNull Module module) {
    throw new UnsupportedOperationException();
  }

  public abstract void upgradeFramework(@NotNull Module module);

  public void createApplicationIfNeeded(@NotNull final Module module) {
    if (findAppRoot(module) == null && module.getUserData(CREATE_APP_STRUCTURE) == Boolean.TRUE) {
      while (ModuleUtilCore.getSdk(module, JavaModuleExtensionImpl.class) == null) {
        if (Messages.showYesNoDialog(
                module.getProject(),
                "Cannot generate "
                    + getDisplayName()
                    + " project structure because JDK is not specified for module \""
                    + module.getName()
                    + "\".\n"
                    + getDisplayName()
                    + " project will not be created if you don't specify JDK.\nDo you want to specify JDK?",
                "Error",
                Messages.getErrorIcon())
            == 1) {
          return;
        }
        ProjectSettingsService.getInstance(module.getProject())
            .showModuleConfigurationDialog(module.getName(), ClasspathEditor.NAME);
      }
      module.putUserData(CREATE_APP_STRUCTURE, null);
      final GeneralCommandLine commandLine = getCreationCommandLine(module);
      if (commandLine == null) return;

      MvcConsole.executeProcess(
          module,
          commandLine,
          new Runnable() {
            public void run() {
              VirtualFile root = findAppRoot(module);
              if (root == null) return;

              PsiDirectory psiDir = PsiManager.getInstance(module.getProject()).findDirectory(root);
              IdeView ide =
                  LangDataKeys.IDE_VIEW.getData(DataManager.getInstance().getDataContext());
              if (ide != null) ide.selectElement(psiDir);

              // also here comes fileCreated(application.properties) which manages roots and run
              // configuration
            }
          },
          true);
    }
  }

  @Nullable
  protected GeneralCommandLine getCreationCommandLine(Module module) {
    String message =
        "Create default "
            + getDisplayName()
            + " directory structure in module '"
            + module.getName()
            + "'?";
    final int result =
        Messages.showDialog(
            module.getProject(),
            message,
            "Create " + getDisplayName() + " application",
            new String[] {"Run 'create-&app'", "Run 'create-&plugin'", "&Cancel"},
            0,
            getIcon());
    if (result < 0 || result > 1) {
      return null;
    }

    return createCommandAndShowErrors(
        null, module, true, new MvcCommand(result == 0 ? "create-app" : "create-plugin"));
  }

  public abstract void updateProjectStructure(@NotNull final Module module);

  public abstract void ensureRunConfigurationExists(@NotNull Module module);

  @Nullable
  public VirtualFile findAppRoot(@Nullable Module module) {
    if (module == null) return null;

    String appDirName = getApplicationDirectoryName();

    for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) {
      if (root.findChild(appDirName) != null) return root;
    }

    return null;
  }

  @Nullable
  public VirtualFile findAppRoot(@Nullable PsiElement element) {
    VirtualFile appDirectory = findAppDirectory(element);
    return appDirectory == null ? null : appDirectory.getParent();
  }

  @Nullable
  public VirtualFile findAppDirectory(@Nullable Module module) {
    if (module == null) return null;

    String appDirName = getApplicationDirectoryName();

    for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) {
      VirtualFile res = root.findChild(appDirName);
      if (res != null) return res;
    }

    return null;
  }

  @Nullable
  public VirtualFile findAppDirectory(@Nullable PsiElement element) {
    if (element == null) return null;

    PsiFile containingFile = element.getContainingFile().getOriginalFile();
    VirtualFile file = containingFile.getVirtualFile();
    if (file == null) return null;

    ProjectFileIndex index =
        ProjectRootManager.getInstance(containingFile.getProject()).getFileIndex();

    VirtualFile root = index.getContentRootForFile(file);
    if (root == null) return null;

    return root.findChild(getApplicationDirectoryName());
  }

  @Nullable
  public abstract VirtualFile getSdkRoot(@Nullable Module module);

  public abstract String getUserLibraryName();

  protected List<File> getImplicitClasspathRoots(@NotNull Module module) {
    final List<File> toExclude = new ArrayList<File>();

    VirtualFile sdkRoot = getSdkRoot(module);
    if (sdkRoot != null) toExclude.add(VfsUtil.virtualToIoFile(sdkRoot));

    ContainerUtil.addIfNotNull(getCommonPluginsDir(module), toExclude);
    final VirtualFile appRoot = findAppRoot(module);
    if (appRoot != null) {
      VirtualFile pluginDir = appRoot.findChild(MvcModuleStructureUtil.PLUGINS_DIRECTORY);
      if (pluginDir != null) toExclude.add(VfsUtil.virtualToIoFile(pluginDir));

      VirtualFile libDir = appRoot.findChild("lib");
      if (libDir != null) toExclude.add(VfsUtil.virtualToIoFile(libDir));
    }

    final Library library = MvcModuleStructureUtil.findUserLibrary(module, getUserLibraryName());
    if (library != null) {
      for (VirtualFile file : library.getFiles(OrderRootType.CLASSES)) {
        toExclude.add(VfsUtil.virtualToIoFile(PathUtil.getLocalFile(file)));
      }
    }
    return toExclude;
  }

  private PathsList removeFrameworkStuff(Module module, List<VirtualFile> rootFiles) {
    final List<File> toExclude = getImplicitClasspathRoots(module);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Before removing framework stuff: " + rootFiles);
      LOG.debug("Implicit roots:" + toExclude);
    }

    PathsList scriptClassPath = new PathsList();
    eachRoot:
    for (VirtualFile file : rootFiles) {
      for (final File excluded : toExclude) {
        if (VfsUtil.isAncestor(excluded, VfsUtil.virtualToIoFile(file), false)) {
          continue eachRoot;
        }
      }
      scriptClassPath.add(file);
    }
    return scriptClassPath;
  }

  public PathsList getApplicationClassPath(Module module) {
    final List<VirtualFile> classPath =
        OrderEnumerator.orderEntries(module)
            .recursively()
            .withoutSdk()
            .getPathsList()
            .getVirtualFiles();

    retainOnlyJarsAndDirectories(classPath);

    removeModuleOutput(module, classPath);

    final Module pluginsModule = findCommonPluginsModule(module);
    if (pluginsModule != null) {
      removeModuleOutput(pluginsModule, classPath);
    }

    return removeFrameworkStuff(module, classPath);
  }

  public abstract boolean updatesWholeProject();

  private static void retainOnlyJarsAndDirectories(List<VirtualFile> woSdk) {
    for (Iterator<VirtualFile> iterator = woSdk.iterator(); iterator.hasNext(); ) {
      VirtualFile file = iterator.next();
      final VirtualFile local = ArchiveVfsUtil.getVirtualFileForJar(file);
      final boolean dir = file.isDirectory();
      final String name = file.getName();
      if (LOG.isDebugEnabled()) {
        LOG.debug(
            "Considering: "
                + file.getPath()
                + "; local="
                + local
                + "; dir="
                + dir
                + "; name="
                + name);
      }
      if (dir || local != null) {
        continue;
      }
      if (name.endsWith(".jar")) {
        continue;
      }
      LOG.debug("Removing");
      iterator.remove();
    }
  }

  private static void removeModuleOutput(Module module, List<VirtualFile> from) {
    CompilerPathsManager compilerPathsManager =
        CompilerPathsManager.getInstance(module.getProject());
    for (ContentFolderType contentFolderType : ContentFolderType.ALL_SOURCE_ROOTS) {
      from.remove(compilerPathsManager.getCompilerOutput(module, contentFolderType));
    }
  }

  public abstract JavaParameters createJavaParameters(
      @NotNull Module module,
      boolean forCreation,
      boolean forTests,
      boolean classpathFromDependencies,
      @Nullable String jvmParams,
      @NotNull MvcCommand command)
      throws ExecutionException;

  protected static void ensureRunConfigurationExists(
      Module module, ConfigurationType configurationType, String name) {
    final RunManagerEx runManager = RunManagerEx.getInstanceEx(module.getProject());
    for (final RunConfiguration runConfiguration :
        runManager.getConfigurations(configurationType)) {
      if (runConfiguration instanceof MvcRunConfiguration
          && ((MvcRunConfiguration) runConfiguration).getModule() == module) {
        return;
      }
    }

    final ConfigurationFactory factory = configurationType.getConfigurationFactories()[0];
    final RunnerAndConfigurationSettings runSettings =
        runManager.createRunConfiguration(name, factory);
    final MvcRunConfiguration configuration = (MvcRunConfiguration) runSettings.getConfiguration();
    configuration.setModule(module);
    runManager.addConfiguration(runSettings, false);
    runManager.setActiveConfiguration(runSettings);

    RunManagerEx.disableTasks(
        module.getProject(),
        configuration,
        CompileStepBeforeRun.ID,
        CompileStepBeforeRunNoErrorCheck.ID);
  }

  public abstract String getFrameworkName();

  public String getDisplayName() {
    return getFrameworkName();
  }

  public abstract Icon getIcon(); // 16*16

  public abstract Icon getToolWindowIcon(); // 13*13

  public abstract String getSdkHomePropertyName();

  @Nullable
  public GeneralCommandLine createCommandAndShowErrors(
      @NotNull Module module, @NotNull String command, String... args) {
    return createCommandAndShowErrors(null, module, new MvcCommand(command, args));
  }

  @Nullable
  public GeneralCommandLine createCommandAndShowErrors(
      @NotNull Module module, @NotNull MvcCommand command) {
    return createCommandAndShowErrors(null, module, command);
  }

  @Nullable
  public GeneralCommandLine createCommandAndShowErrors(
      @Nullable String vmOptions, @NotNull Module module, @NotNull MvcCommand command) {
    return createCommandAndShowErrors(vmOptions, module, false, command);
  }

  @Nullable
  public GeneralCommandLine createCommandAndShowErrors(
      @Nullable String vmOptions,
      @NotNull Module module,
      final boolean forCreation,
      @NotNull MvcCommand command) {
    try {
      return createCommand(module, vmOptions, forCreation, command);
    } catch (ExecutionException e) {
      Messages.showErrorDialog(e.getMessage(), "Failed to run grails command: " + command);
      return null;
    }
  }

  @NotNull
  public GeneralCommandLine createCommand(
      @NotNull Module module,
      @Nullable String jvmParams,
      boolean forCreation,
      @NotNull MvcCommand command)
      throws ExecutionException {
    final JavaParameters params =
        createJavaParameters(module, forCreation, false, true, jvmParams, command);
    addJavaHome(params, module);

    final GeneralCommandLine commandLine = createCommandLine(params);

    final VirtualFile griffonHome = getSdkRoot(module);
    if (griffonHome != null) {
      commandLine
          .getEnvironment()
          .put(getSdkHomePropertyName(), FileUtil.toSystemDependentName(griffonHome.getPath()));
    }

    final VirtualFile root = findAppRoot(module);
    final File ioRoot =
        root != null ? VfsUtilCore.virtualToIoFile(root) : new File(module.getModuleDirPath());
    commandLine.setWorkDirectory(forCreation ? ioRoot.getParentFile() : ioRoot);

    return commandLine;
  }

  public static void addJavaHome(@NotNull JavaParameters params, @NotNull Module module) {
    final Sdk sdk = ModuleUtilCore.getSdk(module, JavaModuleExtensionImpl.class);
    if (sdk != null && sdk.getSdkType() instanceof JavaSdkType) {
      String path = StringUtil.trimEnd(sdk.getHomePath(), File.separator);
      if (StringUtil.isNotEmpty(path)) {
        Map<String, String> env = params.getEnv();
        if (env == null) {
          env = new HashMap<String, String>();
          params.setEnv(env);
        }

        env.put("JAVA_HOME", FileUtil.toSystemDependentName(path));
      }
    }
  }

  public static GeneralCommandLine createCommandLine(@NotNull JavaParameters params)
      throws CantRunException {
    return CommandLineBuilder.createFromJavaParameters(params);
  }

  private void extractPlugins(
      Project project, @Nullable VirtualFile pluginRoot, Map<String, VirtualFile> res) {
    if (pluginRoot != null) {
      VirtualFile[] children = pluginRoot.getChildren();
      if (children != null) {
        for (VirtualFile child : children) {
          String pluginName = getInstalledPluginNameByPath(project, child);
          if (pluginName != null) {
            res.put(pluginName, child);
          }
        }
      }
    }
  }

  public Collection<VirtualFile> getAllPluginRoots(@NotNull Module module, boolean refresh) {
    return getCommonPluginRoots(module, refresh);
  }

  public void collectCommonPluginRoots(
      Map<String, VirtualFile> result, @NotNull Module module, boolean refresh) {
    if (isCommonPluginsModule(module)) {
      for (VirtualFile root : ModuleRootManager.getInstance(module).getContentRoots()) {
        String pluginName = getInstalledPluginNameByPath(module.getProject(), root);
        if (pluginName != null) {
          result.put(pluginName, root);
        }
      }
    } else {
      VirtualFile root = findAppRoot(module);
      if (root == null) return;

      extractPlugins(
          module.getProject(), root.findChild(MvcModuleStructureUtil.PLUGINS_DIRECTORY), result);
      extractPlugins(
          module.getProject(),
          MvcModuleStructureUtil.findFile(getCommonPluginsDir(module), refresh),
          result);
      extractPlugins(
          module.getProject(),
          MvcModuleStructureUtil.findFile(getGlobalPluginsDir(module), refresh),
          result);
    }
  }

  public Collection<VirtualFile> getCommonPluginRoots(@NotNull Module module, boolean refresh) {
    Map<String, VirtualFile> result = new HashMap<String, VirtualFile>();
    collectCommonPluginRoots(result, module, refresh);
    return result.values();
  }

  @Nullable
  public Module findCommonPluginsModule(@NotNull Module module) {
    return ModuleManager.getInstance(module.getProject())
        .findModuleByName(getCommonPluginsModuleName(module));
  }

  public boolean isGlobalPluginModule(@NotNull Module module) {
    return module.getName().startsWith(getGlobalPluginsModuleName());
  }

  @Nullable
  public File getSdkWorkDir(@NotNull Module module) {
    return getDefaultSdkWorkDir(module);
  }

  @Nullable
  public abstract File getDefaultSdkWorkDir(@NotNull Module module);

  @Nullable
  public File getGlobalPluginsDir(@NotNull Module module) {
    final File sdkWorkDir = getSdkWorkDir(module);
    return sdkWorkDir == null ? null : new File(sdkWorkDir, "global-plugins");
  }

  @Nullable
  public File getCommonPluginsDir(@NotNull Module module) {
    final File grailsWorkDir = getSdkWorkDir(module);
    if (grailsWorkDir == null) return null;

    final String applicationName = getApplicationName(module);
    if (applicationName == null) return null;

    return new File(grailsWorkDir, "projects/" + applicationName + "/plugins");
  }

  public String getApplicationName(Module module) {
    final VirtualFile root = findAppRoot(module);
    if (root == null) return null;
    return root.getName();
  }

  protected abstract String getCommonPluginSuffix();

  public abstract String getGlobalPluginsModuleName();

  public String getCommonPluginsModuleName(Module module) {
    return module.getName() + getCommonPluginSuffix();
  }

  public abstract boolean isSDKLibrary(Library library);

  public abstract MvcProjectStructure createProjectStructure(
      @NotNull Module module, boolean auxModule);

  public abstract LibraryKind getLibraryKind();

  public abstract String getSomeFrameworkClass();

  public static void addAvailableSystemScripts(
      final Collection<String> result, @NotNull Module module) {
    VirtualFile scriptRoot = null;

    GlobalSearchScope searchScope =
        GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false);

    for (PsiClass aClass :
        JavaPsiFacade.getInstance(module.getProject()).findClasses("CreateApp_", searchScope)) {
      PsiClass superClass = aClass.getSuperClass();
      if (superClass != null
          && GroovyCommonClassNames.GROOVY_LANG_SCRIPT.equals(superClass.getQualifiedName())) {
        PsiFile psiFile = aClass.getContainingFile();
        if (psiFile != null) {
          VirtualFile file = psiFile.getVirtualFile();
          if (file != null && file.getFileSystem() instanceof ArchiveFileSystem) {
            VirtualFile parent = file.getParent();
            if (parent != null && parent.findChild("Console.class") != null) {
              scriptRoot = parent;
              break;
            }
          }
        }
      }
    }

    if (scriptRoot == null) return;

    Pattern scriptPattern = Pattern.compile("([A-Za-z0-9]+)_?\\.class");

    for (VirtualFile file : scriptRoot.getChildren()) {
      Matcher matcher = scriptPattern.matcher(file.getName());
      if (matcher.matches()) {
        result.add(GroovyNamesUtil.camelToSnake(matcher.group(1)));
      }
    }
  }

  public abstract boolean isToReformatOnCreation(VirtualFile file);

  public static void addAvailableScripts(
      final Collection<String> result, @Nullable final VirtualFile root) {
    if (root == null || !root.isDirectory()) {
      return;
    }

    final VirtualFile scripts = root.findChild("scripts");

    if (scripts == null || !scripts.isDirectory()) {
      return;
    }

    for (VirtualFile child : scripts.getChildren()) {
      if (isScriptFile(child)) {
        result.add(GroovyNamesUtil.camelToSnake(child.getNameWithoutExtension()));
      }
    }
  }

  @Nullable
  public static MvcFramework findCommonPluginModuleFramework(Module module) {
    for (MvcFramework framework : EP_NAME.getExtensions()) {
      if (framework.isCommonPluginsModule(module)) {
        return framework;
      }
    }
    return null;
  }

  public static boolean isScriptFileName(String fileName) {
    return fileName.endsWith(GroovyFileType.DEFAULT_EXTENSION) && fileName.charAt(0) != '_';
  }

  private static boolean isScriptFile(VirtualFile virtualFile) {
    return !virtualFile.isDirectory() && isScriptFileName(virtualFile.getName());
  }

  @Nullable
  public String getInstalledPluginNameByPath(Project project, @NotNull VirtualFile pluginPath) {
    VirtualFile pluginXml = pluginPath.findChild("plugin.xml");
    if (pluginXml == null) return null;

    PsiFile pluginXmlPsi = PsiManager.getInstance(project).findFile(pluginXml);
    if (!(pluginXmlPsi instanceof XmlFile)) return null;

    XmlTag rootTag = ((XmlFile) pluginXmlPsi).getRootTag();
    if (rootTag == null || !"plugin".equals(rootTag.getName())) return null;

    XmlAttribute attrName = rootTag.getAttribute("name");
    if (attrName == null) return null;

    String res = attrName.getValue();
    if (res == null) return null;

    res = res.trim();
    if (res.length() == 0) return null;

    return res;
  }

  @Nullable
  public static MvcFramework getInstance(@Nullable final Module module) {
    if (module == null) {
      return null;
    }

    final Project project = module.getProject();

    return CachedValuesManager.getManager(project)
        .getCachedValue(
            module,
            new CachedValueProvider<MvcFramework>() {
              @Override
              public Result<MvcFramework> compute() {
                final ModificationTracker tracker =
                    MvcModuleStructureSynchronizer.getInstance(project)
                        .getFileAndRootsModificationTracker();
                for (final MvcFramework framework : EP_NAME.getExtensions()) {
                  if (framework.hasSupport(module)) {
                    return Result.create(framework, tracker);
                  }
                }
                return Result.create(null, tracker);
              }
            });
  }

  @Nullable
  public static MvcFramework getInstanceBySdk(@NotNull Module module) {
    for (final MvcFramework framework : EP_NAME.getExtensions()) {
      if (framework.getSdkRoot(module) != null) {
        return framework;
      }
    }
    return null;
  }
}
Пример #13
0
public final class LoadTextUtil {
  @Nls private static final String AUTO_DETECTED_FROM_BOM = "auto-detected from BOM";

  private LoadTextUtil() {}

  @NotNull
  private static Pair<CharSequence, String> convertLineSeparators(@NotNull CharBuffer buffer) {
    int dst = 0;
    char prev = ' ';
    int crCount = 0;
    int lfCount = 0;
    int crlfCount = 0;

    final int length = buffer.length();
    final char[] bufferArray = CharArrayUtil.fromSequenceWithoutCopying(buffer);

    for (int src = 0; src < length; src++) {
      char c = bufferArray != null ? bufferArray[src] : buffer.charAt(src);
      switch (c) {
        case '\r':
          if (bufferArray != null) bufferArray[dst++] = '\n';
          else buffer.put(dst++, '\n');
          crCount++;
          break;
        case '\n':
          if (prev == '\r') {
            crCount--;
            crlfCount++;
          } else {
            if (bufferArray != null) bufferArray[dst++] = '\n';
            else buffer.put(dst++, '\n');
            lfCount++;
          }
          break;
        default:
          if (bufferArray != null) bufferArray[dst++] = c;
          else buffer.put(dst++, c);
          break;
      }
      prev = c;
    }

    String detectedLineSeparator = null;
    if (crlfCount > crCount && crlfCount > lfCount) {
      detectedLineSeparator = "\r\n";
    } else if (crCount > lfCount) {
      detectedLineSeparator = "\r";
    } else if (lfCount > 0) {
      detectedLineSeparator = "\n";
    }

    CharSequence result;
    if (buffer.length() == dst) {
      result = buffer;
    } else {
      // in Mac JDK CharBuffer.subSequence() signature differs from Oracle's
      // more than that, the signature has changed between jd6 and jdk7,
      // so use more generic CharSequence.subSequence() just in case
      @SuppressWarnings("UnnecessaryLocalVariable")
      CharSequence seq = buffer;
      result = seq.subSequence(0, dst);
    }
    return Pair.create(result, detectedLineSeparator);
  }

  @NotNull
  public static Charset detectCharset(
      @NotNull VirtualFile virtualFile, @NotNull byte[] content, @NotNull FileType fileType) {
    Charset charset = null;

    Trinity<Charset, CharsetToolkit.GuessedEncoding, byte[]> guessed =
        guessFromContent(virtualFile, content, content.length);
    if (guessed != null && guessed.first != null) {
      charset = guessed.first;
    } else {
      String charsetName = fileType.getCharset(virtualFile, content);

      if (charsetName == null) {
        Charset specifiedExplicitly = EncodingRegistry.getInstance().getEncoding(virtualFile, true);
        if (specifiedExplicitly != null) {
          charset = specifiedExplicitly;
        }
      } else {
        charset = CharsetToolkit.forName(charsetName);
      }
    }

    if (charset == null) {
      charset = EncodingRegistry.getInstance().getDefaultCharset();
    }
    if (fileType.getName().equals("Properties")
        && EncodingRegistry.getInstance().isNative2Ascii(virtualFile)) {
      charset = Native2AsciiCharset.wrap(charset);
    }
    virtualFile.setCharset(charset);
    return charset;
  }

  @NotNull
  public static Charset detectCharsetAndSetBOM(
      @NotNull VirtualFile virtualFile, @NotNull byte[] content) {
    return doDetectCharsetAndSetBOM(virtualFile, content, true).getFirst();
  }

  @NotNull
  private static Pair.NonNull<Charset, byte[]> doDetectCharsetAndSetBOM(
      @NotNull VirtualFile virtualFile, @NotNull byte[] content, boolean saveBOM) {
    return doDetectCharsetAndSetBOM(virtualFile, content, saveBOM, virtualFile.getFileType());
  }

  @NotNull
  private static Pair.NonNull<Charset, byte[]> doDetectCharsetAndSetBOM(
      @NotNull VirtualFile virtualFile,
      @NotNull byte[] content,
      boolean saveBOM,
      @NotNull FileType fileType) {
    @NotNull
    Charset charset =
        virtualFile.isCharsetSet()
            ? virtualFile.getCharset()
            : detectCharset(virtualFile, content, fileType);
    Pair.NonNull<Charset, byte[]> bomAndCharset = getCharsetAndBOM(content, charset);
    final byte[] bom = bomAndCharset.second;
    if (saveBOM && bom.length != 0) {
      virtualFile.setBOM(bom);
      setCharsetWasDetectedFromBytes(virtualFile, AUTO_DETECTED_FROM_BOM);
    }
    return bomAndCharset;
  }

  private static final boolean GUESS_UTF =
      Boolean.parseBoolean(System.getProperty("idea.guess.utf.encoding", "true"));

  @Nullable(
      "null means no luck, otherwise it's tuple(guessed encoding, hint about content if was unable to guess, BOM)")
  public static Trinity<Charset, CharsetToolkit.GuessedEncoding, byte[]> guessFromContent(
      @NotNull VirtualFile virtualFile, @NotNull byte[] content, int length) {
    Charset defaultCharset =
        ObjectUtils.notNull(
            EncodingManager.getInstance().getEncoding(virtualFile, true),
            CharsetToolkit.getDefaultSystemCharset());
    CharsetToolkit toolkit = GUESS_UTF ? new CharsetToolkit(content, defaultCharset) : null;
    String detectedFromBytes = null;
    try {
      if (GUESS_UTF) {
        toolkit.setEnforce8Bit(true);
        Charset charset = toolkit.guessFromBOM();
        if (charset != null) {
          detectedFromBytes = AUTO_DETECTED_FROM_BOM;
          byte[] bom =
              ObjectUtils.notNull(CharsetToolkit.getMandatoryBom(charset), CharsetToolkit.UTF8_BOM);
          return Trinity.create(charset, null, bom);
        }
        CharsetToolkit.GuessedEncoding guessed = toolkit.guessFromContent(length);
        if (guessed == CharsetToolkit.GuessedEncoding.VALID_UTF8) {
          detectedFromBytes = "auto-detected from bytes";
          return Trinity.create(
              CharsetToolkit.UTF8_CHARSET, guessed, null); // UTF detected, ignore all directives
        }
        if (guessed == CharsetToolkit.GuessedEncoding.SEVEN_BIT) {
          return Trinity.create(null, guessed, null);
        }
      }
      return null;
    } finally {
      setCharsetWasDetectedFromBytes(virtualFile, detectedFromBytes);
    }
  }

  @NotNull
  private static Pair.NonNull<Charset, byte[]> getCharsetAndBOM(
      @NotNull byte[] content, @NotNull Charset charset) {
    if (charset.name().contains(CharsetToolkit.UTF8) && CharsetToolkit.hasUTF8Bom(content)) {
      return Pair.createNonNull(charset, CharsetToolkit.UTF8_BOM);
    }
    try {
      Charset fromBOM = CharsetToolkit.guessFromBOM(content);
      if (fromBOM != null) {
        return Pair.createNonNull(
            fromBOM,
            ObjectUtils.notNull(
                CharsetToolkit.getMandatoryBom(fromBOM), ArrayUtil.EMPTY_BYTE_ARRAY));
      }
    } catch (UnsupportedCharsetException ignore) {
    }

    return Pair.createNonNull(charset, ArrayUtil.EMPTY_BYTE_ARRAY);
  }

  public static void changeLineSeparators(
      @Nullable Project project,
      @NotNull VirtualFile file,
      @NotNull String newSeparator,
      @NotNull Object requestor)
      throws IOException {
    CharSequence currentText =
        getTextByBinaryPresentation(file.contentsToByteArray(), file, true, false);
    String currentSeparator = detectLineSeparator(file, false);
    if (newSeparator.equals(currentSeparator)) {
      return;
    }
    String newText = StringUtil.convertLineSeparators(currentText.toString(), newSeparator);

    file.setDetectedLineSeparator(newSeparator);
    write(project, file, requestor, newText, -1);
  }

  /**
   * Overwrites file with text and sets modification stamp and time stamp to the specified values.
   *
   * <p>Normally you should not use this method.
   *
   * @param requestor any object to control who called this method. Note that it is considered to be
   *     an external change if <code>requestor</code> is <code>null</code>. See {@link
   *     com.intellij.openapi.vfs.VirtualFileEvent#getRequestor}
   * @param newModificationStamp new modification stamp or -1 if no special value should be
   *     set @return <code>Writer</code>
   * @throws java.io.IOException if an I/O error occurs
   * @see VirtualFile#getModificationStamp()
   */
  public static void write(
      @Nullable Project project,
      @NotNull VirtualFile virtualFile,
      @NotNull Object requestor,
      @NotNull String text,
      long newModificationStamp)
      throws IOException {
    Charset existing = virtualFile.getCharset();
    Pair.NonNull<Charset, byte[]> chosen = charsetForWriting(project, virtualFile, text, existing);
    Charset charset = chosen.first;
    byte[] buffer = chosen.second;
    if (!charset.equals(existing)) {
      virtualFile.setCharset(charset);
    }
    setDetectedFromBytesFlagBack(virtualFile, buffer);

    OutputStream outputStream = virtualFile.getOutputStream(requestor, newModificationStamp, -1);
    try {
      outputStream.write(buffer);
    } finally {
      outputStream.close();
    }
  }

  @NotNull
  private static Pair.NonNull<Charset, byte[]> charsetForWriting(
      @Nullable Project project,
      @NotNull VirtualFile virtualFile,
      @NotNull String text,
      @NotNull Charset existing) {
    Charset specified = extractCharsetFromFileContent(project, virtualFile, text);
    Pair.NonNull<Charset, byte[]> chosen = chooseMostlyHarmlessCharset(existing, specified, text);
    Charset charset = chosen.first;

    // in case of "UTF-16", OutputStreamWriter sometimes adds BOM on it's own.
    // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6800103
    byte[] bom = virtualFile.getBOM();
    Charset fromBom = bom == null ? null : CharsetToolkit.guessFromBOM(bom);
    if (fromBom != null && !fromBom.equals(charset)) {
      chosen = Pair.createNonNull(fromBom, toBytes(text, fromBom));
    }
    return chosen;
  }

  public static void setDetectedFromBytesFlagBack(
      @NotNull VirtualFile virtualFile, @NotNull byte[] content) {
    if (virtualFile.getBOM() == null) {
      guessFromContent(virtualFile, content, content.length);
    } else {
      // prevent file to be reloaded in other encoding after save with BOM
      setCharsetWasDetectedFromBytes(virtualFile, AUTO_DETECTED_FROM_BOM);
    }
  }

  @NotNull
  public static Pair.NonNull<Charset, byte[]> chooseMostlyHarmlessCharset(
      @NotNull Charset existing, @NotNull Charset specified, @NotNull String text) {
    try {
      if (specified.equals(existing)) {
        return Pair.createNonNull(specified, toBytes(text, existing));
      }

      byte[] out = isSupported(specified, text);
      if (out != null) {
        return Pair.createNonNull(
            specified, out); // if explicitly specified encoding is safe, return it
      }
      out = isSupported(existing, text);
      if (out != null) {
        return Pair.createNonNull(existing, out); // otherwise stick to the old encoding if it's ok
      }
      return Pair.createNonNull(
          specified, toBytes(text, specified)); // if both are bad there is no difference
    } catch (RuntimeException e) {
      return Pair.createNonNull(
          Charset.defaultCharset(),
          toBytes(text, null)); // if both are bad and there is no hope, use the default charset
    }
  }

  @NotNull
  private static byte[] toBytes(@NotNull String text, @Nullable Charset charset)
      throws RuntimeException {
    //noinspection SSBasedInspection
    return charset == null ? text.getBytes() : text.getBytes(charset);
  }

  @Nullable("null means not supported, otherwise it is converted byte stream")
  private static byte[] isSupported(@NotNull Charset charset, @NotNull String str) {
    try {
      if (!charset.canEncode()) return null;
      byte[] bytes = str.getBytes(charset);
      if (!str.equals(new String(bytes, charset))) {
        return null;
      }

      return bytes;
    } catch (Exception e) {
      return null; // wow, some charsets throw NPE inside .getBytes() when unable to encode
                   // (JIS_X0212-1990)
    }
  }

  @NotNull
  public static Charset extractCharsetFromFileContent(
      @Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull CharSequence text) {
    return ObjectUtils.notNull(
        charsetFromContentOrNull(project, virtualFile, text), virtualFile.getCharset());
  }

  @Nullable("returns null if cannot determine from content")
  public static Charset charsetFromContentOrNull(
      @Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull CharSequence text) {
    return CharsetUtil.extractCharsetFromFileContent(
        project, virtualFile, virtualFile.getFileType(), text);
  }

  private static boolean ourDecompileProgressStarted = false;

  @NotNull
  public static CharSequence loadText(@NotNull final VirtualFile file) {
    if (file instanceof LightVirtualFile) {
      return ((LightVirtualFile) file).getContent();
    }

    if (file.isDirectory()) {
      throw new AssertionError("'" + file.getPresentableUrl() + "' is a directory");
    }

    FileType fileType = file.getFileType();
    if (fileType.isBinary()) {
      final BinaryFileDecompiler decompiler =
          BinaryFileTypeDecompilers.INSTANCE.forFileType(fileType);
      if (decompiler != null) {
        CharSequence text;

        Application app = ApplicationManager.getApplication();
        if (app != null
            && app.isDispatchThread()
            && !app.isWriteAccessAllowed()
            && !ourDecompileProgressStarted) {
          final Ref<CharSequence> result = Ref.create(ArrayUtil.EMPTY_CHAR_SEQUENCE);
          final Ref<Throwable> error = Ref.create();

          ourDecompileProgressStarted = true;
          try {
            ProgressManager.getInstance()
                .run(
                    new Task.Modal(null, "Decompiling " + file.getName(), true) {
                      @Override
                      public void run(@NotNull ProgressIndicator indicator) {
                        indicator.setIndeterminate(true);
                        try {
                          result.set(
                              ApplicationUtil.runWithCheckCanceled(
                                  new Callable<CharSequence>() {
                                    @Override
                                    public CharSequence call() {
                                      return decompiler.decompile(file);
                                    }
                                  },
                                  indicator));
                        } catch (Throwable t) {
                          error.set(t);
                        }
                      }
                    });
          } finally {
            ourDecompileProgressStarted = false;
          }

          ExceptionUtil.rethrowUnchecked(error.get());
          text = result.get();
        } else {
          text = decompiler.decompile(file);
        }

        StringUtil.assertValidSeparators(text);
        return text;
      }

      throw new IllegalArgumentException(
          "Attempt to load text for binary file which doesn't have a decompiler plugged in: "
              + file.getPresentableUrl()
              + ". File type: "
              + fileType.getName());
    }

    try {
      byte[] bytes = file.contentsToByteArray();
      return getTextByBinaryPresentation(bytes, file);
    } catch (IOException e) {
      return ArrayUtil.EMPTY_CHAR_SEQUENCE;
    }
  }

  @NotNull
  public static CharSequence getTextByBinaryPresentation(
      @NotNull final byte[] bytes, @NotNull VirtualFile virtualFile) {
    return getTextByBinaryPresentation(bytes, virtualFile, true, true);
  }

  @NotNull
  public static CharSequence getTextByBinaryPresentation(
      @NotNull byte[] bytes,
      @NotNull VirtualFile virtualFile,
      boolean saveDetectedSeparators,
      boolean saveBOM) {
    return getTextByBinaryPresentation(
        bytes, virtualFile, saveDetectedSeparators, saveBOM, virtualFile.getFileType());
  }

  @NotNull
  public static CharSequence getTextByBinaryPresentation(
      @NotNull byte[] bytes,
      @NotNull VirtualFile virtualFile,
      boolean saveDetectedSeparators,
      boolean saveBOM,
      @NotNull FileType fileType) {
    Pair.NonNull<Charset, byte[]> pair =
        doDetectCharsetAndSetBOM(virtualFile, bytes, saveBOM, fileType);
    Charset charset = pair.getFirst();
    byte[] bom = pair.getSecond();
    int offset = bom.length;

    Pair<CharSequence, String> result = convertBytes(bytes, charset, offset);
    if (saveDetectedSeparators) {
      virtualFile.setDetectedLineSeparator(result.getSecond());
    }
    return result.getFirst();
  }

  /**
   * Get detected line separator, if the file never been loaded, is loaded if checkFile parameter is
   * specified.
   *
   * @param file the file to check
   * @param checkFile if the line separator was not detected before, try to detect it
   * @return the detected line separator or null
   */
  @Nullable
  public static String detectLineSeparator(@NotNull VirtualFile file, boolean checkFile) {
    String lineSeparator = getDetectedLineSeparator(file);
    if (lineSeparator == null && checkFile) {
      try {
        getTextByBinaryPresentation(file.contentsToByteArray(), file);
        lineSeparator = getDetectedLineSeparator(file);
      } catch (IOException e) {
        // null will be returned
      }
    }
    return lineSeparator;
  }

  static String getDetectedLineSeparator(@NotNull VirtualFile file) {
    return file.getDetectedLineSeparator();
  }

  @NotNull
  public static CharSequence getTextByBinaryPresentation(
      @NotNull byte[] bytes, @NotNull Charset charset) {
    Pair.NonNull<Charset, byte[]> pair = getCharsetAndBOM(bytes, charset);
    byte[] bom = pair.getSecond();
    int offset = bom.length;

    final Pair<CharSequence, String> result = convertBytes(bytes, pair.first, offset);
    return result.getFirst();
  }

  // do not need to think about BOM here. it is processed outside
  @NotNull
  private static Pair<CharSequence, String> convertBytes(
      @NotNull byte[] bytes, @NotNull Charset charset, final int startOffset) {
    ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, startOffset, bytes.length - startOffset);

    CharBuffer charBuffer;
    try {
      charBuffer = charset.decode(byteBuffer);
    } catch (Exception e) {
      // esoteric charsets can throw any kind of exception
      charBuffer = CharBuffer.wrap(ArrayUtil.EMPTY_CHAR_ARRAY);
    }
    return convertLineSeparators(charBuffer);
  }

  private static final Key<String> CHARSET_WAS_DETECTED_FROM_BYTES =
      Key.create("CHARSET_WAS_DETECTED_FROM_BYTES");

  @Nullable("null if was not detected, otherwise the reason it was")
  public static String wasCharsetDetectedFromBytes(@NotNull VirtualFile virtualFile) {
    return virtualFile.getUserData(CHARSET_WAS_DETECTED_FROM_BYTES);
  }

  public static void setCharsetWasDetectedFromBytes(
      @NotNull VirtualFile virtualFile,
      @Nullable("null if was not detected, otherwise the reason it was") String reason) {
    virtualFile.putUserData(CHARSET_WAS_DETECTED_FROM_BYTES, reason);
  }
}
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;
  }
}
/** Implements PyFunction. */
public class PyFunctionImpl extends PyBaseElementImpl<PyFunctionStub> implements PyFunction {

  private static final Key<CachedValue<List<PyAssignmentStatement>>> ATTRIBUTES_KEY =
      Key.create("attributes");

  public PyFunctionImpl(ASTNode astNode) {
    super(astNode);
  }

  public PyFunctionImpl(final PyFunctionStub stub) {
    this(stub, PyElementTypes.FUNCTION_DECLARATION);
  }

  public PyFunctionImpl(PyFunctionStub stub, IStubElementType nodeType) {
    super(stub, nodeType);
  }

  private class CachedStructuredDocStringProvider
      implements CachedValueProvider<StructuredDocString> {
    @Nullable
    @Override
    public Result<StructuredDocString> compute() {
      final PyFunctionImpl f = PyFunctionImpl.this;
      return Result.create(DocStringUtil.getStructuredDocString(f), f);
    }
  }

  private CachedStructuredDocStringProvider myCachedStructuredDocStringProvider =
      new CachedStructuredDocStringProvider();

  @Nullable
  @Override
  public String getName() {
    final PyFunctionStub stub = getStub();
    if (stub != null) {
      return stub.getName();
    }

    ASTNode node = getNameNode();
    return node != null ? node.getText() : null;
  }

  public PsiElement getNameIdentifier() {
    final ASTNode nameNode = getNameNode();
    return nameNode != null ? nameNode.getPsi() : null;
  }

  public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
    final ASTNode nameElement = PyUtil.createNewName(this, name);
    final ASTNode nameNode = getNameNode();
    if (nameNode != null) {
      getNode().replaceChild(nameNode, nameElement);
    }
    return this;
  }

  @Override
  public Icon getIcon(int flags) {
    PyPsiUtils.assertValid(this);
    final Property property = getProperty();
    if (property != null) {
      if (property.getGetter().valueOrNull() == this) {
        return PythonIcons.Python.PropertyGetter;
      }
      if (property.getSetter().valueOrNull() == this) {
        return PythonIcons.Python.PropertySetter;
      }
      if (property.getDeleter().valueOrNull() == this) {
        return PythonIcons.Python.PropertyDeleter;
      }
      return PlatformIcons.PROPERTY_ICON;
    }
    if (getContainingClass() != null) {
      return PlatformIcons.METHOD_ICON;
    }
    return PythonIcons.Python.Function;
  }

  @Nullable
  public ASTNode getNameNode() {
    return getNode().findChildByType(PyTokenTypes.IDENTIFIER);
  }

  @NotNull
  public PyParameterList getParameterList() {
    return getRequiredStubOrPsiChild(PyElementTypes.PARAMETER_LIST);
  }

  @Override
  @NotNull
  public PyStatementList getStatementList() {
    final PyStatementList statementList = childToPsi(PyElementTypes.STATEMENT_LIST);
    assert statementList != null : "Statement list missing for function " + getText();
    return statementList;
  }

  public PyClass getContainingClass() {
    final PyFunctionStub stub = getStub();
    if (stub != null) {
      final StubElement parentStub = stub.getParentStub();
      if (parentStub instanceof PyClassStub) {
        return ((PyClassStub) parentStub).getPsi();
      }

      return null;
    }

    final PsiElement parent = PsiTreeUtil.getParentOfType(this, StubBasedPsiElement.class);
    if (parent instanceof PyClass) {
      return (PyClass) parent;
    }
    return null;
  }

  @Nullable
  public PyDecoratorList getDecoratorList() {
    return getStubOrPsiChild(
        PyElementTypes.DECORATOR_LIST); // PsiTreeUtil.getChildOfType(this, PyDecoratorList.class);
  }

  @Nullable
  @Override
  public PyType getReturnType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) {
    final PyType type = getReturnType(context);
    return isAsync() && isAsyncAllowed() ? createCoroutineType(type) : type;
  }

  @Nullable
  private PyType getReturnType(@NotNull TypeEvalContext context) {
    for (PyTypeProvider typeProvider : Extensions.getExtensions(PyTypeProvider.EP_NAME)) {
      final Ref<PyType> returnTypeRef = typeProvider.getReturnType(this, context);
      if (returnTypeRef != null) {
        return derefType(returnTypeRef, typeProvider);
      }
    }

    if (context.allowReturnTypes(this)) {
      final Ref<? extends PyType> yieldTypeRef = getYieldStatementType(context);
      if (yieldTypeRef != null) {
        return yieldTypeRef.get();
      }
      return getReturnStatementType(context);
    }

    return null;
  }

  @Nullable
  @Override
  public PyType getCallType(
      @NotNull TypeEvalContext context, @NotNull PyCallSiteExpression callSite) {
    for (PyTypeProvider typeProvider : Extensions.getExtensions(PyTypeProvider.EP_NAME)) {
      final Ref<PyType> typeRef = typeProvider.getCallType(this, callSite, context);
      if (typeRef != null) {
        return derefType(typeRef, typeProvider);
      }
    }

    final PyExpression receiver = PyTypeChecker.getReceiver(callSite, this);
    final Map<PyExpression, PyNamedParameter> mapping =
        PyCallExpressionHelper.mapArguments(callSite, this, context);
    return getCallType(receiver, mapping, context);
  }

  @Nullable
  private static PyType derefType(
      @NotNull Ref<PyType> typeRef, @NotNull PyTypeProvider typeProvider) {
    final PyType type = typeRef.get();
    if (type != null) {
      type.assertValid(typeProvider.toString());
    }
    return type;
  }

  @Nullable
  @Override
  public PyType getCallType(
      @Nullable PyExpression receiver,
      @NotNull Map<PyExpression, PyNamedParameter> parameters,
      @NotNull TypeEvalContext context) {
    return analyzeCallType(context.getReturnType(this), receiver, parameters, context);
  }

  @Nullable
  private PyType analyzeCallType(
      @Nullable PyType type,
      @Nullable PyExpression receiver,
      @NotNull Map<PyExpression, PyNamedParameter> parameters,
      @NotNull TypeEvalContext context) {
    if (PyTypeChecker.hasGenerics(type, context)) {
      final Map<PyGenericType, PyType> substitutions =
          PyTypeChecker.unifyGenericCall(receiver, parameters, context);
      if (substitutions != null) {
        type = PyTypeChecker.substitute(type, substitutions, context);
      } else {
        type = null;
      }
    }
    if (receiver != null) {
      type = replaceSelf(type, receiver, context);
    }
    if (type != null && isDynamicallyEvaluated(parameters.values(), context)) {
      type = PyUnionType.createWeakType(type);
    }
    return type;
  }

  @Override
  public ItemPresentation getPresentation() {
    return new PyElementPresentation(this) {
      @Nullable
      @Override
      public String getPresentableText() {
        return notNullize(getName(), PyNames.UNNAMED_ELEMENT)
            + getParameterList().getPresentableText(true);
      }

      @Nullable
      @Override
      public String getLocationString() {
        final PyClass containingClass = getContainingClass();
        if (containingClass != null) {
          return "("
              + containingClass.getName()
              + " in "
              + getPackageForFile(getContainingFile())
              + ")";
        }
        return super.getLocationString();
      }
    };
  }

  @Nullable
  private PyType replaceSelf(
      @Nullable PyType returnType,
      @Nullable PyExpression receiver,
      @NotNull TypeEvalContext context) {
    if (receiver != null) {
      // TODO: Currently we substitute only simple subclass types, but we could handle union and
      // collection types as well
      if (returnType instanceof PyClassType) {
        final PyClassType returnClassType = (PyClassType) returnType;
        if (returnClassType.getPyClass() == getContainingClass()) {
          final PyType receiverType = context.getType(receiver);
          if (receiverType instanceof PyClassType
              && PyTypeChecker.match(returnType, receiverType, context)) {
            return returnClassType.isDefinition()
                ? receiverType
                : ((PyClassType) receiverType).toInstance();
          }
        }
      }
    }
    return returnType;
  }

  private static boolean isDynamicallyEvaluated(
      @NotNull Collection<PyNamedParameter> parameters, @NotNull TypeEvalContext context) {
    for (PyNamedParameter parameter : parameters) {
      final PyType type = context.getType(parameter);
      if (type instanceof PyDynamicallyEvaluatedType) {
        return true;
      }
    }
    return false;
  }

  @Nullable
  private Ref<? extends PyType> getYieldStatementType(@NotNull final TypeEvalContext context) {
    Ref<PyType> elementType = null;
    final PyBuiltinCache cache = PyBuiltinCache.getInstance(this);
    final PyStatementList statements = getStatementList();
    final Set<PyType> types = new LinkedHashSet<>();
    statements.accept(
        new PyRecursiveElementVisitor() {
          @Override
          public void visitPyYieldExpression(PyYieldExpression node) {
            final PyExpression expr = node.getExpression();
            final PyType type = expr != null ? context.getType(expr) : null;
            if (node.isDelegating() && type instanceof PyCollectionType) {
              final PyCollectionType collectionType = (PyCollectionType) type;
              // TODO: Select the parameter types that matches T in Iterable[T]
              final List<PyType> elementTypes = collectionType.getElementTypes(context);
              types.add(elementTypes.isEmpty() ? null : elementTypes.get(0));
            } else {
              types.add(type);
            }
          }

          @Override
          public void visitPyFunction(PyFunction node) {
            // Ignore nested functions
          }
        });
    final int n = types.size();
    if (n == 1) {
      elementType = Ref.create(types.iterator().next());
    } else if (n > 0) {
      elementType = Ref.create(PyUnionType.union(types));
    }
    if (elementType != null) {
      final PyClass generator = cache.getClass(PyNames.FAKE_GENERATOR);
      if (generator != null) {
        final List<PyType> parameters =
            Arrays.asList(elementType.get(), null, getReturnStatementType(context));
        return Ref.create(new PyCollectionTypeImpl(generator, false, parameters));
      }
    }
    if (!types.isEmpty()) {
      return Ref.create(null);
    }
    return null;
  }

  @Nullable
  public PyType getReturnStatementType(TypeEvalContext typeEvalContext) {
    final ReturnVisitor visitor = new ReturnVisitor(this, typeEvalContext);
    final PyStatementList statements = getStatementList();
    statements.accept(visitor);
    if (isGeneratedStub() && !visitor.myHasReturns) {
      if (PyNames.INIT.equals(getName())) {
        return PyNoneType.INSTANCE;
      }
      return null;
    }
    return visitor.result();
  }

  @Nullable
  private PyType createCoroutineType(@Nullable PyType returnType) {
    final PyBuiltinCache cache = PyBuiltinCache.getInstance(this);
    if (returnType instanceof PyClassLikeType
        && PyNames.FAKE_COROUTINE.equals(((PyClassLikeType) returnType).getClassQName())) {
      return returnType;
    }
    final PyClass generator = cache.getClass(PyNames.FAKE_COROUTINE);
    return generator != null
        ? new PyCollectionTypeImpl(generator, false, Collections.singletonList(returnType))
        : null;
  }

  public PyFunction asMethod() {
    if (getContainingClass() != null) {
      return this;
    } else {
      return null;
    }
  }

  @Nullable
  @Override
  public PyType getReturnTypeFromDocString() {
    final String typeName = extractReturnType();
    return typeName != null ? PyTypeParser.getTypeByName(this, typeName) : null;
  }

  @Nullable
  @Override
  public String getDeprecationMessage() {
    PyFunctionStub stub = getStub();
    if (stub != null) {
      return stub.getDeprecationMessage();
    }
    return extractDeprecationMessage();
  }

  @Nullable
  public String extractDeprecationMessage() {
    PyStatementList statementList = getStatementList();
    return extractDeprecationMessage(Arrays.asList(statementList.getStatements()));
  }

  @Override
  public PyType getType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) {
    for (PyTypeProvider provider : Extensions.getExtensions(PyTypeProvider.EP_NAME)) {
      final PyType type = provider.getCallableType(this, context);
      if (type != null) {
        return type;
      }
    }
    final PyFunctionTypeImpl type = new PyFunctionTypeImpl(this);
    if (PyKnownDecoratorUtil.hasUnknownDecorator(this, context) && getProperty() == null) {
      return PyUnionType.createWeakType(type);
    }
    return type;
  }

  @Nullable
  public static String extractDeprecationMessage(List<PyStatement> statements) {
    for (PyStatement statement : statements) {
      if (statement instanceof PyExpressionStatement) {
        PyExpressionStatement expressionStatement = (PyExpressionStatement) statement;
        if (expressionStatement.getExpression() instanceof PyCallExpression) {
          PyCallExpression callExpression = (PyCallExpression) expressionStatement.getExpression();
          if (callExpression.isCalleeText(PyNames.WARN)) {
            PyReferenceExpression warningClass =
                callExpression.getArgument(1, PyReferenceExpression.class);
            if (warningClass != null
                && (PyNames.DEPRECATION_WARNING.equals(warningClass.getReferencedName())
                    || PyNames.PENDING_DEPRECATION_WARNING.equals(
                        warningClass.getReferencedName()))) {
              return PyPsiUtils.strValue(callExpression.getArguments()[0]);
            }
          }
        }
      }
    }
    return null;
  }

  @Override
  public String getDocStringValue() {
    final PyFunctionStub stub = getStub();
    if (stub != null) {
      return stub.getDocString();
    }
    return DocStringUtil.getDocStringValue(this);
  }

  @Nullable
  @Override
  public StructuredDocString getStructuredDocString() {
    return CachedValuesManager.getCachedValue(this, myCachedStructuredDocStringProvider);
  }

  private boolean isGeneratedStub() {
    VirtualFile vFile = getContainingFile().getVirtualFile();
    if (vFile != null) {
      vFile = vFile.getParent();
      if (vFile != null) {
        vFile = vFile.getParent();
        if (vFile != null && vFile.getName().equals(PythonSdkType.SKELETON_DIR_NAME)) {
          return true;
        }
      }
    }
    return false;
  }

  @Nullable
  private String extractReturnType() {
    final String ARROW = "->";
    final StructuredDocString structuredDocString = getStructuredDocString();
    if (structuredDocString != null) {
      return structuredDocString.getReturnType();
    }
    final String docString = getDocStringValue();
    if (docString != null && docString.contains(ARROW)) {
      final List<String> lines = StringUtil.split(docString, "\n");
      while (lines.size() > 0 && lines.get(0).trim().length() == 0) {
        lines.remove(0);
      }
      if (lines.size() > 1 && lines.get(1).trim().length() == 0) {
        String firstLine = lines.get(0);
        int pos = firstLine.lastIndexOf(ARROW);
        if (pos >= 0) {
          return firstLine.substring(pos + 2).trim();
        }
      }
    }
    return null;
  }

  private static class ReturnVisitor extends PyRecursiveElementVisitor {
    private final PyFunction myFunction;
    private final TypeEvalContext myContext;
    private PyType myResult = null;
    private boolean myHasReturns = false;
    private boolean myHasRaises = false;

    public ReturnVisitor(PyFunction function, final TypeEvalContext context) {
      myFunction = function;
      myContext = context;
    }

    @Override
    public void visitPyReturnStatement(PyReturnStatement node) {
      if (PsiTreeUtil.getParentOfType(node, ScopeOwner.class, true) == myFunction) {
        final PyExpression expr = node.getExpression();
        PyType returnType;
        returnType = expr == null ? PyNoneType.INSTANCE : myContext.getType(expr);
        if (!myHasReturns) {
          myResult = returnType;
          myHasReturns = true;
        } else {
          myResult = PyUnionType.union(myResult, returnType);
        }
      }
    }

    @Override
    public void visitPyRaiseStatement(PyRaiseStatement node) {
      myHasRaises = true;
    }

    @Nullable
    PyType result() {
      return myHasReturns || myHasRaises ? myResult : PyNoneType.INSTANCE;
    }
  }

  @Override
  protected void acceptPyVisitor(PyElementVisitor pyVisitor) {
    pyVisitor.visitPyFunction(this);
  }

  public int getTextOffset() {
    final ASTNode name = getNameNode();
    return name != null ? name.getStartOffset() : super.getTextOffset();
  }

  public PyStringLiteralExpression getDocStringExpression() {
    final PyStatementList stmtList = getStatementList();
    return DocStringUtil.findDocStringExpression(stmtList);
  }

  @Override
  public String toString() {
    return super.toString() + "('" + getName() + "')";
  }

  public void subtreeChanged() {
    super.subtreeChanged();
    ControlFlowCache.clear(this);
  }

  public Property getProperty() {
    final PyClass containingClass = getContainingClass();
    if (containingClass != null) {
      return containingClass.findPropertyByCallable(this);
    }
    return null;
  }

  @Override
  public PyAnnotation getAnnotation() {
    return getStubOrPsiChild(PyElementTypes.ANNOTATION);
  }

  @Nullable
  @Override
  public PsiComment getTypeComment() {
    final PsiComment inlineComment = PyUtil.getCommentOnHeaderLine(this);
    if (inlineComment != null
        && PyTypingTypeProvider.getTypeCommentValue(inlineComment.getText()) != null) {
      return inlineComment;
    }

    final PyStatementList statements = getStatementList();
    if (statements.getStatements().length != 0) {
      final PsiComment comment = as(statements.getFirstChild(), PsiComment.class);
      if (comment != null && PyTypingTypeProvider.getTypeCommentValue(comment.getText()) != null) {
        return comment;
      }
    }
    return null;
  }

  @Nullable
  @Override
  public String getTypeCommentAnnotation() {
    final PyFunctionStub stub = getStub();
    if (stub != null) {
      return stub.getTypeComment();
    }
    final PsiComment comment = getTypeComment();
    if (comment != null) {
      return PyTypingTypeProvider.getTypeCommentValue(comment.getText());
    }
    return null;
  }

  @NotNull
  @Override
  public SearchScope getUseScope() {
    final ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(this);
    if (scopeOwner instanceof PyFunction) {
      return new LocalSearchScope(scopeOwner);
    }
    return super.getUseScope();
  }

  /**
   * Looks for two standard decorators to a function, or a wrapping assignment that closely follows
   * it.
   *
   * @return a flag describing what was detected.
   */
  @Nullable
  public Modifier getModifier() {
    final String deconame = getClassOrStaticMethodDecorator();
    if (PyNames.CLASSMETHOD.equals(deconame)) {
      return CLASSMETHOD;
    } else if (PyNames.STATICMETHOD.equals(deconame)) {
      return STATICMETHOD;
    }
    // implicit staticmethod __new__
    final PyClass cls = getContainingClass();
    if (cls != null && PyNames.NEW.equals(getName()) && cls.isNewStyleClass(null)) {
      return STATICMETHOD;
    }
    //
    if (getStub() != null) {
      return getWrappersFromStub();
    }
    final String funcName = getName();
    if (funcName != null) {
      PyAssignmentStatement currentAssignment =
          PsiTreeUtil.getNextSiblingOfType(this, PyAssignmentStatement.class);
      while (currentAssignment != null) {
        final String modifier =
            currentAssignment
                .getTargetsToValuesMapping()
                .stream()
                .filter(
                    pair ->
                        pair.getFirst() instanceof PyTargetExpression
                            && funcName.equals(pair.getFirst().getName()))
                .filter(pair -> pair.getSecond() instanceof PyCallExpression)
                .map(
                    pair ->
                        interpretAsModifierWrappingCall((PyCallExpression) pair.getSecond(), this))
                .filter(interpreted -> interpreted != null && interpreted.getSecond() == this)
                .map(interpreted -> interpreted.getFirst())
                .filter(
                    wrapperName ->
                        PyNames.CLASSMETHOD.equals(wrapperName)
                            || PyNames.STATICMETHOD.equals(wrapperName))
                .findAny()
                .orElse(null);

        if (PyNames.CLASSMETHOD.equals(modifier)) {
          return CLASSMETHOD;
        } else if (PyNames.STATICMETHOD.equals(modifier)) {
          return STATICMETHOD;
        }

        currentAssignment =
            PsiTreeUtil.getNextSiblingOfType(currentAssignment, PyAssignmentStatement.class);
      }
    }
    return null;
  }

  @Override
  public boolean isAsync() {
    final PyFunctionStub stub = getStub();
    if (stub != null) {
      return stub.isAsync();
    }
    return getNode().findChildByType(PyTokenTypes.ASYNC_KEYWORD) != null;
  }

  @Override
  public boolean isAsyncAllowed() {
    final LanguageLevel languageLevel = LanguageLevel.forElement(this);
    final String functionName = getName();

    return languageLevel.isAtLeast(LanguageLevel.PYTHON35)
        && (functionName == null
            || ArrayUtil.contains(
                functionName, PyNames.AITER, PyNames.ANEXT, PyNames.AENTER, PyNames.AEXIT)
            || !PyNames.getBuiltinMethods(languageLevel).containsKey(functionName));
  }

  @Nullable
  private Modifier getWrappersFromStub() {
    final StubElement parentStub = getStub().getParentStub();
    final List childrenStubs = parentStub.getChildrenStubs();
    int index = childrenStubs.indexOf(getStub());
    if (index >= 0 && index < childrenStubs.size() - 1) {
      StubElement nextStub = (StubElement) childrenStubs.get(index + 1);
      if (nextStub instanceof PyTargetExpressionStub) {
        final PyTargetExpressionStub targetExpressionStub = (PyTargetExpressionStub) nextStub;
        if (targetExpressionStub.getInitializerType()
            == PyTargetExpressionStub.InitializerType.CallExpression) {
          final QualifiedName qualifiedName = targetExpressionStub.getInitializer();
          if (QualifiedName.fromComponents(PyNames.CLASSMETHOD).equals(qualifiedName)) {
            return CLASSMETHOD;
          }
          if (QualifiedName.fromComponents(PyNames.STATICMETHOD).equals(qualifiedName)) {
            return STATICMETHOD;
          }
        }
      }
    }
    return null;
  }

  /**
   * When a function is decorated many decorators, finds the deepest builtin decorator:
   *
   * <pre>
   * &#x40;foo
   * &#x40;classmethod <b># &lt;-- that's it</b>
   * &#x40;bar
   * def moo(cls):
   * &nbsp;&nbsp;pass
   * </pre>
   *
   * @return name of the built-in decorator, or null (even if there are non-built-in decorators).
   */
  @Nullable
  private String getClassOrStaticMethodDecorator() {
    PyDecoratorList decolist = getDecoratorList();
    if (decolist != null) {
      PyDecorator[] decos = decolist.getDecorators();
      if (decos.length > 0) {
        for (int i = decos.length - 1; i >= 0; i -= 1) {
          PyDecorator deco = decos[i];
          String deconame = deco.getName();
          if (PyNames.CLASSMETHOD.equals(deconame) || PyNames.STATICMETHOD.equals(deconame)) {
            return deconame;
          }
          for (PyKnownDecoratorProvider provider :
              PyUtil.KnownDecoratorProviderHolder.KNOWN_DECORATOR_PROVIDERS) {
            String name = provider.toKnownDecorator(deconame);
            if (name != null) {
              return name;
            }
          }
        }
      }
    }
    return null;
  }

  @Nullable
  @Override
  public String getQualifiedName() {
    return QualifiedNameFinder.getQualifiedName(this);
  }

  @NotNull
  @Override
  public List<PyAssignmentStatement> findAttributes() {
    /**
     * TODO: This method if insanely heavy since it unstubs foreign files. Need to save stubs and
     * use them somehow.
     */
    return CachedValuesManager.getManager(getProject())
        .getCachedValue(
            this,
            ATTRIBUTES_KEY,
            () -> {
              final List<PyAssignmentStatement> result = findAttributesStatic(this);
              return CachedValueProvider.Result.create(
                  result, PsiModificationTracker.MODIFICATION_COUNT);
            },
            false);
  }

  /** @param self should be this */
  @NotNull
  private static List<PyAssignmentStatement> findAttributesStatic(@NotNull final PsiElement self) {
    final List<PyAssignmentStatement> result = new ArrayList<>();
    for (final PyAssignmentStatement statement :
        new PsiQuery(self).siblings(PyAssignmentStatement.class).getElements()) {
      for (final PyQualifiedExpression targetExpression :
          new PsiQuery(statement.getTargets()).filter(PyQualifiedExpression.class).getElements()) {
        final PyExpression qualifier = targetExpression.getQualifier();
        if (qualifier == null) {
          continue;
        }
        final PsiReference qualifierReference = qualifier.getReference();
        if (qualifierReference == null) {
          continue;
        }
        if (qualifierReference.isReferenceTo(self)) {
          result.add(statement);
        }
      }
    }
    return result;
  }

  @NotNull
  @Override
  public ProtectionLevel getProtectionLevel() {
    final int underscoreLevels = PyUtil.getInitialUnderscores(getName());
    for (final ProtectionLevel level : ProtectionLevel.values()) {
      if (level.getUnderscoreLevel() == underscoreLevels) {
        return level;
      }
    }
    return ProtectionLevel.PRIVATE;
  }
}
public class FileDocumentManagerImpl extends FileDocumentManager
    implements ApplicationComponent,
        VirtualFileListener,
        ProjectManagerListener,
        SafeWriteRequestor {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl");

  private static final Key<String> LINE_SEPARATOR_KEY = Key.create("LINE_SEPARATOR_KEY");
  public static final Key<Reference<Document>> DOCUMENT_KEY = Key.create("DOCUMENT_KEY");
  private static final Key<VirtualFile> FILE_KEY = Key.create("FILE_KEY");

  private final Set<Document> myUnsavedDocuments = new ConcurrentHashSet<Document>();

  private final MessageBus myBus;

  private static final Object lock = new Object();
  private final FileDocumentManagerListener myMultiCaster;
  private final TrailingSpacesStripper myTrailingSpacesStripper = new TrailingSpacesStripper();

  private boolean myOnClose = false;

  public FileDocumentManagerImpl(
      @NotNull VirtualFileManager virtualFileManager, @NotNull ProjectManager projectManager) {
    virtualFileManager.addVirtualFileListener(this);
    projectManager.addProjectManagerListener(this);

    myBus = ApplicationManager.getApplication().getMessageBus();
    InvocationHandler handler =
        new InvocationHandler() {
          @Nullable
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            multiCast(method, args);
            return null;
          }
        };

    final ClassLoader loader = FileDocumentManagerListener.class.getClassLoader();
    myMultiCaster =
        (FileDocumentManagerListener)
            Proxy.newProxyInstance(
                loader, new Class[] {FileDocumentManagerListener.class}, handler);
  }

  @SuppressWarnings("OverlyBroadCatchBlock")
  private void multiCast(@NotNull Method method, Object[] args) {
    try {
      method.invoke(myBus.syncPublisher(AppTopics.FILE_DOCUMENT_SYNC), args);
    } catch (ClassCastException e) {
      LOG.error("Arguments: " + Arrays.toString(args), e);
    } catch (Exception e) {
      LOG.error(e);
    }

    // Allows pre-save document modification
    for (FileDocumentManagerListener listener : getListeners()) {
      try {
        method.invoke(listener, args);
      } catch (Exception e) {
        LOG.error(e);
      }
    }

    // stripping trailing spaces
    try {
      method.invoke(myTrailingSpacesStripper, args);
    } catch (Exception e) {
      LOG.error(e);
    }
  }

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

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {}

  @Override
  @Nullable
  public Document getDocument(@NotNull final VirtualFile file) {
    DocumentEx document = (DocumentEx) getCachedDocument(file);
    if (document == null) {
      if (file.isDirectory()
          || isBinaryWithoutDecompiler(file)
          || SingleRootFileViewProvider.isTooLargeForContentLoading(file)) {
        return null;
      }
      final CharSequence text = LoadTextUtil.loadText(file);

      synchronized (lock) {
        document = (DocumentEx) getCachedDocument(file);
        if (document != null) return document; // Double checking

        document = (DocumentEx) createDocument(text);
        document.setModificationStamp(file.getModificationStamp());
        final FileType fileType = file.getFileType();
        document.setReadOnly(!file.isWritable() || fileType.isBinary());
        file.putUserData(DOCUMENT_KEY, new WeakReference<Document>(document));
        document.putUserData(FILE_KEY, file);

        if (!(file instanceof LightVirtualFile
            || file.getFileSystem() instanceof DummyFileSystem)) {
          document.addDocumentListener(
              new DocumentAdapter() {
                @Override
                public void documentChanged(DocumentEvent e) {
                  final Document document = e.getDocument();
                  myUnsavedDocuments.add(document);
                  final Runnable currentCommand =
                      CommandProcessor.getInstance().getCurrentCommand();
                  Project project =
                      currentCommand == null
                          ? null
                          : CommandProcessor.getInstance().getCurrentCommandProject();
                  String lineSeparator = CodeStyleFacade.getInstance(project).getLineSeparator();
                  document.putUserData(LINE_SEPARATOR_KEY, lineSeparator);

                  // avoid documents piling up during batch processing
                  if (areTooManyDocumentsInTheQueue(myUnsavedDocuments)) {
                    saveAllDocumentsLater();
                  }
                }
              });
        }
      }

      myMultiCaster.fileContentLoaded(file, document);
    }

    return document;
  }

  public static boolean areTooManyDocumentsInTheQueue(Collection<Document> documents) {
    if (documents.size() > 100) return true;
    int totalSize = 0;
    for (Document document : documents) {
      totalSize += document.getTextLength();
      if (totalSize > 10 * FileUtilRt.MEGABYTE) return true;
    }
    return false;
  }

  private static Document createDocument(final CharSequence text) {
    return EditorFactory.getInstance().createDocument(text);
  }

  @Override
  @Nullable
  public Document getCachedDocument(@NotNull VirtualFile file) {
    Reference<Document> reference = file.getUserData(DOCUMENT_KEY);
    Document document = reference == null ? null : reference.get();

    if (document != null && isBinaryWithoutDecompiler(file)) {
      file.putUserData(DOCUMENT_KEY, null);
      document.putUserData(FILE_KEY, null);
      return null;
    }

    return document;
  }

  public static void registerDocument(
      @NotNull final Document document, @NotNull VirtualFile virtualFile) {
    synchronized (lock) {
      virtualFile.putUserData(
          DOCUMENT_KEY,
          new SoftReference<Document>(document) {
            @Override
            public Document get() {
              return document;
            }
          });
      document.putUserData(FILE_KEY, virtualFile);
    }
  }

  @Override
  @Nullable
  public VirtualFile getFile(@NotNull Document document) {
    return document.getUserData(FILE_KEY);
  }

  @TestOnly
  public void dropAllUnsavedDocuments() {
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      throw new RuntimeException("This method is only for test mode!");
    }
    ApplicationManager.getApplication().assertWriteAccessAllowed();
    if (!myUnsavedDocuments.isEmpty()) {
      myUnsavedDocuments.clear();
      fireUnsavedDocumentsDropped();
    }
  }

  private void saveAllDocumentsLater() {
    // later because some document might have been blocked by PSI right now
    ApplicationManager.getApplication()
        .invokeLater(
            new Runnable() {
              @Override
              public void run() {
                if (ApplicationManager.getApplication().isDisposed()) {
                  return;
                }
                final Document[] unsavedDocuments = getUnsavedDocuments();
                for (Document document : unsavedDocuments) {
                  VirtualFile file = getFile(document);
                  if (file == null) continue;
                  Project project = ProjectUtil.guessProjectForFile(file);
                  if (project == null) continue;
                  if (PsiDocumentManager.getInstance(project).isDocumentBlockedByPsi(document))
                    continue;

                  saveDocument(document);
                }
              }
            });
  }

  @Override
  public void saveAllDocuments() {
    ApplicationManager.getApplication().assertIsDispatchThread();

    myMultiCaster.beforeAllDocumentsSaving();
    if (myUnsavedDocuments.isEmpty()) return;

    final Map<Document, IOException> failedToSave = new HashMap<Document, IOException>();
    final Set<Document> vetoed = new HashSet<Document>();
    while (true) {
      int count = 0;

      for (Document document : myUnsavedDocuments) {
        if (failedToSave.containsKey(document)) continue;
        if (vetoed.contains(document)) continue;
        try {
          doSaveDocument(document);
        } catch (IOException e) {
          //noinspection ThrowableResultOfMethodCallIgnored
          failedToSave.put(document, e);
        } catch (SaveVetoException e) {
          vetoed.add(document);
        }
        count++;
      }

      if (count == 0) break;
    }

    if (!failedToSave.isEmpty()) {
      handleErrorsOnSave(failedToSave);
    }
  }

  @Override
  public void saveDocument(@NotNull final Document document) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (!myUnsavedDocuments.contains(document)) return;

    try {
      doSaveDocument(document);
    } catch (IOException e) {
      handleErrorsOnSave(Collections.singletonMap(document, e));
    } catch (SaveVetoException ignored) {
    }
  }

  @Override
  public void saveDocumentAsIs(@NotNull Document document) {
    EditorSettingsExternalizable editorSettings = EditorSettingsExternalizable.getInstance();

    String trailer = editorSettings.getStripTrailingSpaces();
    boolean ensureEOLonEOF = editorSettings.isEnsureNewLineAtEOF();
    editorSettings.setStripTrailingSpaces(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_NONE);
    editorSettings.setEnsureNewLineAtEOF(false);
    try {
      saveDocument(document);
    } finally {
      editorSettings.setStripTrailingSpaces(trailer);
      editorSettings.setEnsureNewLineAtEOF(ensureEOLonEOF);
    }
  }

  private static class SaveVetoException extends Exception {}

  private void doSaveDocument(@NotNull final Document document)
      throws IOException, SaveVetoException {
    VirtualFile file = getFile(document);

    if (file == null
        || file instanceof LightVirtualFile
        || file.isValid() && !isFileModified(file)) {
      removeFromUnsaved(document);
      return;
    }

    if (file.isValid() && needsRefresh(file)) {
      file.refresh(false, false);
      if (!myUnsavedDocuments.contains(document)) return;
    }

    for (FileDocumentSynchronizationVetoer vetoer :
        Extensions.getExtensions(FileDocumentSynchronizationVetoer.EP_NAME)) {
      if (!vetoer.maySaveDocument(document)) {
        throw new SaveVetoException();
      }
    }

    final AccessToken token =
        ApplicationManager.getApplication().acquireWriteActionLock(getClass());
    try {
      doSaveDocumentInWriteAction(document, file);
    } finally {
      token.finish();
    }
  }

  private void doSaveDocumentInWriteAction(@NotNull Document document, @NotNull VirtualFile file)
      throws IOException {
    if (!file.isValid()) {
      removeFromUnsaved(document);
      return;
    }

    if (!file.equals(getFile(document))) {
      registerDocument(document, file);
    }

    if (!isSaveNeeded(document, file)) {
      if (document instanceof DocumentEx) {
        ((DocumentEx) document).setModificationStamp(file.getModificationStamp());
      }
      removeFromUnsaved(document);
      updateModifiedProperty(file);
      return;
    }

    myMultiCaster.beforeDocumentSaving(document);

    LOG.assertTrue(file.isValid());

    String text = document.getText();
    String lineSeparator = getLineSeparator(document, file);
    if (!lineSeparator.equals("\n")) {
      text = StringUtil.convertLineSeparators(text, lineSeparator);
    }

    Project project = ProjectLocator.getInstance().guessProjectForFile(file);
    LoadTextUtil.write(project, file, this, text, document.getModificationStamp());

    myUnsavedDocuments.remove(document);
    LOG.assertTrue(!myUnsavedDocuments.contains(document));
    myTrailingSpacesStripper.clearLineModificationFlags(document);
  }

  private static void updateModifiedProperty(@NotNull VirtualFile file) {
    for (Project project : ProjectManager.getInstance().getOpenProjects()) {
      FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
      for (FileEditor editor : fileEditorManager.getAllEditors(file)) {
        if (editor instanceof TextEditorImpl) {
          ((TextEditorImpl) editor).updateModifiedProperty();
        }
      }
    }
  }

  private void removeFromUnsaved(@NotNull Document document) {
    myUnsavedDocuments.remove(document);
    fireUnsavedDocumentsDropped();
    LOG.assertTrue(!myUnsavedDocuments.contains(document));
  }

  private static boolean isSaveNeeded(@NotNull Document document, @NotNull VirtualFile file)
      throws IOException {
    if (file.getFileType().isBinary()
        || document.getTextLength() > 1000 * 1000) { // don't compare if the file is too big
      return true;
    }

    byte[] bytes = file.contentsToByteArray();
    CharSequence loaded = LoadTextUtil.getTextByBinaryPresentation(bytes, file, false, false);

    return !Comparing.equal(document.getCharsSequence(), loaded);
  }

  private static boolean needsRefresh(final VirtualFile file) {
    final VirtualFileSystem fs = file.getFileSystem();
    return fs instanceof NewVirtualFileSystem
        && file.getTimeStamp() != ((NewVirtualFileSystem) fs).getTimeStamp(file);
  }

  @NotNull
  public static String getLineSeparator(@NotNull Document document, @NotNull VirtualFile file) {
    String lineSeparator = LoadTextUtil.getDetectedLineSeparator(file);
    if (lineSeparator == null) {
      lineSeparator = document.getUserData(LINE_SEPARATOR_KEY);
      assert lineSeparator != null : document;
    }
    return lineSeparator;
  }

  @Override
  @NotNull
  public String getLineSeparator(@Nullable VirtualFile file, @Nullable Project project) {
    String lineSeparator = file == null ? null : LoadTextUtil.getDetectedLineSeparator(file);
    if (lineSeparator == null) {
      CodeStyleFacade settingsManager =
          project == null ? CodeStyleFacade.getInstance() : CodeStyleFacade.getInstance(project);
      lineSeparator = settingsManager.getLineSeparator();
    }
    return lineSeparator;
  }

  @Override
  public boolean requestWriting(@NotNull Document document, Project project) {
    final VirtualFile file = getInstance().getFile(document);
    if (project != null && file != null && file.isValid()) {
      return !file.getFileType().isBinary()
          && ReadonlyStatusHandler.ensureFilesWritable(project, file);
    }
    if (document.isWritable()) {
      return true;
    }
    document.fireReadOnlyModificationAttempt();
    return false;
  }

  @Override
  public void reloadFiles(final VirtualFile... files) {
    for (VirtualFile file : files) {
      if (file.exists()) {
        final Document doc = getCachedDocument(file);
        if (doc != null) {
          reloadFromDisk(doc);
        }
      }
    }
  }

  @Override
  @NotNull
  public Document[] getUnsavedDocuments() {
    if (myUnsavedDocuments.isEmpty()) {
      return Document.EMPTY_ARRAY;
    }

    List<Document> list = new ArrayList<Document>(myUnsavedDocuments);
    return list.toArray(new Document[list.size()]);
  }

  @Override
  public boolean isDocumentUnsaved(@NotNull Document document) {
    return myUnsavedDocuments.contains(document);
  }

  @Override
  public boolean isFileModified(@NotNull VirtualFile file) {
    final Document doc = getCachedDocument(file);
    return doc != null
        && isDocumentUnsaved(doc)
        && doc.getModificationStamp() != file.getModificationStamp();
  }

  @Override
  public void propertyChanged(final VirtualFilePropertyEvent event) {
    if (VirtualFile.PROP_WRITABLE.equals(event.getPropertyName())) {
      final VirtualFile file = event.getFile();
      final Document document = getCachedDocument(file);
      if (document == null) return;

      ApplicationManager.getApplication()
          .runWriteAction(
              new ExternalChangeAction() {
                @Override
                public void run() {
                  document.setReadOnly(!event.getFile().isWritable());
                }
              });
      // myUnsavedDocuments.remove(document); //?
    }
  }

  private static boolean isBinaryWithDecompiler(VirtualFile file) {
    final FileType ft = file.getFileType();
    return ft.isBinary() && BinaryFileTypeDecompilers.INSTANCE.forFileType(ft) != null;
  }

  private static boolean isBinaryWithoutDecompiler(VirtualFile file) {
    final FileType ft = file.getFileType();
    return ft.isBinary() && BinaryFileTypeDecompilers.INSTANCE.forFileType(ft) == null;
  }

  @Override
  public void contentsChanged(VirtualFileEvent event) {
    if (event.isFromSave()) return;
    final VirtualFile file = event.getFile();
    final Document document = getCachedDocument(file);
    if (document == null) {
      myMultiCaster.fileWithNoDocumentChanged(file);
      return;
    }

    if (isBinaryWithDecompiler(file)) {
      myMultiCaster.fileWithNoDocumentChanged(
          file); // This will generate PSI event at FileManagerImpl
    }

    long documentStamp = document.getModificationStamp();
    long oldFileStamp = event.getOldModificationStamp();
    if (documentStamp != oldFileStamp) {
      LOG.info("reload from disk?");
      LOG.info("  documentStamp:" + documentStamp);
      LOG.info("  oldFileStamp:" + oldFileStamp);

      if (file.isValid() && askReloadFromDisk(file, document)) {
        reloadFromDisk(document);
      }
    } else {
      reloadFromDisk(document);
    }
  }

  @Override
  public void reloadFromDisk(@NotNull final Document document) {
    ApplicationManager.getApplication().assertIsDispatchThread();

    final VirtualFile file = getFile(document);
    assert file != null;

    if (!fireBeforeFileContentReload(file, document)) {
      return;
    }

    final Project project = ProjectLocator.getInstance().guessProjectForFile(file);
    CommandProcessor.getInstance()
        .executeCommand(
            project,
            new Runnable() {
              @Override
              public void run() {
                ApplicationManager.getApplication()
                    .runWriteAction(
                        new ExternalChangeAction.ExternalDocumentChange(document, project) {
                          @Override
                          public void run() {
                            boolean wasWritable = document.isWritable();
                            DocumentEx documentEx = (DocumentEx) document;
                            documentEx.setReadOnly(false);
                            LoadTextUtil.setCharsetWasDetectedFromBytes(file, null);
                            documentEx.replaceText(
                                LoadTextUtil.loadText(file), file.getModificationStamp());
                            documentEx.setReadOnly(!wasWritable);
                          }
                        });
              }
            },
            UIBundle.message("file.cache.conflict.action"),
            null,
            UndoConfirmationPolicy.REQUEST_CONFIRMATION);

    myUnsavedDocuments.remove(document);

    myMultiCaster.fileContentReloaded(file, document);
  }

  protected boolean askReloadFromDisk(final VirtualFile file, final Document document) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (!isDocumentUnsaved(document)) return true;

    String message = UIBundle.message("file.cache.conflict.message.text", file.getPresentableUrl());
    if (ApplicationManager.getApplication().isUnitTestMode()) throw new RuntimeException(message);
    final DialogBuilder builder = new DialogBuilder((Project) null);
    builder.setCenterPanel(new JLabel(message, Messages.getQuestionIcon(), SwingConstants.CENTER));
    builder.addOkAction().setText(UIBundle.message("file.cache.conflict.load.fs.changes.button"));
    builder
        .addCancelAction()
        .setText(UIBundle.message("file.cache.conflict.keep.memory.changes.button"));
    builder.addAction(
        new AbstractAction(UIBundle.message("file.cache.conflict.show.difference.button")) {
          @Override
          public void actionPerformed(ActionEvent e) {
            String title =
                UIBundle.message(
                    "file.cache.conflict.for.file.dialog.title", file.getPresentableUrl());
            final ProjectEx project =
                (ProjectEx) ProjectLocator.getInstance().guessProjectForFile(file);

            SimpleDiffRequest request = new SimpleDiffRequest(project, title);
            FileType fileType = file.getFileType();
            String fsContent = LoadTextUtil.loadText(file).toString();
            request.setContents(
                new SimpleContent(fsContent, fileType),
                new DocumentContent(project, document, fileType));
            request.setContentTitles(
                UIBundle.message("file.cache.conflict.diff.content.file.system.content"),
                UIBundle.message("file.cache.conflict.diff.content.memory.content"));
            DialogBuilder diffBuilder = new DialogBuilder(project);
            DiffPanelImpl diffPanel =
                (DiffPanelImpl)
                    DiffManager.getInstance()
                        .createDiffPanel(diffBuilder.getWindow(), project, diffBuilder, null);
            diffPanel.getOptions().setShowSourcePolicy(DiffPanelOptions.ShowSourcePolicy.DONT_SHOW);
            diffBuilder.setCenterPanel(diffPanel.getComponent());
            diffBuilder.setDimensionServiceKey("FileDocumentManager.FileCacheConflict");
            diffPanel.setDiffRequest(request);
            diffBuilder
                .addOkAction()
                .setText(UIBundle.message("file.cache.conflict.save.changes.button"));
            diffBuilder.addCancelAction();
            diffBuilder.setTitle(title);
            if (diffBuilder.show() == DialogWrapper.OK_EXIT_CODE) {
              builder.getDialogWrapper().close(DialogWrapper.CANCEL_EXIT_CODE);
            }
          }
        });
    builder.setTitle(UIBundle.message("file.cache.conflict.dialog.title"));
    builder.setButtonsAlignment(SwingConstants.CENTER);
    builder.setHelpId("reference.dialogs.fileCacheConflict");
    return builder.show() == 0;
  }

  @Override
  public void fileCreated(VirtualFileEvent event) {}

  @Override
  public void fileDeleted(VirtualFileEvent event) {
    Document doc = getCachedDocument(event.getFile());
    if (doc != null) {
      myTrailingSpacesStripper.documentDeleted(doc);
    }
  }

  @Override
  public void fileMoved(VirtualFileMoveEvent event) {}

  @Override
  public void fileCopied(VirtualFileCopyEvent event) {
    fileCreated(event);
  }

  @Override
  public void beforePropertyChange(VirtualFilePropertyEvent event) {}

  @Override
  public void beforeContentsChange(VirtualFileEvent event) {}

  @Override
  public void beforeFileDeletion(VirtualFileEvent event) {
    /*
    if (!event.isFromRefresh()) {
      VirtualFile file = event.getFile();
      if (file.getFileSystem() instanceof TempFileSystem) {
        return; //hack: this fs fails in getChildren during beforeFileDeletion
      }
      VfsUtilCore.visitChildrenRecursively(file, new VirtualFileVisitor() {
        @Override
        public boolean visitFile(@NotNull VirtualFile file) {
          Document document = getCachedDocument(file);
          if (document != null) {
            removeFromUnsaved(document);
          }
          return true;
        }
      });
    }
    */

  }

  @Override
  public void beforeFileMovement(VirtualFileMoveEvent event) {}

  @Override
  public void projectOpened(Project project) {}

  @Override
  public boolean canCloseProject(Project project) {
    if (!myUnsavedDocuments.isEmpty()) {
      myOnClose = true;
      try {
        saveAllDocuments();
      } finally {
        myOnClose = false;
      }
    }
    return myUnsavedDocuments.isEmpty();
  }

  @Override
  public void projectClosed(Project project) {}

  @Override
  public void projectClosing(Project project) {}

  private void fireUnsavedDocumentsDropped() {
    myMultiCaster.unsavedDocumentsDropped();
  }

  private boolean fireBeforeFileContentReload(final VirtualFile file, @NotNull Document document) {
    for (FileDocumentSynchronizationVetoer vetoer :
        Extensions.getExtensions(FileDocumentSynchronizationVetoer.EP_NAME)) {
      try {
        if (!vetoer.mayReloadFileContent(file, document)) {
          return false;
        }
      } catch (Exception e) {
        LOG.error(e);
      }
    }

    myMultiCaster.beforeFileContentReload(file, document);
    return true;
  }

  @NotNull
  protected FileDocumentManagerListener[] getListeners() {
    return FileDocumentManagerListener.EP_NAME.getExtensions();
  }

  protected void handleErrorsOnSave(@NotNull Map<Document, IOException> failures) {
    for (IOException exception : failures.values()) {
      LOG.warn(exception);
    }

    final String text =
        StringUtil.join(
            failures.values(),
            new Function<IOException, String>() {
              @Override
              public String fun(IOException e) {
                return e.getMessage();
              }
            },
            "\n");

    final DialogWrapper dialog =
        new DialogWrapper(null) {
          {
            init();
            setTitle(UIBundle.message("cannot.save.files.dialog.title"));
          }

          @Override
          protected void createDefaultActions() {
            super.createDefaultActions();
            myOKAction.putValue(
                Action.NAME,
                UIBundle.message(
                    myOnClose
                        ? "cannot.save.files.dialog.ignore.changes"
                        : "cannot.save.files.dialog.revert.changes"));
            myOKAction.putValue(DEFAULT_ACTION, null);

            if (!myOnClose) {
              myCancelAction.putValue(Action.NAME, CommonBundle.getCloseButtonText());
            }
          }

          @Override
          protected JComponent createCenterPanel() {
            final JPanel panel = new JPanel(new BorderLayout(0, 5));

            panel.add(
                new JLabel(UIBundle.message("cannot.save.files.dialog.message")),
                BorderLayout.NORTH);

            final JTextPane area = new JTextPane();
            area.setText(text);
            area.setEditable(false);
            area.setMinimumSize(new Dimension(area.getMinimumSize().width, 50));
            panel.add(
                new JBScrollPane(
                    area,
                    ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
                    ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER),
                BorderLayout.CENTER);

            return panel;
          }
        };

    dialog.show();

    if (dialog.isOK()) {
      for (Document document : failures.keySet()) {
        reloadFromDisk(document);
      }
    }
  }
}
public class VariableAccessFromInnerClassFix implements IntentionAction {
  private static final Logger LOG =
      Logger.getInstance(
          "#com.intellij.codeInsight.daemon.impl.quickfix.VariableAccessFromInnerClassFix");
  private final PsiVariable myVariable;
  private final PsiClass myClass;
  private final int myFixType;
  private static final int MAKE_FINAL = 0;
  private static final int MAKE_ARRAY = 1;
  private static final int COPY_TO_FINAL = 2;
  private static final Key<Map<PsiVariable, Boolean>>[] VARS =
      new Key[] {
        Key.create("VARS_TO_MAKE_FINAL"), Key.create("VARS_TO_TRANSFORM"), Key.create("???")
      };

  public VariableAccessFromInnerClassFix(PsiVariable variable, PsiClass aClass) {
    myVariable = variable;
    myClass = aClass;
    myFixType = getQuickFixType(variable);
    if (myFixType == -1) return;

    getVariablesToFix().add(variable);
  }

  @Override
  @NotNull
  public String getText() {
    @NonNls String message;
    switch (myFixType) {
      case MAKE_FINAL:
        message = "make.final.text";
        break;
      case MAKE_ARRAY:
        message = "make.final.transform.to.one.element.array";
        break;
      case COPY_TO_FINAL:
        return QuickFixBundle.message("make.final.copy.to.temp", myVariable.getName());
      default:
        return "";
    }
    Collection<PsiVariable> vars = getVariablesToFix();
    String varNames = vars.size() == 1 ? "'" + myVariable.getName() + "'" : "variables";
    return QuickFixBundle.message(message, varNames);
  }

  @Override
  @NotNull
  public String getFamilyName() {
    return QuickFixBundle.message("make.final.family");
  }

  @Override
  public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
    return myClass != null
        && myClass.isValid()
        && myClass.getManager().isInProject(myClass)
        && myVariable != null
        && myVariable.isValid()
        && myFixType != -1
        && !getVariablesToFix().isEmpty()
        && !inOwnInitializer(myVariable, myClass);
  }

  private static boolean inOwnInitializer(PsiVariable variable, PsiClass aClass) {
    return PsiTreeUtil.isAncestor(variable, aClass, false);
  }

  @Override
  public void invoke(@NotNull Project project, Editor editor, PsiFile file) {
    if (!FileModificationService.getInstance().preparePsiElementsForWrite(myClass, myVariable))
      return;
    try {
      switch (myFixType) {
        case MAKE_FINAL:
          makeFinal();
          break;
        case MAKE_ARRAY:
          makeArray();
          break;
        case COPY_TO_FINAL:
          copyToFinal();
          break;
      }
    } catch (IncorrectOperationException e) {
      LOG.error(e);
    } finally {
      getVariablesToFix().clear();
    }
  }

  private void makeArray() {
    for (PsiVariable var : getVariablesToFix()) {
      makeArray(var);
    }
  }

  @NotNull
  private Collection<PsiVariable> getVariablesToFix() {
    Map<PsiVariable, Boolean> vars = myClass.getUserData(VARS[myFixType]);
    if (vars == null)
      myClass.putUserData(
          VARS[myFixType], vars = new ConcurrentWeakHashMap<PsiVariable, Boolean>(1));
    final Map<PsiVariable, Boolean> finalVars = vars;
    return new AbstractCollection<PsiVariable>() {
      @Override
      public boolean add(PsiVariable psiVariable) {
        return finalVars.put(psiVariable, Boolean.TRUE) == null;
      }

      @Override
      public Iterator<PsiVariable> iterator() {
        return finalVars.keySet().iterator();
      }

      @Override
      public int size() {
        return finalVars.size();
      }
    };
  }

  private void makeFinal() {
    for (PsiVariable var : getVariablesToFix()) {
      if (var.isValid()) {
        PsiUtil.setModifierProperty(var, PsiModifier.FINAL, true);
      }
    }
  }

  private void makeArray(PsiVariable variable) throws IncorrectOperationException {
    PsiType type = variable.getType();

    PsiElementFactory factory = JavaPsiFacade.getInstance(myClass.getProject()).getElementFactory();
    PsiType newType = type.createArrayType();

    PsiDeclarationStatement variableDeclarationStatement;
    PsiExpression initializer = variable.getInitializer();
    if (initializer == null) {
      String expression = "[1]";
      while (type instanceof PsiArrayType) {
        expression += "[1]";
        type = ((PsiArrayType) type).getComponentType();
      }
      PsiExpression init =
          factory.createExpressionFromText("new " + type.getCanonicalText() + expression, variable);
      variableDeclarationStatement =
          factory.createVariableDeclarationStatement(variable.getName(), newType, init);
    } else {
      PsiExpression init =
          factory.createExpressionFromText("{ " + initializer.getText() + " }", variable);
      variableDeclarationStatement =
          factory.createVariableDeclarationStatement(variable.getName(), newType, init);
    }
    PsiVariable newVariable = (PsiVariable) variableDeclarationStatement.getDeclaredElements()[0];
    PsiUtil.setModifierProperty(newVariable, PsiModifier.FINAL, true);
    PsiElement newExpression =
        factory.createExpressionFromText(variable.getName() + "[0]", variable);

    PsiElement outerCodeBlock = PsiUtil.getVariableCodeBlock(variable, null);
    if (outerCodeBlock == null) return;
    List<PsiReferenceExpression> outerReferences = new ArrayList<PsiReferenceExpression>();
    collectReferences(outerCodeBlock, variable, outerReferences);
    replaceReferences(outerReferences, newExpression);
    variable.replace(newVariable);
  }

  private void copyToFinal() throws IncorrectOperationException {
    PsiManager psiManager = myClass.getManager();
    PsiElementFactory factory =
        JavaPsiFacade.getInstance(psiManager.getProject()).getElementFactory();
    PsiExpression initializer = factory.createExpressionFromText(myVariable.getName(), myClass);
    String newName = suggestNewName(psiManager.getProject(), myVariable);
    PsiType type = myVariable.getType();
    PsiDeclarationStatement copyDecl =
        factory.createVariableDeclarationStatement(newName, type, initializer);
    PsiVariable newVariable = (PsiVariable) copyDecl.getDeclaredElements()[0];
    PsiUtil.setModifierProperty(newVariable, PsiModifier.FINAL, true);
    PsiElement statement = getStatementToInsertBefore();
    if (statement == null) return;
    statement.getParent().addBefore(copyDecl, statement);
    PsiExpression newExpression = factory.createExpressionFromText(newName, myVariable);
    replaceReferences(myClass, myVariable, newExpression);
  }

  private PsiElement getStatementToInsertBefore() {
    PsiElement declarationScope =
        myVariable instanceof PsiParameter
            ? ((PsiParameter) myVariable).getDeclarationScope()
            : PsiUtil.getVariableCodeBlock(myVariable, null);
    if (declarationScope == null) return null;

    PsiElement statement = myClass;
    nextInnerClass:
    do {
      statement = PsiUtil.getEnclosingStatement(statement);

      if (statement == null || statement.getParent() == null) {
        return null;
      }
      PsiElement element = statement;
      while (element != declarationScope && !(element instanceof PsiFile)) {
        if (element instanceof PsiClass) {
          statement = statement.getParent();
          continue nextInnerClass;
        }
        element = element.getParent();
      }
      return statement;
    } while (true);
  }

  private static String suggestNewName(Project project, PsiVariable variable) {
    // new name should not conflict with another variable at the variable declaration level and
    // usage level
    String name = variable.getName();
    // trim last digit to suggest variable names like i1,i2, i3...
    if (name.length() > 1 && Character.isDigit(name.charAt(name.length() - 1))) {
      name = name.substring(0, name.length() - 1);
    }
    name = "final" + StringUtil.capitalize(StringUtil.trimStart(name, "final"));
    return JavaCodeStyleManager.getInstance(project)
        .suggestUniqueVariableName(name, variable, true);
  }

  private static void replaceReferences(
      PsiElement context, final PsiVariable variable, final PsiElement newExpression) {
    context.accept(
        new JavaRecursiveElementVisitor() {
          @Override
          public void visitReferenceExpression(PsiReferenceExpression expression) {
            if (expression.resolve() == variable)
              try {
                expression.replace(newExpression);
              } catch (IncorrectOperationException e) {
                LOG.error(e);
              }
            super.visitReferenceExpression(expression);
          }
        });
  }

  private static void replaceReferences(
      List<PsiReferenceExpression> references, PsiElement newExpression)
      throws IncorrectOperationException {
    for (PsiReferenceExpression reference : references) {
      reference.replace(newExpression);
    }
  }

  private static void collectReferences(
      PsiElement context,
      final PsiVariable variable,
      final List<PsiReferenceExpression> references) {
    context.accept(
        new JavaRecursiveElementWalkingVisitor() {
          @Override
          public void visitReferenceExpression(PsiReferenceExpression expression) {
            if (expression.resolve() == variable) references.add(expression);
            super.visitReferenceExpression(expression);
          }
        });
  }

  private static int getQuickFixType(PsiVariable variable) {
    PsiElement outerCodeBlock = PsiUtil.getVariableCodeBlock(variable, null);
    if (outerCodeBlock == null) return -1;
    List<PsiReferenceExpression> outerReferences = new ArrayList<PsiReferenceExpression>();
    collectReferences(outerCodeBlock, variable, outerReferences);

    int type = MAKE_FINAL;
    for (PsiReferenceExpression expression : outerReferences) {
      // if it happens that variable referenced from another inner class, make sure it can be make
      // final from there
      PsiClass innerClass =
          HighlightControlFlowUtil.getInnerClassVariableReferencedFrom(variable, expression);

      if (innerClass != null) {
        int thisType = MAKE_FINAL;
        if (writtenInside(variable, innerClass)) {
          // cannot make parameter array
          if (variable instanceof PsiParameter) return -1;
          thisType = MAKE_ARRAY;
        }
        if (thisType == MAKE_FINAL && !canBeFinal(variable, outerReferences)) {
          thisType = COPY_TO_FINAL;
        }
        type = Math.max(type, thisType);
      }
    }
    return type;
  }

  private static boolean canBeFinal(PsiVariable variable, List<PsiReferenceExpression> references) {
    // if there is at least one assignment to this variable, it cannot be final
    Map<PsiElement, Collection<PsiReferenceExpression>> uninitializedVarProblems =
        new THashMap<PsiElement, Collection<PsiReferenceExpression>>();
    Map<PsiElement, Collection<ControlFlowUtil.VariableInfo>> finalVarProblems =
        new THashMap<PsiElement, Collection<ControlFlowUtil.VariableInfo>>();
    for (PsiReferenceExpression expression : references) {
      if (ControlFlowUtil.isVariableAssignedInLoop(expression, variable)) return false;
      HighlightInfo highlightInfo =
          HighlightControlFlowUtil.checkVariableInitializedBeforeUsage(
              expression, variable, uninitializedVarProblems);
      if (highlightInfo != null) return false;
      highlightInfo =
          HighlightControlFlowUtil.checkFinalVariableMightAlreadyHaveBeenAssignedTo(
              variable, expression, finalVarProblems);
      if (highlightInfo != null) return false;
      if (variable instanceof PsiParameter && PsiUtil.isAccessedForWriting(expression))
        return false;
    }
    return true;
  }

  private static boolean writtenInside(PsiVariable variable, PsiElement element) {
    if (element instanceof PsiAssignmentExpression) {
      PsiAssignmentExpression assignmentExpression = (PsiAssignmentExpression) element;
      PsiExpression lExpression = assignmentExpression.getLExpression();
      if (lExpression instanceof PsiReferenceExpression
          && ((PsiReferenceExpression) lExpression).resolve() == variable) return true;
    } else if (PsiUtil.isIncrementDecrementOperation(element)) {
      PsiElement operand =
          element instanceof PsiPostfixExpression
              ? ((PsiPostfixExpression) element).getOperand()
              : ((PsiPrefixExpression) element).getOperand();
      if (operand instanceof PsiReferenceExpression
          && ((PsiReferenceExpression) operand).resolve() == variable) return true;
    }
    PsiElement[] children = element.getChildren();
    for (PsiElement child : children) {
      if (writtenInside(variable, child)) return true;
    }
    return false;
  }

  @Override
  public boolean startInWriteAction() {
    return true;
  }
}
class PsiChangeHandler extends PsiTreeChangeAdapter implements Disposable {
  private static final ExtensionPointName<ChangeLocalityDetector> EP_NAME =
      ExtensionPointName.create("com.intellij.daemon.changeLocalityDetector");
  private /*NOT STATIC!!!*/ final Key<Boolean> UPDATE_ON_COMMIT_ENGAGED =
      Key.create("UPDATE_ON_COMMIT_ENGAGED");

  private final Project myProject;
  private final Map<Document, List<Pair<PsiElement, Boolean>>> changedElements =
      new WeakHashMap<>();
  private final FileStatusMap myFileStatusMap;

  PsiChangeHandler(
      @NotNull Project project,
      @NotNull final PsiDocumentManagerImpl documentManager,
      @NotNull EditorFactory editorFactory,
      @NotNull MessageBusConnection connection,
      @NotNull FileStatusMap fileStatusMap) {
    myProject = project;
    myFileStatusMap = fileStatusMap;
    editorFactory
        .getEventMulticaster()
        .addDocumentListener(
            new DocumentAdapter() {
              @Override
              public void beforeDocumentChange(DocumentEvent e) {
                final Document document = e.getDocument();
                if (documentManager.getSynchronizer().isInSynchronization(document)) return;
                if (documentManager.getCachedPsiFile(document) == null) return;
                if (document.getUserData(UPDATE_ON_COMMIT_ENGAGED) == null) {
                  document.putUserData(UPDATE_ON_COMMIT_ENGAGED, Boolean.TRUE);
                  PsiDocumentManagerBase.addRunOnCommit(
                      document,
                      () -> {
                        if (document.getUserData(UPDATE_ON_COMMIT_ENGAGED) != null) {
                          updateChangesForDocument(document);
                          document.putUserData(UPDATE_ON_COMMIT_ENGAGED, null);
                        }
                      });
                }
              }
            },
            this);

    connection.subscribe(
        PsiDocumentTransactionListener.TOPIC,
        new PsiDocumentTransactionListener() {
          @Override
          public void transactionStarted(
              @NotNull final Document doc, @NotNull final PsiFile file) {}

          @Override
          public void transactionCompleted(
              @NotNull final Document document, @NotNull final PsiFile file) {
            updateChangesForDocument(document);
            document.putUserData(
                UPDATE_ON_COMMIT_ENGAGED,
                null); // ensure we don't call updateChangesForDocument() twice which can lead to
                       // whole file re-highlight
          }
        });
  }

  @Override
  public void dispose() {}

  private void updateChangesForDocument(@NotNull final Document document) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (DaemonListeners.isUnderIgnoredAction(null) || myProject.isDisposed()) return;
    List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document);
    if (toUpdate == null) {
      // The document has been changed, but psi hasn't
      // We may still need to rehighlight the file if there were changes inside highlighted ranges.
      if (UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document)) return;

      // don't create PSI for files in other projects
      PsiElement file = PsiDocumentManager.getInstance(myProject).getCachedPsiFile(document);
      if (file == null) return;

      toUpdate = Collections.singletonList(Pair.create(file, true));
    }
    Application application = ApplicationManager.getApplication();
    final Editor editor = FileEditorManager.getInstance(myProject).getSelectedTextEditor();
    if (editor != null && !application.isUnitTestMode()) {
      application.invokeLater(
          () -> {
            if (!editor.isDisposed()) {
              EditorMarkupModel markupModel = (EditorMarkupModel) editor.getMarkupModel();
              PsiFile file =
                  PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument());
              TrafficLightRenderer.setOrRefreshErrorStripeRenderer(
                  markupModel, myProject, editor.getDocument(), file);
            }
          },
          ModalityState.stateForComponent(editor.getComponent()),
          myProject.getDisposed());
    }

    for (Pair<PsiElement, Boolean> changedElement : toUpdate) {
      PsiElement element = changedElement.getFirst();
      Boolean whiteSpaceOptimizationAllowed = changedElement.getSecond();
      updateByChange(element, document, whiteSpaceOptimizationAllowed);
    }
    changedElements.remove(document);
  }

  @Override
  public void childAdded(@NotNull PsiTreeChangeEvent event) {
    queueElement(event.getParent(), true, event);
  }

  @Override
  public void childRemoved(@NotNull PsiTreeChangeEvent event) {
    queueElement(event.getParent(), true, event);
  }

  @Override
  public void childReplaced(@NotNull PsiTreeChangeEvent event) {
    queueElement(event.getNewChild(), typesEqual(event.getNewChild(), event.getOldChild()), event);
  }

  private static boolean typesEqual(final PsiElement newChild, final PsiElement oldChild) {
    return newChild != null && oldChild != null && newChild.getClass() == oldChild.getClass();
  }

  @Override
  public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
    if (((PsiTreeChangeEventImpl) event).isGenericChange()) {
      return;
    }
    queueElement(event.getParent(), true, event);
  }

  @Override
  public void beforeChildMovement(@NotNull PsiTreeChangeEvent event) {
    queueElement(event.getOldParent(), true, event);
    queueElement(event.getNewParent(), true, event);
  }

  @Override
  public void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) {
    // this event sent always before every PSI change, even not significant one (like after quick
    // typing/backspacing char)
    // mark file dirty just in case
    PsiFile psiFile = event.getFile();
    if (psiFile != null) {
      myFileStatusMap.markFileScopeDirtyDefensively(psiFile, event);
    }
  }

  @Override
  public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
    String propertyName = event.getPropertyName();
    if (!propertyName.equals(PsiTreeChangeEvent.PROP_WRITABLE)) {
      Object oldValue = event.getOldValue();
      if (oldValue instanceof VirtualFile && shouldBeIgnored((VirtualFile) oldValue)) {
        // ignore workspace.xml
        return;
      }
      myFileStatusMap.markAllFilesDirty(event);
    }
  }

  private void queueElement(
      @NotNull PsiElement child,
      final boolean whitespaceOptimizationAllowed,
      @NotNull PsiTreeChangeEvent event) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    PsiFile file = event.getFile();
    if (file == null) file = child.getContainingFile();
    if (file == null) {
      myFileStatusMap.markAllFilesDirty(child);
      return;
    }

    if (!child.isValid()) return;

    PsiDocumentManagerImpl pdm = (PsiDocumentManagerImpl) PsiDocumentManager.getInstance(myProject);
    Document document = pdm.getCachedDocument(file);
    if (document != null) {
      if (pdm.getSynchronizer().getTransaction(document) == null) {
        // content reload, language level change or some other big change
        myFileStatusMap.markAllFilesDirty(child);
        return;
      }

      List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document);
      if (toUpdate == null) {
        toUpdate = new SmartList<>();
        changedElements.put(document, toUpdate);
      }
      toUpdate.add(Pair.create(child, whitespaceOptimizationAllowed));
    }
  }

  private void updateByChange(
      @NotNull PsiElement child,
      @NotNull final Document document,
      final boolean whitespaceOptimizationAllowed) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    final PsiFile file;
    try {
      file = child.getContainingFile();
    } catch (PsiInvalidElementAccessException e) {
      myFileStatusMap.markAllFilesDirty(e);
      return;
    }
    if (file == null || file instanceof PsiCompiledElement) {
      myFileStatusMap.markAllFilesDirty(child);
      return;
    }
    VirtualFile virtualFile = file.getVirtualFile();
    if (virtualFile != null && shouldBeIgnored(virtualFile)) {
      // ignore workspace.xml
      return;
    }

    int fileLength = file.getTextLength();
    if (!file.getViewProvider().isPhysical()) {
      myFileStatusMap.markFileScopeDirty(
          document, new TextRange(0, fileLength), fileLength, "Non-physical file update: " + file);
      return;
    }

    PsiElement element =
        whitespaceOptimizationAllowed
                && UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document)
            ? child
            : child.getParent();
    while (true) {
      if (element == null || element instanceof PsiFile || element instanceof PsiDirectory) {
        myFileStatusMap.markAllFilesDirty("Top element: " + element);
        return;
      }

      final PsiElement scope = getChangeHighlightingScope(element);
      if (scope != null) {
        myFileStatusMap.markFileScopeDirty(
            document, scope.getTextRange(), fileLength, "Scope: " + scope);
        return;
      }

      element = element.getParent();
    }
  }

  private boolean shouldBeIgnored(@NotNull VirtualFile virtualFile) {
    return ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile)
        || ProjectRootManager.getInstance(myProject).getFileIndex().isExcluded(virtualFile);
  }

  @Nullable
  private static PsiElement getChangeHighlightingScope(@NotNull PsiElement element) {
    DefaultChangeLocalityDetector defaultDetector = null;
    for (ChangeLocalityDetector detector : Extensions.getExtensions(EP_NAME)) {
      if (detector instanceof DefaultChangeLocalityDetector) {
        // run default detector last
        assert defaultDetector == null : defaultDetector;
        defaultDetector = (DefaultChangeLocalityDetector) detector;
        continue;
      }
      final PsiElement scope = detector.getChangeHighlightingDirtyScopeFor(element);
      if (scope != null) return scope;
    }
    assert defaultDetector != null
        : "com.intellij.codeInsight.daemon.impl.DefaultChangeLocalityDetector is unregistered";
    return defaultDetector.getChangeHighlightingDirtyScopeFor(element);
  }
}
/** @author Mike */
public class XmlDocumentImpl extends XmlElementImpl implements XmlDocument {

  private static final Key<Boolean> AUTO_GENERATED = Key.create("auto-generated xml schema");

  public static boolean isAutoGeneratedSchema(XmlFile file) {
    return file.getUserData(AUTO_GENERATED) != null;
  }

  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.xml.XmlDocumentImpl");
  private volatile XmlProlog myProlog;
  private volatile XmlTag myRootTag;
  private volatile long myExtResourcesModCount = -1;

  public XmlDocumentImpl() {
    this(XmlElementType.XML_DOCUMENT);
  }

  protected XmlDocumentImpl(IElementType type) {
    super(type);
  }

  @Override
  public void accept(@NotNull PsiElementVisitor visitor) {
    if (visitor instanceof XmlElementVisitor) {
      ((XmlElementVisitor) visitor).visitXmlDocument(this);
    } else {
      visitor.visitElement(this);
    }
  }

  @Override
  public int getChildRole(ASTNode child) {
    LOG.assertTrue(child.getTreeParent() == this);
    IElementType i = child.getElementType();
    if (i == XmlElementType.XML_PROLOG) {
      return XmlChildRole.XML_PROLOG;
    } else if (i == XmlElementType.XML_TAG) {
      return XmlChildRole.XML_TAG;
    } else {
      return ChildRoleBase.NONE;
    }
  }

  @Override
  public XmlProlog getProlog() {
    XmlProlog prolog = myProlog;

    if (prolog == null) {
      synchronized (PsiLock.LOCK) {
        prolog = myProlog;
        if (prolog == null) {
          prolog = (XmlProlog) findElementByTokenType(XmlElementType.XML_PROLOG);
          myProlog = prolog;
        }
      }
    }

    return myProlog;
  }

  @Override
  public XmlTag getRootTag() {
    XmlTag rootTag = myRootTag;

    if (rootTag == null) {
      synchronized (PsiLock.LOCK) {
        rootTag = myRootTag;
        if (rootTag == null) {
          rootTag = (XmlTag) findElementByTokenType(XmlElementType.XML_TAG);
          myRootTag = rootTag;
        }
      }
    }

    return myRootTag;
  }

  @Override
  @SuppressWarnings("ConstantConditions")
  public XmlNSDescriptor getRootTagNSDescriptor() {
    XmlTag rootTag = getRootTag();
    return rootTag != null ? rootTag.getNSDescriptor(rootTag.getNamespace(), false) : null;
  }

  private ConcurrentMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheStrict =
      ContainerUtil.newConcurrentMap();
  private ConcurrentMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheNotStrict =
      ContainerUtil.newConcurrentMap();

  @Override
  public void clearCaches() {
    myDefaultDescriptorsCacheStrict.clear();
    myDefaultDescriptorsCacheNotStrict.clear();
    myProlog = null;
    myRootTag = null;
    super.clearCaches();
  }

  @Override
  public XmlNSDescriptor getDefaultNSDescriptor(final String namespace, final boolean strict) {
    long curExtResourcesModCount =
        ExternalResourceManagerEx.getInstanceEx().getModificationCount(getProject());
    if (myExtResourcesModCount != curExtResourcesModCount) {
      myDefaultDescriptorsCacheNotStrict.clear();
      myDefaultDescriptorsCacheStrict.clear();
      myExtResourcesModCount = curExtResourcesModCount;
    }

    final ConcurrentMap<String, CachedValue<XmlNSDescriptor>> defaultDescriptorsCache;
    if (strict) {
      defaultDescriptorsCache = myDefaultDescriptorsCacheStrict;
    } else {
      defaultDescriptorsCache = myDefaultDescriptorsCacheNotStrict;
    }

    CachedValue<XmlNSDescriptor> cachedValue = defaultDescriptorsCache.get(namespace);
    if (cachedValue == null) {
      defaultDescriptorsCache.put(
          namespace,
          cachedValue =
              new PsiCachedValueImpl<XmlNSDescriptor>(
                  getManager(),
                  new CachedValueProvider<XmlNSDescriptor>() {
                    @Override
                    public Result<XmlNSDescriptor> compute() {
                      final XmlNSDescriptor defaultNSDescriptorInner =
                          getDefaultNSDescriptorInner(namespace, strict);

                      if (isGeneratedFromDtd(defaultNSDescriptorInner)) {
                        return new Result<XmlNSDescriptor>(
                            defaultNSDescriptorInner,
                            XmlDocumentImpl.this,
                            ExternalResourceManager.getInstance());
                      }

                      return new Result<XmlNSDescriptor>(
                          defaultNSDescriptorInner,
                          defaultNSDescriptorInner != null
                              ? defaultNSDescriptorInner.getDependences()
                              : ExternalResourceManager.getInstance());
                    }
                  }));
    }
    return cachedValue.getValue();
  }

  private boolean isGeneratedFromDtd(XmlNSDescriptor defaultNSDescriptorInner) {
    if (defaultNSDescriptorInner == null) {
      return false;
    }
    XmlFile descriptorFile = defaultNSDescriptorInner.getDescriptorFile();
    if (descriptorFile == null) {
      return false;
    }
    @NonNls String otherName = XmlUtil.getContainingFile(this).getName() + ".dtd";
    return descriptorFile.getName().equals(otherName);
  }

  private XmlNSDescriptor getDefaultNSDescriptorInner(
      final String namespace, final boolean strict) {
    final XmlFile containingFile = XmlUtil.getContainingFile(this);
    if (containingFile == null) return null;
    final XmlProlog prolog = getProlog();
    final XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null;
    boolean dtdUriFromDocTypeIsNamespace = false;

    if (XmlUtil.HTML_URI.equals(namespace)) {
      XmlNSDescriptor nsDescriptor =
          doctype != null ? getNsDescriptorFormDocType(doctype, containingFile, true) : null;
      if (doctype != null) {
        LOG.debug(
            "Descriptor from doctype "
                + doctype
                + " is "
                + (nsDescriptor != null ? nsDescriptor.getClass().getCanonicalName() : "NULL"));
      }

      if (nsDescriptor == null) {
        String htmlns =
            ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(getProject());
        if (htmlns.isEmpty()) {
          htmlns = Html5SchemaProvider.getHtml5SchemaLocation();
        }
        nsDescriptor = getDefaultNSDescriptor(htmlns, false);
      }
      return new HtmlNSDescriptorImpl(nsDescriptor);
    } else if (XmlUtil.XHTML_URI.equals(namespace)) {
      String xhtmlNamespace = XmlUtil.getDefaultXhtmlNamespace(getProject());
      if (xhtmlNamespace == null || xhtmlNamespace.isEmpty()) {
        xhtmlNamespace = Html5SchemaProvider.getXhtml5SchemaLocation();
      }
      return getDefaultNSDescriptor(xhtmlNamespace, false);
    } else if (namespace != null && namespace != XmlUtil.EMPTY_URI) {
      if (doctype == null || !namespace.equals(XmlUtil.getDtdUri(doctype))) {
        boolean documentIsSchemaThatDefinesNs =
            namespace.equals(XmlUtil.getTargetSchemaNsFromTag(getRootTag()));

        final XmlFile xmlFile =
            documentIsSchemaThatDefinesNs
                ? containingFile
                : XmlUtil.findNamespace(containingFile, namespace);
        if (xmlFile != null) {
          final XmlDocument document = xmlFile.getDocument();
          if (document != null) {
            return (XmlNSDescriptor) document.getMetaData();
          }
        }
      } else {
        dtdUriFromDocTypeIsNamespace = true;
      }
    }

    if (strict && !dtdUriFromDocTypeIsNamespace) return null;

    if (doctype != null) {
      XmlNSDescriptor descr = getNsDescriptorFormDocType(doctype, containingFile, false);

      if (descr != null) {
        return XmlExtension.getExtension(containingFile)
            .getDescriptorFromDoctype(containingFile, descr);
      }
    }

    if (strict) return null;
    if (namespace == XmlUtil.EMPTY_URI) {
      final XmlFile xmlFile = XmlUtil.findNamespace(containingFile, namespace);
      if (xmlFile != null) {
        return (XmlNSDescriptor) xmlFile.getDocument().getMetaData();
      }
    }
    try {
      final PsiFile fileFromText =
          PsiFileFactory.getInstance(getProject())
              .createFileFromText(
                  containingFile.getName() + ".dtd",
                  DTDLanguage.INSTANCE,
                  XmlUtil.generateDocumentDTD(this, false),
                  false,
                  false);
      if (fileFromText instanceof XmlFile) {
        fileFromText.putUserData(AUTO_GENERATED, Boolean.TRUE);
        return (XmlNSDescriptor) ((XmlFile) fileFromText).getDocument().getMetaData();
      }
    } catch (ProcessCanceledException ex) {
      throw ex;
    } catch (RuntimeException ignored) {
    } // e.g. dtd isn't mapped to xml type

    return null;
  }

  @NotNull
  private static String getFilePathForLogging(@Nullable PsiFile file) {
    if (file == null) {
      return "NULL";
    }
    final VirtualFile vFile = file.getVirtualFile();
    return vFile != null ? vFile.getPath() : "NULL_VFILE";
  }

  @Nullable
  private XmlNSDescriptor getNsDescriptorFormDocType(
      final XmlDoctype doctype, final XmlFile containingFile, final boolean forHtml) {
    XmlNSDescriptor descriptor = getNSDescriptorFromMetaData(doctype.getMarkupDecl(), true);

    final String filePath = getFilePathForLogging(containingFile);

    final String dtdUri = XmlUtil.getDtdUri(doctype);
    LOG.debug(
        "DTD url for doctype " + doctype.getText() + " in file " + filePath + " is " + dtdUri);

    if (dtdUri != null && !dtdUri.isEmpty()) {
      XmlFile xmlFile = XmlUtil.findNamespace(containingFile, dtdUri);
      if (xmlFile == null) {
        // try to auto-detect it
        xmlFile = XmlNamespaceIndex.guessDtd(dtdUri, containingFile);
      }
      final String schemaFilePath = getFilePathForLogging(xmlFile);

      LOG.debug("Schema file for " + filePath + " is " + schemaFilePath);

      XmlNSDescriptor descriptorFromDtd =
          getNSDescriptorFromMetaData(xmlFile == null ? null : xmlFile.getDocument(), forHtml);

      LOG.debug(
          "Descriptor from meta data for schema file "
              + schemaFilePath
              + " is "
              + (descriptorFromDtd != null
                  ? descriptorFromDtd.getClass().getCanonicalName()
                  : "NULL"));

      if (descriptor != null && descriptorFromDtd != null) {
        descriptor =
            new XmlNSDescriptorSequence(new XmlNSDescriptor[] {descriptor, descriptorFromDtd});
      } else if (descriptorFromDtd != null) {
        descriptor = descriptorFromDtd;
      }
    }
    return descriptor;
  }

  @Nullable
  private XmlNSDescriptor getNSDescriptorFromMetaData(
      @Nullable PsiMetaOwner metaOwner, boolean nonEmpty) {
    if (metaOwner == null) return null;
    XmlNSDescriptor descriptor = (XmlNSDescriptor) metaOwner.getMetaData();
    if (descriptor == null) return null;
    if (nonEmpty && descriptor.getRootElementsDescriptors(this).length == 0) {
      return null;
    }
    return descriptor;
  }

  @Override
  public CompositePsiElement clone() {
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict =
        new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheStrict);
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict =
        new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheNotStrict);
    final XmlDocumentImpl copy = (XmlDocumentImpl) super.clone();
    updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict);
    return copy;
  }

  @Override
  public PsiElement copy() {
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict =
        new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheStrict);
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict =
        new HashMap<String, CachedValue<XmlNSDescriptor>>(myDefaultDescriptorsCacheNotStrict);
    final XmlDocumentImpl copy = (XmlDocumentImpl) super.copy();
    updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict);
    return copy;
  }

  private void updateSelfDependentDtdDescriptors(
      XmlDocumentImpl copy,
      HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict,
      HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict) {
    copy.myDefaultDescriptorsCacheNotStrict = ContainerUtil.newConcurrentMap();
    copy.myDefaultDescriptorsCacheStrict = ContainerUtil.newConcurrentMap();

    for (Map.Entry<String, CachedValue<XmlNSDescriptor>> e : cacheStrict.entrySet()) {
      if (e.getValue().hasUpToDateValue()) {
        final XmlNSDescriptor nsDescriptor = e.getValue().getValue();
        if (!isGeneratedFromDtd(nsDescriptor))
          copy.myDefaultDescriptorsCacheStrict.put(e.getKey(), e.getValue());
      }
    }

    for (Map.Entry<String, CachedValue<XmlNSDescriptor>> e : cacheNotStrict.entrySet()) {
      if (e.getValue().hasUpToDateValue()) {
        final XmlNSDescriptor nsDescriptor = e.getValue().getValue();
        if (!isGeneratedFromDtd(nsDescriptor))
          copy.myDefaultDescriptorsCacheNotStrict.put(e.getKey(), e.getValue());
      }
    }
  }

  @Override
  public PsiMetaData getMetaData() {
    return MetaRegistry.getMeta(this);
  }

  @SuppressWarnings({"HardCodedStringLiteral"})
  public void dumpStatistics() {
    System.out.println("Statistics:");
    final TObjectIntHashMap<Object> map = new TObjectIntHashMap<Object>();

    final PsiElementVisitor psiRecursiveElementVisitor =
        new XmlRecursiveElementVisitor() {
          @NonNls private static final String TOKENS_KEY = "Tokens";
          @NonNls private static final String ELEMENTS_KEY = "Elements";

          @Override
          public void visitXmlToken(XmlToken token) {
            inc(TOKENS_KEY);
          }

          @Override
          public void visitElement(PsiElement element) {
            inc(ELEMENTS_KEY);
            super.visitElement(element);
          }

          private void inc(final String key) {
            map.put(key, map.get(key) + 1);
          }
        };

    accept(psiRecursiveElementVisitor);

    final Object[] keys = map.keys();
    for (final Object key : keys) {
      System.out.println(key + ": " + map.get(key));
    }
  }

  @Override
  public TreeElement addInternal(
      final TreeElement first, final ASTNode last, final ASTNode anchor, final Boolean before) {
    final PomModel model = PomManager.getModel(getProject());
    final XmlAspect aspect = model.getModelAspect(XmlAspect.class);
    final TreeElement[] holder = new TreeElement[1];
    try {
      model.runTransaction(
          new PomTransactionBase(this, aspect) {
            @Override
            public PomModelEvent runInner() {
              holder[0] = XmlDocumentImpl.super.addInternal(first, last, anchor, before);
              return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this);
            }
          });
    } catch (IncorrectOperationException ignored) {
    }
    return holder[0];
  }

  @Override
  public void deleteChildInternal(@NotNull final ASTNode child) {
    final PomModel model = PomManager.getModel(getProject());
    final XmlAspect aspect = model.getModelAspect(XmlAspect.class);
    try {
      model.runTransaction(
          new PomTransactionBase(this, aspect) {
            @Override
            public PomModelEvent runInner() {
              XmlDocumentImpl.super.deleteChildInternal(child);
              return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this);
            }
          });
    } catch (IncorrectOperationException ignored) {
    }
  }

  @Override
  public void replaceChildInternal(
      @NotNull final ASTNode child, @NotNull final TreeElement newElement) {
    final PomModel model = PomManager.getModel(getProject());
    final XmlAspect aspect = model.getModelAspect(XmlAspect.class);
    try {
      model.runTransaction(
          new PomTransactionBase(this, aspect) {
            @Override
            public PomModelEvent runInner() {
              XmlDocumentImpl.super.replaceChildInternal(child, newElement);
              return XmlDocumentChangedImpl.createXmlDocumentChanged(model, XmlDocumentImpl.this);
            }
          });
    } catch (IncorrectOperationException ignored) {
    }
  }
}
public class JavaResolveCache {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.resolve.JavaResolveCache");

  private static final NotNullLazyKey<JavaResolveCache, Project> INSTANCE_KEY =
      ServiceManager.createLazyKey(JavaResolveCache.class);

  public static JavaResolveCache getInstance(Project project) {
    return INSTANCE_KEY.getValue(project);
  }

  private final ConcurrentMap<PsiExpression, Reference<PsiType>> myCalculatedTypes =
      new ConcurrentWeakHashMap<PsiExpression, Reference<PsiType>>();

  private final Map<PsiVariable, Object> myVarToConstValueMapPhysical =
      new ConcurrentWeakHashMap<PsiVariable, Object>();
  private final Map<PsiVariable, Object> myVarToConstValueMapNonPhysical =
      new ConcurrentWeakHashMap<PsiVariable, Object>();

  private static final Object NULL = Key.create("NULL");

  public JavaResolveCache(
      @Nullable(
              "can be null in com.intellij.core.JavaCoreApplicationEnvironment.JavaCoreApplicationEnvironment")
          MessageBus messageBus) {
    if (messageBus != null) {
      messageBus
          .connect()
          .subscribe(
              PsiManagerImpl.ANY_PSI_CHANGE_TOPIC,
              new AnyPsiChangeListener() {
                @Override
                public void beforePsiChanged(boolean isPhysical) {
                  clearCaches(isPhysical);
                }

                @Override
                public void afterPsiChanged(boolean isPhysical) {}
              });
    }
  }

  private void clearCaches(boolean isPhysical) {
    myCalculatedTypes.clear();
    if (isPhysical) {
      myVarToConstValueMapPhysical.clear();
    }
    myVarToConstValueMapNonPhysical.clear();
  }

  @Nullable
  public <T extends PsiExpression> PsiType getType(
      @NotNull T expr, @NotNull Function<T, PsiType> f) {
    PsiType type = getCachedType(expr);
    if (type == null) {
      final RecursionGuard.StackStamp dStackStamp = PsiDiamondType.ourDiamondGuard.markStack();
      final RecursionGuard.StackStamp gStackStamp = PsiResolveHelper.ourGraphGuard.markStack();
      type = f.fun(expr);
      if (!dStackStamp.mayCacheNow() || !gStackStamp.mayCacheNow()) {
        return type;
      }
      if (type == null) type = TypeConversionUtil.NULL_TYPE;
      Reference<PsiType> ref = new SoftReference<PsiType>(type);
      myCalculatedTypes.put(expr, ref);

      if (type instanceof PsiClassReferenceType) {
        // convert reference-based class type to the PsiImmediateClassType, since the reference may
        // become invalid
        PsiClassType.ClassResolveResult result = ((PsiClassReferenceType) type).resolveGenerics();
        PsiClass psiClass = result.getElement();
        type =
            psiClass == null
                ? type // for type with unresolved reference, leave it in the cache
                // for clients still might be able to retrieve its getCanonicalText() from the
                // reference text
                : new PsiImmediateClassType(
                    psiClass,
                    result.getSubstitutor(),
                    ((PsiClassReferenceType) type).getLanguageLevel(),
                    type.getAnnotations());
      }
    }

    if (!type.isValid()) {
      if (expr.isValid()) {
        PsiJavaCodeReferenceElement refInside =
            type instanceof PsiClassReferenceType
                ? ((PsiClassReferenceType) type).getReference()
                : null;
        @NonNls
        String typeinfo =
            type
                + " ("
                + type.getClass()
                + ")"
                + (refInside == null
                    ? ""
                    : "; ref inside: "
                        + refInside
                        + " ("
                        + refInside.getClass()
                        + ") valid:"
                        + refInside.isValid());
        LOG.error(
            "Type is invalid: "
                + typeinfo
                + "; expr: '"
                + expr
                + "' ("
                + expr.getClass()
                + ") is valid");
      } else {
        LOG.error("Expression: '" + expr + "' is invalid, must not be used for getType()");
      }
    }

    return type == TypeConversionUtil.NULL_TYPE ? null : type;
  }

  private <T extends PsiExpression> PsiType getCachedType(T expr) {
    Reference<PsiType> reference = myCalculatedTypes.get(expr);
    return SoftReference.dereference(reference);
  }

  @Nullable
  public Object computeConstantValueWithCaching(
      @NotNull PsiVariable variable,
      @NotNull ConstValueComputer computer,
      Set<PsiVariable> visitedVars) {
    boolean physical = variable.isPhysical();

    Map<PsiVariable, Object> map =
        physical ? myVarToConstValueMapPhysical : myVarToConstValueMapNonPhysical;
    Object cached = map.get(variable);
    if (cached == NULL) return null;
    if (cached != null) return cached;

    Object result = computer.execute(variable, visitedVars);
    map.put(variable, result == null ? NULL : result);
    return result;
  }

  public interface ConstValueComputer {
    Object execute(PsiVariable variable, Set<PsiVariable> visitedVars);
  }
}
Пример #21
0
/** @author Eugene Zhuravlev Date: 10/25/11 */
public class GroovyBuilder extends ModuleLevelBuilder {
  public static final String BUILDER_NAME = "groovy";
  private static final Key<Boolean> CHUNK_REBUILD_ORDERED = Key.create("CHUNK_REBUILD_ORDERED");
  private final boolean myForStubs;
  private final String myBuilderName;

  public GroovyBuilder(boolean forStubs) {
    super(forStubs ? BuilderCategory.SOURCE_GENERATOR : BuilderCategory.OVERWRITING_TRANSLATOR);
    myForStubs = forStubs;
    myBuilderName = BUILDER_NAME + (forStubs ? "-stubs" : "-classes");
  }

  public String getName() {
    return myBuilderName;
  }

  public ModuleLevelBuilder.ExitCode build(final CompileContext context, ModuleChunk chunk)
      throws ProjectBuildException {
    try {
      final List<File> toCompile = collectChangedFiles(context, chunk);
      if (toCompile.isEmpty()) {
        return ExitCode.NOTHING_DONE;
      }

      String moduleOutput = getModuleOutput(context, chunk);
      String compilerOutput = getCompilerOutput(moduleOutput);

      final Set<String> toCompilePaths = new LinkedHashSet<String>();
      for (File file : toCompile) {
        toCompilePaths.add(FileUtil.toSystemIndependentName(file.getPath()));
      }

      Map<String, String> class2Src =
          buildClassToSourceMap(chunk, context, toCompilePaths, moduleOutput);

      String ideCharset = chunk.getProject().getProjectCharset();
      String encoding =
          !Comparing.equal(CharsetToolkit.getDefaultSystemCharset().name(), ideCharset)
              ? ideCharset
              : null;
      List<String> patchers = Collections.emptyList(); // todo patchers
      final File tempFile =
          GroovycOSProcessHandler.fillFileWithGroovycParameters(
              compilerOutput,
              toCompilePaths,
              FileUtil.toSystemDependentName(moduleOutput),
              class2Src,
              encoding,
              patchers);

      // todo different outputs in a chunk
      // todo xmx
      final List<String> cmd =
          ExternalProcessUtil.buildJavaCommandLine(
              getJavaExecutable(chunk),
              "org.jetbrains.groovy.compiler.rt.GroovycRunner",
              Collections.<String>emptyList(),
              new ArrayList<String>(generateClasspath(context, chunk)),
              Arrays.asList(
                  "-Xmx384m",
                  "-Dfile.encoding=" + CharsetToolkit.getDefaultSystemCharset().name() /*,
                      "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5239"*/),
              Arrays.<String>asList(myForStubs ? "stubs" : "groovyc", tempFile.getPath()));

      final Process process = Runtime.getRuntime().exec(ArrayUtil.toStringArray(cmd));
      GroovycOSProcessHandler handler =
          GroovycOSProcessHandler.runGroovyc(
              process,
              new Consumer<String>() {
                public void consume(String s) {
                  context.processMessage(new ProgressMessage(s));
                }
              });

      if (handler.shouldRetry()) {
        if (CHUNK_REBUILD_ORDERED.get(context) != null) {
          CHUNK_REBUILD_ORDERED.set(context, null);
        } else {
          CHUNK_REBUILD_ORDERED.set(context, Boolean.TRUE);
          return ExitCode.CHUNK_REBUILD_REQUIRED;
        }
      }

      if (myForStubs) {
        JavaBuilder.addTempSourcePathRoot(context, new File(compilerOutput));
      }

      for (CompilerMessage message : handler.getCompilerMessages()) {
        context.processMessage(message);
      }
      if (!myForStubs
          && updateDependencies(
              context, chunk, toCompile, moduleOutput, handler.getSuccessfullyCompiled())) {
        return ExitCode.ADDITIONAL_PASS_REQUIRED;
      }
      return ExitCode.OK;
    } catch (Exception e) {
      throw new ProjectBuildException(e);
    }
  }

  private static String getJavaExecutable(ModuleChunk chunk) {
    Sdk sdk = chunk.getModules().iterator().next().getSdk();
    if (sdk instanceof JavaSdk) {
      return ((JavaSdk) sdk).getJavaExecutable();
    }
    return SystemProperties.getJavaHome() + "/bin/java";
  }

  private static String getModuleOutput(CompileContext context, ModuleChunk chunk) {
    final Module representativeModule = chunk.getModules().iterator().next();
    File moduleOutputDir =
        context
            .getProjectPaths()
            .getModuleOutputDir(representativeModule, context.isCompilingTests());
    assert moduleOutputDir != null;
    String moduleOutputPath = FileUtil.toCanonicalPath(moduleOutputDir.getPath());
    return moduleOutputPath.endsWith("/") ? moduleOutputPath : moduleOutputPath + "/";
  }

  private String getCompilerOutput(String moduleOutputDir) throws IOException {
    return FileUtil.toCanonicalPath(
        (myForStubs ? FileUtil.createTempDirectory("groovyStubs", null) : new File(moduleOutputDir))
            .getPath());
  }

  private static List<File> collectChangedFiles(CompileContext context, ModuleChunk chunk)
      throws IOException {
    final List<File> toCompile = new ArrayList<File>();
    context.processFilesToRecompile(
        chunk,
        new FileProcessor() {
          public boolean apply(Module module, File file, String sourceRoot) throws IOException {
            final String path = file.getPath();
            if (isGroovyFile(path)) { // todo file type check
              toCompile.add(file);
            }
            return true;
          }
        });
    return toCompile;
  }

  private boolean updateDependencies(
      CompileContext context,
      ModuleChunk chunk,
      List<File> toCompile,
      String moduleOutputPath,
      List<GroovycOSProcessHandler.OutputItem> successfullyCompiled)
      throws IOException {
    final Mappings delta = context.createDelta();
    final List<File> successfullyCompiledFiles = new ArrayList<File>();
    if (!successfullyCompiled.isEmpty()) {

      final Callbacks.Backend callback = delta.getCallback();
      final FileGeneratedEvent generatedEvent = new FileGeneratedEvent();

      for (GroovycOSProcessHandler.OutputItem item : successfullyCompiled) {
        final String sourcePath = FileUtil.toSystemIndependentName(item.sourcePath);
        final String outputPath = FileUtil.toSystemIndependentName(item.outputPath);
        final RootDescriptor moduleAndRoot = context.getModuleAndRoot(new File(sourcePath));
        if (moduleAndRoot != null) {
          final String moduleName = moduleAndRoot.module.getName().toLowerCase(Locale.US);
          context
              .getDataManager()
              .getSourceToOutputMap(moduleName, moduleAndRoot.isTestRoot)
              .appendData(sourcePath, outputPath);
        }
        callback.associate(
            outputPath,
            Callbacks.getDefaultLookup(sourcePath),
            new ClassReader(FileUtil.loadFileBytes(new File(outputPath))));
        successfullyCompiledFiles.add(new File(sourcePath));

        generatedEvent.add(
            moduleOutputPath, FileUtil.getRelativePath(moduleOutputPath, outputPath, '/'));
      }

      context.processMessage(generatedEvent);
    }

    return updateMappings(context, delta, chunk, toCompile, successfullyCompiledFiles);
  }

  private static List<String> generateClasspath(CompileContext context, ModuleChunk chunk) {
    final Set<String> cp = new LinkedHashSet<String>();
    // groovy_rt.jar
    // IMPORTANT! must be the first in classpath
    cp.add(ClasspathBootstrap.getResourcePath(GroovyCompilerWrapper.class).getPath());

    for (File file :
        context
            .getProjectPaths()
            .getClasspathFiles(chunk, ClasspathKind.compile(context.isCompilingTests()), false)) {
      cp.add(FileUtil.toCanonicalPath(file.getPath()));
    }
    for (File file :
        context
            .getProjectPaths()
            .getClasspathFiles(chunk, ClasspathKind.runtime(context.isCompilingTests()), false)) {
      cp.add(FileUtil.toCanonicalPath(file.getPath()));
    }
    return new ArrayList<String>(cp);
  }

  private static boolean isGroovyFile(String path) {
    return path.endsWith(".groovy") || path.endsWith(".gpp");
  }

  private static Map<String, String> buildClassToSourceMap(
      ModuleChunk chunk,
      CompileContext context,
      Set<String> toCompilePaths,
      String moduleOutputPath)
      throws IOException {
    final Map<String, String> class2Src = new HashMap<String, String>();
    for (Module module : chunk.getModules()) {
      final String moduleName = module.getName().toLowerCase(Locale.US);
      final SourceToOutputMapping srcToOut =
          context.getDataManager().getSourceToOutputMap(moduleName, context.isCompilingTests());
      for (String src : srcToOut.getKeys()) {
        if (!toCompilePaths.contains(src) && isGroovyFile(src)) {
          final Collection<String> outs = srcToOut.getState(src);
          if (outs != null) {
            for (String out : outs) {
              if (out.endsWith(".class") && out.startsWith(moduleOutputPath)) {
                final String className =
                    out.substring(moduleOutputPath.length(), out.length() - ".class".length())
                        .replace('/', '.');
                class2Src.put(className, src);
              }
            }
          }
        }
      }
    }
    return class2Src;
  }

  @Override
  public String toString() {
    return "GroovyBuilder{" + "myForStubs=" + myForStubs + '}';
  }

  public String getDescription() {
    return "Groovy builder";
  }
}
public class MagicConstantInspection extends LocalInspectionTool {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInspection.magicConstant.MagicConstantInspection");

  @Nls
  @NotNull
  @Override
  public String getGroupDisplayName() {
    return GroupNames.BUGS_GROUP_NAME;
  }

  @Nls
  @NotNull
  @Override
  public String getDisplayName() {
    return "Magic Constant";
  }

  @NotNull
  @Override
  public String getShortName() {
    return "MagicConstant";
  }

  @NotNull
  @Override
  public PsiElementVisitor buildVisitor(
      @NotNull final ProblemsHolder holder,
      boolean isOnTheFly,
      @NotNull LocalInspectionToolSession session) {
    checkAnnotationsJarAttached(session);
    return new JavaElementVisitor() {
      @Override
      public void visitCallExpression(PsiCallExpression callExpression) {
        checkCall(callExpression, holder);
      }

      @Override
      public void visitAssignmentExpression(PsiAssignmentExpression expression) {
        PsiExpression r = expression.getRExpression();
        if (r == null) return;
        PsiExpression l = expression.getLExpression();
        if (!(l instanceof PsiReferenceExpression)) return;
        PsiElement resolved = ((PsiReferenceExpression) l).resolve();
        if (!(resolved instanceof PsiModifierListOwner)) return;
        PsiModifierListOwner owner = (PsiModifierListOwner) resolved;
        PsiType type = expression.getType();
        checkExpression(r, owner, type, holder);
      }

      @Override
      public void visitReturnStatement(PsiReturnStatement statement) {
        PsiExpression value = statement.getReturnValue();
        if (value == null) return;
        PsiMethod method = PsiTreeUtil.getParentOfType(statement, PsiMethod.class);
        if (method == null) return;
        checkExpression(value, method, value.getType(), holder);
      }

      @Override
      public void visitNameValuePair(PsiNameValuePair pair) {
        PsiAnnotationMemberValue value = pair.getValue();
        if (!(value instanceof PsiExpression)) return;
        PsiReference ref = pair.getReference();
        if (ref == null) return;
        PsiMethod method = (PsiMethod) ref.resolve();
        if (method == null) return;
        checkExpression((PsiExpression) value, method, method.getReturnType(), holder);
      }

      @Override
      public void visitBinaryExpression(PsiBinaryExpression expression) {
        IElementType tokenType = expression.getOperationTokenType();
        if (tokenType != JavaTokenType.EQEQ && tokenType != JavaTokenType.NE) return;
        PsiExpression l = expression.getLOperand();
        PsiExpression r = expression.getROperand();
        if (r == null) return;
        checkBinary(l, r);
        checkBinary(r, l);
      }

      private void checkBinary(PsiExpression l, PsiExpression r) {
        if (l instanceof PsiReference) {
          PsiElement resolved = ((PsiReference) l).resolve();
          if (resolved instanceof PsiModifierListOwner) {
            checkExpression(
                r,
                (PsiModifierListOwner) resolved,
                getType((PsiModifierListOwner) resolved),
                holder);
          }
        } else if (l instanceof PsiMethodCallExpression) {
          PsiMethod method = ((PsiMethodCallExpression) l).resolveMethod();
          if (method != null) {
            checkExpression(r, method, method.getReturnType(), holder);
          }
        }
      }
    };
  }

  private static void checkAnnotationsJarAttached(@NotNull LocalInspectionToolSession session) {
    PsiFile file = session.getFile();
    final Project project = file.getProject();
    PsiClass event =
        JavaPsiFacade.getInstance(project)
            .findClass("java.awt.event.InputEvent", GlobalSearchScope.allScope(project));
    if (event == null) return; // no jdk to attach
    PsiMethod[] methods = event.findMethodsByName("getModifiers", false);
    if (methods.length != 1) return; // no jdk to attach
    PsiMethod getModifiers = methods[0];
    PsiAnnotation annotation =
        ExternalAnnotationsManager.getInstance(project)
            .findExternalAnnotation(getModifiers, MagicConstant.class.getName());
    if (annotation != null) return;
    final VirtualFile virtualFile = PsiUtilCore.getVirtualFile(getModifiers);
    if (virtualFile == null) return; // no jdk to attach
    final List<OrderEntry> entries =
        ProjectRootManager.getInstance(project).getFileIndex().getOrderEntriesForFile(virtualFile);
    Sdk jdk = null;
    for (OrderEntry orderEntry : entries) {
      if (orderEntry instanceof JdkOrderEntry) {
        jdk = ((JdkOrderEntry) orderEntry).getJdk();
        if (jdk != null) break;
      }
    }
    if (jdk == null) return; // no jdk to attach

    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      final Sdk finalJdk = jdk;
      ApplicationManager.getApplication()
          .invokeLater(
              new Runnable() {
                @Override
                public void run() {
                  ApplicationManager.getApplication()
                      .runWriteAction(
                          new Runnable() {
                            public void run() {
                              attachJdkAnnotations(finalJdk);
                            }
                          });
                }
              },
              ModalityState.NON_MODAL,
              project.getDisposed());
    }
  }

  private static void attachJdkAnnotations(Sdk jdk) {
    LocalFileSystem lfs = LocalFileSystem.getInstance();
    VirtualFile root = null;
    if (root == null) { // community idea under idea
      root =
          lfs.findFileByPath(
              FileUtil.toSystemIndependentName(PathManager.getHomePath()) + "/java/jdkAnnotations");
    }
    if (root == null) { // idea under idea
      root =
          lfs.findFileByPath(
              FileUtil.toSystemIndependentName(PathManager.getHomePath())
                  + "/community/java/jdkAnnotations");
    }
    if (root == null) { // build
      root =
          VirtualFileManager.getInstance()
              .findFileByUrl(
                  "jar://"
                      + FileUtil.toSystemIndependentName(PathManager.getHomePath())
                      + "/lib/jdkAnnotations.jar!/");
    }
    if (root == null) {
      LOG.error(
          "jdk annotations not found in: "
              + FileUtil.toSystemIndependentName(PathManager.getHomePath())
              + "/lib/jdkAnnotations.jar!/");
      return;
    }

    SdkModificator modificator = jdk.getSdkModificator();
    modificator.removeRoot(root, AnnotationOrderRootType.getInstance());
    modificator.addRoot(root, AnnotationOrderRootType.getInstance());
    modificator.commitChanges();
  }

  private static void checkExpression(
      PsiExpression expression, PsiModifierListOwner owner, PsiType type, ProblemsHolder holder) {
    AllowedValues allowed = getAllowedValues(owner, type, null);
    if (allowed == null) return;
    PsiElement scope = PsiUtil.getTopLevelEnclosingCodeBlock(expression, null);
    if (scope == null) scope = expression;
    if (!isAllowed(scope, expression, allowed, expression.getManager())) {
      registerProblem(expression, allowed, holder);
    }
  }

  private static void checkCall(
      @NotNull PsiCallExpression methodCall, @NotNull ProblemsHolder holder) {
    PsiMethod method = methodCall.resolveMethod();
    if (method == null) return;
    PsiParameter[] parameters = method.getParameterList().getParameters();
    PsiExpression[] arguments = methodCall.getArgumentList().getExpressions();
    for (int i = 0; i < parameters.length; i++) {
      PsiParameter parameter = parameters[i];
      AllowedValues values = getAllowedValues(parameter, parameter.getType(), null);
      if (values == null) continue;
      if (i >= arguments.length) break;
      PsiExpression argument = arguments[i];
      argument = PsiUtil.deparenthesizeExpression(argument);
      if (argument == null) continue;

      checkMagicParameterArgument(parameter, argument, values, holder);
    }
  }

  static class AllowedValues {
    final PsiAnnotationMemberValue[] values;
    final boolean canBeOred;

    private AllowedValues(PsiAnnotationMemberValue[] values, boolean canBeOred) {
      this.values = values;
      this.canBeOred = canBeOred;
    }

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

      AllowedValues a2 = (AllowedValues) o;
      if (canBeOred != a2.canBeOred) {
        return false;
      }
      Set<PsiAnnotationMemberValue> v1 =
          new THashSet<PsiAnnotationMemberValue>(Arrays.asList(values));
      Set<PsiAnnotationMemberValue> v2 =
          new THashSet<PsiAnnotationMemberValue>(Arrays.asList(a2.values));
      if (v1.size() != v2.size()) {
        return false;
      }
      for (PsiAnnotationMemberValue value : v1) {
        for (PsiAnnotationMemberValue value2 : v2) {
          if (same(value, value2, value.getManager())) {
            v2.remove(value2);
            break;
          }
        }
      }
      return v2.isEmpty();
    }

    @Override
    public int hashCode() {
      int result = values != null ? Arrays.hashCode(values) : 0;
      result = 31 * result + (canBeOred ? 1 : 0);
      return result;
    }

    public boolean isSubsetOf(@NotNull AllowedValues other, @NotNull PsiManager manager) {
      for (PsiAnnotationMemberValue value : values) {
        boolean found = false;
        for (PsiAnnotationMemberValue otherValue : other.values) {
          if (same(value, otherValue, manager)) {
            found = true;
            break;
          }
        }
        if (!found) return false;
      }
      return true;
    }
  }

  private static AllowedValues getAllowedValuesFromMagic(
      @NotNull PsiModifierListOwner element, @NotNull PsiType type, PsiAnnotation magic) {
    if (magic == null) return null;
    PsiAnnotationMemberValue[] allowedValues;
    final boolean canBeOred;
    if (TypeConversionUtil.getTypeRank(type) <= TypeConversionUtil.LONG_RANK) {
      PsiAnnotationMemberValue intValues = magic.findAttributeValue("intValues");
      allowedValues =
          intValues instanceof PsiArrayInitializerMemberValue
              ? ((PsiArrayInitializerMemberValue) intValues).getInitializers()
              : PsiAnnotationMemberValue.EMPTY_ARRAY;
      if (allowedValues.length == 0) {
        PsiAnnotationMemberValue orValue = magic.findAttributeValue("flags");
        allowedValues =
            orValue instanceof PsiArrayInitializerMemberValue
                ? ((PsiArrayInitializerMemberValue) orValue).getInitializers()
                : PsiAnnotationMemberValue.EMPTY_ARRAY;
        canBeOred = true;
      } else {
        canBeOred = false;
      }
    } else if (type.equals(
        PsiType.getJavaLangString(
            element.getManager(), GlobalSearchScope.allScope(element.getProject())))) {
      PsiAnnotationMemberValue strValuesAttr = magic.findAttributeValue("stringValues");
      allowedValues =
          strValuesAttr instanceof PsiArrayInitializerMemberValue
              ? ((PsiArrayInitializerMemberValue) strValuesAttr).getInitializers()
              : PsiAnnotationMemberValue.EMPTY_ARRAY;
      canBeOred = false;
    } else {
      return null; // other types not supported
    }

    if (allowedValues.length != 0) {
      return new AllowedValues(allowedValues, canBeOred);
    }

    // last resort: try valuesFromClass
    PsiAnnotationMemberValue[] values = readFromClass("valuesFromClass", magic, type);
    boolean ored = false;
    if (values == null) {
      values = readFromClass("flagsFromClass", magic, type);
      ored = true;
    }
    if (values == null) return null;
    return new AllowedValues(values, ored);
  }

  private static PsiAnnotationMemberValue[] readFromClass(
      @NonNls String attributeName, @NotNull PsiAnnotation magic, PsiType type) {
    PsiAnnotationMemberValue fromClassAttr = magic.findAttributeValue(attributeName);
    PsiType fromClassType =
        fromClassAttr instanceof PsiClassObjectAccessExpression
            ? ((PsiClassObjectAccessExpression) fromClassAttr).getOperand().getType()
            : null;
    PsiClass fromClass =
        fromClassType instanceof PsiClassType ? ((PsiClassType) fromClassType).resolve() : null;
    if (fromClass == null) return null;
    String fqn = fromClass.getQualifiedName();
    if (fqn == null) return null;
    List<PsiAnnotationMemberValue> constants = new ArrayList<PsiAnnotationMemberValue>();
    for (PsiField field : fromClass.getFields()) {
      if (!field.hasModifierProperty(PsiModifier.PUBLIC)
          || !field.hasModifierProperty(PsiModifier.STATIC)
          || !field.hasModifierProperty(PsiModifier.FINAL)) continue;
      PsiType fieldType = field.getType();
      if (!Comparing.equal(fieldType, type)) continue;
      PsiAssignmentExpression e =
          (PsiAssignmentExpression)
              JavaPsiFacade.getElementFactory(field.getProject())
                  .createExpressionFromText("x=" + fqn + "." + field.getName(), field);
      PsiReferenceExpression refToField = (PsiReferenceExpression) e.getRExpression();
      constants.add(refToField);
    }
    if (constants.isEmpty()) return null;

    return constants.toArray(new PsiAnnotationMemberValue[constants.size()]);
  }

  static AllowedValues getAllowedValues(
      @NotNull PsiModifierListOwner element, PsiType type, Set<PsiClass> visited) {
    PsiAnnotation[] annotations = AnnotationUtil.getAllAnnotations(element, true, null);
    for (PsiAnnotation annotation : annotations) {
      AllowedValues values;
      if (type != null && MagicConstant.class.getName().equals(annotation.getQualifiedName())) {
        // PsiAnnotation magic = AnnotationUtil.findAnnotationInHierarchy(element,
        // Collections.singleton(MagicConstant.class.getName()));
        values = getAllowedValuesFromMagic(element, type, annotation);
        if (values != null) return values;
      }

      PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement();
      PsiElement resolved = ref == null ? null : ref.resolve();
      if (!(resolved instanceof PsiClass) || !((PsiClass) resolved).isAnnotationType()) continue;
      PsiClass aClass = (PsiClass) resolved;
      if (visited == null) visited = new THashSet<PsiClass>();
      if (!visited.add(aClass)) continue;
      values = getAllowedValues(aClass, type, visited);
      if (values != null) return values;
    }

    return parseBeanInfo(element);
  }

  private static AllowedValues parseBeanInfo(@NotNull PsiModifierListOwner owner) {
    PsiMethod method = null;
    if (owner instanceof PsiParameter) {
      PsiParameter parameter = (PsiParameter) owner;
      PsiElement scope = parameter.getDeclarationScope();
      if (!(scope instanceof PsiMethod)) return null;
      PsiElement nav = scope.getNavigationElement();
      if (!(nav instanceof PsiMethod)) return null;
      method = (PsiMethod) nav;
      if (method.isConstructor()) {
        // not a property, try the @ConstructorProperties({"prop"})
        PsiAnnotation annotation =
            AnnotationUtil.findAnnotation(method, "java.beans.ConstructorProperties");
        if (annotation == null) return null;
        PsiAnnotationMemberValue value = annotation.findAttributeValue("value");
        if (!(value instanceof PsiArrayInitializerMemberValue)) return null;
        PsiAnnotationMemberValue[] initializers =
            ((PsiArrayInitializerMemberValue) value).getInitializers();
        PsiElement parent = parameter.getParent();
        if (!(parent instanceof PsiParameterList)) return null;
        int index = ((PsiParameterList) parent).getParameterIndex(parameter);
        if (index >= initializers.length) return null;
        PsiAnnotationMemberValue initializer = initializers[index];
        if (!(initializer instanceof PsiLiteralExpression)) return null;
        Object val = ((PsiLiteralExpression) initializer).getValue();
        if (!(val instanceof String)) return null;
        PsiMethod setter =
            PropertyUtil.findPropertySetter(
                method.getContainingClass(), (String) val, false, false);
        if (setter == null) return null;
        // try the @beaninfo of the corresponding setter
        method = (PsiMethod) setter.getNavigationElement();
      }
    } else if (owner instanceof PsiMethod) {
      PsiElement nav = owner.getNavigationElement();
      if (!(nav instanceof PsiMethod)) return null;
      method = (PsiMethod) nav;
    }
    if (method == null) return null;

    PsiClass aClass = method.getContainingClass();
    if (aClass == null) return null;
    if (PropertyUtil.isSimplePropertyGetter(method)) {
      List<PsiMethod> setters =
          PropertyUtil.getSetters(aClass, PropertyUtil.getPropertyNameByGetter(method));
      if (setters.size() != 1) return null;
      method = setters.get(0);
    }
    if (!PropertyUtil.isSimplePropertySetter(method)) return null;
    PsiDocComment doc = method.getDocComment();
    if (doc == null) return null;
    PsiDocTag beaninfo = doc.findTagByName("beaninfo");
    if (beaninfo == null) return null;
    String data =
        StringUtil.join(
            beaninfo.getDataElements(),
            new Function<PsiElement, String>() {
              @Override
              public String fun(PsiElement element) {
                return element.getText();
              }
            },
            "\n");
    int enumIndex = StringUtil.indexOfSubstringEnd(data, "enum:");
    if (enumIndex == -1) return null;
    data = data.substring(enumIndex);
    int colon = data.indexOf(":");
    int last = colon == -1 ? data.length() : data.substring(0, colon).lastIndexOf("\n");
    data = data.substring(0, last);

    List<PsiAnnotationMemberValue> values = new ArrayList<PsiAnnotationMemberValue>();
    for (String line : StringUtil.splitByLines(data)) {
      List<String> words = StringUtil.split(line, " ", true, true);
      if (words.size() != 2) continue;
      String ref = words.get(1);
      PsiExpression constRef =
          JavaPsiFacade.getElementFactory(aClass.getProject())
              .createExpressionFromText(ref, aClass);
      if (!(constRef instanceof PsiReferenceExpression)) continue;
      PsiReferenceExpression expr = (PsiReferenceExpression) constRef;
      values.add(expr);
    }
    if (values.isEmpty()) return null;
    PsiAnnotationMemberValue[] array = values.toArray(new PsiAnnotationMemberValue[values.size()]);
    return new AllowedValues(array, false);
  }

  private static PsiType getType(PsiModifierListOwner element) {
    return element instanceof PsiVariable
        ? ((PsiVariable) element).getType()
        : element instanceof PsiMethod ? ((PsiMethod) element).getReturnType() : null;
  }

  private static void checkMagicParameterArgument(
      @NotNull PsiParameter parameter,
      PsiExpression argument,
      @NotNull AllowedValues allowedValues,
      @NotNull ProblemsHolder holder) {
    final PsiManager manager = PsiManager.getInstance(holder.getProject());

    if (!argument.getTextRange().isEmpty()
        && !isAllowed(parameter.getDeclarationScope(), argument, allowedValues, manager)) {
      registerProblem(argument, allowedValues, holder);
    }
  }

  private static void registerProblem(
      PsiExpression argument, AllowedValues allowedValues, ProblemsHolder holder) {
    String values =
        StringUtil.join(
            allowedValues.values,
            new Function<PsiAnnotationMemberValue, String>() {
              @Override
              public String fun(PsiAnnotationMemberValue value) {
                if (value instanceof PsiReferenceExpression) {
                  PsiElement resolved = ((PsiReferenceExpression) value).resolve();
                  if (resolved instanceof PsiVariable) {
                    return PsiFormatUtil.formatVariable(
                        (PsiVariable) resolved,
                        PsiFormatUtilBase.SHOW_NAME | PsiFormatUtilBase.SHOW_CONTAINING_CLASS,
                        PsiSubstitutor.EMPTY);
                  }
                }
                return value.getText();
              }
            },
            ", ");
    holder.registerProblem(argument, "Must be one of: " + values);
  }

  private static boolean isAllowed(
      @NotNull final PsiElement scope,
      @NotNull final PsiExpression argument,
      @NotNull final AllowedValues allowedValues,
      @NotNull final PsiManager manager) {
    if (isGoodExpression(argument, allowedValues, scope, manager)) return true;

    return processValuesFlownTo(
        argument,
        scope,
        new Processor<PsiExpression>() {
          @Override
          public boolean process(PsiExpression expression) {
            if (false & !PsiTreeUtil.isAncestor(scope, expression, false)) return true;
            return isGoodExpression(expression, allowedValues, scope, manager);
          }
        });
  }

  private static boolean isGoodExpression(
      PsiExpression expression,
      @NotNull AllowedValues allowedValues,
      @NotNull PsiElement scope,
      @NotNull PsiManager manager) {
    expression = PsiUtil.deparenthesizeExpression(expression);
    if (expression == null) return true;
    if (expression instanceof PsiConditionalExpression) {
      PsiExpression thenExpression = ((PsiConditionalExpression) expression).getThenExpression();
      boolean thenAllowed =
          thenExpression == null || isAllowed(scope, thenExpression, allowedValues, manager);
      if (!thenAllowed) return false;
      PsiExpression elseExpression = ((PsiConditionalExpression) expression).getElseExpression();
      return elseExpression == null || isAllowed(scope, elseExpression, allowedValues, manager);
    }

    if (isOneOf(expression, allowedValues, manager)) return true;

    if (allowedValues.canBeOred) {
      PsiExpression zero = getLiteralExpression(expression, manager, "0");
      if (same(expression, zero, manager)) return true;
      PsiExpression mOne = getLiteralExpression(expression, manager, "-1");
      if (same(expression, mOne, manager)) return true;
      if (expression instanceof PsiPolyadicExpression) {
        IElementType tokenType = ((PsiPolyadicExpression) expression).getOperationTokenType();
        if (JavaTokenType.OR.equals(tokenType) || JavaTokenType.AND.equals(tokenType)) {
          for (PsiExpression operand : ((PsiPolyadicExpression) expression).getOperands()) {
            if (!isAllowed(scope, operand, allowedValues, manager)) return false;
          }
          return true;
        }
      }
      if (expression instanceof PsiPrefixExpression
          && JavaTokenType.TILDE.equals(
              ((PsiPrefixExpression) expression).getOperationTokenType())) {
        PsiExpression operand = ((PsiPrefixExpression) expression).getOperand();
        return operand == null || isAllowed(scope, operand, allowedValues, manager);
      }
    }

    PsiElement resolved = null;
    if (expression instanceof PsiReference) {
      resolved = ((PsiReference) expression).resolve();
    } else if (expression instanceof PsiCallExpression) {
      resolved = ((PsiCallExpression) expression).resolveMethod();
    }

    AllowedValues allowedForRef;
    if (resolved instanceof PsiModifierListOwner
        && (allowedForRef =
                getAllowedValues(
                    (PsiModifierListOwner) resolved,
                    getType((PsiModifierListOwner) resolved),
                    null))
            != null
        && allowedForRef.isSubsetOf(allowedValues, manager)) return true;

    return PsiType.NULL.equals(expression.getType());
  }

  private static Key<Map<String, PsiExpression>> LITERAL_EXPRESSION_CACHE =
      Key.create("LITERAL_EXPRESSION_CACHE");

  private static PsiExpression getLiteralExpression(
      PsiExpression context, PsiManager manager, @NotNull String text) {
    Map<String, PsiExpression> cache = LITERAL_EXPRESSION_CACHE.get(manager);
    if (cache == null) {
      cache = new ConcurrentSoftValueHashMap<String, PsiExpression>();
      cache = manager.putUserDataIfAbsent(LITERAL_EXPRESSION_CACHE, cache);
    }
    PsiExpression expression = cache.get(text);
    if (expression == null) {
      expression =
          JavaPsiFacade.getElementFactory(manager.getProject())
              .createExpressionFromText(text, context);
      cache.put(text, expression);
    }
    return expression;
  }

  private static boolean isOneOf(
      @NotNull PsiExpression expression,
      @NotNull AllowedValues allowedValues,
      @NotNull PsiManager manager) {
    for (PsiAnnotationMemberValue allowedValue : allowedValues.values) {
      if (same(allowedValue, expression, manager)) return true;
    }
    return false;
  }

  private static boolean same(PsiElement e1, PsiElement e2, @NotNull PsiManager manager) {
    if (e1 instanceof PsiLiteralExpression && e2 instanceof PsiLiteralExpression) {
      return Comparing.equal(
          ((PsiLiteralExpression) e1).getValue(), ((PsiLiteralExpression) e2).getValue());
    }
    if (e1 instanceof PsiPrefixExpression
        && e2 instanceof PsiPrefixExpression
        && ((PsiPrefixExpression) e1).getOperationTokenType()
            == ((PsiPrefixExpression) e2).getOperationTokenType()) {
      return same(
          ((PsiPrefixExpression) e1).getOperand(),
          ((PsiPrefixExpression) e2).getOperand(),
          manager);
    }
    if (e1 instanceof PsiReference && e2 instanceof PsiReference) {
      e1 = ((PsiReference) e1).resolve();
      e2 = ((PsiReference) e2).resolve();
    }
    return manager.areElementsEquivalent(e2, e1);
  }

  private static boolean processValuesFlownTo(
      @NotNull final PsiExpression argument,
      PsiElement scope,
      @NotNull final Processor<PsiExpression> processor) {
    SliceAnalysisParams params = new SliceAnalysisParams();
    params.dataFlowToThis = true;
    params.scope = new AnalysisScope(new LocalSearchScope(scope), argument.getProject());

    SliceRootNode rootNode =
        new SliceRootNode(
            scope.getProject(), new DuplicateMap(), SliceManager.createRootUsage(argument, params));

    Collection<? extends AbstractTreeNode> children =
        rootNode.getChildren().iterator().next().getChildren();
    for (AbstractTreeNode child : children) {
      SliceUsage usage = (SliceUsage) child.getValue();
      PsiElement element = usage.getElement();
      if (element instanceof PsiExpression && !processor.process((PsiExpression) element))
        return false;
    }

    return !children.isEmpty();
  }
}
/** Author: msk */
public class EditorsSplitters extends IdePanePanel implements UISettingsListener, Disposable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.fileEditor.impl.EditorsSplitters");
  private static final String PINNED = "pinned";
  private static final String CURRENT_IN_TAB = "current-in-tab";

  private static final Key<Object> DUMMY_KEY = Key.create("EditorsSplitters.dummy.key");

  private EditorWindow myCurrentWindow;
  private final Set<EditorWindow> myWindows = new CopyOnWriteArraySet<EditorWindow>();

  private final FileEditorManagerImpl myManager;
  private Element mySplittersElement; // temporarily used during initialization
  int myInsideChange;
  private final MyFocusWatcher myFocusWatcher;
  private final Alarm myIconUpdaterAlarm = new Alarm();
  private final UIBuilder myUIBuilder = new UIBuilder();

  EditorsSplitters(
      final FileEditorManagerImpl manager,
      DockManager dockManager,
      boolean createOwnDockableContainer) {
    super(new BorderLayout());
    myManager = manager;
    myFocusWatcher = new MyFocusWatcher();
    setFocusTraversalPolicy(new MyFocusTraversalPolicy());
    clear();

    if (createOwnDockableContainer) {
      DockableEditorTabbedContainer dockable =
          new DockableEditorTabbedContainer(myManager.getProject(), this, false);
      Disposer.register(manager.getProject(), dockable);
      dockManager.register(dockable);
    }
    KeymapManagerListener keymapListener =
        new KeymapManagerListener() {
          @Override
          public void activeKeymapChanged(Keymap keymap) {
            invalidate();
            repaint();
          }
        };
    KeymapManager.getInstance().addKeymapManagerListener(keymapListener, this);
  }

  public FileEditorManagerImpl getManager() {
    return myManager;
  }

  public void clear() {
    for (EditorWindow window : myWindows) {
      window.dispose();
    }
    removeAll();
    myWindows.clear();
    setCurrentWindow(null);
    repaint(); // revalidate doesn't repaint correctly after "Close All"
  }

  void startListeningFocus() {
    myFocusWatcher.install(this);
  }

  private void stopListeningFocus() {
    myFocusWatcher.deinstall(this);
  }

  @Override
  public void dispose() {
    myIconUpdaterAlarm.cancelAllRequests();
    stopListeningFocus();
  }

  @Nullable
  public VirtualFile getCurrentFile() {
    if (myCurrentWindow != null) {
      return myCurrentWindow.getSelectedFile();
    }
    return null;
  }

  private boolean showEmptyText() {
    return myCurrentWindow == null || myCurrentWindow.getFiles().length == 0;
  }

  @Override
  protected void paintComponent(Graphics g) {
    if (showEmptyText()) {
      Graphics2D gg = IdeBackgroundUtil.withFrameBackground(g, this);
      super.paintComponent(gg);
      g.setColor(UIUtil.isUnderDarcula() ? UIUtil.getBorderColor() : new Color(0, 0, 0, 50));
      g.drawLine(0, 0, getWidth(), 0);
    }
  }

  public void writeExternal(final Element element) {
    if (getComponentCount() != 0) {
      final Component comp = getComponent(0);
      LOG.assertTrue(comp instanceof JPanel);
      final JPanel panel = (JPanel) comp;
      if (panel.getComponentCount() != 0) {
        element.addContent(writePanel(panel));
      }
    }
  }

  @SuppressWarnings("HardCodedStringLiteral")
  private Element writePanel(final JPanel panel) {
    final Component comp = panel.getComponent(0);
    if (comp instanceof Splitter) {
      final Splitter splitter = (Splitter) comp;
      final Element res = new Element("splitter");
      res.setAttribute("split-orientation", splitter.getOrientation() ? "vertical" : "horizontal");
      res.setAttribute("split-proportion", Float.toString(splitter.getProportion()));
      final Element first = new Element("split-first");
      first.addContent(writePanel((JPanel) splitter.getFirstComponent()));
      final Element second = new Element("split-second");
      second.addContent(writePanel((JPanel) splitter.getSecondComponent()));
      res.addContent(first);
      res.addContent(second);
      return res;
    } else if (comp instanceof JBTabs) {
      final Element res = new Element("leaf");
      Integer limit =
          UIUtil.getClientProperty(
              ((JBTabs) comp).getComponent(), JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY);
      if (limit != null) {
        res.setAttribute(JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY.toString(), String.valueOf(limit));
      }

      writeWindow(res, findWindowWith(comp));
      return res;
    } else if (comp instanceof EditorWindow.TCompForTablessMode) {
      EditorWithProviderComposite composite = ((EditorWindow.TCompForTablessMode) comp).myEditor;
      Element res = new Element("leaf");
      res.addContent(writeComposite(composite.getFile(), composite, false, composite));
      return res;
    } else {
      LOG.error(comp != null ? comp.getClass().getName() : null);
      return null;
    }
  }

  private void writeWindow(@NotNull Element res, @Nullable EditorWindow window) {
    if (window != null) {
      EditorWithProviderComposite[] composites = window.getEditors();
      for (int i = 0; i < composites.length; i++) {
        VirtualFile file = window.getFileAt(i);
        res.addContent(
            writeComposite(
                file, composites[i], window.isFilePinned(file), window.getSelectedEditor()));
      }
    }
  }

  @NotNull
  private Element writeComposite(
      VirtualFile file,
      EditorWithProviderComposite composite,
      boolean pinned,
      EditorWithProviderComposite selectedEditor) {
    Element fileElement = new Element("file");
    fileElement.setAttribute("leaf-file-name", file.getName()); // TODO: all files
    composite.currentStateAsHistoryEntry().writeExternal(fileElement, getManager().getProject());
    fileElement.setAttribute(PINNED, Boolean.toString(pinned));
    fileElement.setAttribute(CURRENT_IN_TAB, Boolean.toString(composite.equals(selectedEditor)));
    return fileElement;
  }

  public void openFiles() {
    if (mySplittersElement != null) {
      initializeProgress();
      final JPanel comp = myUIBuilder.process(mySplittersElement, getTopPanel());
      UIUtil.invokeAndWaitIfNeeded(
          new Runnable() {
            @Override
            public void run() {
              if (comp != null) {
                removeAll();
                add(comp, BorderLayout.CENTER);
                mySplittersElement = null;
              }
              // clear empty splitters
              for (EditorWindow window : getWindows()) {
                if (window.getEditors().length == 0) {
                  for (EditorWindow sibling : window.findSiblings()) {
                    sibling.unsplit(false);
                  }
                }
              }
            }
          });
    }
  }

  private static void initializeProgress() {
    ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
    if (indicator != null) {
      indicator.setText(IdeBundle.message("loading.editors"));
    }
  }

  public int getEditorsCount() {
    return mySplittersElement == null ? 0 : countFiles(mySplittersElement);
  }

  private double myProgressStep;

  public void setProgressStep(double step) {
    myProgressStep = step;
  }

  private void updateProgress() {
    ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
    if (indicator != null) {
      indicator.setFraction(indicator.getFraction() + myProgressStep);
    }
  }

  private static int countFiles(Element element) {
    Integer value =
        new ConfigTreeReader<Integer>() {
          @Override
          protected Integer processFiles(
              @NotNull List<Element> fileElements, @Nullable Integer context) {
            return fileElements.size();
          }

          @Override
          protected Integer processSplitter(
              @NotNull Element element,
              @Nullable Element firstChild,
              @Nullable Element secondChild,
              @Nullable Integer context) {
            Integer first = process(firstChild, null);
            Integer second = process(secondChild, null);
            return (first == null ? 0 : first) + (second == null ? 0 : second);
          }
        }.process(element, null);
    return value == null ? 0 : value;
  }

  public void readExternal(final Element element) {
    mySplittersElement = element;
  }

  @NotNull
  public VirtualFile[] getOpenFiles() {
    final Set<VirtualFile> files = new ArrayListSet<VirtualFile>();
    for (final EditorWindow myWindow : myWindows) {
      final EditorWithProviderComposite[] editors = myWindow.getEditors();
      for (final EditorWithProviderComposite editor : editors) {
        VirtualFile file = editor.getFile();
        // background thread may call this method when invalid file is being removed
        // do not return it here as it will quietly drop out soon
        if (file.isValid()) {
          files.add(file);
        }
      }
    }
    return VfsUtilCore.toVirtualFileArray(files);
  }

  @NotNull
  public VirtualFile[] getSelectedFiles() {
    final Set<VirtualFile> files = new ArrayListSet<VirtualFile>();
    for (final EditorWindow window : myWindows) {
      final VirtualFile file = window.getSelectedFile();
      if (file != null) {
        files.add(file);
      }
    }
    final VirtualFile[] virtualFiles = VfsUtilCore.toVirtualFileArray(files);
    final VirtualFile currentFile = getCurrentFile();
    if (currentFile != null) {
      for (int i = 0; i != virtualFiles.length; ++i) {
        if (Comparing.equal(virtualFiles[i], currentFile)) {
          virtualFiles[i] = virtualFiles[0];
          virtualFiles[0] = currentFile;
          break;
        }
      }
    }
    return virtualFiles;
  }

  @NotNull
  public FileEditor[] getSelectedEditors() {
    List<FileEditor> editors = new ArrayList<FileEditor>();
    Set<EditorWindow> windows = new THashSet<EditorWindow>(myWindows);
    final EditorWindow currentWindow = getCurrentWindow();
    if (currentWindow != null) {
      windows.add(currentWindow);
    }
    for (final EditorWindow window : windows) {
      final EditorWithProviderComposite composite = window.getSelectedEditor();
      if (composite != null) {
        editors.add(composite.getSelectedEditor());
      }
    }
    return editors.toArray(new FileEditor[editors.size()]);
  }

  public void updateFileIcon(@NotNull final VirtualFile file) {
    updateFileIconLater(file);
  }

  private void updateFileIconImmediately(final VirtualFile file) {
    final Collection<EditorWindow> windows = findWindows(file);
    for (EditorWindow window : windows) {
      window.updateFileIcon(file);
    }
  }

  private final Set<VirtualFile> myFilesToUpdateIconsFor = new HashSet<VirtualFile>();

  private void updateFileIconLater(VirtualFile file) {
    myFilesToUpdateIconsFor.add(file);
    myIconUpdaterAlarm.cancelAllRequests();
    myIconUpdaterAlarm.addRequest(
        () -> {
          if (myManager.getProject().isDisposed()) return;
          for (VirtualFile file1 : myFilesToUpdateIconsFor) {
            updateFileIconImmediately(file1);
          }
          myFilesToUpdateIconsFor.clear();
        },
        200,
        ModalityState.stateForComponent(this));
  }

  void updateFileColor(@NotNull final VirtualFile file) {
    final Collection<EditorWindow> windows = findWindows(file);
    for (final EditorWindow window : windows) {
      final int index = window.findEditorIndex(window.findFileComposite(file));
      LOG.assertTrue(index != -1);
      window.setForegroundAt(index, getManager().getFileColor(file));
      window.setWaveColor(index, getManager().isProblem(file) ? JBColor.red : null);
    }
  }

  public void trimToSize(final int editor_tab_limit) {
    for (final EditorWindow window : myWindows) {
      window.trimToSize(editor_tab_limit, null, true);
    }
  }

  public void setTabsPlacement(final int tabPlacement) {
    final EditorWindow[] windows = getWindows();
    for (int i = 0; i != windows.length; ++i) {
      windows[i].setTabsPlacement(tabPlacement);
    }
  }

  void setTabLayoutPolicy(int scrollTabLayout) {
    final EditorWindow[] windows = getWindows();
    for (int i = 0; i != windows.length; ++i) {
      windows[i].setTabLayoutPolicy(scrollTabLayout);
    }
  }

  void updateFileName(@Nullable final VirtualFile updatedFile) {
    final EditorWindow[] windows = getWindows();
    for (int i = 0; i != windows.length; ++i) {
      for (VirtualFile file : windows[i].getFiles()) {
        if (updatedFile == null || file.getName().equals(updatedFile.getName())) {
          windows[i].updateFileName(file);
        }
      }
    }

    Project project = myManager.getProject();

    final IdeFrame frame = getFrame(project);
    if (frame != null) {
      VirtualFile file = getCurrentFile();

      File ioFile = file == null ? null : new File(file.getPresentableUrl());
      String fileTitle = null;
      if (file != null) {
        fileTitle =
            DumbService.isDumb(project)
                ? file.getName()
                : FrameTitleBuilder.getInstance().getFileTitle(project, file);
      }

      frame.setFileTitle(fileTitle, ioFile);
    }
  }

  protected IdeFrame getFrame(Project project) {
    final WindowManagerEx windowManagerEx = WindowManagerEx.getInstanceEx();
    final IdeFrame frame = windowManagerEx.getFrame(project);
    LOG.assertTrue(ApplicationManager.getApplication().isUnitTestMode() || frame != null);
    return frame;
  }

  boolean isInsideChange() {
    return myInsideChange > 0;
  }

  private void setCurrentWindow(@Nullable final EditorWindow currentWindow) {
    if (currentWindow != null && !myWindows.contains(currentWindow)) {
      throw new IllegalArgumentException(currentWindow + " is not a member of this container");
    }
    myCurrentWindow = currentWindow;
  }

  void updateFileBackgroundColor(@NotNull VirtualFile file) {
    final EditorWindow[] windows = getWindows();
    for (int i = 0; i != windows.length; ++i) {
      windows[i].updateFileBackgroundColor(file);
    }
  }

  int getSplitCount() {
    if (getComponentCount() > 0) {
      JPanel panel = (JPanel) getComponent(0);
      return getSplitCount(panel);
    }
    return 0;
  }

  private static int getSplitCount(JComponent component) {
    if (component.getComponentCount() > 0) {
      final JComponent firstChild = (JComponent) component.getComponent(0);
      if (firstChild instanceof Splitter) {
        final Splitter splitter = (Splitter) firstChild;
        return getSplitCount(splitter.getFirstComponent())
            + getSplitCount(splitter.getSecondComponent());
      }
      return 1;
    }
    return 0;
  }

  protected void afterFileClosed(VirtualFile file) {}

  protected void afterFileOpen(VirtualFile file) {}

  @Nullable
  JBTabs getTabsAt(RelativePoint point) {
    Point thisPoint = point.getPoint(this);
    Component c = SwingUtilities.getDeepestComponentAt(this, thisPoint.x, thisPoint.y);
    while (c != null) {
      if (c instanceof JBTabs) {
        return (JBTabs) c;
      }
      c = c.getParent();
    }

    return null;
  }

  boolean isEmptyVisible() {
    EditorWindow[] windows = getWindows();
    for (EditorWindow each : windows) {
      if (!each.isEmptyVisible()) {
        return false;
      }
    }
    return true;
  }

  @Nullable
  private VirtualFile findNextFile(final VirtualFile file) {
    final EditorWindow[] windows = getWindows(); // TODO: use current file as base
    for (int i = 0; i != windows.length; ++i) {
      final VirtualFile[] files = windows[i].getFiles();
      for (final VirtualFile fileAt : files) {
        if (!Comparing.equal(fileAt, file)) {
          return fileAt;
        }
      }
    }
    return null;
  }

  void closeFile(VirtualFile file, boolean moveFocus) {
    final List<EditorWindow> windows = findWindows(file);
    if (!windows.isEmpty()) {
      final VirtualFile nextFile = findNextFile(file);
      for (final EditorWindow window : windows) {
        LOG.assertTrue(window.getSelectedEditor() != null);
        window.closeFile(file, false, moveFocus);
        if (window.getTabCount() == 0 && nextFile != null && myManager.getProject().isOpen()) {
          EditorWithProviderComposite newComposite = myManager.newEditorComposite(nextFile);
          window.setEditor(newComposite, moveFocus); // newComposite can be null
        }
      }
      // cleanup windows with no tabs
      for (final EditorWindow window : windows) {
        if (window.isDisposed()) {
          // call to window.unsplit() which might make its sibling disposed
          continue;
        }
        if (window.getTabCount() == 0) {
          window.unsplit(false);
        }
      }
    }
  }

  @Override
  public void uiSettingsChanged(UISettings source) {
    if (!myManager.getProject().isOpen()) return;
    for (VirtualFile file : getOpenFiles()) {
      updateFileBackgroundColor(file);
      updateFileIcon(file);
      updateFileColor(file);
    }
  }

  private final class MyFocusTraversalPolicy extends IdeFocusTraversalPolicy {
    @Override
    public final Component getDefaultComponentImpl(final Container focusCycleRoot) {
      if (myCurrentWindow != null) {
        final EditorWithProviderComposite selectedEditor = myCurrentWindow.getSelectedEditor();
        if (selectedEditor != null) {
          return IdeFocusTraversalPolicy.getPreferredFocusedComponent(
              selectedEditor.getComponent(), this);
        }
      }
      return IdeFocusTraversalPolicy.getPreferredFocusedComponent(EditorsSplitters.this, this);
    }
  }

  @Nullable
  public JPanel getTopPanel() {
    return getComponentCount() > 0 ? (JPanel) getComponent(0) : null;
  }

  public EditorWindow getCurrentWindow() {
    return myCurrentWindow;
  }

  public EditorWindow getOrCreateCurrentWindow(final VirtualFile file) {
    final List<EditorWindow> windows = findWindows(file);
    if (getCurrentWindow() == null) {
      final Iterator<EditorWindow> iterator = myWindows.iterator();
      if (!windows.isEmpty()) {
        setCurrentWindow(windows.get(0), false);
      } else if (iterator.hasNext()) {
        setCurrentWindow(iterator.next(), false);
      } else {
        createCurrentWindow();
      }
    } else if (!windows.isEmpty()) {
      if (!windows.contains(getCurrentWindow())) {
        setCurrentWindow(windows.get(0), false);
      }
    }
    return getCurrentWindow();
  }

  void createCurrentWindow() {
    LOG.assertTrue(myCurrentWindow == null);
    setCurrentWindow(createEditorWindow());
    add(myCurrentWindow.myPanel, BorderLayout.CENTER);
  }

  protected EditorWindow createEditorWindow() {
    return new EditorWindow(this);
  }

  /**
   * sets the window passed as a current ('focused') window among all splitters. All file openings
   * will be done inside this current window
   *
   * @param window a window to be set as current
   * @param requestFocus whether to request focus to the editor currently selected in this window
   */
  void setCurrentWindow(@Nullable final EditorWindow window, final boolean requestFocus) {
    final EditorWithProviderComposite newEditor =
        window == null ? null : window.getSelectedEditor();

    Runnable fireRunnable = () -> getManager().fireSelectionChanged(newEditor);

    setCurrentWindow(window);

    getManager().updateFileName(window == null ? null : window.getSelectedFile());

    if (window != null) {
      final EditorWithProviderComposite selectedEditor = window.getSelectedEditor();
      if (selectedEditor != null) {
        fireRunnable.run();
      }

      if (requestFocus) {
        window.requestFocus(true);
      }
    } else {
      fireRunnable.run();
    }
  }

  void addWindow(EditorWindow window) {
    myWindows.add(window);
  }

  void removeWindow(EditorWindow window) {
    myWindows.remove(window);
    if (myCurrentWindow == window) {
      myCurrentWindow = null;
    }
  }

  boolean containsWindow(EditorWindow window) {
    return myWindows.contains(window);
  }

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

  public EditorWithProviderComposite[] getEditorsComposites() {
    List<EditorWithProviderComposite> res = new ArrayList<EditorWithProviderComposite>();

    for (final EditorWindow myWindow : myWindows) {
      final EditorWithProviderComposite[] editors = myWindow.getEditors();
      ContainerUtil.addAll(res, editors);
    }
    return res.toArray(new EditorWithProviderComposite[res.size()]);
  }

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

  @NotNull
  public List<EditorWithProviderComposite> findEditorComposites(@NotNull VirtualFile file) {
    List<EditorWithProviderComposite> res = new ArrayList<EditorWithProviderComposite>();
    for (final EditorWindow window : myWindows) {
      final EditorWithProviderComposite fileComposite = window.findFileComposite(file);
      if (fileComposite != null) {
        res.add(fileComposite);
      }
    }
    return res;
  }

  @NotNull
  private List<EditorWindow> findWindows(final VirtualFile file) {
    List<EditorWindow> res = new ArrayList<EditorWindow>();
    for (final EditorWindow window : myWindows) {
      if (window.findFileComposite(file) != null) {
        res.add(window);
      }
    }
    return res;
  }

  @NotNull
  public EditorWindow[] getWindows() {
    return myWindows.toArray(new EditorWindow[myWindows.size()]);
  }

  @NotNull
  EditorWindow[] getOrderedWindows() {
    final List<EditorWindow> res = new ArrayList<EditorWindow>();

    // Collector for windows in tree ordering:
    class Inner {
      private void collect(final JPanel panel) {
        final Component comp = panel.getComponent(0);
        if (comp instanceof Splitter) {
          final Splitter splitter = (Splitter) comp;
          collect((JPanel) splitter.getFirstComponent());
          collect((JPanel) splitter.getSecondComponent());
        } else if (comp instanceof JPanel || comp instanceof JBTabs) {
          final EditorWindow window = findWindowWith(comp);
          if (window != null) {
            res.add(window);
          }
        }
      }
    }

    // get root component and traverse splitters tree:
    if (getComponentCount() != 0) {
      final Component comp = getComponent(0);
      LOG.assertTrue(comp instanceof JPanel);
      final JPanel panel = (JPanel) comp;
      if (panel.getComponentCount() != 0) {
        new Inner().collect(panel);
      }
    }

    LOG.assertTrue(res.size() == myWindows.size());
    return res.toArray(new EditorWindow[res.size()]);
  }

  @Nullable
  private EditorWindow findWindowWith(final Component component) {
    if (component != null) {
      for (final EditorWindow window : myWindows) {
        if (SwingUtilities.isDescendingFrom(component, window.myPanel)) {
          return window;
        }
      }
    }
    return null;
  }

  public boolean isFloating() {
    return false;
  }

  public boolean isPreview() {
    return false;
  }

  private final class MyFocusWatcher extends FocusWatcher {
    @Override
    protected void focusedComponentChanged(final Component component, final AWTEvent cause) {
      EditorWindow newWindow = null;

      if (component != null) {
        newWindow = findWindowWith(component);
      } else if (cause instanceof ContainerEvent
          && cause.getID() == ContainerEvent.COMPONENT_REMOVED) {
        // do not change current window in case of child removal as in JTable.removeEditor
        // otherwise Escape in a toolwindow will not focus editor with JTable content
        return;
      }

      setCurrentWindow(newWindow);
      setCurrentWindow(newWindow, false);
    }
  }

  private abstract static class ConfigTreeReader<T> {
    @Nullable
    public T process(@Nullable Element element, @Nullable T context) {
      if (element == null) {
        return null;
      }
      final Element splitterElement = element.getChild("splitter");
      if (splitterElement != null) {
        final Element first = splitterElement.getChild("split-first");
        final Element second = splitterElement.getChild("split-second");
        return processSplitter(splitterElement, first, second, context);
      }

      final Element leaf = element.getChild("leaf");
      if (leaf == null) {
        return null;
      }

      List<Element> fileElements = leaf.getChildren("file");
      final List<Element> children = new ArrayList<Element>(fileElements.size());

      // trim to EDITOR_TAB_LIMIT, ignoring CLOSE_NON_MODIFIED_FILES_FIRST policy
      int toRemove = fileElements.size() - UISettings.getInstance().EDITOR_TAB_LIMIT;
      for (Element fileElement : fileElements) {
        if (toRemove <= 0
            || Boolean.valueOf(fileElement.getAttributeValue(PINNED)).booleanValue()) {
          children.add(fileElement);
        } else {
          toRemove--;
        }
      }

      return processFiles(children, context);
    }

    @Nullable
    protected abstract T processFiles(@NotNull List<Element> fileElements, @Nullable T context);

    @Nullable
    protected abstract T processSplitter(
        @NotNull Element element,
        @Nullable Element firstChild,
        @Nullable Element secondChild,
        @Nullable T context);
  }

  private class UIBuilder extends ConfigTreeReader<JPanel> {

    @Override
    protected JPanel processFiles(@NotNull List<Element> fileElements, final JPanel context) {
      final Ref<EditorWindow> windowRef = new Ref<EditorWindow>();
      UIUtil.invokeAndWaitIfNeeded(
          new Runnable() {
            @Override
            public void run() {
              windowRef.set(context == null ? createEditorWindow() : findWindowWith(context));
            }
          });
      final EditorWindow window = windowRef.get();
      LOG.assertTrue(window != null);
      VirtualFile focusedFile = null;

      for (int i = 0; i < fileElements.size(); i++) {
        final Element file = fileElements.get(i);
        if (i == 0) {
          EditorTabbedContainer tabbedPane = window.getTabbedPane();
          if (tabbedPane != null) {
            try {
              int limit =
                  Integer.parseInt(
                      file.getParentElement()
                          .getAttributeValue(
                              JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY.toString(),
                              String.valueOf(JBTabsImpl.DEFAULT_MAX_TAB_WIDTH)));
              UIUtil.putClientProperty(
                  tabbedPane.getComponent(), JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY, limit);
            } catch (NumberFormatException e) {
              // ignore
            }
          }
        }
        try {
          final FileEditorManagerImpl fileEditorManager = getManager();
          Element historyElement = file.getChild(HistoryEntry.TAG);
          final HistoryEntry entry =
              HistoryEntry.createLight(fileEditorManager.getProject(), historyElement);
          final VirtualFile virtualFile = entry.getFile();
          if (virtualFile == null)
            throw new InvalidDataException("No file exists: " + entry.getFilePointer().getUrl());
          Document document =
              ApplicationManager.getApplication()
                  .runReadAction(
                      new Computable<Document>() {
                        @Override
                        public Document compute() {
                          return virtualFile.isValid()
                              ? FileDocumentManager.getInstance().getDocument(virtualFile)
                              : null;
                        }
                      });
          final boolean isCurrentInTab =
              Boolean.valueOf(file.getAttributeValue(CURRENT_IN_TAB)).booleanValue();
          Boolean pin = Boolean.valueOf(file.getAttributeValue(PINNED));
          fileEditorManager.openFileImpl4(
              window, virtualFile, entry, isCurrentInTab, isCurrentInTab, pin, i);
          if (isCurrentInTab) {
            focusedFile = virtualFile;
          }
          if (document != null) {
            // This is just to make sure document reference is kept on stack till this point
            // so that document is available for folding state deserialization in HistoryEntry
            // constructor
            // and that document will be created only once during file opening
            document.putUserData(DUMMY_KEY, null);
          }
          updateProgress();
        } catch (InvalidDataException e) {
          if (ApplicationManager.getApplication().isUnitTestMode()) {
            LOG.error(e);
          }
        }
      }
      if (focusedFile != null) {
        getManager().addSelectionRecord(focusedFile, window);
      }
      return window.myPanel;
    }

    @Override
    protected JPanel processSplitter(
        @NotNull Element splitterElement,
        Element firstChild,
        Element secondChild,
        final JPanel context) {
      if (context == null) {
        final boolean orientation =
            "vertical".equals(splitterElement.getAttributeValue("split-orientation"));
        final float proportion =
            Float.valueOf(splitterElement.getAttributeValue("split-proportion")).floatValue();
        final JPanel firstComponent = process(firstChild, null);
        final JPanel secondComponent = process(secondChild, null);
        final Ref<JPanel> panelRef = new Ref<JPanel>();
        UIUtil.invokeAndWaitIfNeeded(
            new Runnable() {
              @Override
              public void run() {
                JPanel panel = new JPanel(new BorderLayout());
                panel.setOpaque(false);
                Splitter splitter = new OnePixelSplitter(orientation, proportion, 0.1f, 0.9f);
                panel.add(splitter, BorderLayout.CENTER);
                splitter.setFirstComponent(firstComponent);
                splitter.setSecondComponent(secondComponent);
                panelRef.set(panel);
              }
            });
        return panelRef.get();
      }
      final Ref<JPanel> firstComponent = new Ref<JPanel>();
      final Ref<JPanel> secondComponent = new Ref<JPanel>();
      UIUtil.invokeAndWaitIfNeeded(
          new Runnable() {
            @Override
            public void run() {
              if (context.getComponent(0) instanceof Splitter) {
                Splitter splitter = (Splitter) context.getComponent(0);
                firstComponent.set((JPanel) splitter.getFirstComponent());
                secondComponent.set((JPanel) splitter.getSecondComponent());
              } else {
                firstComponent.set(context);
                secondComponent.set(context);
              }
            }
          });
      process(firstChild, firstComponent.get());
      process(secondChild, secondComponent.get());
      return context;
    }
  }
}
/** @author spleaner */
public class HtmlUnknownTagInspection extends HtmlLocalInspectionTool {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInspection.htmlInspections.HtmlUnknownTagInspection");

  public JDOMExternalizableStringList myValues;
  public boolean myCustomValuesEnabled = true;
  @NonNls public static final String TAG_SHORT_NAME = "HtmlUnknownTag";
  public static final Key<HtmlUnknownTagInspection> TAG_KEY = Key.create(TAG_SHORT_NAME);

  public HtmlUnknownTagInspection() {
    this("nobr,noembed,comment,noscript,embed,script");
  }

  protected HtmlUnknownTagInspection(@NonNls @NotNull final String defaultValues) {
    myValues = reparseProperties(defaultValues);
  }

  @Override
  @Nls
  @NotNull
  public String getDisplayName() {
    return XmlBundle.message("html.inspections.unknown.tag");
  }

  @Override
  @NonNls
  @NotNull
  public String getShortName() {
    return TAG_SHORT_NAME;
  }

  @Override
  @Nullable
  public JComponent createOptionsPanel() {
    final JPanel result = new JPanel(new BorderLayout());

    final JPanel internalPanel = new JPanel(new BorderLayout());
    result.add(internalPanel, BorderLayout.NORTH);

    final FieldPanel additionalAttributesPanel = new FieldPanel(null, getPanelTitle(), null, null);
    additionalAttributesPanel
        .getTextField()
        .getDocument()
        .addDocumentListener(
            new DocumentAdapter() {
              @Override
              protected void textChanged(DocumentEvent e) {
                final Document document = e.getDocument();
                try {
                  final String text = document.getText(0, document.getLength());
                  if (text != null) {
                    myValues = reparseProperties(text.trim());
                  }
                } catch (BadLocationException e1) {
                  getLogger().error(e1);
                }
              }
            });

    final JCheckBox checkBox = new JCheckBox(getCheckboxTitle());
    checkBox.setSelected(myCustomValuesEnabled);
    checkBox.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            final boolean b = checkBox.isSelected();
            if (b != myCustomValuesEnabled) {
              myCustomValuesEnabled = b;
              additionalAttributesPanel.setEnabled(myCustomValuesEnabled);
            }
          }
        });

    internalPanel.add(checkBox, BorderLayout.NORTH);
    internalPanel.add(additionalAttributesPanel, BorderLayout.CENTER);

    additionalAttributesPanel.setPreferredSize(
        new Dimension(150, additionalAttributesPanel.getPreferredSize().height));
    additionalAttributesPanel.setEnabled(myCustomValuesEnabled);
    additionalAttributesPanel.setText(createPropertiesString());

    return result;
  }

  @NotNull
  protected Logger getLogger() {
    return LOG;
  }

  private String createPropertiesString() {
    final StringBuffer buffer = new StringBuffer();
    for (final String property : myValues) {
      if (buffer.length() == 0) {
        buffer.append(property);
      } else {
        buffer.append(',');
        buffer.append(property);
      }
    }

    return buffer.toString();
  }

  public String getAdditionalEntries() {
    return createPropertiesString();
  }

  protected String getCheckboxTitle() {
    return XmlBundle.message("html.inspections.unknown.tag.checkbox.title");
  }

  protected static JDOMExternalizableStringList reparseProperties(
      @NotNull final String properties) {
    final JDOMExternalizableStringList result = new JDOMExternalizableStringList();

    final StringTokenizer tokenizer = new StringTokenizer(properties, ",");
    while (tokenizer.hasMoreTokens()) {
      result.add(tokenizer.nextToken().toLowerCase().trim());
    }

    return result;
  }

  public void setAdditionalValues(@NotNull final String values) {
    myValues = reparseProperties(values);
  }

  protected String getPanelTitle() {
    return XmlBundle.message("html.inspections.unknown.tag.title");
  }

  protected boolean isCustomValue(@NotNull final String value) {
    return myValues.contains(value.toLowerCase());
  }

  public void addCustomPropertyName(@NotNull final String text) {
    final String s = text.trim().toLowerCase();
    if (!isCustomValue(s)) {
      myValues.add(s);
    }

    if (!isCustomValuesEnabled()) {
      myCustomValuesEnabled = true;
    }
  }

  public boolean isCustomValuesEnabled() {
    return myCustomValuesEnabled;
  }

  private static boolean isAbstractDescriptor(XmlElementDescriptor descriptor) {
    return descriptor == null || descriptor instanceof AnyXmlElementDescriptor;
  }

  // this hack is needed, because we temporarily ignore svg and mathML namespaces
  // todo: provide schemas for svg and mathML and remove this in IDEA XI
  private static boolean isInSpecialHtml5Namespace(XmlTag tag) {
    final String ns = tag.getNamespace();
    return HtmlUtil.SVG_NAMESPACE.equals(ns) || HtmlUtil.MATH_ML_NAMESPACE.endsWith(ns);
  }

  @Override
  protected void checkTag(
      @NotNull final XmlTag tag, @NotNull final ProblemsHolder holder, final boolean isOnTheFly) {
    if (!(tag instanceof HtmlTag)
        || !XmlHighlightVisitor.shouldBeValidated(tag)
        || isInSpecialHtml5Namespace(tag)) {
      return;
    }

    XmlElementDescriptor descriptorFromContext = XmlUtil.getDescriptorFromContext(tag);

    PsiElement parent = tag.getParent();
    XmlElementDescriptor parentDescriptor =
        parent instanceof XmlTag ? ((XmlTag) parent).getDescriptor() : null;

    XmlElementDescriptor ownDescriptor =
        isAbstractDescriptor(descriptorFromContext) ? tag.getDescriptor() : descriptorFromContext;

    if (isAbstractDescriptor(ownDescriptor)
        || (parentDescriptor instanceof HtmlElementDescriptorImpl
            && ownDescriptor instanceof HtmlElementDescriptorImpl
            && isAbstractDescriptor(descriptorFromContext))) {

      final String name = tag.getName();

      if (!isCustomValuesEnabled() || !isCustomValue(name)) {
        final AddCustomTagOrAttributeIntentionAction action =
            new AddCustomTagOrAttributeIntentionAction(
                TAG_KEY, name, XmlEntitiesInspection.UNKNOWN_TAG);

        // todo: support "element is not allowed" message for html5
        // some tags in html5 cannot be found in xhtml5.xsd if they are located in incorrect
        // context, so they get any-element descriptor (ex. "canvas: tag)
        final String message =
            isAbstractDescriptor(ownDescriptor)
                ? XmlErrorMessages.message("unknown.html.tag", name)
                : XmlErrorMessages.message("element.is.not.allowed.here", name);

        final PsiElement startTagName = XmlTagUtil.getStartTagNameElement(tag);
        assert startTagName != null;
        final PsiElement endTagName = XmlTagUtil.getEndTagNameElement(tag);

        List<LocalQuickFix> quickfixes = new ArrayList<LocalQuickFix>();
        quickfixes.add(action);
        if (isOnTheFly) {
          ContainerUtil.addIfNotNull(
              CreateNSDeclarationIntentionFix.createFix(startTagName, ""), quickfixes);
        }
        if (HtmlUtil.isHtml5Tag(name) && !HtmlUtil.hasNonHtml5Doctype(tag)) {
          quickfixes.add(new SwitchToHtml5WithHighPriorityAction());
        }
        ProblemHighlightType highlightType =
            tag.getContainingFile().getContext() == null
                ? ProblemHighlightType.GENERIC_ERROR_OR_WARNING
                : ProblemHighlightType.INFORMATION;
        if (startTagName.getTextLength() > 0) {
          holder.registerProblem(
              startTagName,
              message,
              highlightType,
              quickfixes.toArray(new LocalQuickFix[quickfixes.size()]));
        }

        if (endTagName != null) {
          holder.registerProblem(
              endTagName,
              message,
              highlightType,
              quickfixes.toArray(new LocalQuickFix[quickfixes.size()]));
        }
      }
    }
  }
}
/** @author cdr */
class PassExecutorService implements Disposable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.daemon.impl.PassExecutorService");
  private static final boolean CHECK_CONSISTENCY =
      ApplicationManager.getApplication().isUnitTestMode();

  private final Map<ScheduledPass, Job<Void>> mySubmittedPasses = new ConcurrentHashMap<>();
  private final Project myProject;
  private volatile boolean isDisposed;
  private final AtomicInteger nextPassId = new AtomicInteger(100);

  PassExecutorService(@NotNull Project project) {
    myProject = project;
  }

  @Override
  public void dispose() {
    cancelAll(true);
    isDisposed = true;
  }

  void cancelAll(boolean waitForTermination) {
    for (Job<Void> submittedPass : mySubmittedPasses.values()) {
      submittedPass.cancel();
    }
    if (waitForTermination) {
      try {
        while (!waitFor(50)) {
          int i = 0;
        }
      } catch (ProcessCanceledException ignored) {

      } catch (Error | RuntimeException e) {
        throw e;
      } catch (Throwable throwable) {
        LOG.error(throwable);
      }
    }
    mySubmittedPasses.clear();
  }

  void submitPasses(
      @NotNull Map<FileEditor, HighlightingPass[]> passesMap,
      @NotNull DaemonProgressIndicator updateProgress) {
    if (isDisposed()) return;

    // null keys are ok
    MultiMap<Document, FileEditor> documentToEditors = MultiMap.createSet();
    MultiMap<FileEditor, TextEditorHighlightingPass> documentBoundPasses = MultiMap.createSmart();
    MultiMap<FileEditor, EditorBoundHighlightingPass> editorBoundPasses = MultiMap.createSmart();
    List<Pair<FileEditor, TextEditorHighlightingPass>> passesWithNoDocuments = new ArrayList<>();
    Set<VirtualFile> vFiles = new HashSet<>();

    for (Map.Entry<FileEditor, HighlightingPass[]> entry : passesMap.entrySet()) {
      FileEditor fileEditor = entry.getKey();
      HighlightingPass[] passes = entry.getValue();
      Document document;
      if (fileEditor instanceof TextEditor) {
        Editor editor = ((TextEditor) fileEditor).getEditor();
        LOG.assertTrue(!(editor instanceof EditorWindow));
        document = editor.getDocument();
      } else {
        VirtualFile virtualFile =
            ((FileEditorManagerEx) FileEditorManager.getInstance(myProject)).getFile(fileEditor);
        document =
            virtualFile == null ? null : FileDocumentManager.getInstance().getDocument(virtualFile);
      }
      if (document != null) {
        vFiles.add(FileDocumentManager.getInstance().getFile(document));
      }

      int prevId = 0;
      for (final HighlightingPass pass : passes) {
        if (pass instanceof EditorBoundHighlightingPass) {
          EditorBoundHighlightingPass editorPass = (EditorBoundHighlightingPass) pass;
          editorPass.setId(
              nextPassId.incrementAndGet()); // have to make ids unique for this document
          editorBoundPasses.putValue(fileEditor, editorPass);
        } else {
          TextEditorHighlightingPass textEditorHighlightingPass =
              convertToTextHighlightingPass(pass, document, nextPassId, prevId);
          document = textEditorHighlightingPass.getDocument();
          documentBoundPasses.putValue(fileEditor, textEditorHighlightingPass);
          if (document == null) {
            passesWithNoDocuments.add(Pair.create(fileEditor, textEditorHighlightingPass));
          } else {
            documentToEditors.putValue(document, fileEditor);
          }
          prevId = textEditorHighlightingPass.getId();
        }
      }
    }

    List<ScheduledPass> freePasses = new ArrayList<>(documentToEditors.size() * 5);
    List<ScheduledPass> dependentPasses = new ArrayList<>(documentToEditors.size() * 10);
    // (fileEditor, passId) -> created pass
    Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted = new THashMap<>(passesMap.size());

    final AtomicInteger threadsToStartCountdown = new AtomicInteger(0);
    for (Map.Entry<Document, Collection<FileEditor>> entry : documentToEditors.entrySet()) {
      Collection<FileEditor> fileEditors = entry.getValue();
      Document document = entry.getKey();
      FileEditor preferredFileEditor = getPreferredFileEditor(document, fileEditors);
      List<TextEditorHighlightingPass> passes =
          (List<TextEditorHighlightingPass>) documentBoundPasses.get(preferredFileEditor);
      if (passes.isEmpty()) {
        continue;
      }
      sortById(passes);
      for (TextEditorHighlightingPass currentPass : passes) {
        createScheduledPass(
            preferredFileEditor,
            currentPass,
            toBeSubmitted,
            passes,
            freePasses,
            dependentPasses,
            updateProgress,
            threadsToStartCountdown);
      }
    }

    for (Map.Entry<FileEditor, Collection<EditorBoundHighlightingPass>> entry :
        editorBoundPasses.entrySet()) {
      FileEditor fileEditor = entry.getKey();
      Collection<EditorBoundHighlightingPass> createdEditorBoundPasses = entry.getValue();
      List<TextEditorHighlightingPass> createdDocumentBoundPasses =
          (List<TextEditorHighlightingPass>) documentBoundPasses.get(fileEditor);
      List<TextEditorHighlightingPass> allCreatedPasses =
          new ArrayList<>(createdDocumentBoundPasses);
      allCreatedPasses.addAll(createdEditorBoundPasses);

      for (EditorBoundHighlightingPass pass : createdEditorBoundPasses) {
        createScheduledPass(
            fileEditor,
            pass,
            toBeSubmitted,
            allCreatedPasses,
            freePasses,
            dependentPasses,
            updateProgress,
            threadsToStartCountdown);
      }
    }

    for (Pair<FileEditor, TextEditorHighlightingPass> pair : passesWithNoDocuments) {
      FileEditor fileEditor = pair.first;
      TextEditorHighlightingPass pass = pair.second;
      createScheduledPass(
          fileEditor,
          pass,
          toBeSubmitted,
          ContainerUtil.emptyList(),
          freePasses,
          dependentPasses,
          updateProgress,
          threadsToStartCountdown);
    }

    if (CHECK_CONSISTENCY && !ApplicationInfoImpl.isInPerformanceTest()) {
      assertConsistency(freePasses, toBeSubmitted, threadsToStartCountdown);
    }

    log(
        updateProgress,
        null,
        vFiles + " ----- starting " + threadsToStartCountdown.get(),
        freePasses);

    for (ScheduledPass dependentPass : dependentPasses) {
      mySubmittedPasses.put(dependentPass, Job.NULL_JOB);
    }
    for (ScheduledPass freePass : freePasses) {
      submit(freePass);
    }
  }

  private void assertConsistency(
      List<ScheduledPass> freePasses,
      Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted,
      AtomicInteger threadsToStartCountdown) {
    assert threadsToStartCountdown.get() == toBeSubmitted.size();
    TIntObjectHashMap<Pair<ScheduledPass, Integer>> id2Visits = new TIntObjectHashMap<>();
    for (ScheduledPass freePass : freePasses) {
      id2Visits.put(freePass.myPass.getId(), Pair.create(freePass, 0));
      checkConsistency(freePass, id2Visits);
    }
    id2Visits.forEachEntry(
        (id, pair) -> {
          int count = pair.second;
          assert count == 0 : id;
          return true;
        });
    assert id2Visits.size() == threadsToStartCountdown.get();
  }

  private void checkConsistency(
      ScheduledPass pass, TIntObjectHashMap<Pair<ScheduledPass, Integer>> id2Visits) {
    for (ScheduledPass succ :
        ContainerUtil.concat(pass.mySuccessorsOnCompletion, pass.mySuccessorsOnSubmit)) {
      int succId = succ.myPass.getId();
      Pair<ScheduledPass, Integer> succPair = id2Visits.get(succId);
      if (succPair == null) {
        succPair = Pair.create(succ, succ.myRunningPredecessorsCount.get());
        id2Visits.put(succId, succPair);
      }
      int newPred = succPair.second - 1;
      id2Visits.put(succId, Pair.create(succ, newPred));
      assert newPred >= 0;
      if (newPred == 0) {
        checkConsistency(succ, id2Visits);
      }
    }
  }

  @NotNull
  private TextEditorHighlightingPass convertToTextHighlightingPass(
      @NotNull final HighlightingPass pass,
      final Document document,
      @NotNull AtomicInteger id,
      int previousPassId) {
    TextEditorHighlightingPass textEditorHighlightingPass;
    if (pass instanceof TextEditorHighlightingPass) {
      textEditorHighlightingPass = (TextEditorHighlightingPass) pass;
    } else {
      // run all passes in sequence
      textEditorHighlightingPass =
          new TextEditorHighlightingPass(myProject, document, true) {
            @Override
            public void doCollectInformation(@NotNull ProgressIndicator progress) {
              pass.collectInformation(progress);
            }

            @Override
            public void doApplyInformationToEditor() {
              pass.applyInformationToEditor();
            }
          };
      textEditorHighlightingPass.setId(id.incrementAndGet());
      if (previousPassId != 0) {
        textEditorHighlightingPass.setCompletionPredecessorIds(new int[] {previousPassId});
      }
    }
    return textEditorHighlightingPass;
  }

  @NotNull
  private FileEditor getPreferredFileEditor(
      Document document, @NotNull Collection<FileEditor> fileEditors) {
    assert !fileEditors.isEmpty();
    if (document != null) {
      final VirtualFile file = FileDocumentManager.getInstance().getFile(document);
      if (file != null) {
        final FileEditor selected =
            FileEditorManager.getInstance(myProject).getSelectedEditor(file);
        if (selected != null && fileEditors.contains(selected)) {
          return selected;
        }
      }
    }
    return fileEditors.iterator().next();
  }

  @NotNull
  private ScheduledPass createScheduledPass(
      @NotNull FileEditor fileEditor,
      @NotNull TextEditorHighlightingPass pass,
      @NotNull Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted,
      @NotNull List<TextEditorHighlightingPass> textEditorHighlightingPasses,
      @NotNull List<ScheduledPass> freePasses,
      @NotNull List<ScheduledPass> dependentPasses,
      @NotNull DaemonProgressIndicator updateProgress,
      @NotNull AtomicInteger threadsToStartCountdown) {
    int passId = pass.getId();
    Pair<FileEditor, Integer> key = Pair.create(fileEditor, passId);
    ScheduledPass scheduledPass = toBeSubmitted.get(key);
    if (scheduledPass != null) return scheduledPass;
    scheduledPass = new ScheduledPass(fileEditor, pass, updateProgress, threadsToStartCountdown);
    threadsToStartCountdown.incrementAndGet();
    toBeSubmitted.put(key, scheduledPass);
    for (int predecessorId : pass.getCompletionPredecessorIds()) {
      ScheduledPass predecessor =
          findOrCreatePredecessorPass(
              fileEditor,
              toBeSubmitted,
              textEditorHighlightingPasses,
              freePasses,
              dependentPasses,
              updateProgress,
              threadsToStartCountdown,
              predecessorId);
      if (predecessor != null) {
        predecessor.addSuccessorOnCompletion(scheduledPass);
      }
    }
    for (int predecessorId : pass.getStartingPredecessorIds()) {
      ScheduledPass predecessor =
          findOrCreatePredecessorPass(
              fileEditor,
              toBeSubmitted,
              textEditorHighlightingPasses,
              freePasses,
              dependentPasses,
              updateProgress,
              threadsToStartCountdown,
              predecessorId);
      if (predecessor != null) {
        predecessor.addSuccessorOnSubmit(scheduledPass);
      }
    }
    if (scheduledPass.myRunningPredecessorsCount.get() == 0
        && !freePasses.contains(scheduledPass)) {
      freePasses.add(scheduledPass);
    } else if (!dependentPasses.contains(scheduledPass)) {
      dependentPasses.add(scheduledPass);
    }

    if (pass.isRunIntentionPassAfter() && fileEditor instanceof TextEditor) {
      Editor editor = ((TextEditor) fileEditor).getEditor();
      ShowIntentionsPass ip = new ShowIntentionsPass(myProject, editor, -1);
      ip.setId(nextPassId.incrementAndGet());
      ip.setCompletionPredecessorIds(new int[] {scheduledPass.myPass.getId()});

      createScheduledPass(
          fileEditor,
          ip,
          toBeSubmitted,
          textEditorHighlightingPasses,
          freePasses,
          dependentPasses,
          updateProgress,
          threadsToStartCountdown);
    }

    return scheduledPass;
  }

  private ScheduledPass findOrCreatePredecessorPass(
      @NotNull FileEditor fileEditor,
      @NotNull Map<Pair<FileEditor, Integer>, ScheduledPass> toBeSubmitted,
      @NotNull List<TextEditorHighlightingPass> textEditorHighlightingPasses,
      @NotNull List<ScheduledPass> freePasses,
      @NotNull List<ScheduledPass> dependentPasses,
      @NotNull DaemonProgressIndicator updateProgress,
      @NotNull AtomicInteger myThreadsToStartCountdown,
      final int predecessorId) {
    Pair<FileEditor, Integer> predKey = Pair.create(fileEditor, predecessorId);
    ScheduledPass predecessor = toBeSubmitted.get(predKey);
    if (predecessor == null) {
      TextEditorHighlightingPass textEditorPass =
          findPassById(predecessorId, textEditorHighlightingPasses);
      predecessor =
          textEditorPass == null
              ? null
              : createScheduledPass(
                  fileEditor,
                  textEditorPass,
                  toBeSubmitted,
                  textEditorHighlightingPasses,
                  freePasses,
                  dependentPasses,
                  updateProgress,
                  myThreadsToStartCountdown);
    }
    return predecessor;
  }

  private static TextEditorHighlightingPass findPassById(
      final int id, @NotNull List<TextEditorHighlightingPass> textEditorHighlightingPasses) {
    return ContainerUtil.find(textEditorHighlightingPasses, pass -> pass.getId() == id);
  }

  private void submit(@NotNull ScheduledPass pass) {
    if (!pass.myUpdateProgress.isCanceled()) {
      Job<Void> job =
          JobLauncher.getInstance()
              .submitToJobThread(
                  pass,
                  future -> {
                    try {
                      if (!future.isCancelled()) { // for canceled task .get() generates
                        // CancellationException which is expensive
                        future.get();
                      }
                    } catch (CancellationException | InterruptedException ignored) {
                    } catch (ExecutionException e) {
                      LOG.error(e.getCause());
                    }
                  });
      mySubmittedPasses.put(pass, job);
    }
  }

  private class ScheduledPass implements Runnable {
    private final FileEditor myFileEditor;
    private final TextEditorHighlightingPass myPass;
    private final AtomicInteger myThreadsToStartCountdown;
    private final AtomicInteger myRunningPredecessorsCount = new AtomicInteger(0);
    private final Collection<ScheduledPass> mySuccessorsOnCompletion = new ArrayList<>();
    private final Collection<ScheduledPass> mySuccessorsOnSubmit = new ArrayList<>();
    private final DaemonProgressIndicator myUpdateProgress;

    private ScheduledPass(
        @NotNull FileEditor fileEditor,
        @NotNull TextEditorHighlightingPass pass,
        @NotNull DaemonProgressIndicator progressIndicator,
        @NotNull AtomicInteger threadsToStartCountdown) {
      myFileEditor = fileEditor;
      myPass = pass;
      myThreadsToStartCountdown = threadsToStartCountdown;
      myUpdateProgress = progressIndicator;
    }

    @Override
    public void run() {
      try {
        doRun();
      } catch (RuntimeException | Error e) {
        saveException(e, myUpdateProgress);
        throw e;
      }
    }

    private void doRun() {
      if (myUpdateProgress.isCanceled()) return;

      log(myUpdateProgress, myPass, "Started. ");

      for (ScheduledPass successor : mySuccessorsOnSubmit) {
        int predecessorsToRun = successor.myRunningPredecessorsCount.decrementAndGet();
        if (predecessorsToRun == 0) {
          submit(successor);
        }
      }

      ProgressManager.getInstance()
          .executeProcessUnderProgress(
              () -> {
                boolean success =
                    ApplicationManagerEx.getApplicationEx()
                        .tryRunReadAction(
                            () -> {
                              try {
                                if (DumbService.getInstance(myProject).isDumb()
                                    && !DumbService.isDumbAware(myPass)) {
                                  return;
                                }

                                if (!myUpdateProgress.isCanceled() && !myProject.isDisposed()) {
                                  myPass.collectInformation(myUpdateProgress);
                                }
                              } catch (ProcessCanceledException e) {
                                log(myUpdateProgress, myPass, "Canceled ");

                                if (!myUpdateProgress.isCanceled()) {
                                  myUpdateProgress.cancel(
                                      e); // in case when some smart asses throw PCE just for fun
                                }
                              } catch (RuntimeException | Error e) {
                                myUpdateProgress.cancel(e);
                                LOG.error(e);
                                throw e;
                              }
                            });

                if (!success) {
                  myUpdateProgress.cancel();
                }
              },
              myUpdateProgress);

      log(myUpdateProgress, myPass, "Finished. ");

      if (!myUpdateProgress.isCanceled()) {
        applyInformationToEditorsLater(
            myFileEditor, myPass, myUpdateProgress, myThreadsToStartCountdown);
        for (ScheduledPass successor : mySuccessorsOnCompletion) {
          int predecessorsToRun = successor.myRunningPredecessorsCount.decrementAndGet();
          if (predecessorsToRun == 0) {
            submit(successor);
          }
        }
      }
    }

    @NonNls
    @Override
    public String toString() {
      return "SP: " + myPass;
    }

    private void addSuccessorOnCompletion(@NotNull ScheduledPass successor) {
      mySuccessorsOnCompletion.add(successor);
      successor.myRunningPredecessorsCount.incrementAndGet();
    }

    private void addSuccessorOnSubmit(@NotNull ScheduledPass successor) {
      mySuccessorsOnSubmit.add(successor);
      successor.myRunningPredecessorsCount.incrementAndGet();
    }
  }

  private void applyInformationToEditorsLater(
      @NotNull final FileEditor fileEditor,
      @NotNull final TextEditorHighlightingPass pass,
      @NotNull final DaemonProgressIndicator updateProgress,
      @NotNull final AtomicInteger threadsToStartCountdown) {
    ApplicationManager.getApplication()
        .invokeLater(
            (DumbAwareRunnable)
                () -> {
                  if (isDisposed() || myProject.isDisposed()) {
                    updateProgress.cancel();
                  }
                  if (updateProgress.isCanceled()) {
                    log(updateProgress, pass, " is canceled during apply, sorry");
                    return;
                  }
                  Document document = pass.getDocument();
                  try {
                    if (fileEditor.getComponent().isDisplayable()
                        || ApplicationManager.getApplication().isUnitTestMode()) {
                      pass.applyInformationToEditor();
                      FileStatusMap fileStatusMap =
                          DaemonCodeAnalyzerEx.getInstanceEx(myProject).getFileStatusMap();
                      if (document != null) {
                        fileStatusMap.markFileUpToDate(document, pass.getId());
                      }
                      log(updateProgress, pass, " Applied");
                    }
                  } catch (ProcessCanceledException e) {
                    log(updateProgress, pass, "Error " + e);
                    throw e;
                  } catch (RuntimeException e) {
                    VirtualFile file =
                        document == null
                            ? null
                            : FileDocumentManager.getInstance().getFile(document);
                    FileType fileType = file == null ? null : file.getFileType();
                    String message =
                        "Exception while applying information to "
                            + fileEditor
                            + "("
                            + fileType
                            + ")";
                    log(updateProgress, pass, message + e);
                    throw new RuntimeException(message, e);
                  }
                  if (threadsToStartCountdown.decrementAndGet() == 0) {
                    log(updateProgress, pass, "Stopping ");
                    updateProgress.stopIfRunning();
                  } else {
                    log(
                        updateProgress,
                        pass,
                        "Finished but there are passes in the queue: "
                            + threadsToStartCountdown.get());
                  }
                },
            Registry.is("ide.perProjectModality")
                ? ModalityState.defaultModalityState()
                : ModalityState.stateForComponent(fileEditor.getComponent()));
  }

  protected boolean isDisposed() {
    return isDisposed;
  }

  @NotNull
  List<TextEditorHighlightingPass> getAllSubmittedPasses() {
    List<TextEditorHighlightingPass> result = new ArrayList<>(mySubmittedPasses.size());
    for (ScheduledPass scheduledPass : mySubmittedPasses.keySet()) {
      if (!scheduledPass.myUpdateProgress.isCanceled()) {
        result.add(scheduledPass.myPass);
      }
    }
    sortById(result);
    return result;
  }

  private static void sortById(@NotNull List<TextEditorHighlightingPass> result) {
    ContainerUtil.quickSort(result, (o1, o2) -> o1.getId() - o2.getId());
  }

  private static int getThreadNum() {
    Matcher matcher =
        Pattern.compile("JobScheduler FJ pool (\\d*)/(\\d*)")
            .matcher(Thread.currentThread().getName());
    String num = matcher.matches() ? matcher.group(1) : null;
    return StringUtil.parseInt(num, 0);
  }

  static void log(
      ProgressIndicator progressIndicator,
      TextEditorHighlightingPass pass,
      @NonNls @NotNull Object... info) {
    if (LOG.isDebugEnabled()) {
      CharSequence docText =
          pass == null || pass.getDocument() == null
              ? ""
              : ": '" + StringUtil.first(pass.getDocument().getCharsSequence(), 10, true) + "'";
      synchronized (PassExecutorService.class) {
        String infos = StringUtil.join(info, Functions.TO_STRING(), " ");
        String message =
            StringUtil.repeatSymbol(' ', getThreadNum() * 4)
                + " "
                + pass
                + " "
                + infos
                + "; progress="
                + (progressIndicator == null ? null : progressIndicator.hashCode())
                + " "
                + (progressIndicator == null ? "?" : progressIndicator.isCanceled() ? "X" : "V")
                + docText;
        LOG.debug(message);
        // System.out.println(message);
      }
    }
  }

  private static final Key<Throwable> THROWABLE_KEY = Key.create("THROWABLE_KEY");

  private static void saveException(
      @NotNull Throwable e, @NotNull DaemonProgressIndicator indicator) {
    indicator.putUserDataIfAbsent(THROWABLE_KEY, e);
  }

  @TestOnly
  static Throwable getSavedException(@NotNull DaemonProgressIndicator indicator) {
    return indicator.getUserData(THROWABLE_KEY);
  }

  // return true if terminated
  boolean waitFor(int millis) throws Throwable {
    try {
      for (Job<Void> job : mySubmittedPasses.values()) {
        job.waitForCompletion(millis);
      }
      return true;
    } catch (TimeoutException ignored) {
      return false;
    } catch (InterruptedException e) {
      return true;
    } catch (ExecutionException e) {
      throw e.getCause();
    }
  }
}
Пример #26
0
/** @author Gregory.Shrago */
public class DfaUtil {
  private static final Key<CachedValue<MultiValuesMap<PsiVariable, PsiExpression>>>
      DFA_VARIABLE_INFO_KEY = Key.create("DFA_VARIABLE_INFO_KEY");

  private DfaUtil() {}

  private static final MultiValuesMap<PsiVariable, PsiExpression> TOO_COMPLEX =
      new MultiValuesMap<PsiVariable, PsiExpression>();

  @Nullable("null means DFA analysis has failed (too complex to analyze)")
  public static Collection<PsiExpression> getCachedVariableValues(
      @Nullable final PsiVariable variable, @Nullable final PsiElement context) {
    if (variable == null || context == null) return Collections.emptyList();

    CachedValue<MultiValuesMap<PsiVariable, PsiExpression>> cachedValue =
        context.getUserData(DFA_VARIABLE_INFO_KEY);
    if (cachedValue == null) {
      final PsiElement codeBlock = DfaPsiUtil.getEnclosingCodeBlock(variable, context);
      cachedValue =
          CachedValuesManager.getManager(context.getProject())
              .createCachedValue(
                  new CachedValueProvider<MultiValuesMap<PsiVariable, PsiExpression>>() {
                    @Override
                    public Result<MultiValuesMap<PsiVariable, PsiExpression>> compute() {
                      final MultiValuesMap<PsiVariable, PsiExpression> result;
                      if (codeBlock == null) {
                        result = null;
                      } else {
                        final ValuableInstructionVisitor visitor =
                            new ValuableInstructionVisitor(context);
                        RunnerResult runnerResult =
                            new ValuableDataFlowRunner().analyzeMethod(codeBlock, visitor);
                        if (runnerResult == RunnerResult.OK) {
                          result = visitor.myValues;
                        } else {
                          result = TOO_COMPLEX;
                        }
                      }
                      return new Result<MultiValuesMap<PsiVariable, PsiExpression>>(
                          result, variable);
                    }
                  },
                  false);
      context.putUserData(DFA_VARIABLE_INFO_KEY, cachedValue);
    }
    final MultiValuesMap<PsiVariable, PsiExpression> value = cachedValue.getValue();
    if (value == TOO_COMPLEX) return null;
    final Collection<PsiExpression> expressions = value == null ? null : value.get(variable);
    return expressions == null ? Collections.<PsiExpression>emptyList() : expressions;
  }

  @NotNull
  public static Nullness checkNullness(
      @Nullable final PsiVariable variable, @Nullable final PsiElement context) {
    if (variable == null || context == null) return Nullness.UNKNOWN;

    final PsiElement codeBlock = DfaPsiUtil.getEnclosingCodeBlock(variable, context);
    if (codeBlock == null) {
      return Nullness.UNKNOWN;
    }
    final ValuableInstructionVisitor visitor = new ValuableInstructionVisitor(context);
    RunnerResult result = new ValuableDataFlowRunner().analyzeMethod(codeBlock, visitor);
    if (result != RunnerResult.OK) {
      return Nullness.UNKNOWN;
    }
    if (visitor.myNulls.contains(variable) && !visitor.myNotNulls.contains(variable))
      return Nullness.NULLABLE;
    if (visitor.myNotNulls.contains(variable) && !visitor.myNulls.contains(variable))
      return Nullness.NOT_NULL;
    return Nullness.UNKNOWN;
  }

  @NotNull
  public static Collection<? extends PsiElement> getPossibleInitializationElements(
      final PsiElement qualifierExpression) {
    if (qualifierExpression instanceof PsiMethodCallExpression) {
      return Collections.singletonList(qualifierExpression);
    }
    if (qualifierExpression instanceof PsiReferenceExpression) {
      final PsiElement targetElement = ((PsiReferenceExpression) qualifierExpression).resolve();
      if (!(targetElement instanceof PsiVariable)) {
        return Collections.emptyList();
      }
      final Collection<? extends PsiElement> variableValues =
          getCachedVariableValues((PsiVariable) targetElement, qualifierExpression);
      if (variableValues == null || variableValues.isEmpty()) {
        return DfaPsiUtil.getVariableAssignmentsInFile(
            (PsiVariable) targetElement, false, qualifierExpression);
      }
      return variableValues;
    }
    if (qualifierExpression instanceof PsiLiteralExpression) {
      return Collections.singletonList(qualifierExpression);
    }
    return Collections.emptyList();
  }

  private static class ValuableInstructionVisitor extends StandardInstructionVisitor {
    final MultiValuesMap<PsiVariable, PsiExpression> myValues =
        new MultiValuesMap<PsiVariable, PsiExpression>(true);
    final Set<PsiVariable> myNulls = new THashSet<PsiVariable>();
    final Set<PsiVariable> myNotNulls = new THashSet<PsiVariable>();
    private final PsiElement myContext;

    public ValuableInstructionVisitor(@NotNull PsiElement context) {
      myContext = context;
    }

    @Override
    public DfaInstructionState[] visitPush(
        PushInstruction instruction, DataFlowRunner runner, DfaMemoryState memState) {
      if (myContext == instruction.getPlace()) {
        final Map<DfaVariableValue, DfaVariableState> map =
            ((ValuableDataFlowRunner.MyDfaMemoryState) memState).getVariableStates();
        for (Map.Entry<DfaVariableValue, DfaVariableState> entry : map.entrySet()) {
          ValuableDataFlowRunner.ValuableDfaVariableState state =
              (ValuableDataFlowRunner.ValuableDfaVariableState) entry.getValue();
          DfaVariableValue variableValue = entry.getKey();
          final PsiExpression psiExpression = state.myExpression;
          if (psiExpression != null && variableValue.getQualifier() == null) {
            myValues.put(variableValue.getPsiVariable(), psiExpression);
          }
        }
        DfaValue value = instruction.getValue();
        if (value instanceof DfaVariableValue
            && ((DfaVariableValue) value).getQualifier() == null) {
          if (memState.isNotNull((DfaVariableValue) value)) {
            myNotNulls.add(((DfaVariableValue) value).getPsiVariable());
          }
          if (memState.isNull(value)) {
            myNulls.add(((DfaVariableValue) value).getPsiVariable());
          }
        }
      }
      return super.visitPush(instruction, runner, memState);
    }

    @Override
    public DfaInstructionState[] visitAssign(
        AssignInstruction instruction, DataFlowRunner runner, DfaMemoryState memState) {
      final Instruction nextInstruction = runner.getInstruction(instruction.getIndex() + 1);

      final DfaValue dfaSource = memState.pop();
      final DfaValue dfaDest = memState.pop();

      if (dfaDest instanceof DfaVariableValue) {
        DfaVariableValue var = (DfaVariableValue) dfaDest;
        final PsiExpression rightValue = instruction.getRExpression();
        final PsiElement parent = rightValue == null ? null : rightValue.getParent();
        final IElementType type =
            parent instanceof PsiAssignmentExpression
                ? ((PsiAssignmentExpression) parent).getOperationTokenType()
                : JavaTokenType.EQ;
        // store current value - to use in case of '+='
        final PsiExpression prevValue =
            ((ValuableDataFlowRunner.ValuableDfaVariableState)
                    ((ValuableDataFlowRunner.MyDfaMemoryState) memState).getVariableState(var))
                .myExpression;
        memState.setVarValue(var, dfaSource);
        // state may have been changed so re-retrieve it
        final ValuableDataFlowRunner.ValuableDfaVariableState curState =
            (ValuableDataFlowRunner.ValuableDfaVariableState)
                ((ValuableDataFlowRunner.MyDfaMemoryState) memState).getVariableState(var);
        final PsiExpression curValue = curState.myExpression;
        final PsiExpression nextValue;
        if (type == JavaTokenType.PLUSEQ && prevValue != null) {
          PsiExpression tmpExpression;
          try {
            tmpExpression =
                JavaPsiFacade.getElementFactory(myContext.getProject())
                    .createExpressionFromText(
                        prevValue.getText() + "+" + rightValue.getText(), rightValue);
          } catch (Exception e) {
            tmpExpression = curValue == null ? rightValue : curValue;
          }
          nextValue = tmpExpression;
        } else {
          nextValue = curValue == null ? rightValue : curValue;
        }
        curState.myExpression = nextValue;
      }
      memState.push(dfaDest);
      return new DfaInstructionState[] {new DfaInstructionState(nextInstruction, memState)};
    }
  }
}
Пример #27
0
/** @author dsl */
public class ElementToWorkOn {
  public static final Key<PsiElement> PARENT = Key.create("PARENT");
  private final PsiExpression myExpression;
  private final PsiLocalVariable myLocalVariable;
  public static final Key<String> PREFIX = Key.create("prefix");
  public static final Key<String> SUFFIX = Key.create("suffix");
  public static final Key<RangeMarker> TEXT_RANGE = Key.create("range");
  public static final Key<Boolean> OUT_OF_CODE_BLOCK = Key.create("out_of_code_block");

  private ElementToWorkOn(PsiLocalVariable localVariable, PsiExpression expr) {
    myLocalVariable = localVariable;
    myExpression = expr;
  }

  public PsiExpression getExpression() {
    return myExpression;
  }

  public PsiLocalVariable getLocalVariable() {
    return myLocalVariable;
  }

  public boolean isInvokedOnDeclaration() {
    return myExpression == null;
  }

  public static void processElementToWorkOn(
      final Editor editor,
      final PsiFile file,
      final String refactoringName,
      final String helpId,
      final Project project,
      final Pass<ElementToWorkOn> processor) {
    PsiLocalVariable localVar = null;
    PsiExpression expr = null;

    if (!editor.getSelectionModel().hasSelection()) {
      PsiElement element =
          TargetElementUtilBase.findTargetElement(
              editor,
              TargetElementUtilBase.ELEMENT_NAME_ACCEPTED
                  | TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED
                  | TargetElementUtilBase.LOOKUP_ITEM_ACCEPTED);
      if (element instanceof PsiLocalVariable) {
        localVar = (PsiLocalVariable) element;
        final PsiElement elementAt = file.findElementAt(editor.getCaretModel().getOffset());
        if (elementAt instanceof PsiIdentifier
            && elementAt.getParent() instanceof PsiReferenceExpression) {
          expr = (PsiExpression) elementAt.getParent();
        }
      } else {
        final PsiLocalVariable variable =
            PsiTreeUtil.getParentOfType(
                file.findElementAt(editor.getCaretModel().getOffset()), PsiLocalVariable.class);

        final int offset = editor.getCaretModel().getOffset();
        final PsiElement[] statementsInRange =
            IntroduceVariableBase.findStatementsAtOffset(editor, file, offset);

        if (statementsInRange.length == 1
            && (PsiUtil.hasErrorElementChild(statementsInRange[0])
                || !PsiUtil.isStatement(statementsInRange[0]))) {
          editor.getSelectionModel().selectLineAtCaret();
          final ElementToWorkOn elementToWorkOn =
              getElementToWorkOn(editor, file, refactoringName, helpId, project, localVar, expr);
          if (elementToWorkOn == null
              || elementToWorkOn.getLocalVariable() == null
                  && elementToWorkOn.getExpression() == null) {
            editor.getSelectionModel().removeSelection();
          }
        }

        if (!editor.getSelectionModel().hasSelection()) {
          final List<PsiExpression> expressions =
              IntroduceVariableBase.collectExpressions(file, editor, offset, statementsInRange);
          if (expressions.isEmpty()) {
            editor.getSelectionModel().selectLineAtCaret();
          } else if (expressions.size() == 1) {
            expr = expressions.get(0);
          } else {
            IntroduceTargetChooser.showChooser(
                editor,
                expressions,
                new Pass<PsiExpression>() {
                  @Override
                  public void pass(final PsiExpression selectedValue) {
                    PsiLocalVariable var =
                        null; // replace var if selected expression == var initializer
                    if (variable != null && variable.getInitializer() == selectedValue) {
                      var = variable;
                    }
                    processor.pass(
                        getElementToWorkOn(
                            editor, file, refactoringName, helpId, project, var, selectedValue));
                  }
                },
                new PsiExpressionTrimRenderer.RenderFunction());
            return;
          }
        }
      }
    }

    processor.pass(
        getElementToWorkOn(editor, file, refactoringName, helpId, project, localVar, expr));
  }

  private static ElementToWorkOn getElementToWorkOn(
      final Editor editor,
      final PsiFile file,
      final String refactoringName,
      final String helpId,
      final Project project,
      PsiLocalVariable localVar,
      PsiExpression expr) {
    int startOffset = 0;
    int endOffset = 0;
    if (localVar == null && expr == null) {
      startOffset = editor.getSelectionModel().getSelectionStart();
      endOffset = editor.getSelectionModel().getSelectionEnd();
      expr = CodeInsightUtil.findExpressionInRange(file, startOffset, endOffset);
      if (expr == null) {
        PsiIdentifier ident =
            CodeInsightUtil.findElementInRange(file, startOffset, endOffset, PsiIdentifier.class);
        if (ident != null) {
          localVar = PsiTreeUtil.getParentOfType(ident, PsiLocalVariable.class);
        }
      }
    }

    if (expr == null && localVar == null) {
      PsiElement[] statements = CodeInsightUtil.findStatementsInRange(file, startOffset, endOffset);
      if (statements.length == 1 && statements[0] instanceof PsiExpressionStatement) {
        expr = ((PsiExpressionStatement) statements[0]).getExpression();
      } else if (statements.length == 1 && statements[0] instanceof PsiDeclarationStatement) {
        PsiDeclarationStatement decl = (PsiDeclarationStatement) statements[0];
        PsiElement[] declaredElements = decl.getDeclaredElements();
        if (declaredElements.length == 1 && declaredElements[0] instanceof PsiLocalVariable) {
          localVar = (PsiLocalVariable) declaredElements[0];
        }
      }
    }
    if (localVar == null && expr == null) {
      expr = IntroduceVariableBase.getSelectedExpression(project, file, startOffset, endOffset);
    }

    if (localVar == null && expr == null) {
      String message =
          RefactoringBundle.getCannotRefactorMessage(
              RefactoringBundle.message("error.wrong.caret.position.local.or.expression.name"));
      CommonRefactoringUtil.showErrorHint(project, editor, message, refactoringName, helpId);
      return null;
    }
    return new ElementToWorkOn(localVar, expr);
  }
}
Пример #28
0
class UndoRedoStacksHolder {
  private final Key<LinkedList<UndoableGroup>> STACK_IN_DOCUMENT_KEY =
      Key.create("STACK_IN_DOCUMENT_KEY");

  private final boolean myUndo;

  private final LinkedList<UndoableGroup> myGlobalStack = new LinkedList<UndoableGroup>();
  private final Map<DocumentReference, LinkedList<UndoableGroup>> myDocumentStacks =
      new HashMap<DocumentReference, LinkedList<UndoableGroup>>();
  private final WeakList<Document> myDocumentsWithStacks = new WeakList<Document>();

  public UndoRedoStacksHolder(boolean isUndo) {
    myUndo = isUndo;
  }

  public LinkedList<UndoableGroup> getStack(Document d) {
    return getStack(createReferenceOrGetOriginal(d));
  }

  private static DocumentReference createReferenceOrGetOriginal(Document d) {
    Document original = UndoManagerImpl.getOriginal(d);
    return DocumentReferenceManager.getInstance().create(original);
  }

  public LinkedList<UndoableGroup> getStack(@NotNull DocumentReference r) {
    return r.getFile() != null ? doGetStackForFile(r) : doGetStackForDocument(r);
  }

  private LinkedList<UndoableGroup> doGetStackForFile(DocumentReference r) {
    LinkedList<UndoableGroup> result = myDocumentStacks.get(r);
    if (result == null) {
      result = new LinkedList<UndoableGroup>();
      myDocumentStacks.put(r, result);
    }
    return result;
  }

  private LinkedList<UndoableGroup> doGetStackForDocument(DocumentReference r) {
    // If document is not associated with file, we have to store its stack in document
    // itself to avoid memory leaks caused by holding stacks of all documents, ever created, here.
    // And to know, what documents do exist now, we have to maintain weak reference list of them.

    Document d = r.getDocument();
    LinkedList<UndoableGroup> result = d.getUserData(STACK_IN_DOCUMENT_KEY);
    if (result == null) {
      result = new LinkedList<UndoableGroup>();
      d.putUserData(STACK_IN_DOCUMENT_KEY, result);
      myDocumentsWithStacks.add(d);
    }
    return result;
  }

  public boolean hasActions(Collection<DocumentReference> refs) {
    if (refs.isEmpty()) return !myGlobalStack.isEmpty();
    for (DocumentReference each : refs) {
      if (!getStack(each).isEmpty()) return true;
    }
    return false;
  }

  @NotNull
  public UndoableGroup getLastAction(Collection<DocumentReference> refs) {
    if (refs.isEmpty()) return myGlobalStack.getLast();

    UndoableGroup mostRecentAction = null;
    int mostRecentDocTimestamp = myUndo ? -1 : Integer.MAX_VALUE;

    for (DocumentReference each : refs) {
      LinkedList<UndoableGroup> stack = getStack(each);
      // the stack for a document can be empty in case of compound editors with several documents
      if (stack.isEmpty()) continue;
      UndoableGroup lastAction = stack.getLast();

      int timestamp = lastAction.getCommandTimestamp();
      if (myUndo ? timestamp > mostRecentDocTimestamp : timestamp < mostRecentDocTimestamp) {
        mostRecentAction = lastAction;
        mostRecentDocTimestamp = timestamp;
      }
    }

    // result must not be null
    return mostRecentAction;
  }

  public Set<DocumentReference> collectClashingActions(UndoableGroup group) {
    Set<DocumentReference> result = new THashSet<DocumentReference>();

    for (DocumentReference each : group.getAffectedDocuments()) {
      UndoableGroup last = getStack(each).getLast();
      if (last != group) {
        result.addAll(last.getAffectedDocuments());
      }
    }

    if (group.isGlobal()) {
      UndoableGroup last = myGlobalStack.getLast();
      if (last != group) {
        result.addAll(last.getAffectedDocuments());
      }
    }

    return result;
  }

  public void addToStacks(UndoableGroup group) {
    if (group.isGlobal()) doAddToStack(myGlobalStack, group, UndoManagerImpl.GLOBAL_UNDO_LIMIT);
    for (DocumentReference each : group.getAffectedDocuments()) {
      doAddToStack(getStack(each), group, UndoManagerImpl.LOCAL_UNDO_LIMIT);
    }
  }

  private void doAddToStack(LinkedList<UndoableGroup> stack, UndoableGroup group, int limit) {
    if (!group.isUndoable() && stack.isEmpty()) return;

    stack.addLast(group);
    while (stack.size() > limit) {
      stack.removeFirst();
    }
  }

  public void removeFromStacks(UndoableGroup group) {
    if (group.getAffectedDocuments().isEmpty()) return;

    if (group.isGlobal()) {
      assert myGlobalStack.getLast() == group;
      myGlobalStack.removeLast();
    }
    for (DocumentReference each : group.getAffectedDocuments()) {
      LinkedList<UndoableGroup> stack = getStack(each);
      assert stack.getLast() == group;
      stack.removeLast();
    }
  }

  public void clearStacks(boolean clearGlobal, Set<DocumentReference> affectedDocuments) {
    if (clearGlobal) myGlobalStack.clear();
    for (DocumentReference each : affectedDocuments) {
      List<UndoableGroup> stack = getStack(each);
      stack.clear();

      if (each.getFile() != null) {
        myDocumentStacks.remove(each);
      } else {
        Document d = each.getDocument();
        d.putUserData(STACK_IN_DOCUMENT_KEY, null);
        myDocumentsWithStacks.remove(d);
      }
    }
  }

  public void clearAllStacksInTests() {
    clearStacks(true, getAffectedDocuments());
  }

  public void invalidateAllGlobalActions() {
    doInvalidateAllGlobalActions(myGlobalStack);
    for (DocumentReference each : getAffectedDocuments()) {
      doInvalidateAllGlobalActions(getStack(each));
    }
  }

  private void doInvalidateAllGlobalActions(LinkedList<UndoableGroup> stack) {
    for (UndoableGroup g : stack) {
      g.invalidateIfGlobal();
    }
  }

  public void collectAllAffectedDocuments(Collection<DocumentReference> result) {
    for (UndoableGroup each : myGlobalStack) {
      result.addAll(each.getAffectedDocuments());
    }
    collectLocalAffectedDocuments(result);
  }

  private void collectLocalAffectedDocuments(Collection<DocumentReference> result) {
    result.addAll(myDocumentStacks.keySet());
    for (Document each : myDocumentsWithStacks) {
      result.add(DocumentReferenceManager.getInstance().create(each));
    }
  }

  private Set<DocumentReference> getAffectedDocuments() {
    Set<DocumentReference> result = new THashSet<DocumentReference>();
    collectAllAffectedDocuments(result);
    return result;
  }

  public int getLastCommandTimestamp(DocumentReference r) {
    LinkedList<UndoableGroup> stack = getStack(r);
    if (stack.isEmpty()) return 0;
    return Math.max(stack.getFirst().getCommandTimestamp(), stack.getLast().getCommandTimestamp());
  }
}
Пример #29
0
public class IndentsPass extends TextEditorHighlightingPass implements DumbAware {
  private static final ConcurrentMap<IElementType, String> COMMENT_PREFIXES =
      ContainerUtil.newConcurrentMap();
  private static final String NO_COMMENT_INFO_MARKER =
      "hopefully, noone uses this string as a comment prefix";

  private static final Key<List<RangeHighlighter>> INDENT_HIGHLIGHTERS_IN_EDITOR_KEY =
      Key.create("INDENT_HIGHLIGHTERS_IN_EDITOR_KEY");
  private static final Key<Long> LAST_TIME_INDENTS_BUILT = Key.create("LAST_TIME_INDENTS_BUILT");

  private final EditorEx myEditor;
  private final PsiFile myFile;
  public static final Comparator<TextRange> RANGE_COMPARATOR =
      new Comparator<TextRange>() {
        @Override
        public int compare(TextRange o1, TextRange o2) {
          if (o1.getStartOffset() == o2.getStartOffset()) {
            return o1.getEndOffset() - o2.getEndOffset();
          }

          return o1.getStartOffset() - o2.getStartOffset();
        }
      };

  private static final CustomHighlighterRenderer RENDERER =
      new CustomHighlighterRenderer() {
        @Override
        @SuppressWarnings({"AssignmentToForLoopParameter"})
        public void paint(
            @NotNull Editor editor, @NotNull RangeHighlighter highlighter, @NotNull Graphics g) {
          int startOffset = highlighter.getStartOffset();
          final Document doc = highlighter.getDocument();
          if (startOffset >= doc.getTextLength()) return;

          final int endOffset = highlighter.getEndOffset();
          final int endLine = doc.getLineNumber(endOffset);

          int off;
          int startLine = doc.getLineNumber(startOffset);
          IndentGuideDescriptor descriptor =
              editor.getIndentsModel().getDescriptor(startLine, endLine);

          final CharSequence chars = doc.getCharsSequence();
          do {
            int start = doc.getLineStartOffset(startLine);
            int end = doc.getLineEndOffset(startLine);
            off = CharArrayUtil.shiftForward(chars, start, end, " \t");
            startLine--;
          } while (startLine > 1 && off < doc.getTextLength() && chars.charAt(off) == '\n');

          final VisualPosition startPosition = editor.offsetToVisualPosition(off);
          int indentColumn = startPosition.column;

          // It's considered that indent guide can cross not only white space but comments, javadocs
          // etc. Hence, there is a possible
          // case that the first indent guide line is, say, single-line comment where comment
          // symbols ('//') are located at the first
          // visual column. We need to calculate correct indent guide column then.
          int lineShift = 1;
          if (indentColumn <= 0 && descriptor != null) {
            indentColumn = descriptor.indentLevel;
            lineShift = 0;
          }
          if (indentColumn <= 0) return;

          final FoldingModel foldingModel = editor.getFoldingModel();
          if (foldingModel.isOffsetCollapsed(off)) return;

          final FoldRegion headerRegion =
              foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(doc.getLineNumber(off)));
          final FoldRegion tailRegion =
              foldingModel.getCollapsedRegionAtOffset(
                  doc.getLineStartOffset(doc.getLineNumber(endOffset)));

          if (tailRegion != null && tailRegion == headerRegion) return;

          final boolean selected;
          final IndentGuideDescriptor guide = editor.getIndentsModel().getCaretIndentGuide();
          if (guide != null) {
            final CaretModel caretModel = editor.getCaretModel();
            final int caretOffset = caretModel.getOffset();
            selected =
                caretOffset >= off
                    && caretOffset < endOffset
                    && caretModel.getLogicalPosition().column == indentColumn;
          } else {
            selected = false;
          }

          Point start =
              editor.visualPositionToXY(
                  new VisualPosition(startPosition.line + lineShift, indentColumn));
          final VisualPosition endPosition = editor.offsetToVisualPosition(endOffset);
          Point end =
              editor.visualPositionToXY(new VisualPosition(endPosition.line, endPosition.column));
          int maxY = end.y;
          if (endPosition.line == editor.offsetToVisualPosition(doc.getTextLength()).line) {
            maxY += editor.getLineHeight();
          }

          Rectangle clip = g.getClipBounds();
          if (clip != null) {
            if (clip.y >= maxY || clip.y + clip.height <= start.y) {
              return;
            }
            maxY = Math.min(maxY, clip.y + clip.height);
          }

          final EditorColorsScheme scheme = editor.getColorsScheme();
          g.setColor(
              selected
                  ? scheme.getColor(EditorColors.SELECTED_INDENT_GUIDE_COLOR)
                  : scheme.getColor(EditorColors.INDENT_GUIDE_COLOR));

          // There is a possible case that indent line intersects soft wrap-introduced text.
          // Example:
          //     this is a long line <soft-wrap>
          // that| is soft-wrapped
          //     |
          //     | <- vertical indent
          //
          // Also it's possible that no additional intersections are added because of soft wrap:
          //     this is a long line <soft-wrap>
          //     |   that is soft-wrapped
          //     |
          //     | <- vertical indent
          // We want to use the following approach then:
          //     1. Show only active indent if it crosses soft wrap-introduced text;
          //     2. Show indent as is if it doesn't intersect with soft wrap-introduced text;
          if (selected) {
            g.drawLine(start.x + 2, start.y, start.x + 2, maxY);
          } else {
            int y = start.y;
            int newY = start.y;
            SoftWrapModel softWrapModel = editor.getSoftWrapModel();
            int lineHeight = editor.getLineHeight();
            for (int i = Math.max(0, startLine + lineShift); i < endLine && newY < maxY; i++) {
              List<? extends SoftWrap> softWraps = softWrapModel.getSoftWrapsForLine(i);
              int logicalLineHeight = softWraps.size() * lineHeight;
              if (i > startLine + lineShift) {
                logicalLineHeight +=
                    lineHeight; // We assume that initial 'y' value points just below the target
                // line.
              }
              if (!softWraps.isEmpty() && softWraps.get(0).getIndentInColumns() < indentColumn) {
                if (y < newY
                    || i
                        > startLine
                            + lineShift) { // There is a possible case that soft wrap is located on
                  // indent start line.
                  g.drawLine(start.x + 2, y, start.x + 2, newY + lineHeight);
                }
                newY += logicalLineHeight;
                y = newY;
              } else {
                newY += logicalLineHeight;
              }

              FoldRegion foldRegion =
                  foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(i));
              if (foldRegion != null && foldRegion.getEndOffset() < doc.getTextLength()) {
                i = doc.getLineNumber(foldRegion.getEndOffset());
              }
            }

            if (y < maxY) {
              g.drawLine(start.x + 2, y, start.x + 2, maxY);
            }
          }
        }
      };
  private volatile List<TextRange> myRanges;
  private volatile List<IndentGuideDescriptor> myDescriptors;

  public IndentsPass(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
    super(project, editor.getDocument(), false);
    myEditor = (EditorEx) editor;
    myFile = file;
  }

  @Override
  public void doCollectInformation(@NotNull ProgressIndicator progress) {
    assert myDocument != null;
    final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT);
    if (stamp != null && stamp.longValue() == nowStamp()) return;

    myDescriptors = buildDescriptors();

    ArrayList<TextRange> ranges = new ArrayList<TextRange>();
    for (IndentGuideDescriptor descriptor : myDescriptors) {
      ProgressManager.checkCanceled();
      int endOffset =
          descriptor.endLine < myDocument.getLineCount()
              ? myDocument.getLineStartOffset(descriptor.endLine)
              : myDocument.getTextLength();
      ranges.add(new TextRange(myDocument.getLineStartOffset(descriptor.startLine), endOffset));
    }

    Collections.sort(ranges, RANGE_COMPARATOR);
    myRanges = ranges;
  }

  private long nowStamp() {
    if (!myEditor.getSettings().isIndentGuidesShown()) return -1;
    assert myDocument != null;
    return myDocument.getModificationStamp();
  }

  @Override
  public void doApplyInformationToEditor() {
    final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT);
    if (stamp != null && stamp.longValue() == nowStamp()) return;

    List<RangeHighlighter> oldHighlighters =
        myEditor.getUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY);
    final List<RangeHighlighter> newHighlighters = new ArrayList<RangeHighlighter>();
    final MarkupModel mm = myEditor.getMarkupModel();

    int curRange = 0;

    if (oldHighlighters != null) {
      int curHighlight = 0;
      while (curRange < myRanges.size() && curHighlight < oldHighlighters.size()) {
        TextRange range = myRanges.get(curRange);
        RangeHighlighter highlighter = oldHighlighters.get(curHighlight);

        int cmp = compare(range, highlighter);
        if (cmp < 0) {
          newHighlighters.add(createHighlighter(mm, range));
          curRange++;
        } else if (cmp > 0) {
          highlighter.dispose();
          curHighlight++;
        } else {
          newHighlighters.add(highlighter);
          curHighlight++;
          curRange++;
        }
      }

      for (; curHighlight < oldHighlighters.size(); curHighlight++) {
        RangeHighlighter highlighter = oldHighlighters.get(curHighlight);
        highlighter.dispose();
      }
    }

    final int startRangeIndex = curRange;
    assert myDocument != null;
    DocumentUtil.executeInBulk(
        myDocument,
        myRanges.size() > 10000,
        new Runnable() {
          @Override
          public void run() {
            for (int i = startRangeIndex; i < myRanges.size(); i++) {
              newHighlighters.add(createHighlighter(mm, myRanges.get(i)));
            }
          }
        });

    myEditor.putUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY, newHighlighters);
    myEditor.putUserData(LAST_TIME_INDENTS_BUILT, nowStamp());
    myEditor.getIndentsModel().assumeIndents(myDescriptors);
  }

  private List<IndentGuideDescriptor> buildDescriptors() {
    if (!myEditor.getSettings().isIndentGuidesShown()) return Collections.emptyList();

    IndentsCalculator calculator = new IndentsCalculator();
    calculator.calculate();
    int[] lineIndents = calculator.lineIndents;
    TIntIntHashMap effectiveCommentColumns = calculator.indentAfterUncomment;

    List<IndentGuideDescriptor> descriptors = new ArrayList<IndentGuideDescriptor>();

    IntStack lines = new IntStack();
    IntStack indents = new IntStack();

    lines.push(0);
    indents.push(0);
    assert myDocument != null;
    final CharSequence chars = myDocument.getCharsSequence();
    for (int line = 1; line < lineIndents.length; line++) {
      ProgressManager.checkCanceled();
      int curIndent = lineIndents[line];

      while (!indents.empty() && curIndent <= indents.peek()) {
        ProgressManager.checkCanceled();
        final int level = indents.pop();
        int startLine = lines.pop();
        if (level > 0) {
          boolean addDescriptor =
              effectiveCommentColumns.contains(startLine); // Indent started at comment
          if (!addDescriptor) {
            for (int i = startLine; i < line; i++) {
              if (level != lineIndents[i] && level != effectiveCommentColumns.get(i)) {
                addDescriptor = true;
                break;
              }
            }
          }
          if (addDescriptor) {
            descriptors.add(createDescriptor(level, startLine, line, chars));
          }
        }
      }

      int prevLine = line - 1;
      int prevIndent = lineIndents[prevLine];

      if (curIndent - prevIndent > 1) {
        lines.push(prevLine);
        indents.push(prevIndent);
      }
    }

    while (!indents.empty()) {
      ProgressManager.checkCanceled();
      final int level = indents.pop();
      int startLine = lines.pop();
      if (level > 0) {
        descriptors.add(createDescriptor(level, startLine, myDocument.getLineCount(), chars));
      }
    }
    return descriptors;
  }

  private IndentGuideDescriptor createDescriptor(
      int level, int startLine, int endLine, CharSequence chars) {
    while (startLine > 0 && isBlankLine(startLine, chars)) startLine--;
    return new IndentGuideDescriptor(level, startLine, endLine);
  }

  private boolean isBlankLine(int line, CharSequence chars) {
    Document document = myDocument;
    if (document == null) {
      return true;
    }
    int startOffset = document.getLineStartOffset(line);
    int endOffset = document.getLineEndOffset(line);
    return CharArrayUtil.shiftForward(chars, startOffset, endOffset, " \t")
        >= myDocument.getLineEndOffset(line);
  }

  /**
   * We want to treat comments specially in a way to skip comment prefix on line indent calculation.
   *
   * <p>Example:
   *
   * <pre>
   *   if (true) {
   *     int i1;
   * //    int i2;
   *     int i3;
   *   }
   * </pre>
   *
   * We want to use 'int i2;' start offset as the third line indent (though it has non-white space
   * comment prefix (//) at the first column.
   *
   * <p>This method tries to parse comment prefix for the language implied by the given comment
   * type. It uses {@link #NO_COMMENT_INFO_MARKER} as an indicator that that information is
   * unavailable
   *
   * @param commentType target comment type
   * @return prefix of the comment denoted by the given type if any; {@link #NO_COMMENT_INFO_MARKER}
   *     otherwise
   */
  @NotNull
  private static String getCommentPrefix(@NotNull IElementType commentType) {
    Commenter c = LanguageCommenters.INSTANCE.forLanguage(commentType.getLanguage());
    if (!(c instanceof CodeDocumentationAwareCommenter)) {
      COMMENT_PREFIXES.put(commentType, NO_COMMENT_INFO_MARKER);
      return NO_COMMENT_INFO_MARKER;
    }
    CodeDocumentationAwareCommenter commenter = (CodeDocumentationAwareCommenter) c;

    IElementType lineCommentType = commenter.getLineCommentTokenType();
    String lineCommentPrefix = commenter.getLineCommentPrefix();
    if (lineCommentType != null) {
      COMMENT_PREFIXES.put(
          lineCommentType, lineCommentPrefix == null ? NO_COMMENT_INFO_MARKER : lineCommentPrefix);
    }

    IElementType blockCommentType = commenter.getBlockCommentTokenType();
    String blockCommentPrefix = commenter.getBlockCommentPrefix();
    if (blockCommentType != null) {
      COMMENT_PREFIXES.put(
          blockCommentType,
          blockCommentPrefix == null ? NO_COMMENT_INFO_MARKER : blockCommentPrefix);
    }

    IElementType docCommentType = commenter.getDocumentationCommentTokenType();
    String docCommentPrefix = commenter.getDocumentationCommentPrefix();
    if (docCommentType != null) {
      COMMENT_PREFIXES.put(
          docCommentType, docCommentPrefix == null ? NO_COMMENT_INFO_MARKER : docCommentPrefix);
    }

    COMMENT_PREFIXES.putIfAbsent(commentType, NO_COMMENT_INFO_MARKER);
    return COMMENT_PREFIXES.get(commentType);
  }

  @NotNull
  private static RangeHighlighter createHighlighter(MarkupModel mm, TextRange range) {
    final RangeHighlighter highlighter =
        mm.addRangeHighlighter(
            range.getStartOffset(),
            range.getEndOffset(),
            0,
            null,
            HighlighterTargetArea.EXACT_RANGE);
    highlighter.setCustomRenderer(RENDERER);
    return highlighter;
  }

  private static int compare(@NotNull TextRange r, @NotNull RangeHighlighter h) {
    int answer = r.getStartOffset() - h.getStartOffset();
    return answer != 0 ? answer : r.getEndOffset() - h.getEndOffset();
  }

  private class IndentsCalculator {

    @NotNull public final Map<Language, TokenSet> myComments = ContainerUtilRt.newHashMap();

    /**
     * We need to treat specially commented lines. Consider a situation like below:
     *
     * <pre>
     *   void test() {
     *     if (true) {
     *       int i;
     *  //     int j;
     *     }
     *   }
     * </pre>
     *
     * We don't want to show indent guide after 'int i;' line because un-commented line below ('int
     * j;') would have the same indent level. That's why we remember 'indents after un-comment' at
     * this collection.
     */
    @NotNull
    public final TIntIntHashMap /* line -> indent column after un-comment */ indentAfterUncomment =
        new TIntIntHashMap();

    @NotNull public final int[] lineIndents;
    @NotNull public final CharSequence myChars;

    IndentsCalculator() {
      assert myDocument != null;
      lineIndents = new int[myDocument.getLineCount()];
      myChars = myDocument.getCharsSequence();
    }

    /** Calculates line indents for the {@link #myDocument target document}. */
    void calculate() {
      final FileType fileType = myFile.getFileType();
      int prevLineIndent = -1;

      for (int line = 0; line < lineIndents.length; line++) {
        ProgressManager.checkCanceled();
        int lineStart = myDocument.getLineStartOffset(line);
        int lineEnd = myDocument.getLineEndOffset(line);
        final int nonWhitespaceOffset =
            CharArrayUtil.shiftForward(myChars, lineStart, lineEnd, " \t");
        if (nonWhitespaceOffset == lineEnd) {
          lineIndents[line] = -1; // Blank line marker
        } else {
          final int column =
              ((EditorImpl) myEditor).calcColumnNumber(nonWhitespaceOffset, line, true, myChars);
          if (prevLineIndent > 0 && prevLineIndent > column) {
            lineIndents[line] = calcIndent(line, nonWhitespaceOffset, lineEnd, column);
          } else {
            lineIndents[line] = column;
          }
          prevLineIndent = lineIndents[line];
        }
      }

      int topIndent = 0;
      for (int line = 0; line < lineIndents.length; line++) {
        ProgressManager.checkCanceled();
        if (lineIndents[line] >= 0) {
          topIndent = lineIndents[line];
        } else {
          int startLine = line;
          while (line < lineIndents.length && lineIndents[line] < 0) {
            //noinspection AssignmentToForLoopParameter
            line++;
          }

          int bottomIndent = line < lineIndents.length ? lineIndents[line] : topIndent;

          int indent = Math.min(topIndent, bottomIndent);
          if (bottomIndent < topIndent) {
            int lineStart = myDocument.getLineStartOffset(line);
            int lineEnd = myDocument.getLineEndOffset(line);
            int nonWhitespaceOffset =
                CharArrayUtil.shiftForward(myChars, lineStart, lineEnd, " \t");
            HighlighterIterator iterator =
                myEditor.getHighlighter().createIterator(nonWhitespaceOffset);
            if (BraceMatchingUtil.isRBraceToken(iterator, myChars, fileType)) {
              indent = topIndent;
            }
          }

          for (int blankLine = startLine; blankLine < line; blankLine++) {
            assert lineIndents[blankLine] == -1;
            lineIndents[blankLine] = Math.min(topIndent, indent);
          }

          //noinspection AssignmentToForLoopParameter
          line--; // will be incremented back at the end of the loop;
        }
      }
    }

    /**
     * Tries to calculate given line's indent column assuming that there might be a comment at the
     * given indent offset (see {@link #getCommentPrefix(IElementType)}).
     *
     * @param line target line
     * @param indentOffset start indent offset to use for the given line
     * @param lineEndOffset given line's end offset
     * @param fallbackColumn column to return if it's not possible to apply comment-specific indent
     *     calculation rules
     * @return given line's indent column to use
     */
    private int calcIndent(int line, int indentOffset, int lineEndOffset, int fallbackColumn) {
      final HighlighterIterator it = myEditor.getHighlighter().createIterator(indentOffset);
      IElementType tokenType = it.getTokenType();
      Language language = tokenType.getLanguage();
      TokenSet comments = myComments.get(language);
      if (comments == null) {
        ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(language);
        if (definition != null) {
          comments = definition.getCommentTokens();
        }
        if (comments == null) {
          return fallbackColumn;
        } else {
          myComments.put(language, comments);
        }
      }
      if (comments.contains(tokenType) && indentOffset == it.getStart()) {
        String prefix = COMMENT_PREFIXES.get(tokenType);
        if (prefix == null) {
          prefix = getCommentPrefix(tokenType);
        }
        if (!NO_COMMENT_INFO_MARKER.equals(prefix)) {
          final int indentInsideCommentOffset =
              CharArrayUtil.shiftForward(
                  myChars, indentOffset + prefix.length(), lineEndOffset, " \t");
          if (indentInsideCommentOffset < lineEndOffset) {
            int indent = myEditor.calcColumnNumber(indentInsideCommentOffset, line);
            indentAfterUncomment.put(line, indent - prefix.length());
            return indent;
          }
        }
      }
      return fallbackColumn;
    }
  }
}
public class FoldingModelImpl implements FoldingModelEx, PrioritizedDocumentListener, Dumpable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.editor.impl.EditorFoldingModelImpl");

  private static final Key<LogicalPosition> SAVED_CARET_POSITION =
      Key.create("saved.position.before.folding");

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

  private boolean myIsFoldingEnabled;
  private final EditorImpl myEditor;
  private final FoldRegionsTree myFoldTree;
  private TextAttributes myFoldTextAttributes;
  private boolean myIsBatchFoldingProcessing;
  private boolean myDoNotCollapseCaret;
  private boolean myFoldRegionsProcessed;

  private int mySavedCaretShift;
  private final MultiMap<FoldingGroup, FoldRegion> myGroups =
      new MultiMap<FoldingGroup, FoldRegion>();
  private boolean myDocumentChangeProcessed = true;

  public FoldingModelImpl(EditorImpl editor) {
    myEditor = editor;
    myIsFoldingEnabled = true;
    myIsBatchFoldingProcessing = false;
    myDoNotCollapseCaret = false;
    myFoldTree =
        new FoldRegionsTree() {
          @Override
          protected boolean isFoldingEnabled() {
            return FoldingModelImpl.this.isFoldingEnabled();
          }
        };
    myFoldRegionsProcessed = false;
    refreshSettings();
  }

  @NotNull
  public List<FoldRegion> getGroupedRegions(@NotNull FoldingGroup group) {
    return (List<FoldRegion>) myGroups.get(group);
  }

  @NotNull
  public FoldRegion getFirstRegion(@NotNull FoldingGroup group, FoldRegion child) {
    final List<FoldRegion> regions = getGroupedRegions(group);
    if (regions.isEmpty()) {
      final boolean inAll = Arrays.asList(getAllFoldRegions()).contains(child);
      throw new AssertionError(
          "Folding group without children; the known child is in all: " + inAll);
    }

    FoldRegion main = regions.get(0);
    for (int i = 1; i < regions.size(); i++) {
      FoldRegion region = regions.get(i);
      if (main.getStartOffset() > region.getStartOffset()) {
        main = region;
      }
    }
    return main;
  }

  public int getEndOffset(@NotNull FoldingGroup group) {
    final List<FoldRegion> regions = getGroupedRegions(group);
    int endOffset = 0;
    for (FoldRegion region : regions) {
      if (region.isValid()) {
        endOffset = Math.max(endOffset, region.getEndOffset());
      }
    }
    return endOffset;
  }

  public void refreshSettings() {
    myFoldTextAttributes =
        myEditor.getColorsScheme().getAttributes(EditorColors.FOLDED_TEXT_ATTRIBUTES);
  }

  @Override
  public boolean isFoldingEnabled() {
    return myIsFoldingEnabled;
  }

  @Override
  public boolean isOffsetCollapsed(int offset) {
    assertReadAccess();
    return getCollapsedRegionAtOffset(offset) != null;
  }

  private boolean isOffsetInsideCollapsedRegion(int offset) {
    assertReadAccess();
    FoldRegion region = getCollapsedRegionAtOffset(offset);
    return region != null && region.getStartOffset() < offset;
  }

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

  private static void assertReadAccess() {
    ApplicationManagerEx.getApplicationEx().assertReadAccessAllowed();
  }

  @Override
  public void setFoldingEnabled(boolean isEnabled) {
    assertIsDispatchThreadForEditor();
    myIsFoldingEnabled = isEnabled;
  }

  @Override
  public FoldRegion addFoldRegion(int startOffset, int endOffset, @NotNull String placeholderText) {
    FoldRegion region = createFoldRegion(startOffset, endOffset, placeholderText, null, false);
    if (region == null) return null;
    if (!addFoldRegion(region)) {
      region.dispose();
      return null;
    }

    return region;
  }

  @Override
  public boolean addFoldRegion(@NotNull final FoldRegion region) {
    assertIsDispatchThreadForEditor();
    if (!isFoldingEnabled()) {
      return false;
    }
    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
      return false;
    }

    myFoldRegionsProcessed = true;
    if (myFoldTree.addRegion(region)) {
      final FoldingGroup group = region.getGroup();
      if (group != null) {
        myGroups.putValue(group, region);
      }
      for (FoldingListener listener : myListeners) {
        listener.onFoldRegionStateChange(region);
      }
      return true;
    }

    return false;
  }

  @Override
  public void runBatchFoldingOperation(@NotNull Runnable operation) {
    runBatchFoldingOperation(operation, false, true);
  }

  @Override
  public void runBatchFoldingOperation(@NotNull Runnable operation, boolean moveCaret) {
    runBatchFoldingOperation(operation, false, moveCaret);
  }

  private void runBatchFoldingOperation(
      final Runnable operation, final boolean dontCollapseCaret, final boolean moveCaret) {
    assertIsDispatchThreadForEditor();
    boolean oldDontCollapseCaret = myDoNotCollapseCaret;
    myDoNotCollapseCaret |= dontCollapseCaret;
    boolean oldBatchFlag = myIsBatchFoldingProcessing;
    if (!oldBatchFlag) {
      ((ScrollingModelImpl) myEditor.getScrollingModel()).finishAnimation();
      mySavedCaretShift =
          myEditor.visibleLineToY(myEditor.getCaretModel().getVisualPosition().line)
              - myEditor.getScrollingModel().getVerticalScrollOffset();
    }

    myIsBatchFoldingProcessing = true;
    try {
      operation.run();
    } finally {
      if (!oldBatchFlag) {
        if (myFoldRegionsProcessed) {
          notifyBatchFoldingProcessingDone(moveCaret);
          myFoldRegionsProcessed = false;
        }
        myIsBatchFoldingProcessing = false;
      }
      myDoNotCollapseCaret = oldDontCollapseCaret;
    }
  }

  @Override
  public void runBatchFoldingOperationDoNotCollapseCaret(@NotNull final Runnable operation) {
    runBatchFoldingOperation(operation, true, true);
  }

  /**
   * Disables caret position adjustment after batch folding operation is finished. Should be called
   * from inside batch operation runnable.
   */
  public void flushCaretShift() {
    mySavedCaretShift = -1;
  }

  @Override
  @NotNull
  public FoldRegion[] getAllFoldRegions() {
    assertReadAccess();
    return myFoldTree.fetchAllRegions();
  }

  @Override
  @Nullable
  public FoldRegion getCollapsedRegionAtOffset(int offset) {
    return myFoldTree.fetchOutermost(offset);
  }

  @Nullable
  @Override
  public FoldRegion getFoldRegion(int startOffset, int endOffset) {
    assertReadAccess();
    return myFoldTree.getRegionAt(startOffset, endOffset);
  }

  @Override
  @Nullable
  public FoldRegion getFoldingPlaceholderAt(Point p) {
    assertReadAccess();
    LogicalPosition pos = myEditor.xyToLogicalPosition(p);
    int line = pos.line;

    if (line >= myEditor.getDocument().getLineCount()) return null;

    int offset = myEditor.logicalPositionToOffset(pos);

    return myFoldTree.fetchOutermost(offset);
  }

  @Override
  public void removeFoldRegion(@NotNull final FoldRegion region) {
    assertIsDispatchThreadForEditor();

    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
    }

    region.setExpanded(true);
    final FoldingGroup group = region.getGroup();
    if (group != null) {
      myGroups.remove(group, region);
    }

    myFoldTree.removeRegion(region);
    myFoldRegionsProcessed = true;
    region.dispose();
  }

  public void dispose() {
    doClearFoldRegions();
  }

  @Override
  public void clearFoldRegions() {
    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
      return;
    }
    doClearFoldRegions();
  }

  public void doClearFoldRegions() {
    myGroups.clear();
    myFoldTree.clear();
  }

  public void expandFoldRegion(FoldRegion region) {
    assertIsDispatchThreadForEditor();
    if (region.isExpanded() || region.shouldNeverExpand()) return;

    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
    }

    for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
      LogicalPosition savedPosition = caret.getUserData(SAVED_CARET_POSITION);
      if (savedPosition != null) {
        int savedOffset = myEditor.logicalPositionToOffset(savedPosition);

        FoldRegion[] allCollapsed = myFoldTree.fetchCollapsedAt(savedOffset);
        if (allCollapsed.length == 1 && allCollapsed[0] == region) {
          caret.moveToLogicalPosition(savedPosition);
        }
      }
    }

    myFoldRegionsProcessed = true;
    ((FoldRegionImpl) region).setExpandedInternal(true);
    notifyListenersOnFoldRegionStateChange(region);
  }

  public void collapseFoldRegion(FoldRegion region) {
    assertIsDispatchThreadForEditor();
    if (!region.isExpanded()) return;

    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
    }

    List<Caret> carets = myEditor.getCaretModel().getAllCarets();
    for (Caret caret : carets) {
      LogicalPosition caretPosition = caret.getLogicalPosition();
      int caretOffset = myEditor.logicalPositionToOffset(caretPosition);

      if (FoldRegionsTree.contains(region, caretOffset)) {
        if (myDoNotCollapseCaret) return;
      }
    }
    for (Caret caret : carets) {
      LogicalPosition caretPosition = caret.getLogicalPosition();
      int caretOffset = myEditor.logicalPositionToOffset(caretPosition);

      if (FoldRegionsTree.contains(region, caretOffset)) {
        if (caret.getUserData(SAVED_CARET_POSITION) == null) {
          caret.putUserData(SAVED_CARET_POSITION, caretPosition.withoutVisualPositionInfo());
        }
      }
    }

    myFoldRegionsProcessed = true;
    ((FoldRegionImpl) region).setExpandedInternal(false);
    notifyListenersOnFoldRegionStateChange(region);
  }

  private void notifyBatchFoldingProcessingDone(final boolean moveCaretFromCollapsedRegion) {
    rebuild();

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

    myEditor.updateCaretCursor();
    myEditor.recalculateSizeAndRepaint();
    myEditor.getGutterComponentEx().updateSize();
    myEditor.getGutterComponentEx().repaint();

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

      LogicalPosition positionToUse = null;
      int offsetToUse = -1;

      FoldRegion collapsed = myFoldTree.fetchOutermost(caretOffset);
      LogicalPosition savedPosition = caret.getUserData(SAVED_CARET_POSITION);
      if (savedPosition != null) {
        int savedOffset = myEditor.logicalPositionToOffset(savedPosition);
        FoldRegion collapsedAtSaved = myFoldTree.fetchOutermost(savedOffset);
        if (collapsedAtSaved == null) {
          positionToUse = savedPosition;
        } else {
          offsetToUse = collapsedAtSaved.getStartOffset();
        }
      }

      if (collapsed != null && positionToUse == null) {
        positionToUse = myEditor.offsetToLogicalPosition(collapsed.getStartOffset());
      }

      if (moveCaretFromCollapsedRegion && caret.isUpToDate()) {
        if (offsetToUse >= 0) {
          caret.moveToOffset(offsetToUse);
        } else if (positionToUse != null) {
          caret.moveToLogicalPosition(positionToUse);
        } else {
          caret.moveToLogicalPosition(caretPosition);
        }
      }

      caret.putUserData(SAVED_CARET_POSITION, savedPosition);

      if (isOffsetInsideCollapsedRegion(selectionStart)
          || isOffsetInsideCollapsedRegion(selectionEnd)) {
        caret.removeSelection();
      } else if (selectionStart < myEditor.getDocument().getTextLength()) {
        caret.setSelection(selectionStart, selectionEnd);
      }
    }

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

  @Override
  public void rebuild() {
    if (!myEditor.getDocument().isInBulkUpdate()) {
      myFoldTree.rebuild();
    }
  }

  private void updateCachedOffsets() {
    myFoldTree.updateCachedOffsets();
  }

  public int getFoldedLinesCountBefore(int offset) {
    if (!myDocumentChangeProcessed && myEditor.getDocument().isInEventsHandling()) {
      // There is a possible case that this method is called on document update before fold regions
      // are recalculated.
      // We return zero in such situations then.
      return 0;
    }
    return myFoldTree.getFoldedLinesCountBefore(offset);
  }

  @Override
  @Nullable
  public FoldRegion[] fetchTopLevel() {
    return myFoldTree.fetchTopLevel();
  }

  @Override
  @Nullable
  public FoldRegion fetchOutermost(int offset) {
    return myFoldTree.fetchOutermost(offset);
  }

  public FoldRegion[] fetchCollapsedAt(int offset) {
    return myFoldTree.fetchCollapsedAt(offset);
  }

  @Override
  public boolean intersectsRegion(int startOffset, int endOffset) {
    return myFoldTree.intersectsRegion(startOffset, endOffset);
  }

  public FoldRegion[] fetchVisible() {
    return myFoldTree.fetchVisible();
  }

  @Override
  public int getLastCollapsedRegionBefore(int offset) {
    return myFoldTree.getLastTopLevelIndexBefore(offset);
  }

  @Override
  public TextAttributes getPlaceholderAttributes() {
    return myFoldTextAttributes;
  }

  public void flushCaretPosition() {
    for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
      caret.putUserData(SAVED_CARET_POSITION, null);
    }
  }

  void onBulkDocumentUpdateStarted() {
    myFoldTree.clearCachedValues();
  }

  void onBulkDocumentUpdateFinished() {
    myFoldTree.rebuild();
  }

  @Override
  public void beforeDocumentChange(DocumentEvent event) {
    myDocumentChangeProcessed = false;
  }

  @Override
  public void documentChanged(DocumentEvent event) {
    try {
      if (!((DocumentEx) event.getDocument()).isInBulkUpdate()) {
        updateCachedOffsets();
      }
    } finally {
      myDocumentChangeProcessed = true;
    }
  }

  @Override
  public int getPriority() {
    return EditorDocumentPriorities.FOLD_MODEL;
  }

  @Override
  public FoldRegion createFoldRegion(
      int startOffset,
      int endOffset,
      @NotNull String placeholder,
      @Nullable FoldingGroup group,
      boolean neverExpands) {
    FoldRegionImpl region =
        new FoldRegionImpl(myEditor, startOffset, endOffset, placeholder, group, neverExpands);
    LOG.assertTrue(region.isValid());
    return region;
  }

  @Override
  public void addListener(
      @NotNull final FoldingListener listener, @NotNull Disposable parentDisposable) {
    myListeners.add(listener);
    Disposer.register(
        parentDisposable,
        new Disposable() {
          @Override
          public void dispose() {
            myListeners.remove(listener);
          }
        });
  }

  private void notifyListenersOnFoldRegionStateChange(@NotNull FoldRegion foldRegion) {
    for (FoldingListener listener : myListeners) {
      listener.onFoldRegionStateChange(foldRegion);
    }
  }

  @NotNull
  @Override
  public String dumpState() {
    return Arrays.toString(myFoldTree.fetchTopLevel());
  }

  @Override
  public String toString() {
    return dumpState();
  }
}