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;
    }
  }
예제 #2
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;
  }
}
/** 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;
    }
  }
}
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;
  }
}
public abstract class PsiDocumentManagerBase extends PsiDocumentManager
    implements DocumentListener {
  static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl");
  private static final Key<Document> HARD_REF_TO_DOCUMENT =
      Key.create("HARD_REFERENCE_TO_DOCUMENT");
  private static final Key<PsiFile> HARD_REF_TO_PSI = Key.create("HARD_REFERENCE_TO_PSI");
  private static final Key<List<Runnable>> ACTION_AFTER_COMMIT = Key.create("ACTION_AFTER_COMMIT");

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

  private volatile boolean myIsCommitInProgress;
  private final PsiToDocumentSynchronizer mySynchronizer;

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

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

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

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

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

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

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

    fireFileCreated(document, psiFile);

    return psiFile;
  }

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

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

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

  @Nullable
  FileViewProvider getCachedViewProvider(@NotNull Document document) {
    final VirtualFile virtualFile = getVirtualFile(document);
    if (virtualFile == null) return null;
    return getCachedViewProvider(virtualFile);
  }

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

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

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

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

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

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

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

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

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

    return document;
  }

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

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

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

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

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

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

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

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

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

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

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

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

    return success;
  }

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

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

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

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

  @Override
  public void reparseFiles(@NotNull Collection<VirtualFile> files, boolean includeOpenFiles) {
    FileContentUtilCore.reparseFiles(files);
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  @Override
  public void beforeDocumentChange(@NotNull DocumentEvent event) {
    if (myStopTrackingDocuments) return;

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

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

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

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

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

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

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

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

  @Override
  public void documentChanged(DocumentEvent event) {
    if (myStopTrackingDocuments) return;

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

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

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

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

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

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

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

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

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

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

    myUncommittedDocuments.remove(document);

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

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

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

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

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

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

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

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

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

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

  @NotNull
  public PsiToDocumentSynchronizer getSynchronizer() {
    return mySynchronizer;
  }
}
public class JavaCompletionUtil {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.completion.JavaCompletionUtil");
  public static final Key<PairFunction<PsiExpression, CompletionParameters, PsiType>>
      DYNAMIC_TYPE_EVALUATOR = Key.create("DYNAMIC_TYPE_EVALUATOR");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    javaReference.processVariants(processor);

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

    final boolean pkgContext = inSomePackage(element);

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

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

    return set;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    final PsiManager manager = file.getManager();

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

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

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

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

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

    documentManager.commitAllDocuments();

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

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

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

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

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

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

    return newEndOffset;
  }

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

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

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

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

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

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

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

    context.commitDocument();

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

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

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

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

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

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

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

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

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

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

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

    return true;
  }

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

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

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

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

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

  public static String escapeXmlIfNeeded(InsertionContext context, String generics) {
    if (context.getFile().getViewProvider().getBaseLanguage() == StdLanguages.JSPX) {
      return StringUtil.escapeXml(generics);
    }
    return generics;
  }
}
class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.folding.impl.DocumentFoldingInfo");
  private static final Key<FoldingInfo> FOLDING_INFO_KEY = Key.create("FOLDING_INFO");

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

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

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

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

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

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

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

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

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

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

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

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

  @Override
  public void setToEditor(@NotNull final Editor editor) {
    assertDispatchThread();
    final PsiManager psiManager = PsiManager.getInstance(myProject);
    if (psiManager.isDisposed()) return;

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

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

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

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

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

      boolean state = info != null && info.expanded;
      region.setExpanded(state);
    }
  }

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

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

  @Override
  public void writeExternal(Element element) throws WriteExternalException {
    PsiDocumentManager.getInstance(myProject).commitAllDocuments();

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

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

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

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

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

      e.setAttribute(DATE_ATT, date);
      e.setAttribute(EXPANDED_ATT, Boolean.toString(state));
      String signature =
          Integer.valueOf(marker.getStartOffset()) + ":" + Integer.valueOf(marker.getEndOffset());
      e.setAttribute(SIGNATURE_ATT, signature);
      String placeHolderText = fi == null ? DEFAULT_PLACEHOLDER : fi.placeHolder;
      e.setAttribute(PLACEHOLDER_ATT, placeHolderText);
      element.addContent(e);
    }
  }

  @Override
  public void readExternal(final Element element) {
    ApplicationManager.getApplication()
        .runReadAction(
            new Runnable() {
              @Override
              public void run() {
                clear();

                if (!myFile.isValid()) return;

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

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

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

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

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

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

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

    DocumentFoldingInfo info = (DocumentFoldingInfo) o;

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

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

      FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
      FoldingInfo ofi = other.getUserData(FOLDING_INFO_KEY);
      if (!Comparing.equal(fi, ofi)) return false;
    }
    return true;
  }

  private static class FoldingInfo {
    private final String placeHolder;
    private final boolean expanded;

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

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

      FoldingInfo info = (FoldingInfo) o;

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

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

    public boolean getExpanded() {
      return expanded;
    }
  }
}
public class ProjectManagerImpl extends ProjectManagerEx
    implements NamedJDOMExternalizable, ExportableApplicationComponent {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.project.impl.ProjectManagerImpl");

  public static final int CURRENT_FORMAT_VERSION = 4;

  private static final Key<List<ProjectManagerListener>> LISTENERS_IN_PROJECT_KEY =
      Key.create("LISTENERS_IN_PROJECT_KEY");
  @NonNls private static final String ELEMENT_DEFAULT_PROJECT = "defaultProject";

  @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
  private ProjectImpl
      myDefaultProject; // Only used asynchronously in save and dispose, which itself are
  // synchronized.

  @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
  private Element
      myDefaultProjectRootElement; // Only used asynchronously in save and dispose, which itself are
  // synchronized.

  private final List<Project> myOpenProjects = new ArrayList<Project>();
  private Project[] myOpenProjectsArrayCache = {};
  private final List<ProjectManagerListener> myListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();

  private final Set<Project> myTestProjects = new THashSet<Project>();

  private final Map<VirtualFile, byte[]> mySavedCopies = new HashMap<VirtualFile, byte[]>();
  private final TObjectLongHashMap<VirtualFile> mySavedTimestamps =
      new TObjectLongHashMap<VirtualFile>();
  private final Map<Project, List<Pair<VirtualFile, StateStorage>>> myChangedProjectFiles =
      new HashMap<Project, List<Pair<VirtualFile, StateStorage>>>();
  private final Alarm myChangedFilesAlarm = new Alarm();
  private final List<Pair<VirtualFile, StateStorage>> myChangedApplicationFiles =
      new ArrayList<Pair<VirtualFile, StateStorage>>();
  private final AtomicInteger myReloadBlockCount = new AtomicInteger(0);

  @SuppressWarnings("FieldCanBeLocal")
  private final Map<Project, String> myProjects = new WeakHashMap<Project, String>();

  private static final int MAX_LEAKY_PROJECTS = 42;
  private final ProgressManager myProgressManager;
  private volatile boolean myDefaultProjectWasDisposed = false;

  @NotNull
  private static List<ProjectManagerListener> getListeners(Project project) {
    List<ProjectManagerListener> array = project.getUserData(LISTENERS_IN_PROJECT_KEY);
    if (array == null) return Collections.emptyList();
    return array;
  }

  /** @noinspection UnusedParameters */
  public ProjectManagerImpl(
      VirtualFileManager virtualFileManager,
      RecentProjectsManagerBase recentProjectsManager,
      ProgressManager progressManager) {
    myProgressManager = progressManager;
    Application app = ApplicationManager.getApplication();
    MessageBus messageBus = app.getMessageBus();
    MessageBusConnection connection = messageBus.connect(app);
    connection.subscribe(
        StateStorage.STORAGE_TOPIC,
        new StateStorage.Listener() {
          @Override
          public void storageFileChanged(
              @NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) {
            VirtualFile file = event.getFile();
            if (!file.isDirectory()
                && !(event.getRequestor() instanceof StateStorage.SaveSession)) {
              saveChangedProjectFile(file, null, storage);
            }
          }
        });
    final ProjectManagerListener busPublisher = messageBus.syncPublisher(TOPIC);

    addProjectManagerListener(
        new ProjectManagerListener() {
          @Override
          public void projectOpened(final Project project) {
            MessageBus messageBus = project.getMessageBus();
            MessageBusConnection connection = messageBus.connect(project);
            connection.subscribe(
                StateStorage.STORAGE_TOPIC,
                new StateStorage.Listener() {
                  @Override
                  public void storageFileChanged(
                      @NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) {
                    VirtualFile file = event.getFile();
                    if (!file.isDirectory()
                        && !(event.getRequestor() instanceof StateStorage.SaveSession)) {
                      saveChangedProjectFile(file, project, storage);
                    }
                  }
                });

            busPublisher.projectOpened(project);
            for (ProjectManagerListener listener : getListeners(project)) {
              listener.projectOpened(project);
            }
          }

          @Override
          public void projectClosed(Project project) {
            busPublisher.projectClosed(project);
            for (ProjectManagerListener listener : getListeners(project)) {
              listener.projectClosed(project);
            }
          }

          @Override
          public boolean canCloseProject(Project project) {
            for (ProjectManagerListener listener : getListeners(project)) {
              if (!listener.canCloseProject(project)) {
                return false;
              }
            }
            return true;
          }

          @Override
          public void projectClosing(Project project) {
            busPublisher.projectClosing(project);
            for (ProjectManagerListener listener : getListeners(project)) {
              listener.projectClosing(project);
            }
          }
        });

    registerExternalProjectFileListener(virtualFileManager);
  }

  @Override
  public void disposeComponent() {
    ApplicationManager.getApplication().assertWriteAccessAllowed();
    Disposer.dispose(myChangedFilesAlarm);
    if (myDefaultProject != null) {
      Disposer.dispose(myDefaultProject);

      myDefaultProject = null;
      myDefaultProjectWasDisposed = true;
    }
  }

  @Override
  public void initComponent() {}

  private static final boolean LOG_PROJECT_LEAKAGE_IN_TESTS = false;

  @Override
  @Nullable
  public Project newProject(
      final String projectName,
      @NotNull String filePath,
      boolean useDefaultProjectSettings,
      boolean isDummy) {
    filePath = toCanonicalName(filePath);

    //noinspection ConstantConditions
    if (LOG_PROJECT_LEAKAGE_IN_TESTS && ApplicationManager.getApplication().isUnitTestMode()) {
      for (int i = 0; i < 42; i++) {
        if (myProjects.size() < MAX_LEAKY_PROJECTS) break;
        System.gc();
        TimeoutUtil.sleep(100);
        System.gc();
      }

      if (myProjects.size() >= MAX_LEAKY_PROJECTS) {
        List<Project> copy = new ArrayList<Project>(myProjects.keySet());
        myProjects.clear();
        throw new TooManyProjectLeakedException(copy);
      }
    }

    ProjectImpl project =
        createProject(
            projectName, filePath, false, ApplicationManager.getApplication().isUnitTestMode());
    try {
      initProject(project, useDefaultProjectSettings ? (ProjectImpl) getDefaultProject() : null);
      if (LOG_PROJECT_LEAKAGE_IN_TESTS) {
        myProjects.put(project, null);
      }
      return project;
    } catch (final Exception e) {
      LOG.info(e);
      Messages.showErrorDialog(message(e), ProjectBundle.message("project.load.default.error"));
    }
    return null;
  }

  @NonNls
  private static String message(Throwable e) {
    String message = e.getMessage();
    if (message != null) return message;
    message = e.getLocalizedMessage();
    if (message != null) return message;
    message = e.toString();
    Throwable cause = e.getCause();
    if (cause != null) {
      String causeMessage = message(cause);
      return message + " (cause: " + causeMessage + ")";
    }

    return message;
  }

  private void initProject(@NotNull ProjectImpl project, @Nullable ProjectImpl template)
      throws IOException {
    final ProgressIndicator indicator = myProgressManager.getProgressIndicator();
    if (indicator != null && !project.isDefault()) {
      indicator.setText(ProjectBundle.message("loading.components.for", project.getName()));
      indicator.setIndeterminate(true);
    }

    ApplicationManager.getApplication()
        .getMessageBus()
        .syncPublisher(ProjectLifecycleListener.TOPIC)
        .beforeProjectLoaded(project);

    try {
      if (template != null) {
        project.getStateStore().loadProjectFromTemplate(template);
      } else {
        project.getStateStore().load();
      }
      project.loadProjectComponents();
      project.init();
    } catch (IOException e) {
      scheduleDispose(project);
      throw e;
    } catch (ProcessCanceledException e) {
      scheduleDispose(project);
      throw e;
    }
  }

  private ProjectImpl createProject(
      @Nullable String projectName,
      @NotNull String filePath,
      boolean isDefault,
      boolean isOptimiseTestLoadSpeed) {
    return isDefault
        ? new DefaultProject(this, "", isOptimiseTestLoadSpeed)
        : new ProjectImpl(
            this, new File(filePath).getAbsolutePath(), isOptimiseTestLoadSpeed, projectName);
  }

  private static void scheduleDispose(final ProjectImpl project) {
    if (project.isDefault()) {
      return;
    }

    ApplicationManager.getApplication()
        .invokeLater(
            new Runnable() {
              @Override
              public void run() {
                ApplicationManager.getApplication()
                    .runWriteAction(
                        new Runnable() {
                          @Override
                          public void run() {
                            if (!project.isDisposed()) {
                              Disposer.dispose(project);
                            }
                          }
                        });
              }
            });
  }

  @Override
  @Nullable
  public Project loadProject(@NotNull String filePath)
      throws IOException, JDOMException, InvalidDataException {
    try {
      ProjectImpl project = createProject(null, filePath, false, false);
      initProject(project, null);
      return project;
    } catch (StateStorageException e) {
      throw new IOException(e.getMessage());
    }
  }

  @NotNull
  private static String toCanonicalName(@NotNull final String filePath) {
    try {
      return FileUtil.resolveShortWindowsName(filePath);
    } catch (IOException e) {
      // OK. File does not yet exist so it's canonical path will be equal to its original path.
    }

    return filePath;
  }

  @TestOnly
  public synchronized boolean isDefaultProjectInitialized() {
    return myDefaultProject != null;
  }

  @Override
  @NotNull
  public synchronized Project getDefaultProject() {
    LOG.assertTrue(!myDefaultProjectWasDisposed, "Default project has been already disposed!");
    if (myDefaultProject == null) {
      try {
        myDefaultProject =
            createProject(null, "", true, ApplicationManager.getApplication().isUnitTestMode());
        initProject(myDefaultProject, null);
        myDefaultProjectRootElement = null;
      } catch (IOException e) {
        LOG.error(e);
      } catch (StateStorageException e) {
        LOG.error(e);
      }
    }
    return myDefaultProject;
  }

  public Element getDefaultProjectRootElement() {
    return myDefaultProjectRootElement;
  }

  @Override
  @NotNull
  public Project[] getOpenProjects() {
    synchronized (myOpenProjects) {
      if (myOpenProjectsArrayCache.length != myOpenProjects.size()) {
        LOG.error(
            "Open projects: "
                + myOpenProjects
                + "; cache: "
                + Arrays.asList(myOpenProjectsArrayCache));
      }
      if (myOpenProjectsArrayCache.length > 0
          && myOpenProjectsArrayCache[0] != myOpenProjects.get(0)) {
        LOG.error(
            "Open projects cache corrupted. Open projects: "
                + myOpenProjects
                + "; cache: "
                + Arrays.asList(myOpenProjectsArrayCache));
      }
      if (ApplicationManager.getApplication().isUnitTestMode()) {
        Project[] testProjects = myTestProjects.toArray(new Project[myTestProjects.size()]);
        for (Project testProject : testProjects) {
          assert !testProject.isDisposed() : testProject;
        }
        return ArrayUtil.mergeArrays(myOpenProjectsArrayCache, testProjects);
      }
      return myOpenProjectsArrayCache;
    }
  }

  @Override
  public boolean isProjectOpened(Project project) {
    synchronized (myOpenProjects) {
      return ApplicationManager.getApplication().isUnitTestMode()
              && myTestProjects.contains(project)
          || myOpenProjects.contains(project);
    }
  }

  @Override
  public boolean openProject(final Project project) {
    if (isLight(project)) {
      throw new AssertionError("must not open light project");
    }
    final Application application = ApplicationManager.getApplication();

    if (!application.isUnitTestMode() && !((ProjectEx) project).getStateStore().checkVersion()) {
      return false;
    }

    synchronized (myOpenProjects) {
      if (myOpenProjects.contains(project)) {
        return false;
      }
      myOpenProjects.add(project);
      cacheOpenProjects();
    }
    fireProjectOpened(project);

    final StartupManagerImpl startupManager =
        (StartupManagerImpl) StartupManager.getInstance(project);
    waitForFileWatcher(project);
    boolean ok =
        myProgressManager.runProcessWithProgressSynchronously(
            new Runnable() {
              @Override
              public void run() {
                startupManager.runStartupActivities();

                // dumb mode should start before post-startup activities
                // only when startCacheUpdate is called from UI thread, we can guarantee that
                // when the method returns, the application has entered dumb mode
                UIUtil.invokeAndWaitIfNeeded(
                    new Runnable() {
                      @Override
                      public void run() {
                        startupManager.startCacheUpdate();
                      }
                    });

                startupManager.runPostStartupActivitiesFromExtensions();

                UIUtil.invokeLaterIfNeeded(
                    new Runnable() {
                      @Override
                      public void run() {
                        startupManager.runPostStartupActivities();
                      }
                    });
              }
            },
            ProjectBundle.message("project.load.progress"),
            true,
            project);

    if (!ok) {
      closeProject(project, false, false, true);
      notifyProjectOpenFailed();
      return false;
    }

    if (!application.isHeadlessEnvironment() && !application.isUnitTestMode()) {
      // should be invoked last
      startupManager.runWhenProjectIsInitialized(
          new Runnable() {
            @Override
            public void run() {
              final TrackingPathMacroSubstitutor macroSubstitutor =
                  ((ProjectEx) project)
                      .getStateStore()
                      .getStateStorageManager()
                      .getMacroSubstitutor();
              if (macroSubstitutor != null) {
                StorageUtil.notifyUnknownMacros(macroSubstitutor, project, null);
              }
            }
          });
    }

    return true;
  }

  private void cacheOpenProjects() {
    myOpenProjectsArrayCache = myOpenProjects.toArray(new Project[myOpenProjects.size()]);
  }

  private void waitForFileWatcher(@NotNull Project project) {
    LocalFileSystem fs = LocalFileSystem.getInstance();
    if (!(fs instanceof LocalFileSystemImpl)) return;

    final FileWatcher watcher = ((LocalFileSystemImpl) fs).getFileWatcher();
    if (!watcher.isOperational() || !watcher.isSettingRoots()) return;

    LOG.info("FW/roots waiting started");
    Task.Modal task =
        new Task.Modal(project, ProjectBundle.message("project.load.progress"), true) {
          @Override
          public void run(@NotNull ProgressIndicator indicator) {
            indicator.setIndeterminate(true);
            indicator.setText(ProjectBundle.message("project.load.waiting.watcher"));
            if (indicator instanceof ProgressWindow) {
              ((ProgressWindow) indicator).setCancelButtonText(CommonBundle.message("button.skip"));
            }
            while (watcher.isSettingRoots() && !indicator.isCanceled()) {
              TimeoutUtil.sleep(10);
            }
            LOG.info("FW/roots waiting finished");
          }
        };
    myProgressManager.run(task);
  }

  @Override
  public Project loadAndOpenProject(@NotNull final String filePath) throws IOException {
    final Project project = convertAndLoadProject(filePath);
    if (project == null) {
      WelcomeFrame.showIfNoProjectOpened();
      return null;
    }

    // todo unify this logic with PlatformProjectOpenProcessor
    if (!openProject(project)) {
      WelcomeFrame.showIfNoProjectOpened();
      ApplicationManager.getApplication()
          .runWriteAction(
              new Runnable() {
                @Override
                public void run() {
                  Disposer.dispose(project);
                }
              });
    }

    return project;
  }

  /**
   * Converts and loads the project at the specified path.
   *
   * @param filePath the path to open the project.
   * @return the project, or null if the user has cancelled opening the project.
   */
  @Override
  @Nullable
  public Project convertAndLoadProject(String filePath) throws IOException {
    final String fp = toCanonicalName(filePath);
    final ConversionResult conversionResult = ConversionService.getInstance().convert(fp);
    if (conversionResult.openingIsCanceled()) {
      return null;
    }

    final Project project = loadProjectWithProgress(filePath);
    if (project == null) return null;

    if (!conversionResult.conversionNotNeeded()) {
      StartupManager.getInstance(project)
          .registerPostStartupActivity(
              new Runnable() {
                @Override
                public void run() {
                  conversionResult.postStartupActivity(project);
                }
              });
    }
    return project;
  }

  /**
   * Opens the project at the specified path.
   *
   * @param filePath the path to open the project.
   * @return the project, or null if the user has cancelled opening the project.
   */
  @Nullable
  private Project loadProjectWithProgress(@NotNull final String filePath) throws IOException {
    final ProjectImpl project = createProject(null, toCanonicalName(filePath), false, false);
    try {
      myProgressManager.runProcessWithProgressSynchronously(
          new ThrowableComputable<Project, IOException>() {
            @Override
            @Nullable
            public Project compute() throws IOException {
              initProject(project, null);
              return project;
            }
          },
          ProjectBundle.message("project.load.progress"),
          true,
          project);
    } catch (StateStorageException e) {
      throw new IOException(e);
    } catch (ProcessCanceledException ignore) {
      return null;
    }

    return project;
  }

  private static void notifyProjectOpenFailed() {
    ApplicationManager.getApplication()
        .getMessageBus()
        .syncPublisher(AppLifecycleListener.TOPIC)
        .projectOpenFailed();
    WelcomeFrame.showIfNoProjectOpened();
  }

  private void registerExternalProjectFileListener(VirtualFileManager virtualFileManager) {
    virtualFileManager.addVirtualFileManagerListener(
        new VirtualFileManagerListener() {
          @Override
          public void beforeRefreshStart(boolean asynchronous) {}

          @Override
          public void afterRefreshFinish(boolean asynchronous) {
            scheduleReloadApplicationAndProject();
          }
        });
  }

  private void askToReloadProjectIfConfigFilesChangedExternally() {
    LOG.debug("[RELOAD] myReloadBlockCount = " + myReloadBlockCount.get());
    if (myReloadBlockCount.get() == 0) {
      Set<Project> projects;

      synchronized (myChangedProjectFiles) {
        if (myChangedProjectFiles.isEmpty()) return;
        projects = new HashSet<Project>(myChangedProjectFiles.keySet());
      }

      List<Project> projectsToReload = new ArrayList<Project>();

      for (Project project : projects) {
        if (shouldReloadProject(project)) {
          projectsToReload.add(project);
        }
      }

      for (final Project projectToReload : projectsToReload) {
        reloadProjectImpl(projectToReload, false);
      }
    }
  }

  private boolean tryToReloadApplication() {
    try {
      final Application app = ApplicationManager.getApplication();

      if (app.isDisposed()) return false;
      final HashSet<Pair<VirtualFile, StateStorage>> causes =
          new HashSet<Pair<VirtualFile, StateStorage>>(myChangedApplicationFiles);
      if (causes.isEmpty()) return true;

      final boolean[] reloadOk = {false};
      final LinkedHashSet<String> components = new LinkedHashSet<String>();

      ApplicationManager.getApplication()
          .runWriteAction(
              new Runnable() {
                @Override
                public void run() {
                  try {
                    reloadOk[0] =
                        ((ApplicationImpl) app).getStateStore().reload(causes, components);
                  } catch (StateStorageException e) {
                    Messages.showWarningDialog(
                        ProjectBundle.message("project.reload.failed", e.getMessage()),
                        ProjectBundle.message("project.reload.failed.title"));
                  } catch (IOException e) {
                    Messages.showWarningDialog(
                        ProjectBundle.message("project.reload.failed", e.getMessage()),
                        ProjectBundle.message("project.reload.failed.title"));
                  }
                }
              });

      if (!reloadOk[0] && !components.isEmpty()) {
        String message = "Application components were changed externally and cannot be reloaded:\n";
        for (String component : components) {
          message += component + "\n";
        }

        final boolean canRestart = ApplicationManager.getApplication().isRestartCapable();
        message += "Would you like to " + (canRestart ? "restart " : "shutdown ");
        message += ApplicationNamesInfo.getInstance().getProductName() + "?";

        if (Messages.showYesNoDialog(
                message, "Application Configuration Reload", Messages.getQuestionIcon())
            == Messages.YES) {
          for (Pair<VirtualFile, StateStorage> cause : causes) {
            StateStorage stateStorage = cause.getSecond();
            if (stateStorage instanceof XmlElementStorage) {
              ((XmlElementStorage) stateStorage).disableSaving();
            }
          }
          ApplicationManagerEx.getApplicationEx().restart(true);
        }
      }

      return reloadOk[0];
    } finally {
      myChangedApplicationFiles.clear();
    }
  }

  private boolean shouldReloadProject(final Project project) {
    if (project.isDisposed()) return false;
    final HashSet<Pair<VirtualFile, StateStorage>> causes =
        new HashSet<Pair<VirtualFile, StateStorage>>();

    synchronized (myChangedProjectFiles) {
      final List<Pair<VirtualFile, StateStorage>> changes = myChangedProjectFiles.remove(project);
      if (changes != null) {
        causes.addAll(changes);
      }

      if (causes.isEmpty()) return false;
    }

    final boolean[] reloadOk = {false};

    ApplicationManager.getApplication()
        .runWriteAction(
            new Runnable() {
              @Override
              public void run() {
                try {
                  LOG.debug("[RELOAD] Reloading project/components...");
                  reloadOk[0] = ((ProjectEx) project).getStateStore().reload(causes);
                } catch (StateStorageException e) {
                  Messages.showWarningDialog(
                      ProjectBundle.message("project.reload.failed", e.getMessage()),
                      ProjectBundle.message("project.reload.failed.title"));
                } catch (IOException e) {
                  Messages.showWarningDialog(
                      ProjectBundle.message("project.reload.failed", e.getMessage()),
                      ProjectBundle.message("project.reload.failed.title"));
                }
              }
            });
    if (reloadOk[0]) return false;

    String message;
    if (causes.size() == 1) {
      message =
          ProjectBundle.message(
              "project.reload.external.change.single",
              causes.iterator().next().first.getPresentableUrl());
    } else {
      StringBuilder filesBuilder = new StringBuilder();
      boolean first = true;
      Set<String> alreadyShown = new HashSet<String>();
      for (Pair<VirtualFile, StateStorage> cause : causes) {
        String url = cause.first.getPresentableUrl();
        if (!alreadyShown.contains(url)) {
          if (alreadyShown.size() > 10) {
            filesBuilder
                .append("\n" + "and ")
                .append(causes.size() - alreadyShown.size())
                .append(" more");
            break;
          }
          if (!first) filesBuilder.append("\n");
          first = false;
          filesBuilder.append(url);
          alreadyShown.add(url);
        }
      }
      message =
          ProjectBundle.message("project.reload.external.change.multiple", filesBuilder.toString());
    }

    return Messages.showTwoStepConfirmationDialog(
            message,
            ProjectBundle.message("project.reload.external.change.title"),
            "Reload project",
            Messages.getQuestionIcon())
        == 0;
  }

  @Override
  public boolean isFileSavedToBeReloaded(VirtualFile candidate) {
    return mySavedCopies.containsKey(candidate);
  }

  @Override
  public void blockReloadingProjectOnExternalChanges() {
    myReloadBlockCount.incrementAndGet();
  }

  @Override
  public void unblockReloadingProjectOnExternalChanges() {
    if (myReloadBlockCount.decrementAndGet() == 0) scheduleReloadApplicationAndProject();
  }

  private void scheduleReloadApplicationAndProject() {
    // todo: commented due to "IDEA-61938 Libraries configuration is kept if switching branches"
    // because of save which may happen _before_ project reload ;(

    // ApplicationManager.getApplication().invokeLater(new Runnable() {
    //  public void run() {
    // IdeEventQueue.getInstance().addIdleListener(new Runnable() {
    //  @Override
    //  public void run() {
    //    IdeEventQueue.getInstance().removeIdleListener(this);
    ApplicationManager.getApplication()
        .invokeLater(
            new Runnable() {
              @Override
              public void run() {
                if (!tryToReloadApplication()) return;
                askToReloadProjectIfConfigFilesChangedExternally();
              }
            },
            ModalityState.NON_MODAL);
    // }
    // }, 2000);
    // }
    // }, ModalityState.NON_MODAL);
  }

  @Override
  public void openTestProject(@NotNull final Project project) {
    synchronized (myOpenProjects) {
      assert ApplicationManager.getApplication().isUnitTestMode();
      assert !project.isDisposed() : "Must not open already disposed project";
      myTestProjects.add(project);
    }
  }

  @Override
  public Collection<Project> closeTestProject(@NotNull Project project) {
    synchronized (myOpenProjects) {
      assert ApplicationManager.getApplication().isUnitTestMode();
      myTestProjects.remove(project);
      return myTestProjects;
    }
  }

  @Override
  public void saveChangedProjectFile(final VirtualFile file, final Project project) {
    if (file.exists()) {
      copyToTemp(file);
    }
    registerProjectToReload(project, file, null);
  }

  private void saveChangedProjectFile(
      final VirtualFile file, @Nullable final Project project, final StateStorage storage) {
    if (file.exists()) {
      copyToTemp(file);
    }
    registerProjectToReload(project, file, storage);
  }

  private void registerProjectToReload(
      @Nullable final Project project,
      final VirtualFile cause,
      @Nullable final StateStorage storage) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("[RELOAD] Registering project to reload: " + cause, new Exception());
    }

    if (project != null) {
      synchronized (myChangedProjectFiles) {
        List<Pair<VirtualFile, StateStorage>> changedProjectFiles =
            myChangedProjectFiles.get(project);
        if (changedProjectFiles == null) {
          changedProjectFiles = new ArrayList<Pair<VirtualFile, StateStorage>>();
          myChangedProjectFiles.put(project, changedProjectFiles);
        }

        changedProjectFiles.add(new Pair<VirtualFile, StateStorage>(cause, storage));
      }
    } else {
      myChangedApplicationFiles.add(new Pair<VirtualFile, StateStorage>(cause, storage));
    }

    myChangedFilesAlarm.cancelAllRequests();
    myChangedFilesAlarm.addRequest(
        new Runnable() {
          @Override
          public void run() {
            LOG.debug(
                "[RELOAD] Scheduling reload application & project, myReloadBlockCount = "
                    + myReloadBlockCount);
            if (myReloadBlockCount.get() == 0) {
              scheduleReloadApplicationAndProject();
            }
          }
        },
        444);
  }

  private void copyToTemp(VirtualFile file) {
    try {
      final byte[] bytes = file.contentsToByteArray();
      mySavedCopies.put(file, bytes);
      mySavedTimestamps.put(file, file.getTimeStamp());
    } catch (IOException e) {
      LOG.error(e);
    }
  }

  private void restoreCopy(VirtualFile file) {
    try {
      if (file == null) return; // Externally deleted actually.
      if (!file.isWritable()) return; // IDEA was unable to save it as well. So no need to restore.

      final byte[] bytes = mySavedCopies.get(file);
      if (bytes != null) {
        try {
          file.setBinaryContent(bytes, -1, mySavedTimestamps.get(file));
        } catch (IOException e) {
          Messages.showWarningDialog(
              ProjectBundle.message("project.reload.write.failed", file.getPresentableUrl()),
              ProjectBundle.message("project.reload.write.failed.title"));
        }
      }
    } finally {
      mySavedCopies.remove(file);
      mySavedTimestamps.remove(file);
    }
  }

  @Override
  public void reloadProject(@NotNull final Project p) {
    reloadProjectImpl(p, true);
  }

  public void reloadProjectImpl(@NotNull final Project p, final boolean clearCopyToRestore) {
    if (clearCopyToRestore) {
      mySavedCopies.clear();
      mySavedTimestamps.clear();
    }

    final Project[] project = {p};

    ProjectReloadState.getInstance(project[0]).onBeforeAutomaticProjectReload();
    final Application application = ApplicationManager.getApplication();

    application.invokeLater(
        new Runnable() {
          @Override
          public void run() {
            LOG.debug("Reloading project.");
            ProjectImpl projectImpl = (ProjectImpl) project[0];
            if (projectImpl.isDisposed()) return;
            IProjectStore projectStore = projectImpl.getStateStore();
            final String location = projectImpl.getPresentableUrl();

            final List<IFile> original;
            try {
              final IComponentStore.SaveSession saveSession = projectStore.startSave();
              original = saveSession.getAllStorageFiles(true);
              saveSession.finishSave();
            } catch (IOException e) {
              LOG.error(e);
              return;
            }

            if (project[0].isDisposed() || ProjectUtil.closeAndDispose(project[0])) {
              application.runWriteAction(
                  new Runnable() {
                    @Override
                    public void run() {
                      for (final IFile originalFile : original) {
                        restoreCopy(
                            LocalFileSystem.getInstance().refreshAndFindFileByIoFile(originalFile));
                      }
                    }
                  });

              project[0] = null; // Let it go.

              ProjectUtil.openProject(location, null, true);
            }
          }
        },
        ModalityState.NON_MODAL);
  }

  @Override
  public boolean closeProject(@NotNull final Project project) {
    return closeProject(project, true, false, true);
  }

  public boolean closeProject(
      @NotNull final Project project,
      final boolean save,
      final boolean dispose,
      boolean checkCanClose) {
    if (isLight(project)) {
      throw new AssertionError("must not close light project");
    }
    if (!isProjectOpened(project)) return true;
    if (checkCanClose && !canClose(project)) return false;
    final ShutDownTracker shutDownTracker = ShutDownTracker.getInstance();
    shutDownTracker.registerStopperThread(Thread.currentThread());
    try {
      if (save) {
        FileDocumentManager.getInstance().saveAllDocuments();
        project.save();
      }

      if (checkCanClose && !ensureCouldCloseIfUnableToSave(project)) {
        return false;
      }

      fireProjectClosing(project); // somebody can start progress here, do not wrap in write action

      ApplicationManager.getApplication()
          .runWriteAction(
              new Runnable() {
                @Override
                public void run() {
                  synchronized (myOpenProjects) {
                    myOpenProjects.remove(project);
                    cacheOpenProjects();
                    myTestProjects.remove(project);
                  }

                  myChangedProjectFiles.remove(project);

                  fireProjectClosed(project);

                  if (dispose) {
                    Disposer.dispose(project);
                  }
                }
              });
    } finally {
      shutDownTracker.unregisterStopperThread(Thread.currentThread());
    }

    return true;
  }

  public static boolean isLight(@NotNull Project project) {
    return ApplicationManager.getApplication().isUnitTestMode()
        && project.toString().contains("light_temp_");
  }

  @Override
  public boolean closeAndDispose(@NotNull final Project project) {
    return closeProject(project, true, true, true);
  }

  private void fireProjectClosing(Project project) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("enter: fireProjectClosing()");
    }

    for (ProjectManagerListener listener : myListeners) {
      try {
        listener.projectClosing(project);
      } catch (Exception e) {
        LOG.error(e);
      }
    }
  }

  @Override
  public void addProjectManagerListener(@NotNull ProjectManagerListener listener) {
    myListeners.add(listener);
  }

  @Override
  public void addProjectManagerListener(
      @NotNull final ProjectManagerListener listener, @NotNull Disposable parentDisposable) {
    addProjectManagerListener(listener);
    Disposer.register(
        parentDisposable,
        new Disposable() {
          @Override
          public void dispose() {
            removeProjectManagerListener(listener);
          }
        });
  }

  @Override
  public void removeProjectManagerListener(@NotNull ProjectManagerListener listener) {
    boolean removed = myListeners.remove(listener);
    LOG.assertTrue(removed);
  }

  @Override
  public void addProjectManagerListener(
      @NotNull Project project, @NotNull ProjectManagerListener listener) {
    List<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY);
    if (listeners == null) {
      listeners =
          ((UserDataHolderEx) project)
              .putUserDataIfAbsent(
                  LISTENERS_IN_PROJECT_KEY,
                  ContainerUtil.<ProjectManagerListener>createLockFreeCopyOnWriteList());
    }
    listeners.add(listener);
  }

  @Override
  public void removeProjectManagerListener(
      @NotNull Project project, @NotNull ProjectManagerListener listener) {
    List<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY);
    LOG.assertTrue(listeners != null);
    boolean removed = listeners.remove(listener);
    LOG.assertTrue(removed);
  }

  private void fireProjectOpened(Project project) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("projectOpened");
    }

    for (ProjectManagerListener listener : myListeners) {
      try {
        listener.projectOpened(project);
      } catch (Exception e) {
        LOG.error(e);
      }
    }
  }

  private void fireProjectClosed(Project project) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("projectClosed");
    }

    for (ProjectManagerListener listener : myListeners) {
      try {
        listener.projectClosed(project);
      } catch (Exception e) {
        LOG.error(e);
      }
    }
  }

  @Override
  public boolean canClose(Project project) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("enter: canClose()");
    }

    for (ProjectManagerListener listener : myListeners) {
      try {
        if (!listener.canCloseProject(project)) return false;
      } catch (Throwable e) {
        LOG.warn(e); // DO NOT LET ANY PLUGIN to prevent closing due to exception
      }
    }

    return true;
  }

  private static boolean ensureCouldCloseIfUnableToSave(@NotNull final Project project) {
    final ProjectImpl.UnableToSaveProjectNotification[] notifications =
        NotificationsManager.getNotificationsManager()
            .getNotificationsOfType(ProjectImpl.UnableToSaveProjectNotification.class, project);
    if (notifications.length == 0) return true;

    final String fileNames = StringUtil.join(notifications[0].getFileNames(), "\n");

    final String msg =
        String.format(
            "%s was unable to save some project files,\nare you sure you want to close this project anyway?",
            ApplicationNamesInfo.getInstance().getProductName());
    return Messages.showDialog(
            project,
            msg,
            "Unsaved Project",
            "Read-only files:\n\n" + fileNames,
            new String[] {"Yes", "No"},
            0,
            1,
            Messages.getWarningIcon())
        == 0;
  }

  @Override
  public void writeExternal(Element parentNode) throws WriteExternalException {
    if (myDefaultProject != null) {
      myDefaultProject.save();
    }

    if (myDefaultProjectRootElement
        == null) { // read external isn't called if config folder is absent
      myDefaultProjectRootElement = new Element(ELEMENT_DEFAULT_PROJECT);
    }

    myDefaultProjectRootElement.detach();
    parentNode.addContent(myDefaultProjectRootElement);
  }

  public void setDefaultProjectRootElement(final Element defaultProjectRootElement) {
    myDefaultProjectRootElement = defaultProjectRootElement;
  }

  @Override
  public void readExternal(Element parentNode) throws InvalidDataException {
    myDefaultProjectRootElement = parentNode.getChild(ELEMENT_DEFAULT_PROJECT);

    if (myDefaultProjectRootElement == null) {
      myDefaultProjectRootElement = new Element(ELEMENT_DEFAULT_PROJECT);
    }

    myDefaultProjectRootElement.detach();
  }

  @Override
  public String getExternalFileName() {
    return "project.default";
  }

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

  @Override
  @NotNull
  public File[] getExportFiles() {
    return new File[] {PathManager.getOptionsFile(this)};
  }

  @Override
  @NotNull
  public String getPresentableName() {
    return ProjectBundle.message("project.default.settings");
  }
}
/** This class also controls the auto-reparse and auto-hints. */
public class DaemonCodeAnalyzerImpl extends DaemonCodeAnalyzer
    implements JDOMExternalizable, ProjectComponent {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl");

  private static final Key<List<LineMarkerInfo>> MARKERS_IN_EDITOR_DOCUMENT_KEY =
      Key.create("MARKERS_IN_EDITOR_DOCUMENT");
  private final Project myProject;
  private final DaemonCodeAnalyzerSettings mySettings;
  private final EditorTracker myEditorTracker;
  private DaemonProgressIndicator myUpdateProgress =
      new DaemonProgressIndicator(); // guarded by this

  private final Runnable myUpdateRunnable = createUpdateRunnable();

  private final Alarm myAlarm = new Alarm();
  private boolean myUpdateByTimerEnabled = true;
  private final Collection<VirtualFile> myDisabledHintsFiles = new THashSet<VirtualFile>();
  private final Collection<PsiFile> myDisabledHighlightingFiles = new THashSet<PsiFile>();

  private final FileStatusMap myFileStatusMap;
  private DaemonCodeAnalyzerSettings myLastSettings;

  private IntentionHintComponent myLastIntentionHint; // guarded by this
  private volatile boolean myDisposed; // the only possible transition: false -> true
  private volatile boolean myInitialized; // the only possible transition: false -> true

  @NonNls private static final String DISABLE_HINTS_TAG = "disable_hints";
  @NonNls private static final String FILE_TAG = "file";
  @NonNls private static final String URL_ATT = "url";
  private DaemonListeners myDaemonListeners;
  private final PassExecutorService myPassExecutorService;
  private int myModificationCount = 0;

  private volatile boolean allowToInterrupt = true;
  private StatusBarUpdater myStatusBarUpdater;

  public DaemonCodeAnalyzerImpl(
      Project project,
      DaemonCodeAnalyzerSettings daemonCodeAnalyzerSettings,
      EditorTracker editorTracker) {
    myProject = project;

    mySettings = daemonCodeAnalyzerSettings;
    myEditorTracker = editorTracker;
    myLastSettings = (DaemonCodeAnalyzerSettings) mySettings.clone();

    myFileStatusMap = new FileStatusMap(myProject);
    myPassExecutorService =
        new PassExecutorService(myProject) {
          protected void afterApplyInformationToEditor(
              final TextEditorHighlightingPass pass,
              final FileEditor fileEditor,
              final ProgressIndicator updateProgress) {
            if (fileEditor instanceof TextEditor) {
              log(updateProgress, pass, "Apply ");
              Editor editor = ((TextEditor) fileEditor).getEditor();
              repaintErrorStripeRenderer(editor);
            }
          }

          protected boolean isDisposed() {
            return myDisposed || super.isDisposed();
          }
        };
    Disposer.register(project, myPassExecutorService);
    Disposer.register(project, myFileStatusMap);
  }

  static boolean hasErrors(Project project, Document document) {
    return !processHighlights(
        document,
        project,
        HighlightSeverity.ERROR,
        0,
        document.getTextLength(),
        CommonProcessors.<HighlightInfo>alwaysFalse());
  }

  @NotNull
  @TestOnly
  public static List<HighlightInfo> getHighlights(
      Document document, HighlightSeverity minSeverity, Project project) {
    List<HighlightInfo> infos = new ArrayList<HighlightInfo>();
    processHighlights(
        document,
        project,
        minSeverity,
        0,
        document.getTextLength(),
        new CommonProcessors.CollectProcessor<HighlightInfo>(infos));
    return infos;
  }

  public List<HighlightInfo> runMainPasses(
      @NotNull PsiFile psiFile,
      @NotNull Document document,
      @NotNull final ProgressIndicator progress) {
    final List<HighlightInfo> result = new ArrayList<HighlightInfo>();
    final VirtualFile virtualFile = psiFile.getVirtualFile();
    if (virtualFile != null && !virtualFile.getFileType().isBinary()) {

      final List<TextEditorHighlightingPass> passes =
          TextEditorHighlightingPassRegistrarEx.getInstanceEx(myProject)
              .instantiateMainPasses(psiFile, document);

      Collections.sort(
          passes,
          new Comparator<TextEditorHighlightingPass>() {
            @Override
            public int compare(TextEditorHighlightingPass o1, TextEditorHighlightingPass o2) {
              if (o1 instanceof GeneralHighlightingPass) return -1;
              if (o2 instanceof GeneralHighlightingPass) return 1;
              return 0;
            }
          });

      for (TextEditorHighlightingPass pass : passes) {
        pass.doCollectInformation(progress);
        result.addAll(pass.getInfos());
      }
    }

    return result;
  }

  @TestOnly
  public List<HighlightInfo> runPasses(
      @NotNull PsiFile file,
      @NotNull Document document,
      @NotNull TextEditor textEditor,
      @NotNull int[] toIgnore,
      boolean canChangeDocument,
      @Nullable Runnable callbackWhileWaiting) {
    assert myInitialized;
    assert !myDisposed;
    Application application = ApplicationManager.getApplication();
    application.assertIsDispatchThread();
    assert !application.isWriteAccessAllowed();

    // pump first so that queued event do not interfere
    UIUtil.dispatchAllInvocationEvents();
    UIUtil.dispatchAllInvocationEvents();

    Project project = file.getProject();
    setUpdateByTimerEnabled(false);
    FileStatusMap fileStatusMap = getFileStatusMap();
    for (int ignoreId : toIgnore) {
      fileStatusMap.markFileUpToDate(document, ignoreId);
    }
    fileStatusMap.allowDirt(canChangeDocument);

    TextEditorBackgroundHighlighter highlighter =
        (TextEditorBackgroundHighlighter) textEditor.getBackgroundHighlighter();
    final List<TextEditorHighlightingPass> passes = highlighter.getPasses(toIgnore);
    HighlightingPass[] array = passes.toArray(new HighlightingPass[passes.size()]);

    final DaemonProgressIndicator progress = createUpdateProgress();
    progress.setDebug(LOG.isDebugEnabled());
    myPassExecutorService.submitPasses(
        Collections.singletonMap((FileEditor) textEditor, array), progress, Job.DEFAULT_PRIORITY);
    try {
      while (progress.isRunning()) {
        try {
          if (progress.isCanceled() && progress.isRunning()) {
            // write action sneaked in the AWT. restart
            waitForTermination();
            Throwable savedException = PassExecutorService.getSavedException(progress);
            if (savedException != null) throw savedException;
            return runPasses(
                file, document, textEditor, toIgnore, canChangeDocument, callbackWhileWaiting);
          }
          if (callbackWhileWaiting != null) {
            callbackWhileWaiting.run();
          }
          progress.waitFor(100);
          UIUtil.dispatchAllInvocationEvents();
          Throwable savedException = PassExecutorService.getSavedException(progress);
          if (savedException != null) throw savedException;
        } catch (RuntimeException e) {
          throw e;
        } catch (Error e) {
          e.printStackTrace();
          throw e;
        } catch (Throwable e) {
          e.printStackTrace();
          throw new RuntimeException(e);
        }
      }
      UIUtil.dispatchAllInvocationEvents();
      UIUtil.dispatchAllInvocationEvents();

      return getHighlights(document, null, project);
    } finally {
      fileStatusMap.allowDirt(true);
      waitForTermination();
    }
  }

  @TestOnly
  public void prepareForTest() {
    if (!myInitialized) {
      projectOpened();
    }
    setUpdateByTimerEnabled(false);
    waitForTermination();
  }

  @TestOnly
  public void cleanupAfterTest(boolean dispose) {
    if (!myProject.isOpen()) return;
    stopProcess(false);
    if (dispose) {
      projectClosed();
      Disposer.dispose(myStatusBarUpdater);
      myStatusBarUpdater = null;
      Disposer.dispose(myDaemonListeners);
      myDaemonListeners = null;
    }
    setUpdateByTimerEnabled(false);
    waitForTermination();
  }

  private void waitForTermination() {
    myPassExecutorService.cancelAll(true);
  }

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

  public void initComponent() {}

  public void disposeComponent() {}

  @Override
  public void projectOpened() {
    assert !myInitialized : "Double Initializing";
    myStatusBarUpdater = new StatusBarUpdater(myProject);
    Disposer.register(myProject, myStatusBarUpdater);

    myDaemonListeners = new DaemonListeners(myProject, this, myEditorTracker);
    Disposer.register(myProject, myDaemonListeners);
    reloadScopes();

    myInitialized = true;
    myDisposed = false;
    myFileStatusMap.markAllFilesDirty();
  }

  public void projectClosed() {
    assert myInitialized : "Disposing not initialized component";
    assert !myDisposed : "Double dispose";

    stopProcess(false);

    myDisposed = true;
    myLastSettings = null;
  }

  void repaintErrorStripeRenderer(Editor editor) {
    if (!myProject.isInitialized()) return;
    final Document document = editor.getDocument();
    final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
    final EditorMarkupModel markup = (EditorMarkupModel) editor.getMarkupModel();
    markup.setErrorPanelPopupHandler(new DaemonEditorPopup(psiFile));
    markup.setErrorStripTooltipRendererProvider(new DaemonTooltipRendererProvider(myProject));
    markup.setMinMarkHeight(DaemonCodeAnalyzerSettings.getInstance().ERROR_STRIPE_MARK_MIN_HEIGHT);
    TrafficLightRenderer.setOrRefreshErrorStripeRenderer(markup, myProject, document, psiFile);
  }

  private final List<Pair<NamedScope, NamedScopesHolder>> myScopes =
      ContainerUtil.createEmptyCOWList();

  void reloadScopes() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    List<Pair<NamedScope, NamedScopesHolder>> scopeList =
        new ArrayList<Pair<NamedScope, NamedScopesHolder>>();
    final DependencyValidationManager dependencyValidationManager =
        DependencyValidationManager.getInstance(myProject);
    addScopesToList(scopeList, NamedScopeManager.getInstance(myProject));
    addScopesToList(scopeList, dependencyValidationManager);
    myScopes.clear();
    myScopes.addAll(scopeList);
    dependencyValidationManager.reloadRules();
  }

  private static void addScopesToList(
      final List<Pair<NamedScope, NamedScopesHolder>> scopeList, final NamedScopesHolder holder) {
    NamedScope[] scopes = holder.getScopes();
    for (NamedScope scope : scopes) {
      scopeList.add(Pair.create(scope, holder));
    }
  }

  @NotNull
  public List<Pair<NamedScope, NamedScopesHolder>> getScopeBasedHighlightingCachedScopes() {
    return myScopes;
  }

  public void settingsChanged() {
    DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance();
    if (settings.isCodeHighlightingChanged(myLastSettings)) {
      restart();
    }
    myLastSettings = (DaemonCodeAnalyzerSettings) settings.clone();
  }

  public void updateVisibleHighlighters(@NotNull Editor editor) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    // no need, will not work anyway
  }

  public void setUpdateByTimerEnabled(boolean value) {
    myUpdateByTimerEnabled = value;
    stopProcess(value);
  }

  public boolean isUpdateByTimerEnabled() {
    return myUpdateByTimerEnabled;
  }

  public void setImportHintsEnabled(@NotNull PsiFile file, boolean value) {
    VirtualFile vFile = file.getVirtualFile();
    if (value) {
      myDisabledHintsFiles.remove(vFile);
      stopProcess(true);
    } else {
      myDisabledHintsFiles.add(vFile);
      HintManager.getInstance().hideAllHints();
    }
  }

  public void resetImportHintsEnabledForProject() {
    myDisabledHintsFiles.clear();
  }

  public void setHighlightingEnabled(@NotNull PsiFile file, boolean value) {
    if (value) {
      myDisabledHighlightingFiles.remove(file);
    } else {
      myDisabledHighlightingFiles.add(file);
    }
  }

  public boolean isHighlightingAvailable(PsiFile file) {
    if (myDisabledHighlightingFiles.contains(file)) return false;

    if (file == null || !file.isPhysical()) return false;
    if (file instanceof PsiCompiledElement) return false;
    final FileType fileType = file.getFileType();
    if (fileType == StdFileTypes.GUI_DESIGNER_FORM) {
      return true;
    }
    // To enable T.O.D.O. highlighting
    return !fileType.isBinary();
  }

  public boolean isImportHintsEnabled(@NotNull PsiFile file) {
    return isAutohintsAvailable(file) && !myDisabledHintsFiles.contains(file.getVirtualFile());
  }

  public boolean isAutohintsAvailable(PsiFile file) {
    return isHighlightingAvailable(file) && !(file instanceof PsiCompiledElement);
  }

  public void restart() {
    myFileStatusMap.markAllFilesDirty();
    stopProcess(true);
  }

  @Override
  public void restart(@NotNull PsiFile file) {
    Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file);
    if (document == null) return;
    myFileStatusMap.markFileScopeDirty(
        document, new TextRange(0, document.getTextLength()), file.getTextLength());
    stopProcess(true);
  }

  public List<TextEditorHighlightingPass> getPassesToShowProgressFor(Document document) {
    List<TextEditorHighlightingPass> allPasses = myPassExecutorService.getAllSubmittedPasses();
    List<TextEditorHighlightingPass> result =
        new ArrayList<TextEditorHighlightingPass>(allPasses.size());
    for (TextEditorHighlightingPass pass : allPasses) {
      if (pass.getDocument() == document || pass.getDocument() == null) {
        result.add(pass);
      }
    }
    return result;
  }

  public boolean isAllAnalysisFinished(@NotNull PsiFile file) {
    if (myDisposed) return false;
    Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file);
    return document != null
        && document.getModificationStamp() == file.getModificationStamp()
        && myFileStatusMap.allDirtyScopesAreNull(document);
  }

  public boolean isErrorAnalyzingFinished(PsiFile file) {
    if (myDisposed) return false;
    Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file);
    return document != null
        && document.getModificationStamp() == file.getModificationStamp()
        && myFileStatusMap.getFileDirtyScope(document, Pass.UPDATE_ALL) == null;
  }

  public FileStatusMap getFileStatusMap() {
    return myFileStatusMap;
  }

  public synchronized int getModificationCount() {
    return myModificationCount;
  }

  public synchronized boolean isRunning() {
    return myUpdateProgress != null && !myUpdateProgress.isCanceled();
  }

  synchronized void stopProcess(boolean toRestartAlarm) {
    if (!allowToInterrupt) throw new RuntimeException("Cannot interrupt daemon");

    cancelUpdateProgress(toRestartAlarm, "by Stop process");
    myAlarm.cancelAllRequests();
    boolean restart = toRestartAlarm && !myDisposed && myInitialized;
    if (restart) {
      myAlarm.addRequest(myUpdateRunnable, mySettings.AUTOREPARSE_DELAY);
    }
  }

  private synchronized void cancelUpdateProgress(final boolean start, @NonNls String reason) {
    PassExecutorService.log(myUpdateProgress, null, reason, start);
    myModificationCount++;

    if (myUpdateProgress != null) {
      myUpdateProgress.cancel();
      myPassExecutorService.cancelAll(false);
      myUpdateProgress = null;
    }
  }

  public static boolean processHighlights(
      @NotNull Document document,
      @NotNull Project project,
      @Nullable("null means all") final HighlightSeverity minSeverity,
      final int startOffset,
      final int endOffset,
      @NotNull final Processor<HighlightInfo> processor) {
    LOG.assertTrue(ApplicationManager.getApplication().isReadAccessAllowed());

    final SeverityRegistrar severityRegistrar = SeverityRegistrar.getInstance(project);
    MarkupModelEx model = (MarkupModelEx) DocumentMarkupModel.forDocument(document, project, true);
    return model.processRangeHighlightersOverlappingWith(
        startOffset,
        endOffset,
        new Processor<RangeHighlighterEx>() {
          public boolean process(RangeHighlighterEx marker) {
            Object tt = marker.getErrorStripeTooltip();
            if (!(tt instanceof HighlightInfo)) return true;
            HighlightInfo info = (HighlightInfo) tt;
            return minSeverity != null
                    && severityRegistrar.compare(info.getSeverity(), minSeverity) < 0
                || info.highlighter == null
                || processor.process(info);
          }
        });
  }

  public static boolean processHighlightsOverlappingOutside(
      @NotNull Document document,
      @NotNull Project project,
      @Nullable("null means all") final HighlightSeverity minSeverity,
      final int startOffset,
      final int endOffset,
      @NotNull final Processor<HighlightInfo> processor) {
    LOG.assertTrue(ApplicationManager.getApplication().isReadAccessAllowed());

    final SeverityRegistrar severityRegistrar = SeverityRegistrar.getInstance(project);
    MarkupModelEx model = (MarkupModelEx) DocumentMarkupModel.forDocument(document, project, true);
    return model.processRangeHighlightersOutside(
        startOffset,
        endOffset,
        new Processor<RangeHighlighterEx>() {
          public boolean process(RangeHighlighterEx marker) {
            Object tt = marker.getErrorStripeTooltip();
            if (!(tt instanceof HighlightInfo)) return true;
            HighlightInfo info = (HighlightInfo) tt;
            return minSeverity != null
                    && severityRegistrar.compare(info.getSeverity(), minSeverity) < 0
                || info.highlighter == null
                || processor.process(info);
          }
        });
  }

  public static boolean processHighlightsNearOffset(
      @NotNull Document document,
      @NotNull Project project,
      @NotNull final HighlightSeverity minSeverity,
      final int offset,
      final boolean includeFixRange,
      @NotNull final Processor<HighlightInfo> processor) {
    return processHighlights(
        document,
        project,
        null,
        0,
        document.getTextLength(),
        new Processor<HighlightInfo>() {
          public boolean process(HighlightInfo info) {
            if (!isOffsetInsideHighlightInfo(offset, info, includeFixRange)) return true;

            int compare = info.getSeverity().compareTo(minSeverity);
            return compare < 0 || processor.process(info);
          }
        });
  }

  @Nullable
  public HighlightInfo findHighlightByOffset(
      Document document, final int offset, final boolean includeFixRange) {
    final List<HighlightInfo> foundInfoList = new SmartList<HighlightInfo>();
    processHighlightsNearOffset(
        document,
        myProject,
        HighlightSeverity.INFORMATION,
        offset,
        includeFixRange,
        new Processor<HighlightInfo>() {
          public boolean process(HighlightInfo info) {
            if (!foundInfoList.isEmpty()) {
              HighlightInfo foundInfo = foundInfoList.get(0);
              int compare = foundInfo.getSeverity().compareTo(info.getSeverity());
              if (compare < 0) {
                foundInfoList.clear();
              } else if (compare > 0) {
                return true;
              }
            }
            foundInfoList.add(info);
            return true;
          }
        });

    if (foundInfoList.isEmpty()) return null;
    if (foundInfoList.size() == 1) return foundInfoList.get(0);
    return new HighlightInfoComposite(foundInfoList);
  }

  private static boolean isOffsetInsideHighlightInfo(
      int offset, HighlightInfo info, boolean includeFixRange) {
    RangeHighlighterEx highlighter = info.highlighter;
    if (highlighter == null || !highlighter.isValid()) return false;
    int startOffset = highlighter.getStartOffset();
    int endOffset = highlighter.getEndOffset();
    if (startOffset <= offset && offset <= endOffset) {
      return true;
    }
    if (!includeFixRange) return false;
    RangeMarker fixMarker = info.fixMarker;
    if (fixMarker != null) { // null means its range is the same as highlighter
      if (!fixMarker.isValid()) return false;
      startOffset = fixMarker.getStartOffset();
      endOffset = fixMarker.getEndOffset();
      return startOffset <= offset && offset <= endOffset;
    }
    return false;
  }

  @Nullable
  public static List<LineMarkerInfo> getLineMarkers(Document document, Project project) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
    return markup.getUserData(MARKERS_IN_EDITOR_DOCUMENT_KEY);
  }

  public static void setLineMarkers(
      @NotNull Document document, List<LineMarkerInfo> lineMarkers, Project project) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
    markup.putUserData(MARKERS_IN_EDITOR_DOCUMENT_KEY, lineMarkers);
  }

  public synchronized void setLastIntentionHint(
      Project project,
      PsiFile file,
      Editor editor,
      ShowIntentionsPass.IntentionsInfo intentions,
      boolean hasToRecreate) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    hideLastIntentionHint();
    IntentionHintComponent hintComponent =
        IntentionHintComponent.showIntentionHint(project, file, editor, intentions, false);
    if (hasToRecreate) {
      hintComponent.recreate();
    }
    myLastIntentionHint = hintComponent;
  }

  public synchronized void hideLastIntentionHint() {
    if (myLastIntentionHint != null && myLastIntentionHint.isVisible()) {
      myLastIntentionHint.hide();
      myLastIntentionHint = null;
    }
  }

  public synchronized IntentionHintComponent getLastIntentionHint() {
    return myLastIntentionHint;
  }

  public void writeExternal(Element parentNode) throws WriteExternalException {
    Element disableHintsElement = new Element(DISABLE_HINTS_TAG);
    parentNode.addContent(disableHintsElement);

    List<String> array = new ArrayList<String>();
    for (VirtualFile file : myDisabledHintsFiles) {
      if (file.isValid()) {
        array.add(file.getUrl());
      }
    }
    Collections.sort(array);

    for (String url : array) {
      Element fileElement = new Element(FILE_TAG);
      fileElement.setAttribute(URL_ATT, url);
      disableHintsElement.addContent(fileElement);
    }
  }

  public void readExternal(Element parentNode) throws InvalidDataException {
    myDisabledHintsFiles.clear();

    Element element = parentNode.getChild(DISABLE_HINTS_TAG);
    if (element != null) {
      for (Object o : element.getChildren(FILE_TAG)) {
        Element e = (Element) o;

        String url = e.getAttributeValue(URL_ATT);
        if (url != null) {
          VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url);
          if (file != null) {
            myDisabledHintsFiles.add(file);
          }
        }
      }
    }
  }

  private Runnable createUpdateRunnable() {
    return new Runnable() {
      public void run() {
        if (myDisposed || !myProject.isInitialized()) return;
        if (PowerSaveMode.isEnabled()) return;
        Editor activeEditor = FileEditorManager.getInstance(myProject).getSelectedTextEditor();

        Runnable runnable =
            new Runnable() {
              public void run() {
                PassExecutorService.log(
                    myUpdateProgress,
                    null,
                    "Update Runnable. myUpdateByTimerEnabled:",
                    myUpdateByTimerEnabled,
                    " something disposed:",
                    PowerSaveMode.isEnabled() || myDisposed || !myProject.isInitialized(),
                    " activeEditors:",
                    myProject.isDisposed() ? null : myDaemonListeners.getSelectedEditors());
                if (!myUpdateByTimerEnabled) return;
                if (myDisposed) return;
                ApplicationManager.getApplication().assertIsDispatchThread();

                final Collection<FileEditor> activeEditors = myDaemonListeners.getSelectedEditors();
                if (activeEditors.isEmpty()) return;

                ApplicationManager.getApplication().assertIsDispatchThread();
                if (ApplicationManager.getApplication().isWriteAccessAllowed()) {
                  // makes no sense to start from within write action, will cancel anyway
                  // we'll restart when write action finish
                  return;
                }
                if (PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments()) {
                  ((PsiDocumentManagerImpl) PsiDocumentManager.getInstance(myProject))
                      .cancelAndRunWhenAllCommitted("restart daemon when all committed", this);
                  return;
                }

                Map<FileEditor, HighlightingPass[]> passes =
                    new THashMap<FileEditor, HighlightingPass[]>(activeEditors.size());
                for (FileEditor fileEditor : activeEditors) {
                  BackgroundEditorHighlighter highlighter = fileEditor.getBackgroundHighlighter();
                  if (highlighter != null) {
                    HighlightingPass[] highlightingPasses = highlighter.createPassesForEditor();
                    passes.put(fileEditor, highlightingPasses);
                  }
                }
                // cancel all after calling createPasses() since there are perverts {@link
                // com.intellij.util.xml.ui.DomUIFactoryImpl} who are changing PSI there
                cancelUpdateProgress(true, "Cancel by alarm");
                myAlarm.cancelAllRequests();
                DaemonProgressIndicator progress = createUpdateProgress();
                myPassExecutorService.submitPasses(passes, progress, Job.DEFAULT_PRIORITY);
              }
            };
        if (activeEditor == null) {
          runnable.run();
        } else {
          ((PsiDocumentManagerImpl) PsiDocumentManager.getInstance(myProject))
              .cancelAndRunWhenAllCommitted("start daemon when all committed", runnable);
        }
      }
    };
  }

  private synchronized DaemonProgressIndicator createUpdateProgress() {
    DaemonProgressIndicator progress =
        new DaemonProgressIndicator() {
          @Override
          public void stopIfRunning() {
            super.stopIfRunning();
            myProject.getMessageBus().syncPublisher(DAEMON_EVENT_TOPIC).daemonFinished();
          }
        };
    progress.start();
    myUpdateProgress = progress;
    return progress;
  }

  public boolean canChangeFileSilently(PsiFileSystemItem file) {
    return myDaemonListeners.canChangeFileSilently(file);
  }

  public void autoImportReferenceAtCursor(@NotNull Editor editor, @NotNull PsiFile file) {
    for (ReferenceImporter importer : Extensions.getExtensions(ReferenceImporter.EP_NAME)) {
      if (importer.autoImportReferenceAtCursor(editor, file)) break;
    }
  }

  @NotNull
  @TestOnly
  public static List<HighlightInfo> getFileLevelHighlights(Project project, PsiFile file) {
    return UpdateHighlightersUtil.getFileLeveleHighlights(project, file);
  }

  @TestOnly
  public void allowToInterrupt(boolean can) {
    allowToInterrupt = can;
  }

  @TestOnly
  public synchronized DaemonProgressIndicator getUpdateProgress() {
    return myUpdateProgress;
  }
}
/** @author cdr */
public class InjectedLanguageManagerImpl extends InjectedLanguageManager implements Disposable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl");
  private final Project myProject;
  private final DumbService myDumbService;
  private final AtomicReference<MultiHostInjector> myPsiManagerRegisteredInjectorsAdapter =
      new AtomicReference<MultiHostInjector>();
  private volatile DaemonProgressIndicator myProgress;

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

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

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

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

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

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

  @Override
  public void dispose() {}

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

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

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

    final Processor<DocumentWindow> commitProcessor =
        new Processor<DocumentWindow>() {
          @Override
          public boolean process(DocumentWindow documentWindow) {
            if (myProject.isDisposed()) return false;
            ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
            if (indicator != null && indicator.isCanceled()) return false;
            if (documentManager.isUncommited(hostDocument) || !hostPsiFile.isValid())
              return false; // will be committed later

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

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

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

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

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

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

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

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

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

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

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

  @Override
  public String getUnescapedText(@NotNull final PsiElement injectedNode) {
    final StringBuilder text = new StringBuilder(injectedNode.getTextLength());
    // gather text from (patched) leaves
    injectedNode.accept(
        new PsiRecursiveElementWalkingVisitor() {
          @Override
          public void visitElement(PsiElement element) {
            String unescaped = element.getCopyableUserData(UNESCAPED_TEXT);
            if (unescaped != null) {
              text.append(unescaped);
              return;
            }
            if (element.getFirstChild() == null) {
              text.append(element.getText());
              return;
            }
            super.visitElement(element);
          }
        });
    return text.toString();
  }

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

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

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

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

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

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

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

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

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

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

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

  private static class PsiManagerRegisteredInjectorsAdapter implements MultiHostInjector {
    @Override
    public void getLanguagesToInject(
        @NotNull final MultiHostRegistrar injectionPlacesRegistrar, @NotNull PsiElement context) {
      final PsiLanguageInjectionHost host = (PsiLanguageInjectionHost) context;
      InjectedLanguagePlaces placesRegistrar =
          new InjectedLanguagePlaces() {
            @Override
            public void addPlace(
                @NotNull Language language,
                @NotNull TextRange rangeInsideHost,
                @NonNls @Nullable String prefix,
                @NonNls @Nullable String suffix) {
              ProperTextRange.assertProperRange(rangeInsideHost);
              injectionPlacesRegistrar
                  .startInjecting(language)
                  .addPlace(prefix, suffix, host, rangeInsideHost)
                  .doneInjecting();
            }
          };
      for (LanguageInjector injector :
          Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME)) {
        injector.getLanguagesToInject(host, placesRegistrar);
      }
    }

    @Override
    @NotNull
    public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
      return Arrays.asList(PsiLanguageInjectionHost.class);
    }
  }
}
public class PostprocessReformattingAspect implements PomModelAspect {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformattingAspect");
  private final Project myProject;
  private final PsiManager myPsiManager;
  private final TreeAspect myTreeAspect;
  private final Map<FileViewProvider, List<ASTNode>> myReformatElements =
      new HashMap<FileViewProvider, List<ASTNode>>();
  private volatile int myDisabledCounter = 0;
  private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>();
  private final AtomicInteger myPostponedCounter = new AtomicInteger();
  private static final Key<Throwable> REFORMAT_ORIGINATOR = Key.create("REFORMAT_ORIGINATOR");
  private static final boolean STORE_REFORMAT_ORIGINATOR_STACKTRACE =
      ApplicationManager.getApplication().isInternal();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    String actualPsiTree = treeDebugBuilder.psiToString(actualPsi);

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

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

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

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

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

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

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

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

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

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

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

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

    return result;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

    documentManager.commitDocument(document);
    return codeFormatter;
  }

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

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

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

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

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

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

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

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

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

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

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

    public int getOldIndent() {
      return myOldIndent;
    }
  }

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

  private class ReformatRangesAction implements PostponedAction {
    private final FormatTextRanges myRanges;

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

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

    @Override
    public void dispose() {}
  }

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

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

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

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

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

  private PsiClassImplUtil() {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  public enum MemberType {
    CLASS,
    FIELD,
    METHOD
  }

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

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

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

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

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

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

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

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

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

      ClassIconRequest that = (ClassIconRequest) o;

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

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

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

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

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

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

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

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

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

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

      return new LocalSearchScope(file);
    }
  }

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

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

    return false;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return types;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return resolved;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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