@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);
 }
 public void registerFix(
     @Nullable IntentionAction action,
     @Nullable List<IntentionAction> options,
     @Nullable String displayName,
     @Nullable TextRange fixRange,
     @Nullable HighlightDisplayKey key) {
   if (action == null) return;
   if (fixRange == null) fixRange = new TextRange(startOffset, endOffset);
   if (quickFixActionRanges == null) {
     quickFixActionRanges = ContainerUtil.createLockFreeCopyOnWriteList();
   }
   IntentionActionDescriptor desc =
       new IntentionActionDescriptor(action, options, displayName, null, key, getProblemGroup());
   quickFixActionRanges.add(Pair.create(desc, fixRange));
   fixStartOffset = Math.min(fixStartOffset, fixRange.getStartOffset());
   fixEndOffset = Math.max(fixEndOffset, fixRange.getEndOffset());
   if (action instanceof HintAction) {
     setHint(true);
   }
 }
 private static Set<String> calcDevPatternClassNames(@NotNull final Project project) {
   final List<String> roots = ContainerUtil.createLockFreeCopyOnWriteList();
   JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
   PsiClass beanClass =
       psiFacade.findClass(PatternClassBean.class.getName(), GlobalSearchScope.allScope(project));
   if (beanClass != null) {
     GlobalSearchScope scope =
         GlobalSearchScope.getScopeRestrictedByFileTypes(
             GlobalSearchScope.allScope(project), StdFileTypes.XML);
     final TextOccurenceProcessor occurenceProcessor =
         new TextOccurenceProcessor() {
           @Override
           public boolean execute(@NotNull PsiElement element, int offsetInElement) {
             XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class);
             String className = tag == null ? null : tag.getAttributeValue("className");
             if (StringUtil.isNotEmpty(className) && tag.getLocalName().endsWith("patternClass")) {
               roots.add(className);
             }
             return true;
           }
         };
     final StringSearcher searcher = new StringSearcher("patternClass", true, true);
     CacheManager.SERVICE
         .getInstance(beanClass.getProject())
         .processFilesWithWord(
             new Processor<PsiFile>() {
               @Override
               public boolean process(PsiFile psiFile) {
                 LowLevelSearchUtil.processElementsContainingWordInElement(
                     occurenceProcessor, psiFile, searcher, true, new EmptyProgressIndicator());
                 return true;
               }
             },
             searcher.getPattern(),
             UsageSearchContext.IN_FOREIGN_LANGUAGES,
             scope,
             searcher.isCaseSensitive());
   }
   return ContainerUtil.newHashSet(roots);
 }
/** @author Roman Chernyatchik */
public class StatisticsPanel implements DataProvider {
  public static final DataKey<StatisticsPanel> SM_TEST_RUNNER_STATISTICS =
      DataKey.create("SM_TEST_RUNNER_STATISTICS");

  private TableView<SMTestProxy> myStatisticsTableView;
  private JPanel myContentPane;
  private final Storage.PropertiesComponentStorage myStorage =
      new Storage.PropertiesComponentStorage("sm_test_statistics_table_columns");

  private final StatisticsTableModel myTableModel;
  private final List<PropagateSelectionHandler> myPropagateSelectionHandlers =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private final Project myProject;
  private final TestFrameworkRunningModel myFrameworkRunningModel;

  public StatisticsPanel(final Project project, final TestFrameworkRunningModel model) {
    myProject = project;
    myTableModel = new StatisticsTableModel();
    myStatisticsTableView.setModelAndUpdateColumns(myTableModel);
    myFrameworkRunningModel = model;

    final Runnable gotoSuiteOrParentAction = createGotoSuiteOrParentAction();
    new DoubleClickListener() {
      @Override
      protected boolean onDoubleClick(MouseEvent e) {
        gotoSuiteOrParentAction.run();
        return true;
      }
    }.installOn(myStatisticsTableView);

    // Fire selection changed and move focus on SHIFT+ENTER
    final KeyStroke shiftEnterKey =
        KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK);
    SMRunnerUtil.registerAsAction(
        shiftEnterKey,
        "select-test-proxy-in-test-view",
        new Runnable() {
          public void run() {
            showSelectedProxyInTestsTree();
          }
        },
        myStatisticsTableView);

    // Expand selected or go to parent on ENTER
    final KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
    SMRunnerUtil.registerAsAction(
        enterKey, "go-to-selected-suite-or-parent", gotoSuiteOrParentAction, myStatisticsTableView);
    // Contex menu in Table
    PopupHandler.installPopupHandler(
        myStatisticsTableView, IdeActions.GROUP_TESTTREE_POPUP, ActionPlaces.TESTTREE_VIEW_POPUP);
    // set this statistic tab as dataprovider for test's table view
    DataManager.registerDataProvider(myStatisticsTableView, this);
  }

  public void addPropagateSelectionListener(final PropagateSelectionHandler handler) {
    myPropagateSelectionHandlers.add(handler);
  }

  public JPanel getContentPane() {
    return myContentPane;
  }

  public SMTRunnerEventsListener createTestEventsListener() {
    return new SMTRunnerEventsAdapter() {
      @Override
      public void onSuiteStarted(@NotNull final SMTestProxy suite) {
        if (myTableModel.shouldUpdateModelBySuite(suite)) {
          updateAndRestoreSelection();
        }
      }

      @Override
      public void onSuiteFinished(@NotNull final SMTestProxy suite) {
        if (myTableModel.shouldUpdateModelBySuite(suite)) {
          updateAndRestoreSelection();
        }
      }

      @Override
      public void onTestStarted(@NotNull final SMTestProxy test) {
        if (myTableModel.shouldUpdateModelByTest(test)) {
          updateAndRestoreSelection();
        }
      }

      @Override
      public void onTestFinished(@NotNull final SMTestProxy test) {
        if (myTableModel.shouldUpdateModelByTest(test)) {
          updateAndRestoreSelection();
        }
      }

      private void updateAndRestoreSelection() {
        SMRunnerUtil.addToInvokeLater(
            new Runnable() {
              public void run() {
                BaseTableView.restore(myStorage, myStatisticsTableView);
                // statisticsTableView can be null in JUnit tests
                final SMTestProxy oldSelection = myStatisticsTableView.getSelectedObject();

                // update module
                myTableModel.updateModel();

                // restore selection if it is possible
                if (oldSelection != null) {
                  final int newRow = myTableModel.getIndexOf(oldSelection);
                  if (newRow > -1) {
                    myStatisticsTableView.setRowSelectionInterval(newRow, newRow);
                  }
                }
              }
            });
      }
    };
  }

  public Object getData(@NonNls final String dataId) {
    if (SM_TEST_RUNNER_STATISTICS.is(dataId)) {
      return this;
    }
    return TestsUIUtil.getData(getSelectedItem(), dataId, myFrameworkRunningModel);
  }

  /**
   * On event - change selection and probably requests focus. Is used when we want navigate from
   * other component to this
   *
   * @return Listener
   */
  public PropagateSelectionHandler createSelectMeListener() {
    return new PropagateSelectionHandler() {
      public void handlePropagateSelectionRequest(
          @Nullable final SMTestProxy selectedTestProxy,
          @NotNull final Object sender,
          final boolean requestFocus) {
        selectProxy(selectedTestProxy, sender, requestFocus);
      }
    };
  }

  public void selectProxy(
      @Nullable final SMTestProxy selectedTestProxy,
      @NotNull final Object sender,
      final boolean requestFocus) {
    SMRunnerUtil.addToInvokeLater(
        new Runnable() {
          public void run() {
            // Select tab if focus was requested
            if (requestFocus) {
              IdeFocusManager.getInstance(myProject).requestFocus(myStatisticsTableView, true);
            }

            // Select proxy in table
            selectProxy(selectedTestProxy);
          }
        });
  }

  protected void showSelectedProxyInTestsTree() {
    final Collection<SMTestProxy> proxies = myStatisticsTableView.getSelection();
    if (proxies.isEmpty()) {
      return;
    }
    final SMTestProxy proxy = proxies.iterator().next();
    myStatisticsTableView.clearSelection();
    fireOnPropagateSelection(proxy);
  }

  protected Runnable createGotoSuiteOrParentAction() {
    // Expand selected or go to parent
    return new Runnable() {
      public void run() {
        final SMTestProxy selectedProxy = getSelectedItem();
        if (selectedProxy == null) {
          return;
        }

        final int i = myStatisticsTableView.getSelectedRow();
        assert i >= 0; // because something is selected

        // if selected element is suite - we should expand it
        if (selectedProxy.isSuite()) {
          // expand and select first (Total) row
          showInTableAndSelectRow(selectedProxy, selectedProxy);
        }
      }
    };
  }

  protected void selectProxy(@Nullable final SMTestProxy selectedTestProxy) {
    // Send event to model
    myTableModel.updateModelOnProxySelected(selectedTestProxy);

    // Now we want to select proxy in table (if it is possible)
    if (selectedTestProxy != null) {
      findAndSelectInTable(selectedTestProxy);
    }
  }
  /**
   * Selects row in table
   *
   * @param rowIndex Row's index
   */
  protected void selectRow(final int rowIndex) {
    SMRunnerUtil.addToInvokeLater(
        new Runnable() {
          public void run() {
            // updates model
            myStatisticsTableView.setRowSelectionInterval(rowIndex, rowIndex);

            // Scroll to visible
            TableUtil.scrollSelectionToVisible(myStatisticsTableView);
          }
        });
  }

  /** Selects row in table */
  protected void selectRowOf(final SMTestProxy proxy) {
    SMRunnerUtil.addToInvokeLater(
        new Runnable() {
          public void run() {
            final int rowIndex = myTableModel.getIndexOf(proxy);
            myStatisticsTableView.setRowSelectionInterval(rowIndex, rowIndex >= 0 ? rowIndex : 0);
            // Scroll to visible
            TableUtil.scrollSelectionToVisible(myStatisticsTableView);
          }
        });
  }

  @Nullable
  protected SMTestProxy getSelectedItem() {
    return myStatisticsTableView.getSelectedObject();
  }

  protected List<SMTestProxy> getTableItems() {
    return myTableModel.getItems();
  }

  private void findAndSelectInTable(final SMTestProxy proxy) {
    SMRunnerUtil.addToInvokeLater(
        new Runnable() {
          public void run() {
            final int rowIndex = myTableModel.getIndexOf(proxy);
            if (rowIndex >= 0) {
              myStatisticsTableView.setRowSelectionInterval(rowIndex, rowIndex);
            }
          }
        });
  }

  private void fireOnPropagateSelection(final SMTestProxy selectedTestProxy) {
    for (PropagateSelectionHandler handler : myPropagateSelectionHandlers) {
      handler.handlePropagateSelectionRequest(selectedTestProxy, this, true);
    }
  }

  private void createUIComponents() {
    myStatisticsTableView = new TableView<SMTestProxy>();
  }

  private void showInTableAndSelectRow(final SMTestProxy suite, final SMTestProxy suiteProxy) {
    selectProxy(suite);
    selectRowOf(suiteProxy);
  }

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

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

  protected volatile boolean myIsCommitInProgress;
  protected final PsiToDocumentSynchronizer mySynchronizer;

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

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

  @Override
  public void projectOpened() {}

  @Override
  public void projectClosed() {}

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

  @Override
  public void initComponent() {}

  @Override
  public void disposeComponent() {}

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

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

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

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

    fireFileCreated(document, psiFile);

    return psiFile;
  }

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

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

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

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

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

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

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

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

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

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

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

    return document;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

    return success;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  public PsiToDocumentSynchronizer getSynchronizer() {
    return mySynchronizer;
  }
}
/** @author oleg */
public class PydevConsoleRunner extends AbstractConsoleRunnerWithHistory<PythonConsoleView> {
  private static final Logger LOG = Logger.getInstance(PydevConsoleRunner.class.getName());

  @SuppressWarnings("SpellCheckingInspection")
  public static final String PYDEV_PYDEVCONSOLE_PY = "pydev/pydevconsole.py";

  public static final int PORTS_WAITING_TIMEOUT = 20000;

  private Sdk mySdk;
  @NotNull private CommandLineArgumentsProvider myCommandLineArgumentsProvider;
  private int[] myPorts;
  private PydevConsoleCommunication myPydevConsoleCommunication;
  private PyConsoleProcessHandler myProcessHandler;
  private PydevConsoleExecuteActionHandler myConsoleExecuteActionHandler;
  private List<ConsoleListener> myConsoleListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private final PyConsoleType myConsoleType;
  private Map<String, String> myEnvironmentVariables;
  private String myCommandLine;
  private String[] myStatementsToExecute = ArrayUtil.EMPTY_STRING_ARRAY;
  private ConsoleHistoryController myHistoryController;

  public static Key<ConsoleCommunication> CONSOLE_KEY =
      new Key<ConsoleCommunication>("PYDEV_CONSOLE_KEY");

  public static Key<Sdk> CONSOLE_SDK = new Key<Sdk>("PYDEV_CONSOLE_SDK_KEY");

  private static final long APPROPRIATE_TO_WAIT = 60000;
  private PyRemoteSdkCredentials myRemoteCredentials;
  private ToolWindow myToolWindow;

  private String myConsoleTitle = null;

  protected PydevConsoleRunner(
      @NotNull final Project project,
      @NotNull Sdk sdk,
      @NotNull final PyConsoleType consoleType,
      @Nullable final String workingDir,
      Map<String, String> environmentVariables) {
    super(project, consoleType.getTitle(), workingDir);
    mySdk = sdk;
    myConsoleType = consoleType;
    myEnvironmentVariables = environmentVariables;
  }

  public void setStatementsToExecute(String... statementsToExecute) {
    myStatementsToExecute = statementsToExecute;
  }

  public static Map<String, String> addDefaultEnvironments(Sdk sdk, Map<String, String> envs) {
    Charset defaultCharset = EncodingManager.getInstance().getDefaultCharset();

    final String encoding = defaultCharset != null ? defaultCharset.name() : "utf-8";
    setPythonIOEncoding(setPythonUnbuffered(envs), encoding);

    PythonSdkFlavor.initPythonPath(envs, true, PythonCommandLineState.getAddedPaths(sdk));
    return envs;
  }

  @Override
  protected List<AnAction> fillToolBarActions(
      final DefaultActionGroup toolbarActions,
      final Executor defaultExecutor,
      final RunContentDescriptor contentDescriptor) {
    AnAction backspaceHandlingAction = createBackspaceHandlingAction();
    // toolbarActions.add(backspaceHandlingAction);
    AnAction interruptAction = createInterruptAction();

    AnAction rerunAction = createRerunAction();
    toolbarActions.add(rerunAction);

    List<AnAction> actions =
        super.fillToolBarActions(toolbarActions, defaultExecutor, contentDescriptor);

    actions.add(0, rerunAction);

    actions.add(backspaceHandlingAction);
    actions.add(interruptAction);

    actions.add(createSplitLineAction());

    AnAction showVarsAction = new ShowVarsAction();
    toolbarActions.add(showVarsAction);
    toolbarActions.add(myHistoryController.getBrowseHistory());

    toolbarActions.add(new ConnectDebuggerAction());
    return actions;
  }

  @NotNull
  public static PydevConsoleRunner createAndRun(
      @NotNull final Project project,
      @NotNull final Sdk sdk,
      @NotNull final PyConsoleType consoleType,
      @Nullable final String workingDirectory,
      @NotNull final Map<String, String> environmentVariables,
      @Nullable final ToolWindow toolWindow,
      final String... statements2execute) {
    final PydevConsoleRunner consoleRunner =
        create(project, sdk, consoleType, workingDirectory, environmentVariables);
    consoleRunner.setToolWindow(toolWindow);
    consoleRunner.setStatementsToExecute(statements2execute);
    consoleRunner.run();
    return consoleRunner;
  }

  public void run() {
    UIUtil.invokeAndWaitIfNeeded(
        new Runnable() {
          @Override
          public void run() {
            FileDocumentManager.getInstance().saveAllDocuments();
          }
        });

    myPorts = findAvailablePorts(getProject(), myConsoleType);

    assert myPorts != null;

    myCommandLineArgumentsProvider =
        createCommandLineArgumentsProvider(mySdk, myEnvironmentVariables, myPorts);

    UIUtil.invokeLaterIfNeeded(
        new Runnable() {
          @Override
          public void run() {
            ProgressManager.getInstance()
                .run(
                    new Task.Backgroundable(getProject(), "Connecting to console", false) {
                      @Override
                      public void run(@NotNull final ProgressIndicator indicator) {
                        indicator.setText("Connecting to console...");
                        try {
                          initAndRun(myStatementsToExecute);
                        } catch (ExecutionException e) {
                          LOG.warn("Error running console", e);
                          assert myProject != null;
                          ExecutionHelper.showErrors(
                              myProject, Arrays.<Exception>asList(e), getTitle(), null);
                        }
                      }
                    });
          }
        });
  }

  public static PydevConsoleRunner create(
      Project project, Sdk sdk, PyConsoleType consoleType, String workingDirectory) {
    return create(project, sdk, consoleType, workingDirectory, Maps.<String, String>newHashMap());
  }

  @NotNull
  private static PydevConsoleRunner create(
      @NotNull final Project project,
      @NotNull final Sdk sdk,
      @NotNull final PyConsoleType consoleType,
      @Nullable final String workingDirectory,
      @NotNull final Map<String, String> environmentVariables) {
    return new PydevConsoleRunner(
        project, sdk, consoleType, workingDirectory, environmentVariables);
  }

  private static int[] findAvailablePorts(Project project, PyConsoleType consoleType) {
    final int[] ports;
    try {
      // File "pydev/console/pydevconsole.py", line 223, in <module>
      // port, client_port = sys.argv[1:3]
      ports = NetUtils.findAvailableSocketPorts(2);
    } catch (IOException e) {
      ExecutionHelper.showErrors(
          project, Arrays.<Exception>asList(e), consoleType.getTitle(), null);
      return null;
    }
    return ports;
  }

  private static CommandLineArgumentsProvider createCommandLineArgumentsProvider(
      final Sdk sdk, final Map<String, String> environmentVariables, int[] ports) {
    final ArrayList<String> args = new ArrayList<String>();
    args.add(sdk.getHomePath());
    final String versionString = sdk.getVersionString();
    if (versionString == null || !versionString.toLowerCase().contains("jython")) {
      args.add("-u");
    }
    args.add(
        FileUtil.toSystemDependentName(PythonHelpersLocator.getHelperPath(PYDEV_PYDEVCONSOLE_PY)));
    for (int port : ports) {
      args.add(String.valueOf(port));
    }
    return new CommandLineArgumentsProvider() {
      @Override
      public String[] getArguments() {
        return ArrayUtil.toStringArray(args);
      }

      @Override
      public boolean passParentEnvs() {
        return false;
      }

      @Override
      public Map<String, String> getAdditionalEnvs() {
        return addDefaultEnvironments(sdk, environmentVariables);
      }
    };
  }

  @Override
  protected PythonConsoleView createConsoleView() {
    PythonConsoleView consoleView = new PythonConsoleView(getProject(), getConsoleTitle(), mySdk);
    myPydevConsoleCommunication.setConsoleFile(consoleView.getConsoleVirtualFile());
    consoleView.addMessageFilter(new PythonTracebackFilter(getProject()));
    return consoleView;
  }

  @Override
  protected Process createProcess() throws ExecutionException {
    if (PySdkUtil.isRemote(mySdk)) {
      PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance();
      if (manager != null) {
        return createRemoteConsoleProcess(
            manager,
            myCommandLineArgumentsProvider.getArguments(),
            myCommandLineArgumentsProvider.getAdditionalEnvs());
      }
      throw new PythonRemoteInterpreterManager.PyRemoteInterpreterExecutionException();
    } else {
      myCommandLine = myCommandLineArgumentsProvider.getCommandLineString();
      Map<String, String> envs = myCommandLineArgumentsProvider.getAdditionalEnvs();
      if (envs != null) {
        EncodingEnvironmentUtil.fixDefaultEncodingIfMac(envs, getProject());
      }
      final Process server =
          ProcessRunner.createProcess(
              getWorkingDir(), envs, myCommandLineArgumentsProvider.getArguments());
      try {
        myPydevConsoleCommunication =
            new PydevConsoleCommunication(getProject(), myPorts[0], server, myPorts[1]);
      } catch (Exception e) {
        throw new ExecutionException(e.getMessage());
      }
      return server;
    }
  }

  private Process createRemoteConsoleProcess(
      PythonRemoteInterpreterManager manager, String[] command, Map<String, String> env)
      throws ExecutionException {
    PyRemoteSdkAdditionalDataBase data =
        (PyRemoteSdkAdditionalDataBase) mySdk.getSdkAdditionalData();
    assert data != null;

    GeneralCommandLine commandLine = new GeneralCommandLine(command);

    commandLine.getEnvironment().putAll(env);

    commandLine
        .getParametersList()
        .set(
            1,
            PythonRemoteInterpreterManager.toSystemDependent(
                new File(data.getHelpersPath(), PYDEV_PYDEVCONSOLE_PY).getPath(),
                PySourcePosition.isWindowsPath(data.getInterpreterPath())));
    commandLine.getParametersList().set(2, "0");
    commandLine.getParametersList().set(3, "0");

    myCommandLine = commandLine.getCommandLineString();

    try {
      myRemoteCredentials = data.getRemoteSdkCredentials(true);
      PathMappingSettings mappings = manager.setupMappings(getProject(), data, null);

      RemoteSshProcess remoteProcess =
          manager.createRemoteProcess(
              getProject(), myRemoteCredentials, mappings, commandLine, true);

      Couple<Integer> remotePorts = getRemotePortsFromProcess(remoteProcess);

      remoteProcess.addLocalTunnel(myPorts[0], myRemoteCredentials.getHost(), remotePorts.first);
      remoteProcess.addRemoteTunnel(remotePorts.second, "localhost", myPorts[1]);

      myPydevConsoleCommunication =
          new PydevRemoteConsoleCommunication(getProject(), myPorts[0], remoteProcess, myPorts[1]);
      return remoteProcess;
    } catch (Exception e) {
      throw new ExecutionException(e.getMessage());
    }
  }

  private static Couple<Integer> getRemotePortsFromProcess(RemoteSshProcess process)
      throws ExecutionException {
    Scanner s = new Scanner(process.getInputStream());

    return Couple.of(readInt(s, process), readInt(s, process));
  }

  private static int readInt(Scanner s, Process process) throws ExecutionException {
    long started = System.currentTimeMillis();

    while (System.currentTimeMillis() - started < PORTS_WAITING_TIMEOUT) {
      if (s.hasNextLine()) {
        String line = s.nextLine();
        try {
          return Integer.parseInt(line);
        } catch (NumberFormatException ignored) {
          continue;
        }
      }

      try {

        Thread.sleep(200);
      } catch (InterruptedException ignored) {
      }

      if (process.exitValue() != 0) {
        String error;
        try {
          error =
              "Console process terminated with error:\n"
                  + StreamUtil.readText(process.getErrorStream());
        } catch (Exception ignored) {
          error = "Console process terminated with exit code " + process.exitValue();
        }
        throw new ExecutionException(error);
      } else {
        break;
      }
    }

    throw new ExecutionException("Couldn't read integer value from stream");
  }

  @Override
  protected PyConsoleProcessHandler createProcessHandler(final Process process) {
    if (PySdkUtil.isRemote(mySdk)) {
      PythonRemoteInterpreterManager manager = PythonRemoteInterpreterManager.getInstance();
      if (manager != null) {
        PyRemoteSdkAdditionalDataBase data =
            (PyRemoteSdkAdditionalDataBase) mySdk.getSdkAdditionalData();
        assert data != null;
        myProcessHandler =
            manager.createConsoleProcessHandler(
                process,
                myRemoteCredentials,
                getConsoleView(),
                myPydevConsoleCommunication,
                myCommandLine,
                CharsetToolkit.UTF8_CHARSET,
                manager.setupMappings(getProject(), data, null));
      } else {
        LOG.error("Can't create remote console process handler");
      }
    } else {
      myProcessHandler =
          new PyConsoleProcessHandler(
              process,
              getConsoleView(),
              myPydevConsoleCommunication,
              myCommandLine,
              CharsetToolkit.UTF8_CHARSET);
    }
    return myProcessHandler;
  }

  public void initAndRun(final String... statements2execute) throws ExecutionException {
    super.initAndRun();

    if (handshake()) {

      ApplicationManager.getApplication()
          .invokeLater(
              new Runnable() {

                @Override
                public void run() {
                  // Propagate console communication to language console
                  final PythonConsoleView consoleView = getConsoleView();

                  consoleView.setConsoleCommunication(myPydevConsoleCommunication);
                  consoleView.setSdk(mySdk);
                  consoleView.setExecutionHandler(myConsoleExecuteActionHandler);
                  myProcessHandler.addProcessListener(
                      new ProcessAdapter() {
                        @Override
                        public void onTextAvailable(ProcessEvent event, Key outputType) {
                          consoleView.print(event.getText(), outputType);
                        }
                      });

                  enableConsoleExecuteAction();

                  for (String statement : statements2execute) {
                    consoleView.executeStatement(statement + "\n", ProcessOutputTypes.SYSTEM);
                  }

                  fireConsoleInitializedEvent(consoleView);
                }
              });
    } else {
      getConsoleView().print("Couldn't connect to console process.", ProcessOutputTypes.STDERR);
      myProcessHandler.destroyProcess();
      finishConsole();
    }
  }

  @Override
  protected String constructConsoleTitle(@NotNull String consoleTitle) {
    if (myConsoleTitle == null) {
      myConsoleTitle = super.constructConsoleTitle(consoleTitle);
    }
    return myConsoleTitle;
  }

  @Override
  protected void showConsole(Executor defaultExecutor, RunContentDescriptor contentDescriptor) {
    PythonConsoleToolWindow terminalView = PythonConsoleToolWindow.getInstance(getProject());
    terminalView.init(getToolWindow(), contentDescriptor);
  }

  protected AnAction createRerunAction() {
    return new RestartAction(this);
  }

  private AnAction createInterruptAction() {
    AnAction anAction =
        new AnAction() {
          @Override
          public void actionPerformed(final AnActionEvent e) {
            if (myPydevConsoleCommunication.isExecuting()) {
              getConsoleView().print("^C", ProcessOutputTypes.SYSTEM);
            }
            myPydevConsoleCommunication.interrupt();
          }

          @Override
          public void update(final AnActionEvent e) {
            EditorEx consoleEditor = getConsoleView().getConsole().getConsoleEditor();
            boolean enabled =
                IJSwingUtilities.hasFocus(consoleEditor.getComponent())
                    && !consoleEditor.getSelectionModel().hasSelection();
            e.getPresentation().setEnabled(enabled);
          }
        };
    anAction.registerCustomShortcutSet(
        KeyEvent.VK_C,
        InputEvent.CTRL_MASK,
        getConsoleView().getConsole().getConsoleEditor().getComponent());
    anAction.getTemplatePresentation().setVisible(false);
    return anAction;
  }

  private AnAction createBackspaceHandlingAction() {
    final AnAction upAction =
        new AnAction() {
          @Override
          public void actionPerformed(final AnActionEvent e) {
            new WriteCommandAction(
                getLanguageConsole().getProject(), getLanguageConsole().getFile()) {
              @Override
              protected void run(@NotNull final Result result) throws Throwable {
                String text = getLanguageConsole().getEditorDocument().getText();
                String newText =
                    text.substring(
                        0, text.length() - myConsoleExecuteActionHandler.getPythonIndent());
                getLanguageConsole().getEditorDocument().setText(newText);
                getLanguageConsole()
                    .getConsoleEditor()
                    .getCaretModel()
                    .moveToOffset(newText.length());
              }
            }.execute();
          }

          @Override
          public void update(final AnActionEvent e) {
            e.getPresentation()
                .setEnabled(
                    myConsoleExecuteActionHandler.getCurrentIndentSize()
                            >= myConsoleExecuteActionHandler.getPythonIndent()
                        && isIndentSubstring(getLanguageConsole().getEditorDocument().getText()));
          }
        };
    upAction.registerCustomShortcutSet(KeyEvent.VK_BACK_SPACE, 0, null);
    upAction.getTemplatePresentation().setVisible(false);
    return upAction;
  }

  private boolean isIndentSubstring(String text) {
    int indentSize = myConsoleExecuteActionHandler.getPythonIndent();
    return text.length() >= indentSize
        && CharMatcher.WHITESPACE.matchesAllOf(text.substring(text.length() - indentSize));
  }

  private void enableConsoleExecuteAction() {
    myConsoleExecuteActionHandler.setEnabled(true);
  }

  private boolean handshake() {
    boolean res;
    long started = System.currentTimeMillis();
    do {
      try {
        res = myPydevConsoleCommunication.handshake();
      } catch (XmlRpcException ignored) {
        res = false;
      }
      if (res) {
        break;
      } else {
        long now = System.currentTimeMillis();
        if (now - started > APPROPRIATE_TO_WAIT) {
          break;
        } else {
          try {
            Thread.sleep(100);
          } catch (InterruptedException ignored) {
          }
        }
      }
    } while (true);
    return res;
  }

  @Override
  protected AnAction createStopAction() {
    final AnAction generalStopAction = super.createStopAction();
    return createConsoleStoppingAction(generalStopAction);
  }

  @Override
  protected AnAction createCloseAction(
      Executor defaultExecutor, final RunContentDescriptor descriptor) {
    final AnAction generalCloseAction = super.createCloseAction(defaultExecutor, descriptor);

    final AnAction stopAction =
        new DumbAwareAction() {
          @Override
          public void update(AnActionEvent e) {
            generalCloseAction.update(e);
          }

          @Override
          public void actionPerformed(AnActionEvent e) {
            e = stopConsole(e);

            clearContent(descriptor);

            generalCloseAction.actionPerformed(e);
          }
        };
    stopAction.copyFrom(generalCloseAction);
    return stopAction;
  }

  private void clearContent(RunContentDescriptor descriptor) {
    Content content = getToolWindow().getContentManager().findContent(descriptor.getDisplayName());
    assert content != null;
    getToolWindow().getContentManager().removeContent(content, true);
  }

  private AnAction createConsoleStoppingAction(final AnAction generalStopAction) {
    final AnAction stopAction =
        new DumbAwareAction() {
          @Override
          public void update(AnActionEvent e) {
            generalStopAction.update(e);
          }

          @Override
          public void actionPerformed(AnActionEvent e) {
            e = stopConsole(e);

            generalStopAction.actionPerformed(e);
          }
        };
    stopAction.copyFrom(generalStopAction);
    return stopAction;
  }

  private AnActionEvent stopConsole(AnActionEvent e) {
    if (myPydevConsoleCommunication != null) {
      e =
          new AnActionEvent(
              e.getInputEvent(),
              e.getDataContext(),
              e.getPlace(),
              e.getPresentation(),
              e.getActionManager(),
              e.getModifiers());
      try {
        closeCommunication();
        // waiting for REPL communication before destroying process handler
        Thread.sleep(300);
      } catch (Exception ignored) {
        // Ignore
      }
    }
    return e;
  }

  protected AnAction createSplitLineAction() {

    class ConsoleSplitLineAction extends EditorAction {

      private static final String CONSOLE_SPLIT_LINE_ACTION_ID = "Console.SplitLine";

      public ConsoleSplitLineAction() {
        super(
            new EditorWriteActionHandler() {

              private final SplitLineAction mySplitLineAction = new SplitLineAction();

              @Override
              public boolean isEnabled(Editor editor, DataContext dataContext) {
                return mySplitLineAction.getHandler().isEnabled(editor, dataContext);
              }

              @Override
              public void executeWriteAction(
                  Editor editor, @Nullable Caret caret, DataContext dataContext) {
                ((EditorWriteActionHandler) mySplitLineAction.getHandler())
                    .executeWriteAction(editor, caret, dataContext);
                editor.getCaretModel().getCurrentCaret().moveCaretRelatively(0, 1, false, true);
              }
            });
      }

      public void setup() {
        EmptyAction.setupAction(this, CONSOLE_SPLIT_LINE_ACTION_ID, null);
      }
    }

    ConsoleSplitLineAction action = new ConsoleSplitLineAction();
    action.setup();
    return action;
  }

  private void closeCommunication() {
    if (!myProcessHandler.isProcessTerminated()) {
      myPydevConsoleCommunication.close();
    }
  }

  @NotNull
  @Override
  protected ProcessBackedConsoleExecuteActionHandler createExecuteActionHandler() {
    myConsoleExecuteActionHandler =
        new PydevConsoleExecuteActionHandler(
            getConsoleView(), getProcessHandler(), myPydevConsoleCommunication);
    myConsoleExecuteActionHandler.setEnabled(false);
    myHistoryController =
        new ConsoleHistoryController(
            myConsoleType.getTypeId(),
            "",
            getLanguageConsole(),
            myConsoleExecuteActionHandler.getConsoleHistoryModel());
    myHistoryController.install();
    return myConsoleExecuteActionHandler;
  }

  public PydevConsoleCommunication getPydevConsoleCommunication() {
    return myPydevConsoleCommunication;
  }

  public static boolean isInPydevConsole(final PsiElement element) {
    return element instanceof PydevConsoleElement || getConsoleCommunication(element) != null;
  }

  public static boolean isInPydevConsole(final VirtualFile file) {
    return file.getName().contains("Python Console");
  }

  public static boolean isPythonConsole(@Nullable final FileElement element) {
    return getPythonConsoleData(element) != null;
  }

  @Nullable
  public static PythonConsoleData getPythonConsoleData(@Nullable FileElement element) {
    if (element == null
        || element.getPsi() == null
        || element.getPsi().getContainingFile() == null) {
      return null;
    }

    VirtualFile file = getConsoleFile(element.getPsi().getContainingFile());

    if (file == null) {
      return null;
    }
    return file.getUserData(PyConsoleUtil.PYTHON_CONSOLE_DATA);
  }

  private static VirtualFile getConsoleFile(PsiFile psiFile) {
    VirtualFile file = psiFile.getViewProvider().getVirtualFile();
    if (file instanceof LightVirtualFile) {
      file = ((LightVirtualFile) file).getOriginalFile();
    }
    return file;
  }

  @Nullable
  public static ConsoleCommunication getConsoleCommunication(final PsiElement element) {
    final PsiFile containingFile = element.getContainingFile();
    return containingFile != null ? containingFile.getCopyableUserData(CONSOLE_KEY) : null;
  }

  @Nullable
  public static Sdk getConsoleSdk(final PsiElement element) {
    final PsiFile containingFile = element.getContainingFile();
    return containingFile != null ? containingFile.getCopyableUserData(CONSOLE_SDK) : null;
  }

  @Override
  protected boolean shouldAddNumberToTitle() {
    return true;
  }

  public void addConsoleListener(ConsoleListener consoleListener) {
    myConsoleListeners.add(consoleListener);
  }

  public void removeConsoleListener(ConsoleListener consoleListener) {
    myConsoleListeners.remove(consoleListener);
  }

  private void fireConsoleInitializedEvent(LanguageConsoleView consoleView) {
    for (ConsoleListener listener : myConsoleListeners) {
      listener.handleConsoleInitialized(consoleView);
    }
  }

  public ToolWindow getToolWindow() {
    if (myToolWindow == null) {
      myToolWindow =
          ToolWindowManager.getInstance(getProject())
              .getToolWindow(PythonConsoleToolWindowFactory.ID);
    }
    return myToolWindow;
  }

  public void setToolWindow(ToolWindow toolWindow) {
    myToolWindow = toolWindow;
  }

  public interface ConsoleListener {
    void handleConsoleInitialized(LanguageConsoleView consoleView);
  }

  private static class RestartAction extends AnAction {
    private PydevConsoleRunner myConsoleRunner;

    private RestartAction(PydevConsoleRunner runner) {
      copyFrom(ActionManager.getInstance().getAction(IdeActions.ACTION_RERUN));
      getTemplatePresentation().setIcon(AllIcons.Actions.Restart);
      myConsoleRunner = runner;
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      myConsoleRunner.rerun();
    }
  }

  private void rerun() {
    new Task.Backgroundable(getProject(), "Restarting console", true) {
      @Override
      public void run(@NotNull ProgressIndicator indicator) {
        UIUtil.invokeLaterIfNeeded(
            new Runnable() {
              @Override
              public void run() {
                closeCommunication();
              }
            });

        myProcessHandler.waitFor();

        UIUtil.invokeLaterIfNeeded(
            new Runnable() {
              @Override
              public void run() {
                PydevConsoleRunner.this.run();
              }
            });
      }
    }.queue();
  }

  private class ShowVarsAction extends ToggleAction implements DumbAware {
    private boolean mySelected = false;

    public ShowVarsAction() {
      super("Show Variables", "Shows active console variables", AllIcons.Debugger.Watches);
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return mySelected;
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      mySelected = state;

      if (mySelected) {
        getConsoleView().showVariables(myPydevConsoleCommunication);
      } else {
        getConsoleView().restoreWindow();
      }
    }
  }

  private class ConnectDebuggerAction extends ToggleAction implements DumbAware {
    private boolean mySelected = false;
    private XDebugSession mySession = null;

    public ConnectDebuggerAction() {
      super(
          "Attach Debugger",
          "Enables tracing of code executed in console",
          AllIcons.Actions.StartDebugger);
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return mySelected;
    }

    @Override
    public void update(AnActionEvent e) {
      if (mySession != null) {
        e.getPresentation().setEnabled(false);
      } else {
        e.getPresentation().setEnabled(true);
      }
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      mySelected = state;

      if (mySelected) {
        try {
          mySession = connectToDebugger();
        } catch (Exception e1) {
          LOG.error(e1);
          Messages.showErrorDialog("Can't connect to debugger", "Error Connecting Debugger");
        }
      } else {
        // TODO: disable debugging
      }
    }
  }

  private XDebugSession connectToDebugger() throws ExecutionException {
    final ServerSocket serverSocket = PythonCommandLineState.createServerSocket();

    final XDebugSession session =
        XDebuggerManager.getInstance(getProject())
            .startSessionAndShowTab(
                "Python Console Debugger",
                PythonIcons.Python.Python,
                null,
                true,
                new XDebugProcessStarter() {
                  @NotNull
                  public XDebugProcess start(@NotNull final XDebugSession session) {
                    PythonDebugLanguageConsoleView debugConsoleView =
                        new PythonDebugLanguageConsoleView(getProject(), mySdk);

                    PyConsoleDebugProcessHandler consoleDebugProcessHandler =
                        new PyConsoleDebugProcessHandler(myProcessHandler);

                    PyConsoleDebugProcess consoleDebugProcess =
                        new PyConsoleDebugProcess(
                            session, serverSocket, debugConsoleView, consoleDebugProcessHandler);

                    PythonDebugConsoleCommunication communication =
                        PyDebugRunner.initDebugConsoleView(
                            getProject(),
                            consoleDebugProcess,
                            debugConsoleView,
                            consoleDebugProcessHandler);

                    myPydevConsoleCommunication.setDebugCommunication(communication);
                    debugConsoleView.attachToProcess(consoleDebugProcessHandler);

                    consoleDebugProcess.waitForNextConnection();

                    try {
                      consoleDebugProcess.connect(myPydevConsoleCommunication);
                    } catch (Exception e) {
                      LOG.error(e); // TODO
                    }

                    myProcessHandler.notifyTextAvailable(
                        "\nDebugger connected.\n", ProcessOutputTypes.STDERR);

                    return consoleDebugProcess;
                  }
                });

    return session;
  }

  @Override
  protected List<String> getActiveConsoleNames(final String consoleTitle) {
    return FluentIterable.from(
            Lists.newArrayList(
                PythonConsoleToolWindow.getInstance(getProject())
                    .getToolWindow()
                    .getContentManager()
                    .getContents()))
        .transform(
            new Function<Content, String>() {
              @Override
              public String apply(Content input) {
                return input.getDisplayName();
              }
            })
        .filter(
            new Predicate<String>() {
              @Override
              public boolean apply(String input) {
                return input.contains(consoleTitle);
              }
            })
        .toList();
  }
}
Exemple #7
0
/** @author: Roman Chernyatchik */
public class SMTestRunnerResultsForm extends TestResultsPanel
    implements TestFrameworkRunningModel, TestResultsViewer, SMTRunnerEventsListener {
  @NonNls public static final String HISTORY_DATE_FORMAT = "yyyy.MM.dd 'at' HH'h' mm'm' ss's'";

  @NonNls
  private static final String DEFAULT_SM_RUNNER_SPLITTER_PROPERTY =
      "SMTestRunner.Splitter.Proportion";

  public static final Color DARK_YELLOW = JBColor.YELLOW.darker();
  private static final Logger LOG =
      Logger.getInstance("#" + SMTestRunnerResultsForm.class.getName());

  private SMTRunnerTestTreeView myTreeView;

  private TestsProgressAnimator myAnimator;

  /** Fake parent suite for all tests and suites */
  private final SMTestProxy.SMRootTestProxy myTestsRootNode;

  private SMTRunnerTreeBuilder myTreeBuilder;

  private final List<EventsListener> myEventListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();

  private PropagateSelectionHandler myShowStatisticForProxyHandler;

  private final Project myProject;

  private int myTotalTestCount = 0;
  private int myStartedTestCount = 0;
  private int myFinishedTestCount = 0;
  private int myFailedTestCount = 0;
  private int myIgnoredTestCount = 0;
  private long myStartTime;
  private long myEndTime;
  private StatisticsPanel myStatisticsPane;

  // custom progress
  private String myCurrentCustomProgressCategory;
  private final Set<String> myMentionedCategories = new LinkedHashSet<String>();
  private boolean myTestsRunning = true;
  private AbstractTestProxy myLastSelected;
  private Alarm myUpdateQueue;
  private Set<Update> myRequests = Collections.synchronizedSet(new HashSet<Update>());

  public SMTestRunnerResultsForm(
      @NotNull final JComponent console, final TestConsoleProperties consoleProperties) {
    this(console, AnAction.EMPTY_ARRAY, consoleProperties, null);
  }

  public SMTestRunnerResultsForm(
      @NotNull final JComponent console,
      AnAction[] consoleActions,
      final TestConsoleProperties consoleProperties,
      @Nullable String splitterPropertyName) {
    super(
        console,
        consoleActions,
        consoleProperties,
        StringUtil.notNullize(splitterPropertyName, DEFAULT_SM_RUNNER_SPLITTER_PROPERTY),
        0.2f);
    myProject = consoleProperties.getProject();

    // Create tests common suite root
    //noinspection HardCodedStringLiteral
    myTestsRootNode = new SMTestProxy.SMRootTestProxy();
    // todo myTestsRootNode.setOutputFilePath(runConfiguration.getOutputFilePath());

    // Fire selection changed and move focus on SHIFT+ENTER
    // TODO[romeo] improve
    /*
    final ArrayList<Component> components = new ArrayList<Component>();
    components.add(myTreeView);
    components.add(myTabs.getComponent());
    myContentPane.setFocusTraversalPolicy(new MyFocusTraversalPolicy(components));
    myContentPane.setFocusCycleRoot(true);
    */
  }

  @Override
  public void initUI() {
    super.initUI();

    if (Registry.is("tests.view.old.statistics.panel")) {
      final KeyStroke shiftEnterKey =
          KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_MASK);
      SMRunnerUtil.registerAsAction(
          shiftEnterKey,
          "show-statistics-for-test-proxy",
          new Runnable() {
            public void run() {
              showStatisticsForSelectedProxy();
            }
          },
          myTreeView);
    }
  }

  protected ToolbarPanel createToolbarPanel() {
    return new SMTRunnerToolbarPanel(myProperties, this, this);
  }

  protected JComponent createTestTreeView() {
    myTreeView = new SMTRunnerTestTreeView();

    myTreeView.setLargeModel(true);
    myTreeView.attachToModel(this);
    myTreeView.setTestResultsViewer(this);
    if (Registry.is("tests.view.old.statistics.panel")) {
      addTestsTreeSelectionListener(
          new TreeSelectionListener() {
            @Override
            public void valueChanged(TreeSelectionEvent e) {
              AbstractTestProxy selectedTest = getTreeView().getSelectedTest();
              if (selectedTest instanceof SMTestProxy) {
                myStatisticsPane.selectProxy(((SMTestProxy) selectedTest), this, false);
              }
            }
          });
    }

    final SMTRunnerTreeStructure structure = new SMTRunnerTreeStructure(myProject, myTestsRootNode);
    myTreeBuilder = new SMTRunnerTreeBuilder(myTreeView, structure);
    myTreeBuilder.setTestsComparator(TestConsoleProperties.SORT_ALPHABETICALLY.value(myProperties));
    Disposer.register(this, myTreeBuilder);

    myAnimator = new TestsProgressAnimator(myTreeBuilder);

    TrackRunningTestUtil.installStopListeners(
        myTreeView,
        myProperties,
        new Pass<AbstractTestProxy>() {
          @Override
          public void pass(AbstractTestProxy testProxy) {
            if (testProxy == null) return;
            // drill to the first leaf
            while (!testProxy.isLeaf()) {
              final List<? extends AbstractTestProxy> children = testProxy.getChildren();
              if (!children.isEmpty()) {
                final AbstractTestProxy firstChild = children.get(0);
                if (firstChild != null) {
                  testProxy = firstChild;
                  continue;
                }
              }
              break;
            }

            // pretend the selection on the first leaf
            // so if test would be run, tracking would be restarted
            myLastSelected = testProxy;
          }
        });

    // TODO always hide root node
    // myTreeView.setRootVisible(false);
    myUpdateQueue = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
    return myTreeView;
  }

  protected JComponent createStatisticsPanel() {
    // Statistics tab
    final StatisticsPanel statisticsPane = new StatisticsPanel(myProject, this);
    // handler to select in results viewer by statistics pane events
    statisticsPane.addPropagateSelectionListener(createSelectMeListener());
    // handler to select test statistics pane by result viewer events
    setShowStatisticForProxyHandler(statisticsPane.createSelectMeListener());

    myStatisticsPane = statisticsPane;
    return myStatisticsPane.getContentPane();
  }

  public StatisticsPanel getStatisticsPane() {
    return myStatisticsPane;
  }

  public void addTestsTreeSelectionListener(final TreeSelectionListener listener) {
    myTreeView.getSelectionModel().addTreeSelectionListener(listener);
  }

  /**
   * Is used for navigation from tree view to other UI components
   *
   * @param handler
   */
  public void setShowStatisticForProxyHandler(final PropagateSelectionHandler handler) {
    myShowStatisticForProxyHandler = handler;
  }

  /**
   * Returns root node, fake parent suite for all tests and suites
   *
   * @param testsRoot
   * @return
   */
  public void onTestingStarted(@NotNull SMTestProxy.SMRootTestProxy testsRoot) {
    myAnimator.setCurrentTestCase(myTestsRootNode);
    myTreeBuilder.updateFromRoot();

    // Status line
    myStatusLine.setStatusColor(ColorProgressBar.GREEN);

    // Tests tree
    selectAndNotify(myTestsRootNode);

    myStartTime = System.currentTimeMillis();
    boolean printTestingStartedTime = true;
    if (myProperties instanceof SMTRunnerConsoleProperties) {
      printTestingStartedTime =
          ((SMTRunnerConsoleProperties) myProperties).isPrintTestingStartedTime();
    }
    if (printTestingStartedTime) {
      myTestsRootNode.addSystemOutput(
          "Testing started at " + DateFormatUtil.formatTime(myStartTime) + " ...\n");
    }

    updateStatusLabel(false);

    // TODO : show info - "Loading..." msg

    fireOnTestingStarted();
  }

  public void onTestingFinished(@NotNull SMTestProxy.SMRootTestProxy testsRoot) {
    myEndTime = System.currentTimeMillis();

    if (myTotalTestCount == 0) {
      myTotalTestCount = myStartedTestCount;
      myStatusLine.setFraction(1);
    }

    updateStatusLabel(true);
    updateIconProgress(true);

    myAnimator.stopMovie();
    myTreeBuilder.updateFromRoot();

    LvcsHelper.addLabel(this);

    final Runnable onDone =
        new Runnable() {
          @Override
          public void run() {
            myTestsRunning = false;
            final boolean sortByDuration =
                TestConsoleProperties.SORT_BY_DURATION.value(myProperties);
            if (sortByDuration) {
              myTreeBuilder.setStatisticsComparator(myProperties, sortByDuration);
            }
          }
        };
    if (myLastSelected == null) {
      selectAndNotify(myTestsRootNode, onDone);
    } else {
      onDone.run();
    }

    fireOnTestingFinished();

    if (testsRoot.wasTerminated() && myStatusLine.getStatusColor() == ColorProgressBar.GREEN) {
      myStatusLine.setStatusColor(JBColor.LIGHT_GRAY);
    }

    if (testsRoot.isEmptySuite()
        && testsRoot.isTestsReporterAttached()
        && myProperties instanceof SMTRunnerConsoleProperties
        && ((SMTRunnerConsoleProperties) myProperties).fixEmptySuite()) {
      return;
    }
    final TestsUIUtil.TestResultPresentation presentation =
        new TestsUIUtil.TestResultPresentation(testsRoot, myStartTime > 0, null)
            .getPresentation(
                myFailedTestCount,
                myFinishedTestCount - myFailedTestCount - myIgnoredTestCount,
                myTotalTestCount - myFinishedTestCount,
                myIgnoredTestCount);
    TestsUIUtil.notifyByBalloon(myProperties.getProject(), testsRoot, myProperties, presentation);
    addToHistory(testsRoot, myProperties, this);
  }

  private static void addToHistory(
      final SMTestProxy.SMRootTestProxy root,
      TestConsoleProperties consoleProperties,
      Disposable parentDisposable) {
    final RunProfile configuration = consoleProperties.getConfiguration();
    if (configuration instanceof RunConfiguration
        && !(consoleProperties instanceof ImportedTestConsoleProperties)) {
      final MySaveHistoryTask backgroundable =
          new MySaveHistoryTask(consoleProperties, root, (RunConfiguration) configuration);
      final BackgroundableProcessIndicator processIndicator =
          new BackgroundableProcessIndicator(backgroundable);
      Disposer.register(
          parentDisposable,
          new Disposable() {
            @Override
            public void dispose() {
              processIndicator.cancel();
              backgroundable.dispose();
            }
          });
      Disposer.register(parentDisposable, processIndicator);
      ProgressManager.getInstance()
          .runProcessWithProgressAsynchronously(backgroundable, processIndicator);
    }
  }

  public void onTestsCountInSuite(final int count) {
    updateCountersAndProgressOnTestCount(count, false);
  }

  /**
   * Adds test to tree and updates status line. Test proxy should be initialized, proxy parent must
   * be some suite (already added to tree)
   *
   * @param testProxy Proxy
   */
  public void onTestStarted(@NotNull final SMTestProxy testProxy) {
    if (!testProxy.isConfig()) {
      updateOnTestStarted(false);
    }
    _addTestOrSuite(testProxy);
    fireOnTestNodeAdded(testProxy);
  }

  @Override
  public void onSuiteTreeNodeAdded(SMTestProxy testProxy) {
    myTotalTestCount++;
  }

  @Override
  public void onSuiteTreeStarted(SMTestProxy suite) {}

  public void onTestFailed(@NotNull final SMTestProxy test) {
    updateOnTestFailed(false);
    if (test.isConfig()) {
      myStartedTestCount++;
      myFinishedTestCount++;
    }
    updateIconProgress(false);

    // still expand failure when user selected another test
    if (myLastSelected != null
        && TestConsoleProperties.TRACK_RUNNING_TEST.value(myProperties)
        && TestConsoleProperties.HIDE_PASSED_TESTS.value(myProperties)) {
      myTreeBuilder.expand(test, null);
    }
  }

  public void onTestIgnored(@NotNull final SMTestProxy test) {
    updateOnTestIgnored();
  }

  /**
   * Adds suite to tree Suite proxy should be initialized, proxy parent must be some suite (already
   * added to tree) If parent is null, then suite will be added to tests root.
   *
   * @param newSuite Tests suite
   */
  public void onSuiteStarted(@NotNull final SMTestProxy newSuite) {
    _addTestOrSuite(newSuite);
  }

  public void onCustomProgressTestsCategory(@Nullable String categoryName, int testCount) {
    myCurrentCustomProgressCategory = categoryName;
    updateCountersAndProgressOnTestCount(testCount, true);
  }

  public void onCustomProgressTestStarted() {
    updateOnTestStarted(true);
  }

  public void onCustomProgressTestFailed() {
    updateOnTestFailed(true);
  }

  @Override
  public void onCustomProgressTestFinished() {
    updateOnTestFinished(true);
  }

  public void onTestFinished(@NotNull final SMTestProxy test) {
    if (!test.isConfig()) {
      updateOnTestFinished(false);
    }
    updateIconProgress(false);
  }

  public void onSuiteFinished(@NotNull final SMTestProxy suite) {
    // Do nothing
  }

  public SMTestProxy.SMRootTestProxy getTestsRootNode() {
    return myTestsRootNode;
  }

  public TestConsoleProperties getProperties() {
    return myProperties;
  }

  public void setFilter(final Filter filter) {
    // is used by Test Runner actions, e.g. hide passed, etc
    final SMTRunnerTreeStructure treeStructure = myTreeBuilder.getSMRunnerTreeStructure();
    treeStructure.setFilter(filter);

    // TODO - show correct info if no children are available
    // (e.g no tests found or all tests passed, etc.)
    // treeStructure.getChildElements(treeStructure.getRootElement()).length == 0

    myTreeBuilder.queueUpdate();
  }

  public boolean isRunning() {
    return myTestsRunning;
  }

  public TestTreeView getTreeView() {
    return myTreeView;
  }

  @Override
  public SMTRunnerTreeBuilder getTreeBuilder() {
    return myTreeBuilder;
  }

  public boolean hasTestSuites() {
    return getRoot().getChildren().size() > 0;
  }

  @NotNull
  public AbstractTestProxy getRoot() {
    return myTestsRootNode;
  }

  /**
   * Manual test proxy selection in tests tree. E.g. do select root node on testing started or do
   * select current node if TRACK_RUNNING_TEST is enabled
   *
   * <p>
   *
   * <p>Will select proxy in Event Dispatch Thread. Invocation of this method may be not in event
   * dispatch thread
   *
   * @param testProxy Test or suite
   */
  @Override
  public void selectAndNotify(AbstractTestProxy testProxy) {
    selectAndNotify(testProxy, null);
  }

  private void selectAndNotify(
      @Nullable final AbstractTestProxy testProxy, @Nullable Runnable onDone) {
    selectWithoutNotify(testProxy, onDone);

    // Is used by Statistic tab to differ use selection in tree
    // from manual selection from API (e.g. test runner events)
    if (Registry.is("tests.view.old.statistics.panel")) {
      showStatisticsForSelectedProxy(testProxy, false);
    }
  }

  public void addEventsListener(final EventsListener listener) {
    myEventListeners.add(listener);
    addTestsTreeSelectionListener(
        new TreeSelectionListener() {
          public void valueChanged(final TreeSelectionEvent e) {
            // We should fire event only if it was generated by this component,
            // e.g. it is focused. Otherwise it is side effect of selecting proxy in
            // try by other component
            // if (myTreeView.isFocusOwner()) {
            @Nullable
            final SMTestProxy selectedProxy = (SMTestProxy) getTreeView().getSelectedTest();
            listener.onSelected(
                selectedProxy, SMTestRunnerResultsForm.this, SMTestRunnerResultsForm.this);
            // }
          }
        });
  }

  public void dispose() {
    super.dispose();
    myShowStatisticForProxyHandler = null;
    myEventListeners.clear();
    myStatisticsPane.doDispose();
  }

  public void showStatisticsForSelectedProxy() {
    TestConsoleProperties.SHOW_STATISTICS.set(myProperties, true);
    final AbstractTestProxy selectedProxy = myTreeView.getSelectedTest();
    showStatisticsForSelectedProxy(selectedProxy, true);
  }

  private void showStatisticsForSelectedProxy(
      final AbstractTestProxy selectedProxy, final boolean requestFocus) {
    if (selectedProxy instanceof SMTestProxy && myShowStatisticForProxyHandler != null) {
      myShowStatisticForProxyHandler.handlePropagateSelectionRequest(
          (SMTestProxy) selectedProxy, this, requestFocus);
    }
  }

  protected int getTotalTestCount() {
    return myTotalTestCount;
  }

  protected int getStartedTestCount() {
    return myStartedTestCount;
  }

  protected int getFinishedTestCount() {
    return myFinishedTestCount;
  }

  protected int getFailedTestCount() {
    return myFailedTestCount;
  }

  protected int getIgnoredTestCount() {
    return myIgnoredTestCount;
  }

  protected Color getTestsStatusColor() {
    return myStatusLine.getStatusColor();
  }

  public Set<String> getMentionedCategories() {
    return myMentionedCategories;
  }

  protected long getStartTime() {
    return myStartTime;
  }

  protected long getEndTime() {
    return myEndTime;
  }

  private void _addTestOrSuite(@NotNull final SMTestProxy newTestOrSuite) {

    final SMTestProxy parentSuite = newTestOrSuite.getParent();
    assert parentSuite != null;

    // Tree
    final Update update =
        new Update(parentSuite) {
          @Override
          public void run() {
            myRequests.remove(this);
            myTreeBuilder.updateTestsSubtree(parentSuite);
          }
        };
    if (ApplicationManager.getApplication().isUnitTestMode()) {
      update.run();
    } else if (myRequests.add(update) && !myUpdateQueue.isDisposed()) {
      myUpdateQueue.addRequest(update, 100);
    }
    myTreeBuilder.repaintWithParents(newTestOrSuite);

    myAnimator.setCurrentTestCase(newTestOrSuite);

    if (TestConsoleProperties.TRACK_RUNNING_TEST.value(myProperties)) {
      if (myLastSelected == null || myLastSelected == newTestOrSuite) {
        myLastSelected = null;
        selectAndNotify(newTestOrSuite);
      }
    }
  }

  private void fireOnTestNodeAdded(final SMTestProxy test) {
    for (EventsListener eventListener : myEventListeners) {
      eventListener.onTestNodeAdded(this, test);
    }
  }

  private void fireOnTestingFinished() {
    for (EventsListener eventListener : myEventListeners) {
      eventListener.onTestingFinished(this);
    }
  }

  private void fireOnTestingStarted() {
    for (EventsListener eventListener : myEventListeners) {
      eventListener.onTestingStarted(this);
    }
  }

  private void selectWithoutNotify(
      final AbstractTestProxy testProxy, @Nullable final Runnable onDone) {
    if (testProxy == null) {
      return;
    }

    SMRunnerUtil.runInEventDispatchThread(
        new Runnable() {
          public void run() {
            if (myTreeBuilder.isDisposed()) {
              return;
            }
            myTreeBuilder.select(testProxy, onDone);
          }
        },
        ModalityState.NON_MODAL);
  }

  private void updateStatusLabel(final boolean testingFinished) {
    if (myFailedTestCount > 0) {
      myStatusLine.setStatusColor(ColorProgressBar.RED);
    }

    if (testingFinished) {
      if (myTotalTestCount == 0) {
        myStatusLine.setStatusColor(
            myTestsRootNode.wasLaunched() || !myTestsRootNode.isTestsReporterAttached()
                ? JBColor.LIGHT_GRAY
                : ColorProgressBar.RED);
      }
      // else color will be according failed/passed tests
    }

    // launchedAndFinished - is launched and not in progress. If we remove "launched' that
    // onTestingStarted() before
    // initializing will be "launchedAndFinished"
    final boolean launchedAndFinished =
        myTestsRootNode.wasLaunched() && !myTestsRootNode.isInProgress();
    if (!TestsPresentationUtil.hasNonDefaultCategories(myMentionedCategories)) {
      myStatusLine.formatTestMessage(
          myTotalTestCount,
          myFinishedTestCount,
          myFailedTestCount,
          myIgnoredTestCount,
          myTestsRootNode.getDuration(),
          myEndTime);
    } else {
      myStatusLine.setText(
          TestsPresentationUtil.getProgressStatus_Text(
              myStartTime,
              myEndTime,
              myTotalTestCount,
              myFinishedTestCount,
              myFailedTestCount,
              myMentionedCategories,
              launchedAndFinished));
    }
  }

  /** for java unit tests */
  public void performUpdate() {
    myTreeBuilder.performUpdate();
  }

  private void updateIconProgress(boolean updateWithAttention) {
    final int totalTestCount, doneTestCount;
    if (myTotalTestCount == 0) {
      totalTestCount = 2;
      doneTestCount = 1;
    } else {
      totalTestCount = myTotalTestCount;
      doneTestCount = myFinishedTestCount;
    }
    TestsUIUtil.showIconProgress(
        myProject, doneTestCount, totalTestCount, myFailedTestCount, updateWithAttention);
  }

  /**
   * On event change selection and probably requests focus. Is used when we want navigate from other
   * component to this
   *
   * @return Listener
   */
  public PropagateSelectionHandler createSelectMeListener() {
    return new PropagateSelectionHandler() {
      public void handlePropagateSelectionRequest(
          @Nullable final SMTestProxy selectedTestProxy,
          @NotNull final Object sender,
          final boolean requestFocus) {
        SMRunnerUtil.addToInvokeLater(
            new Runnable() {
              public void run() {
                selectWithoutNotify(selectedTestProxy, null);

                // Request focus if necessary
                if (requestFocus) {
                  // myTreeView.requestFocusInWindow();
                  IdeFocusManager.getInstance(myProject).requestFocus(myTreeView, true);
                }
              }
            });
      }
    };
  }

  private void updateCountersAndProgressOnTestCount(
      final int count, final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;

    // This is for better support groups of TestSuites
    // Each group notifies about it's size
    myTotalTestCount += count;
    updateStatusLabel(false);
  }

  private void updateOnTestStarted(final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;

    // for mixed tests results : mention category only if it contained tests
    myMentionedCategories.add(
        myCurrentCustomProgressCategory != null
            ? myCurrentCustomProgressCategory
            : TestsPresentationUtil.DEFAULT_TESTS_CATEGORY);

    myStartedTestCount++;

    // fix total count if it is corrupted
    // but if test count wasn't set at all let's process such case separately
    if (myStartedTestCount > myTotalTestCount && myTotalTestCount != 0) {
      myTotalTestCount = myStartedTestCount;
    }

    updateStatusLabel(false);
  }

  private void updateProgressOnTestDone() {
    int doneTestCount = myFinishedTestCount;
    // update progress
    if (myTotalTestCount != 0) {
      // if total is set
      myStatusLine.setFraction((double) doneTestCount / myTotalTestCount);
    } else {
      // if at least one test was launcher than just set progress in the middle to show user that
      // tests are running
      myStatusLine.setFraction(doneTestCount > 0 ? 0.5 : 0);
    }
  }

  private void updateOnTestFailed(final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;
    myFailedTestCount++;
    updateProgressOnTestDone();
    updateStatusLabel(false);
  }

  private void updateOnTestFinished(final boolean isCustomMessage) {
    if (!isModeConsistent(isCustomMessage)) return;
    myFinishedTestCount++;
    updateProgressOnTestDone();
  }

  private void updateOnTestIgnored() {
    myIgnoredTestCount++;
    updateProgressOnTestDone();
    updateStatusLabel(false);
  }

  private boolean isModeConsistent(boolean isCustomMessage) {
    // check that we are in consistent mode
    return isCustomMessage != (myCurrentCustomProgressCategory == null);
  }

  private static class MySaveHistoryTask extends Task.Backgroundable {

    private final TestConsoleProperties myConsoleProperties;
    private SMTestProxy.SMRootTestProxy myRoot;
    private RunConfiguration myConfiguration;
    private File myOutputFile;

    public MySaveHistoryTask(
        TestConsoleProperties consoleProperties,
        SMTestProxy.SMRootTestProxy root,
        RunConfiguration configuration) {
      super(consoleProperties.getProject(), "Save Test Results", true);
      myConsoleProperties = consoleProperties;
      myRoot = root;
      myConfiguration = configuration;
    }

    @Override
    public void run(@NotNull ProgressIndicator indicator) {
      try {
        SAXTransformerFactory transformerFactory =
            (SAXTransformerFactory) TransformerFactory.newInstance();
        TransformerHandler handler = transformerFactory.newTransformerHandler();
        handler.getTransformer().setOutputProperty(OutputKeys.INDENT, "yes");
        handler
            .getTransformer()
            .setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

        final String configurationNameIncludedDate =
            PathUtil.suggestFileName(myConfiguration.getName())
                + " - "
                + new SimpleDateFormat(HISTORY_DATE_FORMAT).format(new Date());

        myOutputFile =
            new File(
                AbstractImportTestsAction.getTestHistoryRoot(myProject),
                configurationNameIncludedDate + ".xml");
        FileUtilRt.createParentDirs(myOutputFile);
        handler.setResult(new StreamResult(new FileWriter(myOutputFile)));
        final SMTestProxy.SMRootTestProxy root = myRoot;
        final RunConfiguration configuration = myConfiguration;
        if (root != null && configuration != null) {
          TestResultsXmlFormatter.execute(root, configuration, myConsoleProperties, handler);
        }
      } catch (ProcessCanceledException e) {
        throw e;
      } catch (Exception e) {
        LOG.info("Export to history failed", e);
      }
    }

    @Override
    public void onSuccess() {
      if (myOutputFile != null && myOutputFile.exists()) {
        AbstractImportTestsAction.adjustHistory(myProject);
        TestHistoryConfiguration.getInstance(myProject)
            .registerHistoryItem(
                myOutputFile.getName(),
                myConfiguration.getName(),
                myConfiguration.getType().getId());
      }
    }

    public void dispose() {
      myConfiguration = null;
      myRoot = null;
      myOutputFile = null;
    }
  }
}
public class PsiManagerImpl extends PsiManagerEx {
  private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiManagerImpl");

  private final Project myProject;
  private final FileIndexFacade myExcludedFileIndex;
  private final MessageBus myMessageBus;
  private final PsiModificationTracker myModificationTracker;

  private final FileManager myFileManager;

  private final List<PsiTreeChangePreprocessor> myTreeChangePreprocessors =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private final List<PsiTreeChangeListener> myTreeChangeListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private boolean myTreeChangeEventIsFiring = false;

  private boolean myIsDisposed;

  private VirtualFileFilter myAssertOnFileLoadingFilter = VirtualFileFilter.NONE;

  private final AtomicInteger myBatchFilesProcessingModeCount = new AtomicInteger(0);

  public static final Topic<AnyPsiChangeListener> ANY_PSI_CHANGE_TOPIC =
      Topic.create(
          "ANY_PSI_CHANGE_TOPIC", AnyPsiChangeListener.class, Topic.BroadcastDirection.TO_PARENT);

  public PsiManagerImpl(
      Project project,
      FileDocumentManager fileDocumentManager,
      PsiBuilderFactory psiBuilderFactory,
      FileIndexFacade excludedFileIndex,
      MessageBus messageBus,
      PsiModificationTracker modificationTracker) {
    myProject = project;
    myExcludedFileIndex = excludedFileIndex;
    myMessageBus = messageBus;
    myModificationTracker = modificationTracker;

    // We need to initialize PsiBuilderFactory service so it won't initialize under PsiLock from
    // ChameleonTransform
    @SuppressWarnings({"UnusedDeclaration", "UnnecessaryLocalVariable"})
    Object used = psiBuilderFactory;

    boolean isProjectDefault = project.isDefault();

    myFileManager =
        isProjectDefault
            ? new EmptyFileManager(this)
            : new FileManagerImpl(this, fileDocumentManager, excludedFileIndex);

    myTreeChangePreprocessors.add((PsiTreeChangePreprocessor) modificationTracker);
    Collections.addAll(
        myTreeChangePreprocessors,
        Extensions.getExtensions(PsiTreeChangePreprocessor.EP_NAME, myProject));

    Disposer.register(
        project,
        new Disposable() {
          @Override
          public void dispose() {
            myIsDisposed = true;
          }
        });
  }

  @Override
  public boolean isDisposed() {
    return myIsDisposed;
  }

  @Override
  public void dropResolveCaches() {
    FileManager fileManager = myFileManager;
    if (fileManager instanceof FileManagerImpl) { // mock tests
      ((FileManagerImpl) fileManager).processQueue();
    }
    beforeChange(true);
    beforeChange(false);
  }

  @Override
  public boolean isInProject(@NotNull PsiElement element) {
    PsiFile file = element.getContainingFile();
    if (file != null
        && file.isPhysical()
        && file.getViewProvider().getVirtualFile() instanceof LightVirtualFile) return true;

    if (element instanceof PsiDirectoryContainer) {
      PsiDirectory[] dirs = ((PsiDirectoryContainer) element).getDirectories();
      for (PsiDirectory dir : dirs) {
        if (!isInProject(dir)) return false;
      }
      return true;
    }

    VirtualFile virtualFile = null;
    if (file != null) {
      virtualFile = file.getViewProvider().getVirtualFile();
    } else if (element instanceof PsiFileSystemItem) {
      virtualFile = ((PsiFileSystemItem) element).getVirtualFile();
    }

    if (virtualFile != null) {
      return myExcludedFileIndex.isInContent(virtualFile);
    }
    return false;
  }

  @TestOnly
  public void setAssertOnFileLoadingFilter(
      @NotNull VirtualFileFilter filter, @NotNull Disposable parentDisposable) {
    // Find something to ensure there's no changed files waiting to be processed in repository
    // indices.
    myAssertOnFileLoadingFilter = filter;
    Disposer.register(
        parentDisposable,
        new Disposable() {
          @Override
          public void dispose() {
            myAssertOnFileLoadingFilter = VirtualFileFilter.NONE;
          }
        });
  }

  @Override
  public boolean isAssertOnFileLoading(@NotNull VirtualFile file) {
    return myAssertOnFileLoadingFilter.accept(file);
  }

  @Override
  @NotNull
  public Project getProject() {
    return myProject;
  }

  @Override
  @NotNull
  public FileManager getFileManager() {
    return myFileManager;
  }

  @Override
  public boolean areElementsEquivalent(PsiElement element1, PsiElement element2) {
    ProgressIndicatorProvider
        .checkCanceled(); // We hope this method is being called often enough to cancel daemon
                          // processes smoothly

    if (element1 == element2) return true;
    if (element1 == null || element2 == null) {
      return false;
    }

    return element1.equals(element2)
        || element1.isEquivalentTo(element2)
        || element2.isEquivalentTo(element1);
  }

  @Override
  public PsiFile findFile(@NotNull VirtualFile file) {
    return myFileManager.findFile(file);
  }

  @Override
  @Nullable
  public FileViewProvider findViewProvider(@NotNull VirtualFile file) {
    return myFileManager.findViewProvider(file);
  }

  @TestOnly
  public void cleanupForNextTest() {
    myFileManager.cleanupForNextTest();
    LOG.assertTrue(ApplicationManager.getApplication().isUnitTestMode());
  }

  @Override
  public PsiDirectory findDirectory(@NotNull VirtualFile file) {
    ProgressIndicatorProvider.checkCanceled();

    return myFileManager.findDirectory(file);
  }

  @Override
  public void reloadFromDisk(@NotNull PsiFile file) {
    myFileManager.reloadFromDisk(file);
  }

  @Override
  public void addPsiTreeChangeListener(@NotNull PsiTreeChangeListener listener) {
    myTreeChangeListeners.add(listener);
  }

  @Override
  public void addPsiTreeChangeListener(
      @NotNull final PsiTreeChangeListener listener, @NotNull Disposable parentDisposable) {
    addPsiTreeChangeListener(listener);
    Disposer.register(
        parentDisposable,
        new Disposable() {
          @Override
          public void dispose() {
            removePsiTreeChangeListener(listener);
          }
        });
  }

  @Override
  public void removePsiTreeChangeListener(@NotNull PsiTreeChangeListener listener) {
    myTreeChangeListeners.remove(listener);
  }

  @Override
  public void beforeChildAddition(@NotNull PsiTreeChangeEventImpl event) {
    beforeChange(true);
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_ADDITION);
    if (LOG.isDebugEnabled()) {
      LOG.debug("beforeChildAddition: parent = " + event.getParent());
    }
    fireEvent(event);
  }

  @Override
  public void beforeChildRemoval(@NotNull PsiTreeChangeEventImpl event) {
    beforeChange(true);
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_REMOVAL);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "beforeChildRemoval: child = " + event.getChild() + ", parent = " + event.getParent());
    }
    fireEvent(event);
  }

  @Override
  public void beforeChildReplacement(@NotNull PsiTreeChangeEventImpl event) {
    beforeChange(true);
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_REPLACEMENT);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "beforeChildReplacement: oldChild = "
              + event.getOldChild()
              + ", parent = "
              + event.getParent());
    }
    fireEvent(event);
  }

  public void beforeChildrenChange(@NotNull PsiTreeChangeEventImpl event) {
    beforeChange(true);
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILDREN_CHANGE);
    if (LOG.isDebugEnabled()) {
      LOG.debug("beforeChildrenChange: parent = " + event.getParent());
    }
    fireEvent(event);
  }

  public void beforeChildMovement(@NotNull PsiTreeChangeEventImpl event) {
    beforeChange(true);
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_MOVEMENT);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "beforeChildMovement: child = "
              + event.getChild()
              + ", oldParent = "
              + event.getOldParent()
              + ", newParent = "
              + event.getNewParent());
    }
    fireEvent(event);
  }

  public void beforePropertyChange(@NotNull PsiTreeChangeEventImpl event) {
    beforeChange(true);
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.BEFORE_PROPERTY_CHANGE);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "beforePropertyChange: element = "
              + event.getElement()
              + ", propertyName = "
              + event.getPropertyName()
              + ", oldValue = "
              + event.getOldValue());
    }
    fireEvent(event);
  }

  public void childAdded(@NotNull PsiTreeChangeEventImpl event) {
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.CHILD_ADDED);
    if (LOG.isDebugEnabled()) {
      LOG.debug("childAdded: child = " + event.getChild() + ", parent = " + event.getParent());
    }
    fireEvent(event);
    afterChange(true);
  }

  public void childRemoved(@NotNull PsiTreeChangeEventImpl event) {
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.CHILD_REMOVED);
    if (LOG.isDebugEnabled()) {
      LOG.debug("childRemoved: child = " + event.getChild() + ", parent = " + event.getParent());
    }
    fireEvent(event);
    afterChange(true);
  }

  public void childReplaced(@NotNull PsiTreeChangeEventImpl event) {
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.CHILD_REPLACED);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "childReplaced: oldChild = "
              + event.getOldChild()
              + ", newChild = "
              + event.getNewChild()
              + ", parent = "
              + event.getParent());
    }
    fireEvent(event);
    afterChange(true);
  }

  public void childMoved(@NotNull PsiTreeChangeEventImpl event) {
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.CHILD_MOVED);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "childMoved: child = "
              + event.getChild()
              + ", oldParent = "
              + event.getOldParent()
              + ", newParent = "
              + event.getNewParent());
    }
    fireEvent(event);
    afterChange(true);
  }

  public void childrenChanged(@NotNull PsiTreeChangeEventImpl event) {
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.CHILDREN_CHANGED);
    if (LOG.isDebugEnabled()) {
      LOG.debug("childrenChanged: parent = " + event.getParent());
    }
    fireEvent(event);
    afterChange(true);
  }

  public void propertyChanged(@NotNull PsiTreeChangeEventImpl event) {
    event.setCode(PsiTreeChangeEventImpl.PsiEventType.PROPERTY_CHANGED);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "propertyChanged: element = "
              + event.getElement()
              + ", propertyName = "
              + event.getPropertyName()
              + ", oldValue = "
              + event.getOldValue()
              + ", newValue = "
              + event.getNewValue());
    }
    fireEvent(event);
    afterChange(true);
  }

  public void addTreeChangePreprocessor(@NotNull PsiTreeChangePreprocessor preprocessor) {
    myTreeChangePreprocessors.add(preprocessor);
  }

  private void fireEvent(@NotNull PsiTreeChangeEventImpl event) {
    boolean isRealTreeChange =
        event.getCode() != PsiTreeChangeEventImpl.PsiEventType.PROPERTY_CHANGED
            && event.getCode() != PsiTreeChangeEventImpl.PsiEventType.BEFORE_PROPERTY_CHANGE;

    PsiFile file = event.getFile();
    if (file == null || file.isPhysical()) {
      ApplicationManager.getApplication().assertWriteAccessAllowed();
    }
    if (isRealTreeChange) {
      LOG.assertTrue(
          !myTreeChangeEventIsFiring, "Changes to PSI are not allowed inside event processing");
      myTreeChangeEventIsFiring = true;
    }
    try {
      for (PsiTreeChangePreprocessor preprocessor : myTreeChangePreprocessors) {
        preprocessor.treeChanged(event);
      }

      for (PsiTreeChangeListener listener : myTreeChangeListeners) {
        try {
          switch (event.getCode()) {
            case BEFORE_CHILD_ADDITION:
              listener.beforeChildAddition(event);
              break;

            case BEFORE_CHILD_REMOVAL:
              listener.beforeChildRemoval(event);
              break;

            case BEFORE_CHILD_REPLACEMENT:
              listener.beforeChildReplacement(event);
              break;

            case BEFORE_CHILD_MOVEMENT:
              listener.beforeChildMovement(event);
              break;

            case BEFORE_CHILDREN_CHANGE:
              listener.beforeChildrenChange(event);
              break;

            case BEFORE_PROPERTY_CHANGE:
              listener.beforePropertyChange(event);
              break;

            case CHILD_ADDED:
              listener.childAdded(event);
              break;

            case CHILD_REMOVED:
              listener.childRemoved(event);
              break;

            case CHILD_REPLACED:
              listener.childReplaced(event);
              break;

            case CHILD_MOVED:
              listener.childMoved(event);
              break;

            case CHILDREN_CHANGED:
              listener.childrenChanged(event);
              break;

            case PROPERTY_CHANGED:
              listener.propertyChanged(event);
              break;
          }
        } catch (Exception e) {
          LOG.error(e);
        }
      }
    } finally {
      if (isRealTreeChange) {
        myTreeChangeEventIsFiring = false;
      }
    }
  }

  @Override
  public void registerRunnableToRunOnChange(@NotNull final Runnable runnable) {
    myMessageBus
        .connect()
        .subscribe(
            ANY_PSI_CHANGE_TOPIC,
            new AnyPsiChangeListener() {
              @Override
              public void beforePsiChanged(boolean isPhysical) {
                if (isPhysical) runnable.run();
              }

              @Override
              public void afterPsiChanged(boolean isPhysical) {}
            });
  }

  @Override
  public void registerRunnableToRunOnAnyChange(
      @NotNull final Runnable runnable) { // includes non-physical changes
    myMessageBus
        .connect()
        .subscribe(
            ANY_PSI_CHANGE_TOPIC,
            new AnyPsiChangeListener() {
              @Override
              public void beforePsiChanged(boolean isPhysical) {
                runnable.run();
              }

              @Override
              public void afterPsiChanged(boolean isPhysical) {}
            });
  }

  @Override
  public void registerRunnableToRunAfterAnyChange(
      @NotNull final Runnable runnable) { // includes non-physical changes
    myMessageBus
        .connect()
        .subscribe(
            ANY_PSI_CHANGE_TOPIC,
            new AnyPsiChangeListener() {
              @Override
              public void beforePsiChanged(boolean isPhysical) {}

              @Override
              public void afterPsiChanged(boolean isPhysical) {
                runnable.run();
              }
            });
  }

  @Override
  public void beforeChange(boolean isPhysical) {
    myMessageBus.syncPublisher(ANY_PSI_CHANGE_TOPIC).beforePsiChanged(isPhysical);
  }

  @Override
  public void afterChange(boolean isPhysical) {
    myMessageBus.syncPublisher(ANY_PSI_CHANGE_TOPIC).afterPsiChanged(isPhysical);
  }

  @Override
  @NotNull
  public PsiModificationTracker getModificationTracker() {
    return myModificationTracker;
  }

  @Override
  public void startBatchFilesProcessingMode() {
    myBatchFilesProcessingModeCount.incrementAndGet();
  }

  @Override
  public void finishBatchFilesProcessingMode() {
    myBatchFilesProcessingModeCount.decrementAndGet();
    LOG.assertTrue(myBatchFilesProcessingModeCount.get() >= 0);
  }

  @Override
  public boolean isBatchFilesProcessingMode() {
    return myBatchFilesProcessingModeCount.get() > 0;
  }
}
 @Override
 protected List<BaseInjection> create(final String key) {
   return ContainerUtil.createLockFreeCopyOnWriteList();
 }
public class RefResolveServiceImpl extends RefResolveService implements Runnable, Disposable {
  private static final Logger LOG = Logger.getInstance(RefResolveServiceImpl.class);
  private final AtomicInteger fileCount = new AtomicInteger();
  private final AtomicLong bytesSize = new AtomicLong();
  private final AtomicLong refCount = new AtomicLong();
  private final PersistentIntList storage;
  private final Deque<VirtualFile> filesToResolve =
      new ArrayDeque<VirtualFile>(); // guarded by filesToResolve
  private final ConcurrentBitSet fileIsInQueue = new ConcurrentBitSet();
  private final ConcurrentBitSet fileIsResolved;
  private final ApplicationEx myApplication;
  private volatile boolean myDisposed;
  private volatile boolean upToDate;
  private final AtomicInteger enableVetoes =
      new AtomicInteger(); // number of disable() calls. To enable the service, there should be at
                           // least corresponding number of enable() calls.
  private final FileWriter log;
  private final ProjectFileIndex myProjectFileIndex;

  public RefResolveServiceImpl(
      final Project project,
      final MessageBus messageBus,
      final PsiManager psiManager,
      StartupManager startupManager,
      ApplicationEx application,
      ProjectFileIndex projectFileIndex)
      throws IOException {
    super(project);
    ((FutureTask) resolveProcess).run();
    myApplication = application;
    myProjectFileIndex = projectFileIndex;
    if (ENABLED) {
      log = new FileWriter(new File(getStorageDirectory(), "log.txt"));

      File dataFile = new File(getStorageDirectory(), "data");
      fileIsResolved = ConcurrentBitSet.readFrom(new File(getStorageDirectory(), "bitSet"));
      log("Read resolved file bitset: " + fileIsResolved);

      int maxId = FSRecords.getMaxId();
      PersistentIntList list = new PersistentIntList(dataFile, dataFile.exists() ? 0 : maxId);
      if (list.getSize() == maxId) {
        storage = list;
      } else {
        // just to be safe, re-resolve all if VFS files count changes since last restart
        list.dispose();
        storage = new PersistentIntList(dataFile, maxId);
        log(
            "VFS maxId changed: was "
                + list.getSize()
                + "; now: "
                + maxId
                + "; re-resolving everything");
        fileIsResolved.clear();
      }
      Disposer.register(this, storage);
      if (!application.isUnitTestMode()) {
        startupManager.runWhenProjectIsInitialized(
            () -> {
              initListeners(messageBus, psiManager);
              startThread();
            });
      }
      Disposer.register(
          this,
          new Disposable() {
            @Override
            public void dispose() {
              try {
                save();
                log.close();
              } catch (IOException e) {
                LOG.error(e);
              }
            }
          });
    } else {
      log = null;
      fileIsResolved = null;
      storage = null;
    }
  }

  @NotNull
  private static List<VirtualFile> toVf(@NotNull int[] ids) {
    List<VirtualFile> res = new ArrayList<VirtualFile>();
    for (int id : ids) {
      VirtualFile file = PersistentFS.getInstance().findFileById(id);
      if (file != null) {
        res.add(file);
      }
    }
    return res;
  }

  @NotNull
  private static String toVfString(@NotNull int[] backIds) {
    List<VirtualFile> list = toVf(backIds);
    return toVfString(list);
  }

  @NotNull
  private static String toVfString(@NotNull Collection<VirtualFile> list) {
    List<VirtualFile> sub = new ArrayList<VirtualFile>(list).subList(0, Math.min(list.size(), 100));
    return list.size()
        + " files: "
        + StringUtil.join(sub, file -> file.getName(), ", ")
        + (list.size() == sub.size() ? "" : "...");
  }

  private void initListeners(@NotNull MessageBus messageBus, @NotNull PsiManager psiManager) {
    messageBus
        .connect()
        .subscribe(
            VirtualFileManager.VFS_CHANGES,
            new BulkFileListener.Adapter() {
              @Override
              public void after(@NotNull List<? extends VFileEvent> events) {
                fileCount.set(0);
                List<VirtualFile> files =
                    ContainerUtil.mapNotNull(
                        events,
                        new Function<VFileEvent, VirtualFile>() {
                          @Override
                          public VirtualFile fun(VFileEvent event) {
                            return event.getFile();
                          }
                        });
                queue(files, "VFS events " + events.size());
              }
            });
    psiManager.addPsiTreeChangeListener(
        new PsiTreeChangeAdapter() {
          @Override
          public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
            PsiFile file = event.getFile();
            VirtualFile virtualFile = PsiUtilCore.getVirtualFile(file);
            if (virtualFile != null) {
              queue(Collections.singletonList(virtualFile), event);
            }
          }

          @Override
          public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
            childrenChanged(event);
          }
        });

    messageBus
        .connect()
        .subscribe(
            DumbService.DUMB_MODE,
            new DumbService.DumbModeListener() {
              @Override
              public void enteredDumbMode() {
                disable();
              }

              @Override
              public void exitDumbMode() {
                enable();
              }
            });
    messageBus
        .connect()
        .subscribe(
            PowerSaveMode.TOPIC,
            new PowerSaveMode.Listener() {
              @Override
              public void powerSaveStateChanged() {
                if (PowerSaveMode.isEnabled()) {
                  enable();
                } else {
                  disable();
                }
              }
            });
    myApplication.addApplicationListener(
        new ApplicationAdapter() {
          @Override
          public void beforeWriteActionStart(@NotNull Object action) {
            disable();
          }

          @Override
          public void writeActionFinished(@NotNull Object action) {
            enable();
          }

          @Override
          public void applicationExiting() {
            disable();
          }
        },
        this);
    VirtualFileManager.getInstance()
        .addVirtualFileManagerListener(
            new VirtualFileManagerListener() {
              @Override
              public void beforeRefreshStart(boolean asynchronous) {
                disable();
              }

              @Override
              public void afterRefreshFinish(boolean asynchronous) {
                enable();
              }
            },
            this);
    HeavyProcessLatch.INSTANCE.addListener(
        new HeavyProcessLatch.HeavyProcessListener() {
          @Override
          public void processStarted() {}

          @Override
          public void processFinished() {
            wakeUp();
          }
        },
        this);
  }

  // return true if file was added to queue
  private boolean queueIfNeeded(VirtualFile virtualFile, @NotNull Project project) {
    return toResolve(virtualFile, project) && queueUpdate(virtualFile);
  }

  private boolean toResolve(VirtualFile virtualFile, @NotNull Project project) {
    if (virtualFile != null
        && virtualFile.isValid()
        && project.isInitialized()
        && myProjectFileIndex.isInSourceContent(virtualFile)
        && isSupportedFileType(virtualFile)) {
      return true;
    }

    // else mark it as resolved so we will not have to check it again
    if (virtualFile instanceof VirtualFileWithId) {
      int id = getAbsId(virtualFile);
      fileIsResolved.set(id);
    }

    return false;
  }

  public static boolean isSupportedFileType(@NotNull VirtualFile virtualFile) {
    if (virtualFile.isDirectory()) return true;
    if (virtualFile.getFileType() == StdFileTypes.JAVA) return true;
    if (virtualFile.getFileType() == StdFileTypes.XML
        && !ProjectCoreUtil.isProjectOrWorkspaceFile(virtualFile)) return true;
    if ("groovy".equals(virtualFile.getExtension())) return true;
    return false;
  }

  @NotNull
  private File getStorageDirectory() {
    String dirName =
        myProject.getName() + "." + Integer.toHexString(myProject.getPresentableUrl().hashCode());
    File dir = new File(PathManager.getSystemPath(), "refs/" + dirName);
    FileUtil.createDirectory(dir);
    return dir;
  }

  private void log(String m) {
    // System.out.println(m);
    logf(m);
  }

  private void logf(String m) {
    if (LOG.isDebugEnabled()) {
      try {
        log.write(
            DateFormat.getDateTimeInstance().format(new Date())
                + " "
                + m
                + /*"    ; gap="+storage.gap+*/ "\n");
      } catch (IOException e) {
        LOG.error(e);
      }
    }
  }

  private void flushLog() {
    try {
      log.flush();
    } catch (IOException e) {
      LOG.error(e);
    }
  }

  // return true if file was added to queue
  private boolean queueUpdate(@NotNull VirtualFile file) {
    synchronized (filesToResolve) {
      if (!(file instanceof VirtualFileWithId)) return false;
      int fileId = getAbsId(file);
      countAndMarkUnresolved(file, new LinkedHashSet<VirtualFile>(), true);
      boolean alreadyAdded = fileIsInQueue.set(fileId);
      if (!alreadyAdded) {
        filesToResolve.add(file);
      }
      upToDate = false;
      wakeUpUnderLock();
      return !alreadyAdded;
    }
  }

  private void wakeUp() {
    synchronized (filesToResolve) {
      wakeUpUnderLock();
    }
  }

  private void wakeUpUnderLock() {
    filesToResolve.notifyAll();
  }

  private void waitForQueue() throws InterruptedException {
    synchronized (filesToResolve) {
      filesToResolve.wait(1000);
    }
  }

  private void startThread() {
    new Thread(this, "Ref resolve service").start();
    upToDate = true;
    queueUnresolvedFilesSinceLastRestart();
  }

  private void queueUnresolvedFilesSinceLastRestart() {
    PersistentFS fs = PersistentFS.getInstance();
    int maxId = FSRecords.getMaxId();
    TIntArrayList list = new TIntArrayList();
    for (int id = fileIsResolved.nextClearBit(1);
        id >= 0 && id < maxId;
        id = fileIsResolved.nextClearBit(id + 1)) {
      int nextSetBit = fileIsResolved.nextSetBit(id);
      int endOfRun = Math.min(maxId, nextSetBit == -1 ? maxId : nextSetBit);
      do {
        VirtualFile virtualFile = fs.findFileById(id);
        if (queueIfNeeded(virtualFile, myProject)) {
          list.add(id);
        } else {
          fileIsResolved.set(id);
        }
      } while (++id < endOfRun);
    }
    log("Initially added to resolve " + toVfString(list.toNativeArray()));
  }

  @Override
  public void dispose() {
    myDisposed = true;
  }

  private void save() throws IOException {
    log("Saving resolved file bitset: " + fileIsResolved);
    fileIsResolved.writeTo(new File(getStorageDirectory(), "bitSet"));
    log("list.size = " + storage.getSize());
  }

  private volatile Future<?> resolveProcess =
      new FutureTask<Object>(EmptyRunnable.getInstance(), null); // write from EDT only

  @Override
  public void run() {
    while (!myDisposed) {
      boolean isEmpty;
      synchronized (filesToResolve) {
        isEmpty = filesToResolve.isEmpty();
      }
      if (enableVetoes.get() > 0
          || isEmpty
          || !resolveProcess.isDone()
          || HeavyProcessLatch.INSTANCE.isRunning()
          || PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments()) {
        try {
          waitForQueue();
        } catch (InterruptedException e) {
          break;
        }
        continue;
      }
      final Set<VirtualFile> files = pollFilesToResolve();
      if (files.isEmpty()) continue;

      upToDate = false;

      myApplication.invokeLater(
          () -> {
            if (!resolveProcess.isDone()) return;
            log("Started to resolve " + files.size() + " files");

            Task.Backgroundable backgroundable =
                new Task.Backgroundable(myProject, "Resolving files...", false) {
                  @Override
                  public void run(@NotNull final ProgressIndicator indicator) {
                    if (!myApplication.isDisposed()) {
                      processBatch(indicator, files);
                    }
                  }
                };
            ProgressIndicator indicator;
            if (files.size() > 1) {
              // show progress
              indicator = new BackgroundableProcessIndicator(backgroundable);
            } else {
              indicator = new MyProgress();
            }
            resolveProcess =
                ((ProgressManagerImpl) ProgressManager.getInstance())
                    .runProcessWithProgressAsynchronously(backgroundable, indicator, null);
          },
          myProject.getDisposed());

      flushLog();
    }
  }

  private volatile int resolvedInPreviousBatch;

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

      long end = System.currentTimeMillis();
      log(
          "Resolved batch of "
              + (totalSize - toProcess.size())
              + " from "
              + totalSize
              + " files in "
              + ((end - start) / 1000)
              + "sec. (Gap: "
              + storage.gap
              + ")");
      synchronized (filesToResolve) {
        upToDate = filesToResolve.isEmpty();
        log("upToDate = " + upToDate);
        if (upToDate) {
          for (Listener listener : myListeners) {
            listener.allFilesResolved();
          }
        }
      }
    }
  }

  private boolean processFilesConcurrently(
      @NotNull Set<VirtualFile> files,
      @NotNull final ProgressIndicator indicator,
      @NotNull final Processor<VirtualFile> processor) {
    final List<VirtualFile> fileList = new ArrayList<VirtualFile>(files);
    // fine but grabs all CPUs
    // return JobLauncher.getInstance().invokeConcurrentlyUnderProgress(fileList, indicator, false,
    // false, processor);

    int parallelism = CacheUpdateRunner.indexingThreadCount();
    final Callable<Boolean> processFileFromSet =
        () -> {
          final boolean[] result = {true};
          ProgressManager.getInstance()
              .executeProcessUnderProgress(
                  () -> {
                    while (true) {
                      ProgressManager.checkCanceled();
                      VirtualFile file;
                      synchronized (fileList) {
                        file = fileList.isEmpty() ? null : fileList.remove(fileList.size() - 1);
                      }
                      if (file == null) {
                        break;
                      }
                      if (!processor.process(file)) {
                        result[0] = false;
                        break;
                      }
                    }
                  },
                  indicator);
          return result[0];
        };
    List<Future<Boolean>> futures =
        ContainerUtil.map(
            Collections.nCopies(parallelism, ""),
            s -> myApplication.executeOnPooledThread(processFileFromSet));

    List<Boolean> results =
        ContainerUtil.map(
            futures,
            future -> {
              try {
                return future.get();
              } catch (Exception e) {
                LOG.error(e);
              }
              return false;
            });

    return !ContainerUtil.exists(
        results,
        result -> {
          return result != null && !result; // null means PCE
        });
  }

  @NotNull
  private Set<VirtualFile> pollFilesToResolve() {
    Set<VirtualFile> set;
    synchronized (filesToResolve) {
      int queuedSize = filesToResolve.size();
      set = new LinkedHashSet<VirtualFile>(queuedSize);
      // someone might have cleared this bit to mark file as processed
      for (VirtualFile file : filesToResolve) {
        if (fileIsInQueue.clear(getAbsId(file))) {
          set.add(file);
        }
      }
      filesToResolve.clear();
    }
    return countAndMarkUnresolved(set, false);
  }

  private static int getAbsId(@NotNull VirtualFile file) {
    return Math.abs(((VirtualFileWithId) file).getId());
  }

  @NotNull
  private Set<VirtualFile> countAndMarkUnresolved(
      @NotNull Collection<VirtualFile> files, boolean inDbOnly) {
    Set<VirtualFile> result = new LinkedHashSet<VirtualFile>();
    for (VirtualFile file : files) {
      countAndMarkUnresolved(file, result, inDbOnly);
    }
    return result;
  }

  private void countAndMarkUnresolved(
      @NotNull VirtualFile file, @NotNull final Set<VirtualFile> result, final boolean inDbOnly) {
    if (file.isDirectory()) {
      VfsUtilCore.visitChildrenRecursively(
          file,
          new VirtualFileVisitor() {
            @Override
            public boolean visitFile(@NotNull VirtualFile file) {
              return doCountAndMarkUnresolved(file, result);
            }

            @Nullable
            @Override
            public Iterable<VirtualFile> getChildrenIterable(@NotNull VirtualFile file) {
              return inDbOnly ? ((NewVirtualFile) file).iterInDbChildren() : null;
            }
          });
    } else {
      doCountAndMarkUnresolved(file, result);
    }
  }

  // return true if continue to process sub-directories of the {@code file}, false if the file is
  // already processed
  private boolean doCountAndMarkUnresolved(
      @NotNull VirtualFile file, @NotNull Set<VirtualFile> result) {
    if (file.isDirectory()) {
      fileIsResolved.set(getAbsId(file));
      return result.add(file);
    }
    if (toResolve(file, myProject)) {
      result.add(file);
      fileIsResolved.clear(getAbsId(file));
    }
    return true;
  }

  private void enable() {
    // decrement but only if it's positive
    int vetoes;
    do {
      vetoes = enableVetoes.get();
      if (vetoes == 0) break;
    } while (!enableVetoes.compareAndSet(vetoes, vetoes - 1));
    wakeUp();
  }

  private void disable() {
    enableVetoes.incrementAndGet();
    wakeUp();
  }

  // returns list of resolved files if updated successfully, or null if write action or dumb mode
  // started
  private int[] processFile(
      @NotNull final VirtualFile file, int fileId, @NotNull final ProgressIndicator indicator) {
    final TIntHashSet forward;
    try {
      forward = calcForwardRefs(file, indicator);
    } catch (IndexNotReadyException e) {
      return null;
    } catch (ApplicationUtil.CannotRunReadActionException e) {
      return null;
    } catch (ProcessCanceledException e) {
      throw e;
    } catch (Exception e) {
      log(ExceptionUtil.getThrowableText(e));
      flushLog();
      return null;
    }

    int[] forwardIds = forward.toArray();
    fileIsResolved.set(fileId);
    logf(
        "  ---- " + file.getPresentableUrl() + " processed. forwardIds: " + toVfString(forwardIds));
    for (Listener listener : myListeners) {
      listener.fileResolved(file);
    }
    return forwardIds;
  }

  private void storeIds(@NotNull ConcurrentIntObjectMap<int[]> fileToForwardIds) {
    int forwardSize = 0;
    int backwardSize = 0;
    final TIntObjectHashMap<TIntArrayList> fileToBackwardIds =
        new TIntObjectHashMap<TIntArrayList>(fileToForwardIds.size());
    for (ConcurrentIntObjectMap.IntEntry<int[]> entry : fileToForwardIds.entries()) {
      int fileId = entry.getKey();
      int[] forwardIds = entry.getValue();
      forwardSize += forwardIds.length;
      for (int forwardId : forwardIds) {
        TIntArrayList backIds = fileToBackwardIds.get(forwardId);
        if (backIds == null) {
          backIds = new TIntArrayList();
          fileToBackwardIds.put(forwardId, backIds);
        }
        backIds.add(fileId);
        backwardSize++;
      }
    }
    log("backwardSize = " + backwardSize);
    log("forwardSize = " + forwardSize);
    log("fileToForwardIds.size() = " + fileToForwardIds.size());
    log("fileToBackwardIds.size() = " + fileToBackwardIds.size());
    assert forwardSize == backwardSize;

    // wrap in read action so that sudden quit (in write action) would not interrupt us
    myApplication.runReadAction(
        () -> {
          if (!myApplication.isDisposed()) {
            fileToBackwardIds.forEachEntry(
                new TIntObjectProcedure<TIntArrayList>() {
                  @Override
                  public boolean execute(int fileId, TIntArrayList backIds) {
                    storage.addAll(fileId, backIds.toNativeArray());
                    return true;
                  }
                });
          }
        });
  }

  @NotNull
  private TIntHashSet calcForwardRefs(
      @NotNull final VirtualFile virtualFile, @NotNull final ProgressIndicator indicator)
      throws IndexNotReadyException, ApplicationUtil.CannotRunReadActionException {

    final TIntHashSet forward = new TIntHashSet();

    final PsiFile psiFile =
        ApplicationUtil.tryRunReadAction(
            () -> {
              if (myProject.isDisposed()) throw new ProcessCanceledException();
              if (fileCount.incrementAndGet() % 100 == 0) {
                PsiManager.getInstance(myProject).dropResolveCaches();
                try {
                  storage.flush();
                  log.flush();
                } catch (IOException e) {
                  LOG.error(e);
                }
              }

              return PsiManager.getInstance(myProject).findFile(virtualFile);
            });
    final int fileId = getAbsId(virtualFile);
    if (psiFile != null) {
      bytesSize.addAndGet(virtualFile.getLength());
      final Set<PsiElement> resolved = new THashSet<PsiElement>();
      ApplicationUtil.tryRunReadAction(
          new Runnable() {
            @Override
            public void run() {
              indicator.checkCanceled();

              if (psiFile instanceof PsiJavaFile) {
                psiFile.accept(
                    new JavaRecursiveElementWalkingVisitor() {
                      @Override
                      public void visitReferenceElement(PsiJavaCodeReferenceElement reference) {
                        indicator.checkCanceled();
                        resolveReference(reference, resolved);

                        super.visitReferenceElement(reference);
                      }
                    });
              } else {
                psiFile.accept(
                    new PsiRecursiveElementWalkingVisitor() {
                      @Override
                      public void visitElement(PsiElement element) {
                        for (PsiReference reference : element.getReferences()) {
                          indicator.checkCanceled();
                          resolveReference(reference, resolved);
                        }
                        super.visitElement(element);
                      }
                    });
              }

              indicator.checkCanceled();
              for (PsiElement element : resolved) {
                PsiFile file = element.getContainingFile();
                addIdAndSuperClasses(file, forward);
              }
            }
          });
    }

    forward.remove(fileId);
    return forward;
  }

  private void resolveReference(
      @NotNull PsiReference reference, @NotNull Set<PsiElement> resolved) {
    PsiElement element = reference.resolve();
    if (element != null) {
      resolved.add(element);
    }
    refCount.incrementAndGet();
  }

  private static void addIdAndSuperClasses(PsiFile file, @NotNull TIntHashSet forward) {
    if (file instanceof PsiJavaFile
        && file.getName().equals("Object.class")
        && ((PsiJavaFile) file).getPackageName().equals("java.lang")) {
      return;
    }
    VirtualFile virtualFile = PsiUtilCore.getVirtualFile(file);
    if (virtualFile instanceof VirtualFileWithId
        && forward.add(getAbsId(virtualFile))
        && file instanceof PsiClassOwner) {
      for (PsiClass aClass : ((PsiClassOwner) file).getClasses()) {
        for (PsiClass superClass : aClass.getSupers()) {
          addIdAndSuperClasses(superClass.getContainingFile(), forward);
        }
      }
    }
  }

  @Override
  @Nullable
  public int[] getBackwardIds(@NotNull VirtualFileWithId file) {
    if (!isUpToDate()) return null;
    int fileId = getAbsId((VirtualFile) file);
    return storage.get(fileId);
  }

  private String prevLog = "";
  private static final Set<JavaSourceRootType> SOURCE_ROOTS =
      ContainerUtil.newTroveSet(JavaSourceRootType.SOURCE, JavaSourceRootType.TEST_SOURCE);

  @NotNull
  @Override
  public GlobalSearchScope restrictByBackwardIds(
      @NotNull final VirtualFile virtualFile, @NotNull GlobalSearchScope scope) {
    final int[] backIds =
        RefResolveService.getInstance(myProject).getBackwardIds((VirtualFileWithId) virtualFile);
    if (backIds == null) {
      return scope;
    }
    String files = toVfString(backIds);
    String log = "Restricting scope of " + virtualFile.getName() + " to " + files;
    if (!log.equals(prevLog)) {
      log(log);
      flushLog();
      prevLog = log;
    }
    GlobalSearchScope restrictedByBackwardIds =
        new GlobalSearchScope() {
          @Override
          public boolean contains(@NotNull VirtualFile file) {
            if (!(file instanceof VirtualFileWithId)
                || file.equals(virtualFile)
                || ArrayUtil.indexOf(backIds, getAbsId(file)) != -1) return true;
            return false
                & !myProjectFileIndex.isUnderSourceRootOfType(
                    file,
                    SOURCE_ROOTS); // filter out source file which we know for sure does not
                                   // reference the element
          }

          @Override
          public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
            return 0;
          }

          @Override
          public boolean isSearchInModuleContent(@NotNull Module aModule) {
            return true;
          }

          @Override
          public boolean isSearchInLibraries() {
            return false;
          }
        };
    return scope.intersectWith(restrictedByBackwardIds);
  }

  @Override
  public boolean queue(@NotNull Collection<VirtualFile> files, @NotNull Object reason) {
    if (files.isEmpty()) {
      return false;
    }
    boolean queued = false;
    List<VirtualFile> added = new ArrayList<VirtualFile>(files.size());
    for (VirtualFile file : files) {
      boolean wasAdded = queueIfNeeded(file, myProject);
      if (wasAdded) {
        added.add(file);
      }
      queued |= wasAdded;
    }
    if (queued) {
      log("Queued to resolve (from " + reason + "): " + toVfString(added));
      flushLog();
    }
    return queued;
  }

  @Override
  public boolean isUpToDate() {
    return ENABLED && !myDisposed && upToDate;
  }

  @Override
  public int getQueueSize() {
    synchronized (filesToResolve) {
      return filesToResolve.size();
    }
  }

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

  @Override
  public void addListener(@NotNull Disposable parent, @NotNull final Listener listener) {
    myListeners.add(listener);
    Disposer.register(
        parent,
        new Disposable() {
          @Override
          public void dispose() {
            myListeners.remove(listener);
          }
        });
  }

  private static class MyProgress extends ProgressIndicatorBase implements Disposable {
    @Override
    public void dispose() {}
  }
}
  private static class ColorWheel extends JComponent {
    private static final int BORDER_SIZE = 5;
    private float myBrightness = 1f;
    private float myHue = 1f;
    private float mySaturation = 0f;

    private Image myImage;
    private Rectangle myWheel;

    private boolean myShouldInvalidate = true;

    private Color myColor;

    private final List<ColorListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
    private int myOpacity;

    private ColorWheel() {
      setOpaque(true);
      addComponentListener(
          new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
              myShouldInvalidate = true;
            }
          });

      addMouseMotionListener(
          new MouseAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
              final int x = e.getX();
              final int y = e.getY();
              int mx = myWheel.x + myWheel.width / 2;
              int my = myWheel.y + myWheel.height / 2;
              double s;
              double h;
              s =
                  Math.sqrt((double) ((x - mx) * (x - mx) + (y - my) * (y - my)))
                      / (myWheel.height / 2);
              h = -Math.atan2((double) (y - my), (double) (x - mx)) / (2 * Math.PI);
              if (h < 0) h += 1.0;
              if (s > 1) s = 1.0;

              setHSBValue((float) h, (float) s, myBrightness, myOpacity);
            }
          });

      addMouseListener(
          new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
              final int x = e.getX();
              final int y = e.getY();
              int mx = myWheel.x + myWheel.width / 2;
              int my = myWheel.y + myWheel.height / 2;
              double s;
              double h;
              s =
                  Math.sqrt((double) ((x - mx) * (x - mx) + (y - my) * (y - my)))
                      / (myWheel.height / 2);
              h = -Math.atan2((double) (y - my), (double) (x - mx)) / (2 * Math.PI);
              if (h < 0) h += 1.0;
              if (s <= 1) {
                setHSBValue((float) h, (float) s, myBrightness, myOpacity);
              }
            }
          });
    }

    private void setHSBValue(float h, float s, float b, int opacity) {
      //noinspection UseJBColor
      Color rgb = new Color(Color.HSBtoRGB(h, s, b));
      setColor(ColorUtil.toAlpha(rgb, opacity), this);
    }

    private void setColor(Color color, Object source) {
      float[] hsb = new float[3];
      Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), hsb);
      myColor = color;
      myHue = hsb[0];
      mySaturation = hsb[1];
      myBrightness = hsb[2];
      myOpacity = color.getAlpha();

      fireColorChanged(source);

      repaint();
    }

    public void addListener(ColorListener listener) {
      myListeners.add(listener);
    }

    private void fireColorChanged(Object source) {
      for (ColorListener listener : myListeners) {
        listener.colorChanged(myColor, source);
      }
    }

    public void setBrightness(float brightness) {
      if (brightness != myBrightness) {
        myImage = null;
        setHSBValue(myHue, mySaturation, brightness, myOpacity);
      }
    }

    public void setOpacity(int opacity) {
      if (opacity != myOpacity) {
        setHSBValue(myHue, mySaturation, myBrightness, opacity);
      }
    }

    @Override
    public Dimension getPreferredSize() {
      return getMinimumSize();
    }

    @Override
    public Dimension getMinimumSize() {
      return JBUI.size(300, 300);
    }

    @Override
    protected void paintComponent(Graphics g) {
      Graphics2D g2d = (Graphics2D) g;

      final Dimension size = getSize();
      int _size = Math.min(size.width, size.height);
      _size = Math.min(_size, 600);

      if (myImage != null && myShouldInvalidate) {
        if (myImage.getWidth(null) != _size) {
          myImage = null;
        }
      }

      myShouldInvalidate = false;

      if (myImage == null) {
        myImage =
            createImage(
                new ColorWheelImageProducer(
                    _size - BORDER_SIZE * 2, _size - BORDER_SIZE * 2, myBrightness));
        myWheel =
            new Rectangle(
                BORDER_SIZE, BORDER_SIZE, _size - BORDER_SIZE * 2, _size - BORDER_SIZE * 2);
      }

      g.setColor(UIManager.getColor("Panel.background"));
      g.fillRect(0, 0, getWidth(), getHeight());

      g2d.setComposite(
          AlphaComposite.getInstance(AlphaComposite.SRC_OVER, ((float) myOpacity) / 255f));
      g.drawImage(myImage, myWheel.x, myWheel.y, null);

      g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 1.0f));

      int mx = myWheel.x + myWheel.width / 2;
      int my = myWheel.y + myWheel.height / 2;
      //noinspection UseJBColor
      g.setColor(Color.WHITE);
      int arcw = (int) (myWheel.width * mySaturation / 2);
      int arch = (int) (myWheel.height * mySaturation / 2);
      double th = myHue * 2 * Math.PI;
      final int x = (int) (mx + arcw * Math.cos(th));
      final int y = (int) (my - arch * Math.sin(th));
      g.fillRect(x - 2, y - 2, 4, 4);
      //noinspection UseJBColor
      g.setColor(Color.BLACK);
      g.drawRect(x - 2, y - 2, 4, 4);
    }

    public void dropImage() {
      myImage = null;
    }
  }
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");
  }
}
/** @author anna Date: 04-Dec-2007 */
public class DetectedPluginsPanel extends OrderPanel<PluginDownloader> {
  private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  private JEditorPane myDescriptionPanel = new JEditorPane();
  private PluginHeaderPanel myHeader;

  public DetectedPluginsPanel() {
    super(PluginDownloader.class);
    final JTable entryTable = getEntryTable();
    myHeader = new PluginHeaderPanel(null, entryTable);
    entryTable.setTableHeader(null);
    entryTable.setDefaultRenderer(
        PluginDownloader.class,
        new ColoredTableCellRenderer() {
          protected void customizeCellRenderer(
              final JTable table,
              final Object value,
              final boolean selected,
              final boolean hasFocus,
              final int row,
              final int column) {
            setBorder(null);
            final PluginDownloader downloader = (PluginDownloader) value;
            if (downloader != null) {
              final String pluginName = downloader.getPluginName();
              append(pluginName, SimpleTextAttributes.REGULAR_ATTRIBUTES);
              final IdeaPluginDescriptor ideaPluginDescriptor =
                  PluginManager.getPlugin(PluginId.getId(downloader.getPluginId()));
              if (ideaPluginDescriptor != null) {
                final String oldPluginName = ideaPluginDescriptor.getName();
                if (!Comparing.strEqual(pluginName, oldPluginName)) {
                  append(" - " + oldPluginName, SimpleTextAttributes.REGULAR_ATTRIBUTES);
                }
              }
              final String loadedVersion = downloader.getPluginVersion();
              if (loadedVersion != null
                  || (ideaPluginDescriptor != null && ideaPluginDescriptor.getVersion() != null)) {
                final String installedVersion =
                    ideaPluginDescriptor != null && ideaPluginDescriptor.getVersion() != null
                        ? "v. "
                            + ideaPluginDescriptor.getVersion()
                            + (loadedVersion != null ? " -> " : "")
                        : "";
                final String availableVersion = loadedVersion != null ? loadedVersion : "";
                append(
                    " (" + installedVersion + availableVersion + ")",
                    SimpleTextAttributes.GRAY_ATTRIBUTES);
              }
            }
          }
        });
    entryTable
        .getSelectionModel()
        .addListSelectionListener(
            new ListSelectionListener() {
              public void valueChanged(ListSelectionEvent e) {
                final int selectedRow = entryTable.getSelectedRow();
                if (selectedRow != -1) {
                  final PluginDownloader selection = getValueAt(selectedRow);
                  final IdeaPluginDescriptor descriptor = selection.getDescriptor();
                  if (descriptor != null) {
                    PluginManagerMain.pluginInfoUpdate(
                        descriptor, null, myDescriptionPanel, myHeader, null);
                  }
                }
              }
            });
    setCheckboxColumnName("");
    myDescriptionPanel.setPreferredSize(new Dimension(400, -1));
    myDescriptionPanel.setEditable(false);
    myDescriptionPanel.setContentType(UIUtil.HTML_MIME);
    myDescriptionPanel.addHyperlinkListener(new PluginManagerMain.MyHyperlinkListener());
    removeAll();

    final Splitter splitter = new Splitter(false);
    splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(entryTable));
    splitter.setSecondComponent(ScrollPaneFactory.createScrollPane(myDescriptionPanel));
    add(splitter, BorderLayout.CENTER);
  }

  public String getCheckboxColumnName() {
    return "";
  }

  public boolean isCheckable(final PluginDownloader downloader) {
    return true;
  }

  public boolean isChecked(final PluginDownloader downloader) {
    return !getSkippedPlugins().contains(downloader.getPluginId());
  }

  public void setChecked(final PluginDownloader downloader, final boolean checked) {
    if (checked) {
      getSkippedPlugins().remove(downloader.getPluginId());
    } else {
      getSkippedPlugins().add(downloader.getPluginId());
    }
    for (Listener listener : myListeners) {
      listener.stateChanged();
    }
  }

  protected Set<String> getSkippedPlugins() {
    return UpdateChecker.getDisabledToUpdatePlugins();
  }

  public void addStateListener(Listener l) {
    myListeners.add(l);
  }

  public interface Listener {
    void stateChanged();
  }
}
  private static class MyPsiElementVisitor extends PsiElementVisitor {
    private final boolean highlightErrorElements;
    private final boolean runAnnotators;
    final List<Pair<PsiFile, HighlightInfo>> result = ContainerUtil.createLockFreeCopyOnWriteList();

    public MyPsiElementVisitor(boolean highlightErrorElements, boolean runAnnotators) {
      this.highlightErrorElements = highlightErrorElements;
      this.runAnnotators = runAnnotators;
    }

    @Override
    public void visitFile(PsiFile file) {
      final VirtualFile virtualFile = file.getVirtualFile();
      if (virtualFile == null) {
        return;
      }

      final Project project = file.getProject();
      Document document = PsiDocumentManager.getInstance(project).getDocument(file);
      if (document == null) return;
      final HighlightInfoFilter[] filters =
          ApplicationManager.getApplication()
              .getExtensions(HighlightInfoFilter.EXTENSION_POINT_NAME);
      GeneralHighlightingPass pass =
          new GeneralHighlightingPass(project, file, document, 0, file.getTextLength(), true) {
            @NotNull
            @Override
            protected HighlightVisitor[] createHighlightVisitors() {
              return new HighlightVisitor[] {
                new DefaultHighlightVisitor(project, highlightErrorElements, runAnnotators, true)
              };
            }

            @Override
            protected HighlightInfoHolder createInfoHolder(final PsiFile file) {
              return new HighlightInfoHolder(file, getColorsScheme(), filters) {
                @Override
                public boolean add(@Nullable HighlightInfo info) {
                  if (info == null) return true;
                  if (info.type == HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT) return true;
                  if (info.getSeverity() == HighlightSeverity.INFORMATION) return true;

                  result.add(Pair.create(file, info));

                  return true;
                }
              };
            }

            @Override
            protected void killAbandonedHighlightsUnder(
                @NotNull TextRange range,
                @Nullable List<HighlightInfo> holder,
                @NotNull ProgressIndicator progress) {
              // do not mess with real editor highlights
            }

            @Override
            protected boolean isFailFastOnAcquireReadAction() {
              return false;
            }
          };
      DaemonProgressIndicator progress = new DaemonProgressIndicator();
      progress.start();
      pass.collectInformation(progress);
    }
  }
@State(
    name = "DebuggerManager",
    storages = {@Storage(file = StoragePathMacros.WORKSPACE_FILE)})
public class DebuggerManagerImpl extends DebuggerManagerEx
    implements PersistentStateComponent<Element> {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.debugger.impl.DebuggerManagerImpl");

  private final Project myProject;
  private final HashMap<ProcessHandler, DebuggerSession> mySessions =
      new HashMap<ProcessHandler, DebuggerSession>();
  private final BreakpointManager myBreakpointManager;
  private final List<NameMapper> myNameMappers = ContainerUtil.createLockFreeCopyOnWriteList();
  private final List<Function<DebugProcess, PositionManager>> myCustomPositionManagerFactories =
      new ArrayList<Function<DebugProcess, PositionManager>>();

  private final EventDispatcher<DebuggerManagerListener> myDispatcher =
      EventDispatcher.create(DebuggerManagerListener.class);
  private final MyDebuggerStateManager myDebuggerStateManager = new MyDebuggerStateManager();

  private final DebuggerContextListener mySessionListener =
      new DebuggerContextListener() {
        @Override
        public void changeEvent(DebuggerContextImpl newContext, DebuggerSession.Event event) {

          final DebuggerSession session = newContext.getDebuggerSession();
          if (event == DebuggerSession.Event.PAUSE
              && myDebuggerStateManager.myDebuggerSession != session) {
            // if paused in non-active session; switch current session
            myDebuggerStateManager.setState(
                newContext,
                session != null ? session.getState() : DebuggerSession.State.DISPOSED,
                event,
                null);
            return;
          }

          if (myDebuggerStateManager.myDebuggerSession == session) {
            myDebuggerStateManager.fireStateChanged(newContext, event);
          }
          if (event == DebuggerSession.Event.ATTACHED) {
            myDispatcher.getMulticaster().sessionAttached(session);
          } else if (event == DebuggerSession.Event.DETACHED) {
            myDispatcher.getMulticaster().sessionDetached(session);
          } else if (event == DebuggerSession.Event.DISPOSE) {
            dispose(session);
            if (myDebuggerStateManager.myDebuggerSession == session) {
              myDebuggerStateManager.setState(
                  DebuggerContextImpl.EMPTY_CONTEXT,
                  DebuggerSession.State.DISPOSED,
                  DebuggerSession.Event.DISPOSE,
                  null);
            }
          }
        }
      };
  @NonNls private static final String DEBUG_KEY_NAME = "idea.xdebug.key";

  @Override
  public void addClassNameMapper(final NameMapper mapper) {
    myNameMappers.add(mapper);
  }

  @Override
  public void removeClassNameMapper(final NameMapper mapper) {
    myNameMappers.remove(mapper);
  }

  @Override
  public String getVMClassQualifiedName(@NotNull final PsiClass aClass) {
    for (NameMapper nameMapper : myNameMappers) {
      final String qName = nameMapper.getQualifiedName(aClass);
      if (qName != null) {
        return qName;
      }
    }
    return aClass.getQualifiedName();
  }

  @Override
  public void addDebuggerManagerListener(DebuggerManagerListener listener) {
    myDispatcher.addListener(listener);
  }

  @Override
  public void removeDebuggerManagerListener(DebuggerManagerListener listener) {
    myDispatcher.removeListener(listener);
  }

  public DebuggerManagerImpl(
      Project project, StartupManager startupManager, EditorColorsManager colorsManager) {
    myProject = project;
    myBreakpointManager = new BreakpointManager(myProject, startupManager, this);
    if (!project.isDefault()) {
      colorsManager.addEditorColorsListener(
          new EditorColorsListener() {
            @Override
            public void globalSchemeChange(EditorColorsScheme scheme) {
              getBreakpointManager().updateBreakpointsUI();
            }
          },
          project);
    }
  }

  @Override
  public DebuggerSession getSession(DebugProcess process) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    for (final DebuggerSession debuggerSession : getSessions()) {
      if (process == debuggerSession.getProcess()) return debuggerSession;
    }
    return null;
  }

  @Override
  public Collection<DebuggerSession> getSessions() {
    synchronized (mySessions) {
      final Collection<DebuggerSession> values = mySessions.values();
      return values.isEmpty()
          ? Collections.<DebuggerSession>emptyList()
          : new ArrayList<DebuggerSession>(values);
    }
  }

  @Override
  public void disposeComponent() {}

  @Override
  public void initComponent() {}

  @Override
  public void projectClosed() {}

  @Override
  public void projectOpened() {
    myBreakpointManager.init();
  }

  @Nullable
  @Override
  public Element getState() {
    Element state = new Element("state");
    myBreakpointManager.writeExternal(state);
    return state;
  }

  @Override
  public void loadState(Element state) {
    myBreakpointManager.readExternal(state);
  }

  public void writeExternal(Element element) throws WriteExternalException {
    myBreakpointManager.writeExternal(element);
  }

  @Override
  @Nullable
  public DebuggerSession attachVirtualMachine(@NotNull DebugEnvironment environment)
      throws ExecutionException {
    ApplicationManager.getApplication().assertIsDispatchThread();
    final DebugProcessEvents debugProcess = new DebugProcessEvents(myProject);
    debugProcess.addDebugProcessListener(
        new DebugProcessAdapter() {
          @Override
          public void processAttached(final DebugProcess process) {
            process.removeDebugProcessListener(this);
            for (Function<DebugProcess, PositionManager> factory :
                myCustomPositionManagerFactories) {
              final PositionManager positionManager = factory.fun(process);
              if (positionManager != null) {
                process.appendPositionManager(positionManager);
              }
            }
            for (PositionManagerFactory factory :
                Extensions.getExtensions(PositionManagerFactory.EP_NAME, myProject)) {
              final PositionManager manager = factory.createPositionManager(debugProcess);
              if (manager != null) {
                process.appendPositionManager(manager);
              }
            }
          }

          @Override
          public void processDetached(final DebugProcess process, final boolean closedByUser) {
            debugProcess.removeDebugProcessListener(this);
          }

          @Override
          public void attachException(
              final RunProfileState state,
              final ExecutionException exception,
              final RemoteConnection remoteConnection) {
            debugProcess.removeDebugProcessListener(this);
          }
        });
    DebuggerSession session =
        DebuggerSession.create(environment.getSessionName(), debugProcess, environment);
    ExecutionResult executionResult = session.getProcess().getExecutionResult();
    if (executionResult == null) {
      return null;
    }
    session.getContextManager().addListener(mySessionListener);
    getContextManager()
        .setState(
            DebuggerContextUtil.createDebuggerContext(
                session, session.getContextManager().getContext().getSuspendContext()),
            session.getState(),
            DebuggerSession.Event.CONTEXT,
            null);

    final ProcessHandler processHandler = executionResult.getProcessHandler();

    synchronized (mySessions) {
      mySessions.put(processHandler, session);
    }

    if (!(processHandler instanceof RemoteDebugProcessHandler)) {
      // add listener only to non-remote process handler:
      // on Unix systems destroying process does not cause VMDeathEvent to be generated,
      // so we need to call debugProcess.stop() explicitly for graceful termination.
      // RemoteProcessHandler on the other hand will call debugProcess.stop() as a part of
      // destroyProcess() and detachProcess() implementation,
      // so we shouldn't add the listener to avoid calling stop() twice
      processHandler.addProcessListener(
          new ProcessAdapter() {
            @Override
            public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
              final DebugProcessImpl debugProcess = getDebugProcess(event.getProcessHandler());
              if (debugProcess != null) {
                // if current thread is a "debugger manager thread", stop will execute synchronously
                // it is KillableColoredProcessHandler responsibility to terminate VM
                debugProcess.stop(
                    willBeDestroyed
                        && !(event.getProcessHandler() instanceof KillableColoredProcessHandler));

                // wait at most 10 seconds: the problem is that debugProcess.stop() can hang if
                // there are troubles in the debuggee
                // if processWillTerminate() is called from AWT thread debugProcess.waitFor() will
                // block it and the whole app will hang
                if (!DebuggerManagerThreadImpl.isManagerThread()) {
                  if (SwingUtilities.isEventDispatchThread()) {
                    ProgressManager.getInstance()
                        .runProcessWithProgressSynchronously(
                            new Runnable() {
                              @Override
                              public void run() {
                                ProgressManager.getInstance()
                                    .getProgressIndicator()
                                    .setIndeterminate(true);
                                debugProcess.waitFor(10000);
                              }
                            },
                            "Waiting For Debugger Response",
                            false,
                            debugProcess.getProject());
                  } else {
                    debugProcess.waitFor(10000);
                  }
                }
              }
            }
          });
    }
    myDispatcher.getMulticaster().sessionCreated(session);
    return session;
  }

  @Override
  public DebugProcessImpl getDebugProcess(final ProcessHandler processHandler) {
    synchronized (mySessions) {
      DebuggerSession session = mySessions.get(processHandler);
      return session != null ? session.getProcess() : null;
    }
  }

  @SuppressWarnings("UnusedDeclaration")
  @Nullable
  public DebuggerSession getDebugSession(final ProcessHandler processHandler) {
    synchronized (mySessions) {
      return mySessions.get(processHandler);
    }
  }

  @Override
  public void addDebugProcessListener(
      final ProcessHandler processHandler, final DebugProcessListener listener) {
    DebugProcessImpl debugProcess = getDebugProcess(processHandler);
    if (debugProcess != null) {
      debugProcess.addDebugProcessListener(listener);
    } else {
      processHandler.addProcessListener(
          new ProcessAdapter() {
            @Override
            public void startNotified(ProcessEvent event) {
              DebugProcessImpl debugProcess = getDebugProcess(processHandler);
              if (debugProcess != null) {
                debugProcess.addDebugProcessListener(listener);
              }
              processHandler.removeProcessListener(this);
            }
          });
    }
  }

  @Override
  public void removeDebugProcessListener(
      final ProcessHandler processHandler, final DebugProcessListener listener) {
    DebugProcessImpl debugProcess = getDebugProcess(processHandler);
    if (debugProcess != null) {
      debugProcess.removeDebugProcessListener(listener);
    } else {
      processHandler.addProcessListener(
          new ProcessAdapter() {
            @Override
            public void startNotified(ProcessEvent event) {
              DebugProcessImpl debugProcess = getDebugProcess(processHandler);
              if (debugProcess != null) {
                debugProcess.removeDebugProcessListener(listener);
              }
              processHandler.removeProcessListener(this);
            }
          });
    }
  }

  @Override
  public boolean isDebuggerManagerThread() {
    return DebuggerManagerThreadImpl.isManagerThread();
  }

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

  @Override
  public BreakpointManager getBreakpointManager() {
    return myBreakpointManager;
  }

  @Override
  public DebuggerContextImpl getContext() {
    return getContextManager().getContext();
  }

  @Override
  public DebuggerStateManager getContextManager() {
    return myDebuggerStateManager;
  }

  @Override
  public void registerPositionManagerFactory(
      final Function<DebugProcess, PositionManager> factory) {
    myCustomPositionManagerFactories.add(factory);
  }

  @Override
  public void unregisterPositionManagerFactory(
      final Function<DebugProcess, PositionManager> factory) {
    myCustomPositionManagerFactories.remove(factory);
  }

  private static boolean hasWhitespace(String string) {
    int length = string.length();
    for (int i = 0; i < length; i++) {
      if (Character.isWhitespace(string.charAt(i))) {
        return true;
      }
    }
    return false;
  }

  /* Remoting */
  private static void checkTargetJPDAInstalled(JavaParameters parameters)
      throws ExecutionException {
    final Sdk jdk = parameters.getJdk();
    if (jdk == null) {
      throw new ExecutionException(DebuggerBundle.message("error.jdk.not.specified"));
    }
    final JavaSdkVersion version = JavaSdk.getInstance().getVersion(jdk);
    String versionString = jdk.getVersionString();
    if (version == JavaSdkVersion.JDK_1_0 || version == JavaSdkVersion.JDK_1_1) {
      throw new ExecutionException(
          DebuggerBundle.message("error.unsupported.jdk.version", versionString));
    }
    if (SystemInfo.isWindows && version == JavaSdkVersion.JDK_1_2) {
      final VirtualFile homeDirectory = jdk.getHomeDirectory();
      if (homeDirectory == null || !homeDirectory.isValid()) {
        throw new ExecutionException(
            DebuggerBundle.message("error.invalid.jdk.home", versionString));
      }
      //noinspection HardCodedStringLiteral
      File dllFile =
          new File(
              homeDirectory.getPath().replace('/', File.separatorChar)
                  + File.separator
                  + "bin"
                  + File.separator
                  + "jdwp.dll");
      if (!dllFile.exists()) {
        GetJPDADialog dialog = new GetJPDADialog();
        dialog.show();
        throw new ExecutionException(DebuggerBundle.message("error.debug.libraries.missing"));
      }
    }
  }

  /** for Target JDKs versions 1.2.x - 1.3.0 the Classic VM should be used for debugging */
  private static boolean shouldForceClassicVM(Sdk jdk) {
    if (SystemInfo.isMac) {
      return false;
    }
    if (jdk == null) return false;

    String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
    if (version != null) {
      if (version.compareTo("1.4") >= 0) {
        return false;
      }
      if (version.startsWith("1.2") && SystemInfo.isWindows) {
        return true;
      }
      version += ".0";
      if (version.startsWith("1.3.0") && SystemInfo.isWindows) {
        return true;
      }
      if ((version.startsWith("1.3.1_07") || version.startsWith("1.3.1_08"))
          && SystemInfo.isWindows) {
        return false; // fixes bug for these JDKs that it cannot start with -classic option
      }
    }

    return DebuggerSettings.getInstance().FORCE_CLASSIC_VM;
  }

  @SuppressWarnings({"HardCodedStringLiteral"})
  public static RemoteConnection createDebugParameters(
      final JavaParameters parameters,
      final boolean debuggerInServerMode,
      int transport,
      final String debugPort,
      boolean checkValidity)
      throws ExecutionException {
    if (checkValidity) {
      checkTargetJPDAInstalled(parameters);
    }

    final boolean useSockets = transport == DebuggerSettings.SOCKET_TRANSPORT;

    String address = "";
    if (StringUtil.isEmptyOrSpaces(debugPort)) {
      try {
        address = DebuggerUtils.getInstance().findAvailableDebugAddress(useSockets);
      } catch (ExecutionException e) {
        if (checkValidity) {
          throw e;
        }
      }
    } else {
      address = debugPort;
    }

    final TransportServiceWrapper transportService =
        TransportServiceWrapper.getTransportService(useSockets);
    final String debugAddress =
        debuggerInServerMode && useSockets ? "127.0.0.1:" + address : address;
    String debuggeeRunProperties =
        "transport=" + transportService.transportId() + ",address=" + debugAddress;
    if (debuggerInServerMode) {
      debuggeeRunProperties += ",suspend=y,server=n";
    } else {
      debuggeeRunProperties += ",suspend=n,server=y";
    }

    if (hasWhitespace(debuggeeRunProperties)) {
      debuggeeRunProperties = "\"" + debuggeeRunProperties + "\"";
    }
    final String _debuggeeRunProperties = debuggeeRunProperties;

    ApplicationManager.getApplication()
        .runReadAction(
            new Runnable() {
              @Override
              @SuppressWarnings({"HardCodedStringLiteral"})
              public void run() {
                JavaSdkUtil.addRtJar(parameters.getClassPath());

                final Sdk jdk = parameters.getJdk();
                final boolean forceClassicVM = shouldForceClassicVM(jdk);
                final boolean forceNoJIT = shouldForceNoJIT(jdk);
                final String debugKey = System.getProperty(DEBUG_KEY_NAME, "-Xdebug");
                final boolean needDebugKey =
                    shouldAddXdebugKey(jdk)
                        || !"-Xdebug".equals(debugKey) /*the key is non-standard*/;

                if (forceClassicVM || forceNoJIT || needDebugKey || !isJVMTIAvailable(jdk)) {
                  parameters
                      .getVMParametersList()
                      .replaceOrPrepend("-Xrunjdwp:", "-Xrunjdwp:" + _debuggeeRunProperties);
                } else {
                  // use newer JVMTI if available
                  parameters.getVMParametersList().replaceOrPrepend("-Xrunjdwp:", "");
                  parameters
                      .getVMParametersList()
                      .replaceOrPrepend(
                          "-agentlib:jdwp=", "-agentlib:jdwp=" + _debuggeeRunProperties);
                }

                if (forceNoJIT) {
                  parameters
                      .getVMParametersList()
                      .replaceOrPrepend("-Djava.compiler=", "-Djava.compiler=NONE");
                  parameters.getVMParametersList().replaceOrPrepend("-Xnoagent", "-Xnoagent");
                }

                if (needDebugKey) {
                  parameters.getVMParametersList().replaceOrPrepend(debugKey, debugKey);
                } else {
                  // deliberately skip outdated parameter because it can disable full-speed
                  // debugging for some jdk builds
                  // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6272174
                  parameters.getVMParametersList().replaceOrPrepend("-Xdebug", "");
                }

                parameters
                    .getVMParametersList()
                    .replaceOrPrepend("-classic", forceClassicVM ? "-classic" : "");
              }
            });

    return new RemoteConnection(useSockets, "127.0.0.1", address, debuggerInServerMode);
  }

  private static boolean shouldForceNoJIT(Sdk jdk) {
    if (DebuggerSettings.getInstance().DISABLE_JIT) {
      return true;
    }
    if (jdk != null) {
      final String version =
          JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
      if (version != null && (version.startsWith("1.2") || version.startsWith("1.3"))) {
        return true;
      }
    }
    return false;
  }

  private static boolean shouldAddXdebugKey(Sdk jdk) {
    if (jdk == null) {
      return true; // conservative choice
    }
    if (DebuggerSettings.getInstance().DISABLE_JIT) {
      return true;
    }

    // if (ApplicationManager.getApplication().isUnitTestMode()) {
    // need this in unit tests to avoid false alarms when comparing actual output with expected
    // output
    // return true;
    // }

    final String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
    return version == null
        ||
        // version.startsWith("1.5") ||
        version.startsWith("1.4")
        || version.startsWith("1.3")
        || version.startsWith("1.2")
        || version.startsWith("1.1")
        || version.startsWith("1.0");
  }

  private static boolean isJVMTIAvailable(Sdk jdk) {
    if (jdk == null) {
      return false; // conservative choice
    }

    final String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
    if (version == null) {
      return false;
    }
    return !(version.startsWith("1.4")
        || version.startsWith("1.3")
        || version.startsWith("1.2")
        || version.startsWith("1.1")
        || version.startsWith("1.0"));
  }

  public static RemoteConnection createDebugParameters(
      final JavaParameters parameters,
      GenericDebuggerRunnerSettings settings,
      boolean checkValidity)
      throws ExecutionException {
    return createDebugParameters(
        parameters,
        settings.LOCAL,
        settings.getTransport(),
        settings.getDebugPort(),
        checkValidity);
  }

  private static class MyDebuggerStateManager extends DebuggerStateManager {
    private DebuggerSession myDebuggerSession;

    @Override
    public DebuggerContextImpl getContext() {
      return myDebuggerSession == null
          ? DebuggerContextImpl.EMPTY_CONTEXT
          : myDebuggerSession.getContextManager().getContext();
    }

    @Override
    public void setState(
        final DebuggerContextImpl context,
        DebuggerSession.State state,
        DebuggerSession.Event event,
        String description) {
      ApplicationManager.getApplication().assertIsDispatchThread();
      myDebuggerSession = context.getDebuggerSession();
      if (myDebuggerSession != null) {
        myDebuggerSession.getContextManager().setState(context, state, event, description);
      } else {
        fireStateChanged(context, event);
      }
    }
  }

  private void dispose(DebuggerSession session) {
    ProcessHandler processHandler = session.getProcess().getProcessHandler();
    synchronized (mySessions) {
      DebuggerSession removed = mySessions.remove(processHandler);
      LOG.assertTrue(removed != null);
      myDispatcher.getMulticaster().sessionRemoved(session);
    }
  }
}
public class ExecutionManagerImpl extends ExecutionManager implements Disposable {
  public static final Key<Object> EXECUTION_SESSION_ID_KEY = Key.create("EXECUTION_SESSION_ID_KEY");
  public static final Key<Boolean> EXECUTION_SKIP_RUN = Key.create("EXECUTION_SKIP_RUN");

  private static final Logger LOG = Logger.getInstance(ExecutionManagerImpl.class);
  private static final ProcessHandler[] EMPTY_PROCESS_HANDLERS = new ProcessHandler[0];

  private final Project myProject;
  private final Alarm myAwaitingTerminationAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
  private final Map<RunProfile, ExecutionEnvironment> myAwaitingRunProfiles =
      ContainerUtil.newHashMap();
  private final List<Trinity<RunContentDescriptor, RunnerAndConfigurationSettings, Executor>>
      myRunningConfigurations = ContainerUtil.createLockFreeCopyOnWriteList();
  private RunContentManagerImpl myContentManager;
  private volatile boolean myForceCompilationInTests;

  @SuppressWarnings("MethodOverridesStaticMethodOfSuperclass")
  @NotNull
  public static ExecutionManagerImpl getInstance(@NotNull Project project) {
    return (ExecutionManagerImpl) ServiceManager.getService(project, ExecutionManager.class);
  }

  protected ExecutionManagerImpl(@NotNull Project project) {
    myProject = project;
  }

  @NotNull
  private static ExecutionEnvironmentBuilder createEnvironmentBuilder(
      @NotNull Project project,
      @NotNull Executor executor,
      @Nullable RunnerAndConfigurationSettings configuration) {
    ExecutionEnvironmentBuilder builder = new ExecutionEnvironmentBuilder(project, executor);

    ProgramRunner runner =
        RunnerRegistry.getInstance()
            .getRunner(
                executor.getId(), configuration != null ? configuration.getConfiguration() : null);
    if (runner == null && configuration != null) {
      LOG.error("Cannot find runner for " + configuration.getName());
    } else if (runner != null) {
      assert configuration != null;
      builder.runnerAndSettings(runner, configuration);
    }
    return builder;
  }

  public static boolean isProcessRunning(@Nullable RunContentDescriptor descriptor) {
    ProcessHandler processHandler = descriptor == null ? null : descriptor.getProcessHandler();
    return processHandler != null && !processHandler.isProcessTerminated();
  }

  private static void start(@NotNull ExecutionEnvironment environment) {
    RunnerAndConfigurationSettings settings = environment.getRunnerAndConfigurationSettings();
    ProgramRunnerUtil.executeConfiguration(
        environment, settings != null && settings.isEditBeforeRun(), true);
  }

  private static boolean userApprovesStopForSameTypeConfigurations(
      Project project, String configName, int instancesCount) {
    RunManagerImpl runManager = RunManagerImpl.getInstanceImpl(project);
    final RunManagerConfig config = runManager.getConfig();
    if (!config.isRestartRequiresConfirmation()) return true;

    DialogWrapper.DoNotAskOption option =
        new DialogWrapper.DoNotAskOption() {
          @Override
          public boolean isToBeShown() {
            return config.isRestartRequiresConfirmation();
          }

          @Override
          public void setToBeShown(boolean value, int exitCode) {
            config.setRestartRequiresConfirmation(value);
          }

          @Override
          public boolean canBeHidden() {
            return true;
          }

          @Override
          public boolean shouldSaveOptionsOnCancel() {
            return false;
          }

          @NotNull
          @Override
          public String getDoNotShowMessage() {
            return CommonBundle.message("dialog.options.do.not.show");
          }
        };
    return Messages.showOkCancelDialog(
            project,
            ExecutionBundle.message(
                "rerun.singleton.confirmation.message", configName, instancesCount),
            ExecutionBundle.message("process.is.running.dialog.title", configName),
            ExecutionBundle.message("rerun.confirmation.button.text"),
            CommonBundle.message("button.cancel"),
            Messages.getQuestionIcon(),
            option)
        == Messages.OK;
  }

  private static boolean userApprovesStopForIncompatibleConfigurations(
      Project project,
      String configName,
      List<RunContentDescriptor> runningIncompatibleDescriptors) {
    RunManagerImpl runManager = RunManagerImpl.getInstanceImpl(project);
    final RunManagerConfig config = runManager.getConfig();
    if (!config.isStopIncompatibleRequiresConfirmation()) return true;

    DialogWrapper.DoNotAskOption option =
        new DialogWrapper.DoNotAskOption() {
          @Override
          public boolean isToBeShown() {
            return config.isStopIncompatibleRequiresConfirmation();
          }

          @Override
          public void setToBeShown(boolean value, int exitCode) {
            config.setStopIncompatibleRequiresConfirmation(value);
          }

          @Override
          public boolean canBeHidden() {
            return true;
          }

          @Override
          public boolean shouldSaveOptionsOnCancel() {
            return false;
          }

          @NotNull
          @Override
          public String getDoNotShowMessage() {
            return CommonBundle.message("dialog.options.do.not.show");
          }
        };

    final StringBuilder names = new StringBuilder();
    for (final RunContentDescriptor descriptor : runningIncompatibleDescriptors) {
      String name = descriptor.getDisplayName();
      if (names.length() > 0) {
        names.append(", ");
      }
      names.append(
          StringUtil.isEmpty(name)
              ? ExecutionBundle.message("run.configuration.no.name")
              : String.format("'%s'", name));
    }

    //noinspection DialogTitleCapitalization
    return Messages.showOkCancelDialog(
            project,
            ExecutionBundle.message(
                "stop.incompatible.confirmation.message",
                configName,
                names.toString(),
                runningIncompatibleDescriptors.size()),
            ExecutionBundle.message(
                "incompatible.configuration.is.running.dialog.title",
                runningIncompatibleDescriptors.size()),
            ExecutionBundle.message("stop.incompatible.confirmation.button.text"),
            CommonBundle.message("button.cancel"),
            Messages.getQuestionIcon(),
            option)
        == Messages.OK;
  }

  private static void stop(@Nullable RunContentDescriptor descriptor) {
    ProcessHandler processHandler = descriptor != null ? descriptor.getProcessHandler() : null;
    if (processHandler == null) {
      return;
    }

    if (processHandler instanceof KillableProcess && processHandler.isProcessTerminating()) {
      ((KillableProcess) processHandler).killProcess();
      return;
    }

    if (!processHandler.isProcessTerminated()) {
      if (processHandler.detachIsDefault()) {
        processHandler.detachProcess();
      } else {
        processHandler.destroyProcess();
      }
    }
  }

  @Override
  public void dispose() {
    for (Trinity<RunContentDescriptor, RunnerAndConfigurationSettings, Executor> trinity :
        myRunningConfigurations) {
      Disposer.dispose(trinity.first);
    }
    myRunningConfigurations.clear();
  }

  @NotNull
  @Override
  public RunContentManager getContentManager() {
    if (myContentManager == null) {
      myContentManager = new RunContentManagerImpl(myProject, DockManager.getInstance(myProject));
      Disposer.register(myProject, myContentManager);
    }
    return myContentManager;
  }

  @NotNull
  @Override
  public ProcessHandler[] getRunningProcesses() {
    if (myContentManager == null) return EMPTY_PROCESS_HANDLERS;
    List<ProcessHandler> handlers = null;
    for (RunContentDescriptor descriptor : getContentManager().getAllDescriptors()) {
      ProcessHandler processHandler = descriptor.getProcessHandler();
      if (processHandler != null) {
        if (handlers == null) {
          handlers = new SmartList<>();
        }
        handlers.add(processHandler);
      }
    }
    return handlers == null
        ? EMPTY_PROCESS_HANDLERS
        : handlers.toArray(new ProcessHandler[handlers.size()]);
  }

  @Override
  public void compileAndRun(
      @NotNull final Runnable startRunnable,
      @NotNull final ExecutionEnvironment environment,
      @Nullable final RunProfileState state,
      @Nullable final Runnable onCancelRunnable) {
    long id = environment.getExecutionId();
    if (id == 0) {
      id = environment.assignNewExecutionId();
    }

    RunProfile profile = environment.getRunProfile();
    if (!(profile instanceof RunConfiguration)) {
      startRunnable.run();
      return;
    }

    final RunConfiguration runConfiguration = (RunConfiguration) profile;
    final List<BeforeRunTask> beforeRunTasks =
        RunManagerEx.getInstanceEx(myProject).getBeforeRunTasks(runConfiguration);
    if (beforeRunTasks.isEmpty()) {
      startRunnable.run();
    } else {
      DataContext context = environment.getDataContext();
      final DataContext projectContext =
          context != null ? context : SimpleDataContext.getProjectContext(myProject);
      final long finalId = id;
      final Long executionSessionId = new Long(id);
      ApplicationManager.getApplication()
          .executeOnPooledThread(
              () -> {
                for (BeforeRunTask task : beforeRunTasks) {
                  if (myProject.isDisposed()) {
                    return;
                  }
                  @SuppressWarnings("unchecked")
                  BeforeRunTaskProvider<BeforeRunTask> provider =
                      BeforeRunTaskProvider.getProvider(myProject, task.getProviderId());
                  if (provider == null) {
                    LOG.warn(
                        "Cannot find BeforeRunTaskProvider for id='" + task.getProviderId() + "'");
                    continue;
                  }
                  ExecutionEnvironment taskEnvironment =
                      new ExecutionEnvironmentBuilder(environment).contentToReuse(null).build();
                  taskEnvironment.setExecutionId(finalId);
                  EXECUTION_SESSION_ID_KEY.set(taskEnvironment, executionSessionId);
                  if (!provider.executeTask(
                      projectContext, runConfiguration, taskEnvironment, task)) {
                    if (onCancelRunnable != null) {
                      SwingUtilities.invokeLater(onCancelRunnable);
                    }
                    return;
                  }
                }

                doRun(environment, startRunnable);
              });
    }
  }

  protected void doRun(
      @NotNull final ExecutionEnvironment environment, @NotNull final Runnable startRunnable) {
    Boolean allowSkipRun = environment.getUserData(EXECUTION_SKIP_RUN);
    if (allowSkipRun != null && allowSkipRun) {
      environment
          .getProject()
          .getMessageBus()
          .syncPublisher(EXECUTION_TOPIC)
          .processNotStarted(environment.getExecutor().getId(), environment);
    } else {
      // important! Do not use DumbService.smartInvokeLater here because it depends on modality
      // state
      // and execution of startRunnable could be skipped if modality state check fails
      //noinspection SSBasedInspection
      SwingUtilities.invokeLater(
          () -> {
            if (!myProject.isDisposed()) {
              if (!Registry.is("dumb.aware.run.configurations")) {
                DumbService.getInstance(myProject).runWhenSmart(startRunnable);
              } else {
                try {
                  DumbService.getInstance(myProject).setAlternativeResolveEnabled(true);
                  startRunnable.run();
                } catch (IndexNotReadyException ignored) {
                  ExecutionUtil.handleExecutionError(
                      environment,
                      new ExecutionException("cannot start while indexing is in progress."));
                } finally {
                  DumbService.getInstance(myProject).setAlternativeResolveEnabled(false);
                }
              }
            }
          });
    }
  }

  @Override
  public void startRunProfile(
      @NotNull final RunProfileStarter starter,
      @NotNull final RunProfileState state,
      @NotNull final ExecutionEnvironment environment) {
    final Project project = environment.getProject();
    RunContentDescriptor reuseContent = getContentManager().getReuseContent(environment);
    if (reuseContent != null) {
      reuseContent.setExecutionId(environment.getExecutionId());
      environment.setContentToReuse(reuseContent);
    }

    final Executor executor = environment.getExecutor();
    project
        .getMessageBus()
        .syncPublisher(EXECUTION_TOPIC)
        .processStartScheduled(executor.getId(), environment);

    Runnable startRunnable;
    startRunnable =
        () -> {
          if (project.isDisposed()) {
            return;
          }

          RunProfile profile = environment.getRunProfile();
          boolean started = false;
          try {
            project
                .getMessageBus()
                .syncPublisher(EXECUTION_TOPIC)
                .processStarting(executor.getId(), environment);

            final RunContentDescriptor descriptor = starter.execute(state, environment);
            if (descriptor != null) {
              final Trinity<RunContentDescriptor, RunnerAndConfigurationSettings, Executor>
                  trinity =
                      Trinity.create(
                          descriptor, environment.getRunnerAndConfigurationSettings(), executor);
              myRunningConfigurations.add(trinity);
              Disposer.register(descriptor, () -> myRunningConfigurations.remove(trinity));
              getContentManager()
                  .showRunContent(executor, descriptor, environment.getContentToReuse());
              final ProcessHandler processHandler = descriptor.getProcessHandler();
              if (processHandler != null) {
                if (!processHandler.isStartNotified()) {
                  processHandler.startNotify();
                }
                project
                    .getMessageBus()
                    .syncPublisher(EXECUTION_TOPIC)
                    .processStarted(executor.getId(), environment, processHandler);
                started = true;

                ProcessExecutionListener listener =
                    new ProcessExecutionListener(
                        project, executor.getId(), environment, processHandler, descriptor);
                processHandler.addProcessListener(listener);

                // Since we cannot guarantee that the listener is added before process handled is
                // start notified,
                // we have to make sure the process termination events are delivered to the clients.
                // Here we check the current process state and manually deliver events, while
                // the ProcessExecutionListener guarantees each such event is only delivered once
                // either by this code, or by the ProcessHandler.

                boolean terminating = processHandler.isProcessTerminating();
                boolean terminated = processHandler.isProcessTerminated();
                if (terminating || terminated) {
                  listener.processWillTerminate(
                      new ProcessEvent(processHandler), false /*doesn't matter*/);

                  if (terminated) {
                    //noinspection ConstantConditions
                    int exitCode = processHandler.getExitCode();
                    listener.processTerminated(new ProcessEvent(processHandler, exitCode));
                  }
                }
              }
              environment.setContentToReuse(descriptor);
            }
          } catch (ProcessCanceledException e) {
            LOG.info(e);
          } catch (ExecutionException e) {
            ExecutionUtil.handleExecutionError(project, executor.getToolWindowId(), profile, e);
            LOG.info(e);
          } finally {
            if (!started) {
              project
                  .getMessageBus()
                  .syncPublisher(EXECUTION_TOPIC)
                  .processNotStarted(executor.getId(), environment);
            }
          }
        };

    if (ApplicationManager.getApplication().isUnitTestMode() && !myForceCompilationInTests) {
      startRunnable.run();
    } else {
      compileAndRun(
          () -> TransactionGuard.submitTransaction(project, startRunnable),
          environment,
          state,
          () -> {
            if (!project.isDisposed()) {
              project
                  .getMessageBus()
                  .syncPublisher(EXECUTION_TOPIC)
                  .processNotStarted(executor.getId(), environment);
            }
          });
    }
  }

  @Override
  public void restartRunProfile(
      @NotNull Project project,
      @NotNull Executor executor,
      @NotNull ExecutionTarget target,
      @Nullable RunnerAndConfigurationSettings configuration,
      @Nullable ProcessHandler processHandler) {
    ExecutionEnvironmentBuilder builder =
        createEnvironmentBuilder(project, executor, configuration);
    if (processHandler != null) {
      for (RunContentDescriptor descriptor : getContentManager().getAllDescriptors()) {
        if (descriptor.getProcessHandler() == processHandler) {
          builder.contentToReuse(descriptor);
          break;
        }
      }
    }
    restartRunProfile(builder.target(target).build());
  }

  @Override
  public void restartRunProfile(@NotNull final ExecutionEnvironment environment) {
    RunnerAndConfigurationSettings configuration = environment.getRunnerAndConfigurationSettings();

    List<RunContentDescriptor> runningIncompatible;
    if (configuration == null) {
      runningIncompatible = Collections.emptyList();
    } else {
      runningIncompatible = getIncompatibleRunningDescriptors(configuration);
    }

    RunContentDescriptor contentToReuse = environment.getContentToReuse();
    final List<RunContentDescriptor> runningOfTheSameType = new SmartList<>();
    if (configuration != null && configuration.isSingleton()) {
      runningOfTheSameType.addAll(getRunningDescriptorsOfTheSameConfigType(configuration));
    } else if (isProcessRunning(contentToReuse)) {
      runningOfTheSameType.add(contentToReuse);
    }

    List<RunContentDescriptor> runningToStop =
        ContainerUtil.concat(runningOfTheSameType, runningIncompatible);
    if (!runningToStop.isEmpty()) {
      if (configuration != null) {
        if (!runningOfTheSameType.isEmpty()
            && (runningOfTheSameType.size() > 1
                || contentToReuse == null
                || runningOfTheSameType.get(0) != contentToReuse)
            && !userApprovesStopForSameTypeConfigurations(
                environment.getProject(), configuration.getName(), runningOfTheSameType.size())) {
          return;
        }
        if (!runningIncompatible.isEmpty()
            && !userApprovesStopForIncompatibleConfigurations(
                myProject, configuration.getName(), runningIncompatible)) {
          return;
        }
      }

      for (RunContentDescriptor descriptor : runningToStop) {
        stop(descriptor);
      }
    }

    if (myAwaitingRunProfiles.get(environment.getRunProfile()) == environment) {
      // defense from rerunning exactly the same ExecutionEnvironment
      return;
    }
    myAwaitingRunProfiles.put(environment.getRunProfile(), environment);

    awaitTermination(
        new Runnable() {
          @Override
          public void run() {
            if (myAwaitingRunProfiles.get(environment.getRunProfile()) != environment) {
              // a new rerun has been requested before starting this one, ignore this rerun
              return;
            }
            if ((DumbService.getInstance(myProject).isDumb()
                    && !Registry.is("dumb.aware.run.configurations"))
                || ExecutorRegistry.getInstance().isStarting(environment)) {
              awaitTermination(this, 100);
              return;
            }

            for (RunContentDescriptor descriptor : runningOfTheSameType) {
              ProcessHandler processHandler = descriptor.getProcessHandler();
              if (processHandler != null && !processHandler.isProcessTerminated()) {
                awaitTermination(this, 100);
                return;
              }
            }
            myAwaitingRunProfiles.remove(environment.getRunProfile());
            start(environment);
          }
        },
        50);
  }

  private void awaitTermination(@NotNull Runnable request, long delayMillis) {
    if (ApplicationManager.getApplication().isUnitTestMode()) {
      ApplicationManager.getApplication().invokeLater(request, ModalityState.any());
    } else {
      myAwaitingTerminationAlarm.addRequest(request, delayMillis);
    }
  }

  @TestOnly
  public void setForceCompilationInTests(boolean forceCompilationInTests) {
    myForceCompilationInTests = forceCompilationInTests;
  }

  @NotNull
  private List<RunContentDescriptor> getRunningDescriptorsOfTheSameConfigType(
      @NotNull final RunnerAndConfigurationSettings configurationAndSettings) {
    return getRunningDescriptors(
        runningConfigurationAndSettings ->
            configurationAndSettings == runningConfigurationAndSettings);
  }

  @NotNull
  private List<RunContentDescriptor> getIncompatibleRunningDescriptors(
      @NotNull RunnerAndConfigurationSettings configurationAndSettings) {
    final RunConfiguration configurationToCheckCompatibility =
        configurationAndSettings.getConfiguration();
    return getRunningDescriptors(
        runningConfigurationAndSettings -> {
          RunConfiguration runningConfiguration =
              runningConfigurationAndSettings == null
                  ? null
                  : runningConfigurationAndSettings.getConfiguration();
          if (runningConfiguration == null
              || !(runningConfiguration instanceof CompatibilityAwareRunProfile)) {
            return false;
          }
          return ((CompatibilityAwareRunProfile) runningConfiguration)
              .mustBeStoppedToRun(configurationToCheckCompatibility);
        });
  }

  @NotNull
  public List<RunContentDescriptor> getRunningDescriptors(
      @NotNull Condition<RunnerAndConfigurationSettings> condition) {
    List<RunContentDescriptor> result = new SmartList<>();
    for (Trinity<RunContentDescriptor, RunnerAndConfigurationSettings, Executor> trinity :
        myRunningConfigurations) {
      if (trinity.getSecond() != null && condition.value(trinity.getSecond())) {
        ProcessHandler processHandler = trinity.getFirst().getProcessHandler();
        if (processHandler != null /*&& !processHandler.isProcessTerminating()*/
            && !processHandler.isProcessTerminated()) {
          result.add(trinity.getFirst());
        }
      }
    }
    return result;
  }

  @NotNull
  public Set<Executor> getExecutors(RunContentDescriptor descriptor) {
    Set<Executor> result = new HashSet<>();
    for (Trinity<RunContentDescriptor, RunnerAndConfigurationSettings, Executor> trinity :
        myRunningConfigurations) {
      if (descriptor == trinity.first) result.add(trinity.third);
    }
    return result;
  }

  private static class ProcessExecutionListener extends ProcessAdapter {
    @NotNull private final Project myProject;
    @NotNull private final String myExecutorId;
    @NotNull private final ExecutionEnvironment myEnvironment;
    @NotNull private final ProcessHandler myProcessHandler;
    @NotNull private final RunContentDescriptor myDescriptor;
    @NotNull private final AtomicBoolean myWillTerminateNotified = new AtomicBoolean();
    @NotNull private final AtomicBoolean myTerminateNotified = new AtomicBoolean();

    public ProcessExecutionListener(
        @NotNull Project project,
        @NotNull String executorId,
        @NotNull ExecutionEnvironment environment,
        @NotNull ProcessHandler processHandler,
        @NotNull RunContentDescriptor descriptor) {
      myProject = project;
      myExecutorId = executorId;
      myEnvironment = environment;
      myProcessHandler = processHandler;
      myDescriptor = descriptor;
    }

    @Override
    public void processTerminated(ProcessEvent event) {
      if (myProject.isDisposed()) return;
      if (!myTerminateNotified.compareAndSet(false, true)) return;

      ApplicationManager.getApplication()
          .invokeLater(
              () -> {
                RunnerLayoutUi ui = myDescriptor.getRunnerLayoutUi();
                if (ui != null && !ui.isDisposed()) {
                  ui.updateActionsNow();
                }
              },
              ModalityState.any());

      myProject
          .getMessageBus()
          .syncPublisher(EXECUTION_TOPIC)
          .processTerminated(myExecutorId, myEnvironment, myProcessHandler, event.getExitCode());

      SaveAndSyncHandler saveAndSyncHandler = SaveAndSyncHandler.getInstance();
      if (saveAndSyncHandler != null) {
        saveAndSyncHandler.scheduleRefresh();
      }
    }

    @Override
    public void processWillTerminate(ProcessEvent event, boolean shouldNotBeUsed) {
      if (myProject.isDisposed()) return;
      if (!myWillTerminateNotified.compareAndSet(false, true)) return;

      myProject
          .getMessageBus()
          .syncPublisher(EXECUTION_TOPIC)
          .processTerminating(myExecutorId, myEnvironment, myProcessHandler);
    }
  }
}
/** @author dsl */
public class VirtualFilePointerContainerImpl extends TraceableDisposable
    implements VirtualFilePointerContainer, Disposable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer");

  @NotNull
  private final List<VirtualFilePointer> myList = ContainerUtil.createLockFreeCopyOnWriteList();

  @NotNull private final VirtualFilePointerManager myVirtualFilePointerManager;
  @NotNull private final Disposable myParent;
  private final VirtualFilePointerListener myListener;
  private volatile Trinity<String[], VirtualFile[], VirtualFile[]> myCachedThings;
  private volatile long myTimeStampOfCachedThings = -1;
  @NonNls public static final String URL_ATTR = "url";
  private boolean myDisposed;
  private static final boolean TRACE_CREATION =
      LOG.isDebugEnabled() || ApplicationManager.getApplication().isUnitTestMode();

  public VirtualFilePointerContainerImpl(
      @NotNull VirtualFilePointerManager manager,
      @NotNull Disposable parentDisposable,
      @Nullable VirtualFilePointerListener listener) {
    //noinspection HardCodedStringLiteral
    super(
        TRACE_CREATION && !ApplicationInfoImpl.isInPerformanceTest()
            ? new Throwable(
                "parent = '"
                    + parentDisposable
                    + "' ("
                    + parentDisposable.getClass()
                    + "); listener="
                    + listener)
            : null);
    myVirtualFilePointerManager = manager;
    myParent = parentDisposable;
    myListener = listener;
  }

  @Override
  public void readExternal(@NotNull final Element rootChild, @NotNull final String childElements)
      throws InvalidDataException {
    final List urls = rootChild.getChildren(childElements);
    for (Object url : urls) {
      Element pathElement = (Element) url;
      final String urlAttribute = pathElement.getAttributeValue(URL_ATTR);
      if (urlAttribute == null) throw new InvalidDataException("path element without url");
      add(urlAttribute);
    }
  }

  @Override
  public void writeExternal(
      @NotNull final Element element, @NotNull final String childElementName) {
    for (VirtualFilePointer pointer : myList) {
      String url = pointer.getUrl();
      final Element rootPathElement = new Element(childElementName);
      rootPathElement.setAttribute(URL_ATTR, url);
      element.addContent(rootPathElement);
    }
  }

  @Override
  public void moveUp(@NotNull String url) {
    int index = indexOf(url);
    if (index <= 0) return;
    dropCaches();
    ContainerUtil.swapElements(myList, index - 1, index);
  }

  @Override
  public void moveDown(@NotNull String url) {
    int index = indexOf(url);
    if (index < 0 || index + 1 >= myList.size()) return;
    dropCaches();
    ContainerUtil.swapElements(myList, index, index + 1);
  }

  private int indexOf(@NotNull final String url) {
    for (int i = 0; i < myList.size(); i++) {
      final VirtualFilePointer pointer = myList.get(i);
      if (url.equals(pointer.getUrl())) {
        return i;
      }
    }

    return -1;
  }

  @Override
  public void killAll() {
    myList.clear();
  }

  @Override
  public void add(@NotNull VirtualFile file) {
    assert !myDisposed;
    dropCaches();
    final VirtualFilePointer pointer = create(file);
    myList.add(pointer);
  }

  @Override
  public void add(@NotNull String url) {
    assert !myDisposed;
    dropCaches();
    final VirtualFilePointer pointer = create(url);
    myList.add(pointer);
  }

  @Override
  public void remove(@NotNull VirtualFilePointer pointer) {
    assert !myDisposed;
    dropCaches();
    final boolean result = myList.remove(pointer);
    LOG.assertTrue(result);
  }

  @Override
  @NotNull
  public List<VirtualFilePointer> getList() {
    assert !myDisposed;
    return Collections.unmodifiableList(myList);
  }

  @Override
  public void addAll(@NotNull VirtualFilePointerContainer that) {
    assert !myDisposed;
    dropCaches();

    List<VirtualFilePointer> thatList = ((VirtualFilePointerContainerImpl) that).myList;
    for (final VirtualFilePointer pointer : thatList) {
      myList.add(duplicate(pointer));
    }
  }

  private void dropCaches() {
    myTimeStampOfCachedThings =
        -1; // make it never equal to myVirtualFilePointerManager.getModificationCount()
    myCachedThings = EMPTY;
  }

  @Override
  @NotNull
  public String[] getUrls() {
    return getOrCache().first;
  }

  @NotNull
  private Trinity<String[], VirtualFile[], VirtualFile[]> getOrCache() {
    assert !myDisposed;
    long timeStamp = myTimeStampOfCachedThings;
    Trinity<String[], VirtualFile[], VirtualFile[]> cached = myCachedThings;
    return timeStamp == myVirtualFilePointerManager.getModificationCount() ? cached : cacheThings();
  }

  private static final Trinity<String[], VirtualFile[], VirtualFile[]> EMPTY =
      Trinity.create(
          ArrayUtil.EMPTY_STRING_ARRAY, VirtualFile.EMPTY_ARRAY, VirtualFile.EMPTY_ARRAY);

  @NotNull
  private Trinity<String[], VirtualFile[], VirtualFile[]> cacheThings() {
    Trinity<String[], VirtualFile[], VirtualFile[]> result;
    if (myList.isEmpty()) {
      result = EMPTY;
    } else {
      VirtualFilePointer[] vf = myList.toArray(new VirtualFilePointer[myList.size()]);
      List<VirtualFile> cachedFiles = new ArrayList<VirtualFile>(vf.length);
      List<String> cachedUrls = new ArrayList<String>(vf.length);
      List<VirtualFile> cachedDirectories = new ArrayList<VirtualFile>(vf.length / 3);
      boolean allFilesAreDirs = true;
      for (VirtualFilePointer v : vf) {
        VirtualFile file = v.getFile();
        String url = v.getUrl();
        cachedUrls.add(url);
        if (file != null) {
          cachedFiles.add(file);
          if (file.isDirectory()) {
            cachedDirectories.add(file);
          } else {
            allFilesAreDirs = false;
          }
        }
      }
      VirtualFile[] directories = VfsUtilCore.toVirtualFileArray(cachedDirectories);
      VirtualFile[] files =
          allFilesAreDirs ? directories : VfsUtilCore.toVirtualFileArray(cachedFiles);
      String[] urlsArray = ArrayUtil.toStringArray(cachedUrls);
      result = Trinity.create(urlsArray, files, directories);
    }
    myCachedThings = result;
    myTimeStampOfCachedThings = myVirtualFilePointerManager.getModificationCount();
    return result;
  }

  @Override
  @NotNull
  public VirtualFile[] getFiles() {
    return getOrCache().second;
  }

  @Override
  @NotNull
  public VirtualFile[] getDirectories() {
    return getOrCache().third;
  }

  @Override
  @Nullable
  public VirtualFilePointer findByUrl(@NotNull String url) {
    assert !myDisposed;
    for (VirtualFilePointer pointer : myList) {
      if (url.equals(pointer.getUrl())) return pointer;
    }
    return null;
  }

  @Override
  public void clear() {
    dropCaches();
    killAll();
  }

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

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

    final VirtualFilePointerContainerImpl virtualFilePointerContainer =
        (VirtualFilePointerContainerImpl) o;

    return myList.equals(virtualFilePointerContainer.myList);
  }

  public int hashCode() {
    return myList.hashCode();
  }

  protected VirtualFilePointer create(@NotNull VirtualFile file) {
    return myVirtualFilePointerManager.create(file, myParent, myListener);
  }

  protected VirtualFilePointer create(@NotNull String url) {
    return myVirtualFilePointerManager.create(url, myParent, myListener);
  }

  protected VirtualFilePointer duplicate(@NotNull VirtualFilePointer virtualFilePointer) {
    return myVirtualFilePointerManager.duplicate(virtualFilePointer, myParent, myListener);
  }

  @NotNull
  @NonNls
  @Override
  public String toString() {
    return "VFPContainer: " + myList /*+"; parent:"+myParent*/;
  }

  @Override
  @NotNull
  public VirtualFilePointerContainer clone(@NotNull Disposable parent) {
    return clone(parent, null);
  }

  @Override
  @NotNull
  public VirtualFilePointerContainer clone(
      @NotNull Disposable parent, @Nullable VirtualFilePointerListener listener) {
    assert !myDisposed;
    VirtualFilePointerContainer clone =
        myVirtualFilePointerManager.createContainer(parent, listener);
    for (VirtualFilePointer pointer : myList) {
      clone.add(pointer.getUrl());
    }
    return clone;
  }

  @Override
  public void dispose() {
    assert !myDisposed;
    myDisposed = true;
    kill(null);
  }
}
/** @author ven */
public class CoverageDataManagerImpl extends CoverageDataManager {
  private static final String REPLACE_ACTIVE_SUITES = "&Replace active suites";
  private static final String ADD_TO_ACTIVE_SUITES = "&Add to active suites";
  private static final String DO_NOT_APPLY_COLLECTED_COVERAGE = "Do not apply &collected coverage";

  private final List<CoverageSuiteListener> myListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private static final Logger LOG =
      Logger.getInstance("#" + CoverageDataManagerImpl.class.getName());
  @NonNls private static final String SUITE = "SUITE";

  private final Project myProject;
  private final Set<CoverageSuite> myCoverageSuites = new HashSet<CoverageSuite>();
  private boolean myIsProjectClosing = false;

  private final Object myLock = new Object();
  private boolean mySubCoverageIsActive;

  public CoverageSuitesBundle getCurrentSuitesBundle() {
    return myCurrentSuitesBundle;
  }

  private CoverageSuitesBundle myCurrentSuitesBundle;

  private final Object ANNOTATORS_LOCK = new Object();
  private final Map<Editor, SrcFileAnnotator> myAnnotators =
      new HashMap<Editor, SrcFileAnnotator>();

  public CoverageDataManagerImpl(final Project project) {
    myProject = project;
    EditorColorsManager.getInstance()
        .addEditorColorsListener(
            new EditorColorsAdapter() {
              @Override
              public void globalSchemeChange(EditorColorsScheme scheme) {
                chooseSuitesBundle(myCurrentSuitesBundle);
              }
            },
            project);
    addSuiteListener(new CoverageViewSuiteListener(this, myProject), myProject);
  }

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

  public void initComponent() {}

  public void disposeComponent() {}

  public void readExternal(Element element) throws InvalidDataException {
    //noinspection unchecked
    for (Element suiteElement : element.getChildren(SUITE)) {
      final CoverageRunner coverageRunner = BaseCoverageSuite.readRunnerAttribute(suiteElement);
      // skip unknown runners
      if (coverageRunner == null) {
        // collect gc
        final CoverageFileProvider fileProvider =
            BaseCoverageSuite.readDataFileProviderAttribute(suiteElement);
        if (fileProvider.isValid()) {
          // deleteCachedCoverage(fileProvider.getCoverageDataFilePath());
        }
        continue;
      }

      CoverageSuite suite = null;
      for (CoverageEngine engine : CoverageEngine.EP_NAME.getExtensions()) {
        if (coverageRunner.acceptsCoverageEngine(engine)) {
          suite = engine.createEmptyCoverageSuite(coverageRunner);
          if (suite != null) {
            break;
          }
        }
      }
      if (suite != null) {
        try {
          suite.readExternal(suiteElement);
          myCoverageSuites.add(suite);
        } catch (NumberFormatException e) {
          // try next suite
        }
      }
    }
  }

  public void writeExternal(final Element element) throws WriteExternalException {
    for (CoverageSuite coverageSuite : myCoverageSuites) {
      final Element suiteElement = new Element(SUITE);
      element.addContent(suiteElement);
      coverageSuite.writeExternal(suiteElement);
    }
  }

  public CoverageSuite addCoverageSuite(
      final String name,
      final CoverageFileProvider fileProvider,
      final String[] filters,
      final long lastCoverageTimeStamp,
      @Nullable final String suiteToMergeWith,
      final CoverageRunner coverageRunner,
      final boolean collectLineInfo,
      final boolean tracingEnabled) {
    final CoverageSuite suite =
        createCoverageSuite(
            coverageRunner,
            name,
            fileProvider,
            filters,
            lastCoverageTimeStamp,
            suiteToMergeWith,
            collectLineInfo,
            tracingEnabled);
    if (suiteToMergeWith == null || !name.equals(suiteToMergeWith)) {
      removeCoverageSuite(suite);
    }
    myCoverageSuites.remove(suite); // remove previous instance
    myCoverageSuites.add(suite); // add new instance
    return suite;
  }

  @Override
  public CoverageSuite addExternalCoverageSuite(
      String selectedFileName,
      long timeStamp,
      CoverageRunner coverageRunner,
      CoverageFileProvider fileProvider) {
    final CoverageSuite suite =
        createCoverageSuite(
            coverageRunner,
            selectedFileName,
            fileProvider,
            ArrayUtil.EMPTY_STRING_ARRAY,
            timeStamp,
            null,
            false,
            false);
    myCoverageSuites.add(suite);
    return suite;
  }

  @Override
  public CoverageSuite addCoverageSuite(final CoverageEnabledConfiguration config) {
    final String name = config.getName() + " Coverage Results";
    final String covFilePath = config.getCoverageFilePath();
    assert covFilePath != null; // Shouldn't be null here!

    final CoverageRunner coverageRunner = config.getCoverageRunner();
    LOG.assertTrue(coverageRunner != null, "Coverage runner id = " + config.getRunnerId());

    final DefaultCoverageFileProvider fileProvider =
        new DefaultCoverageFileProvider(new File(covFilePath));
    final CoverageSuite suite = createCoverageSuite(config, name, coverageRunner, fileProvider);

    // remove previous instance
    removeCoverageSuite(suite);

    // add new instance
    myCoverageSuites.add(suite);
    return suite;
  }

  public void removeCoverageSuite(final CoverageSuite suite) {
    final String fileName = suite.getCoverageDataFileName();

    boolean deleteTraces = suite.isTracingEnabled();
    if (!FileUtil.isAncestor(PathManager.getSystemPath(), fileName, false)) {
      String message = "Would you like to delete file \'" + fileName + "\' ";
      if (deleteTraces) {
        message +=
            "and traces directory \'"
                + FileUtil.getNameWithoutExtension(new File(fileName))
                + "\' ";
      }
      message += "on disk?";
      if (Messages.showYesNoDialog(
              myProject, message, CommonBundle.getWarningTitle(), Messages.getWarningIcon())
          == Messages.YES) {
        deleteCachedCoverage(fileName, deleteTraces);
      }
    } else {
      deleteCachedCoverage(fileName, deleteTraces);
    }

    myCoverageSuites.remove(suite);
    if (myCurrentSuitesBundle != null && myCurrentSuitesBundle.contains(suite)) {
      CoverageSuite[] suites = myCurrentSuitesBundle.getSuites();
      suites = ArrayUtil.remove(suites, suite);
      chooseSuitesBundle(suites.length > 0 ? new CoverageSuitesBundle(suites) : null);
    }
  }

  private void deleteCachedCoverage(String coverageDataFileName, boolean deleteTraces) {
    FileUtil.delete(new File(coverageDataFileName));
    if (deleteTraces) {
      FileUtil.delete(getTracesDirectory(coverageDataFileName));
    }
  }

  public CoverageSuite[] getSuites() {
    return myCoverageSuites.toArray(new CoverageSuite[myCoverageSuites.size()]);
  }

  public void chooseSuitesBundle(final CoverageSuitesBundle suite) {
    if (myCurrentSuitesBundle == suite && suite == null) {
      return;
    }

    LOG.assertTrue(!myProject.isDefault());

    fireBeforeSuiteChosen();

    mySubCoverageIsActive = false;
    if (myCurrentSuitesBundle != null) {
      myCurrentSuitesBundle
          .getCoverageEngine()
          .getCoverageAnnotator(myProject)
          .onSuiteChosen(suite);
    }

    myCurrentSuitesBundle = suite;
    disposeAnnotators();

    if (suite == null) {
      triggerPresentationUpdate();
      return;
    }

    for (CoverageSuite coverageSuite : myCurrentSuitesBundle.getSuites()) {
      final boolean suiteFileExists =
          coverageSuite.getCoverageDataFileProvider().ensureFileExists();
      if (!suiteFileExists) {
        chooseSuitesBundle(null);
        return;
      }
    }

    renewCoverageData(suite);

    fireAfterSuiteChosen();
  }

  public void coverageGathered(@NotNull final CoverageSuite suite) {
    ApplicationManager.getApplication()
        .invokeLater(
            new Runnable() {
              public void run() {
                if (myProject.isDisposed()) return;
                if (myCurrentSuitesBundle != null) {
                  final String message =
                      CodeInsightBundle.message(
                          "display.coverage.prompt", suite.getPresentableName());

                  final CoverageOptionsProvider coverageOptionsProvider =
                      CoverageOptionsProvider.getInstance(myProject);
                  final DialogWrapper.DoNotAskOption doNotAskOption =
                      new DialogWrapper.DoNotAskOption() {
                        @Override
                        public boolean isToBeShown() {
                          return coverageOptionsProvider.getOptionToReplace() == 3;
                        }

                        @Override
                        public void setToBeShown(boolean value, int exitCode) {
                          coverageOptionsProvider.setOptionsToReplace(value ? 3 : exitCode);
                        }

                        @Override
                        public boolean canBeHidden() {
                          return true;
                        }

                        @Override
                        public boolean shouldSaveOptionsOnCancel() {
                          return true;
                        }

                        @NotNull
                        @Override
                        public String getDoNotShowMessage() {
                          return CommonBundle.message("dialog.options.do.not.show");
                        }
                      };
                  final String[] options =
                      myCurrentSuitesBundle.getCoverageEngine() == suite.getCoverageEngine()
                          ? new String[] {
                            REPLACE_ACTIVE_SUITES,
                            ADD_TO_ACTIVE_SUITES,
                            DO_NOT_APPLY_COLLECTED_COVERAGE
                          }
                          : new String[] {REPLACE_ACTIVE_SUITES, DO_NOT_APPLY_COLLECTED_COVERAGE};
                  final int answer =
                      doNotAskOption.isToBeShown()
                          ? Messages.showDialog(
                              message,
                              CodeInsightBundle.message("code.coverage"),
                              options,
                              1,
                              Messages.getQuestionIcon(),
                              doNotAskOption)
                          : coverageOptionsProvider.getOptionToReplace();
                  if (answer == DialogWrapper.OK_EXIT_CODE) {
                    chooseSuitesBundle(new CoverageSuitesBundle(suite));
                  } else if (answer == 1) {
                    chooseSuitesBundle(
                        new CoverageSuitesBundle(
                            ArrayUtil.append(myCurrentSuitesBundle.getSuites(), suite)));
                  }
                } else {
                  chooseSuitesBundle(new CoverageSuitesBundle(suite));
                }
              }
            });
  }

  public void triggerPresentationUpdate() {
    renewInformationInEditors();
    UIUtil.invokeLaterIfNeeded(
        new Runnable() {
          public void run() {
            if (myProject.isDisposed()) return;
            ProjectView.getInstance(myProject).refresh();
            CoverageViewManager.getInstance(myProject).setReady(true);
          }
        });
  }

  public void attachToProcess(
      @NotNull final ProcessHandler handler,
      @NotNull final RunConfigurationBase configuration,
      final RunnerSettings runnerSettings) {
    handler.addProcessListener(
        new ProcessAdapter() {
          public void processTerminated(final ProcessEvent event) {
            processGatheredCoverage(configuration, runnerSettings);
          }
        });
  }

  @Override
  public void processGatheredCoverage(
      @NotNull RunConfigurationBase configuration, RunnerSettings runnerSettings) {
    if (runnerSettings instanceof CoverageRunnerData) {
      processGatheredCoverage(configuration);
    }
  }

  public static void processGatheredCoverage(RunConfigurationBase configuration) {
    final Project project = configuration.getProject();
    if (project.isDisposed()) return;
    final CoverageDataManager coverageDataManager = CoverageDataManager.getInstance(project);
    final CoverageEnabledConfiguration coverageEnabledConfiguration =
        CoverageEnabledConfiguration.getOrCreate(configuration);
    //noinspection ConstantConditions
    final CoverageSuite coverageSuite = coverageEnabledConfiguration.getCurrentCoverageSuite();
    if (coverageSuite != null) {
      ((BaseCoverageSuite) coverageSuite).setConfiguration(configuration);
      coverageDataManager.coverageGathered(coverageSuite);
    }
  }

  protected void renewCoverageData(@NotNull final CoverageSuitesBundle suite) {
    if (myCurrentSuitesBundle != null) {
      myCurrentSuitesBundle
          .getCoverageEngine()
          .getCoverageAnnotator(myProject)
          .renewCoverageData(suite, this);
    }
  }

  private void renewInformationInEditors() {
    final FileEditorManager fileEditorManager = FileEditorManager.getInstance(myProject);
    final VirtualFile[] openFiles = fileEditorManager.getOpenFiles();
    for (VirtualFile openFile : openFiles) {
      final FileEditor[] allEditors = fileEditorManager.getAllEditors(openFile);
      applyInformationToEditor(allEditors, openFile);
    }
  }

  private void applyInformationToEditor(FileEditor[] editors, final VirtualFile file) {
    final PsiFile psiFile =
        doInReadActionIfProjectOpen(
            new Computable<PsiFile>() {
              @Nullable
              @Override
              public PsiFile compute() {
                return PsiManager.getInstance(myProject).findFile(file);
              }
            });
    if (psiFile != null && myCurrentSuitesBundle != null && psiFile.isPhysical()) {
      final CoverageEngine engine = myCurrentSuitesBundle.getCoverageEngine();
      if (!engine.coverageEditorHighlightingApplicableTo(psiFile)) {
        return;
      }

      for (FileEditor editor : editors) {
        if (editor instanceof TextEditor) {
          final Editor textEditor = ((TextEditor) editor).getEditor();
          SrcFileAnnotator annotator;
          synchronized (ANNOTATORS_LOCK) {
            annotator = myAnnotators.remove(textEditor);
          }
          if (annotator != null) {
            Disposer.dispose(annotator);
          }
          break;
        }
      }

      for (FileEditor editor : editors) {
        if (editor instanceof TextEditor) {
          final Editor textEditor = ((TextEditor) editor).getEditor();
          SrcFileAnnotator annotator = getAnnotator(textEditor);
          if (annotator == null) {
            annotator = new SrcFileAnnotator(psiFile, textEditor);
            synchronized (ANNOTATORS_LOCK) {
              myAnnotators.put(textEditor, annotator);
            }
          }

          if (myCurrentSuitesBundle != null
              && engine.acceptedByFilters(psiFile, myCurrentSuitesBundle)) {
            annotator.showCoverageInformation(myCurrentSuitesBundle);
          }
        }
      }
    }
  }

  public void projectOpened() {
    EditorFactory.getInstance()
        .addEditorFactoryListener(new CoverageEditorFactoryListener(), myProject);
    ProjectManagerAdapter projectManagerListener =
        new ProjectManagerAdapter() {
          public void projectClosing(Project project) {
            synchronized (myLock) {
              myIsProjectClosing = true;
            }
          }
        };
    ProjectManager.getInstance().addProjectManagerListener(myProject, projectManagerListener);
  }

  public void projectClosed() {}

  public <T> T doInReadActionIfProjectOpen(Computable<T> computation) {
    synchronized (myLock) {
      if (myIsProjectClosing) return null;
    }
    return ApplicationManager.getApplication().runReadAction(computation);
  }

  public void selectSubCoverage(
      @NotNull final CoverageSuitesBundle suite, final List<String> testNames) {
    suite.restoreCoverageData();
    final ProjectData data = suite.getCoverageData();
    if (data == null) return;
    mySubCoverageIsActive = true;
    final Map<String, Set<Integer>> executionTrace = new HashMap<String, Set<Integer>>();
    for (CoverageSuite coverageSuite : suite.getSuites()) {
      final String fileName = coverageSuite.getCoverageDataFileName();
      final File tracesDir = getTracesDirectory(fileName);
      for (String testName : testNames) {
        final File file = new File(tracesDir, FileUtil.sanitizeFileName(testName) + ".tr");
        if (file.exists()) {
          DataInputStream in = null;
          try {
            in = new DataInputStream(new FileInputStream(file));
            int traceSize = in.readInt();
            for (int i = 0; i < traceSize; i++) {
              final String className = in.readUTF();
              final int linesSize = in.readInt();
              Set<Integer> lines = executionTrace.get(className);
              if (lines == null) {
                lines = new HashSet<Integer>();
                executionTrace.put(className, lines);
              }
              for (int l = 0; l < linesSize; l++) {
                lines.add(in.readInt());
              }
            }
          } catch (Exception e) {
            LOG.error(e);
          } finally {
            try {
              in.close();
            } catch (IOException e) {
              LOG.error(e);
            }
          }
        }
      }
    }
    final ProjectData projectData = new ProjectData();
    for (String className : executionTrace.keySet()) {
      ClassData loadedClassData = projectData.getClassData(className);
      if (loadedClassData == null) {
        loadedClassData = projectData.getOrCreateClassData(className);
      }
      final Set<Integer> lineNumbers = executionTrace.get(className);
      final ClassData oldData = data.getClassData(className);
      LOG.assertTrue(oldData != null, "missed className: \"" + className + "\"");
      final Object[] oldLines = oldData.getLines();
      LOG.assertTrue(oldLines != null);
      int maxNumber = oldLines.length;
      for (Integer lineNumber : lineNumbers) {
        if (lineNumber >= maxNumber) {
          maxNumber = lineNumber + 1;
        }
      }
      final LineData[] lines = new LineData[maxNumber];
      for (Integer line : lineNumbers) {
        final int lineIdx = line.intValue() - 1;
        String methodSig = null;
        if (lineIdx < oldData.getLines().length) {
          final LineData oldLineData = oldData.getLineData(lineIdx);
          if (oldLineData != null) {
            methodSig = oldLineData.getMethodSignature();
          }
        }
        final LineData lineData = new LineData(lineIdx, methodSig);
        if (methodSig != null) {
          loadedClassData.registerMethodSignature(lineData);
        }
        lineData.setStatus(LineCoverage.FULL);
        lines[lineIdx] = lineData;
      }
      loadedClassData.setLines(lines);
    }
    suite.setCoverageData(projectData);
    renewCoverageData(suite);
  }

  private File getTracesDirectory(final String fileName) {
    return new File(
        new File(fileName).getParentFile(), FileUtil.getNameWithoutExtension(new File(fileName)));
  }

  public void restoreMergedCoverage(@NotNull final CoverageSuitesBundle suite) {
    mySubCoverageIsActive = false;
    suite.restoreCoverageData();
    renewCoverageData(suite);
  }

  @Override
  public void addSuiteListener(final CoverageSuiteListener listener, Disposable parentDisposable) {
    myListeners.add(listener);
    Disposer.register(
        parentDisposable,
        new Disposable() {
          public void dispose() {
            myListeners.remove(listener);
          }
        });
  }

  public void fireBeforeSuiteChosen() {
    for (CoverageSuiteListener listener : myListeners) {
      listener.beforeSuiteChosen();
    }
  }

  public void fireAfterSuiteChosen() {
    for (CoverageSuiteListener listener : myListeners) {
      listener.afterSuiteChosen();
    }
  }

  public boolean isSubCoverageActive() {
    return mySubCoverageIsActive;
  }

  @Nullable
  public SrcFileAnnotator getAnnotator(Editor editor) {
    synchronized (ANNOTATORS_LOCK) {
      return myAnnotators.get(editor);
    }
  }

  public void disposeAnnotators() {
    synchronized (ANNOTATORS_LOCK) {
      for (SrcFileAnnotator annotator : myAnnotators.values()) {
        if (annotator != null) {
          Disposer.dispose(annotator);
        }
      }
      myAnnotators.clear();
    }
  }

  @NotNull
  private CoverageSuite createCoverageSuite(
      final CoverageEnabledConfiguration config,
      final String name,
      final CoverageRunner coverageRunner,
      final DefaultCoverageFileProvider fileProvider) {
    CoverageSuite suite = null;
    for (CoverageEngine engine : CoverageEngine.EP_NAME.getExtensions()) {
      if (coverageRunner.acceptsCoverageEngine(engine)
          && engine.isApplicableTo(config.getConfiguration())) {
        suite = engine.createCoverageSuite(coverageRunner, name, fileProvider, config);
        if (suite != null) {
          break;
        }
      }
    }
    LOG.assertTrue(
        suite != null,
        "Cannot create coverage suite for runner: " + coverageRunner.getPresentableName());
    return suite;
  }

  @NotNull
  private CoverageSuite createCoverageSuite(
      final CoverageRunner coverageRunner,
      final String name,
      final CoverageFileProvider fileProvider,
      final String[] filters,
      final long lastCoverageTimeStamp,
      final String suiteToMergeWith,
      final boolean collectLineInfo,
      final boolean tracingEnabled) {

    CoverageSuite suite = null;
    for (CoverageEngine engine : CoverageEngine.EP_NAME.getExtensions()) {
      if (coverageRunner.acceptsCoverageEngine(engine)) {
        suite =
            engine.createCoverageSuite(
                coverageRunner,
                name,
                fileProvider,
                filters,
                lastCoverageTimeStamp,
                suiteToMergeWith,
                collectLineInfo,
                tracingEnabled,
                false,
                myProject);
        if (suite != null) {
          break;
        }
      }
    }

    LOG.assertTrue(
        suite != null,
        "Cannot create coverage suite for runner: " + coverageRunner.getPresentableName());
    return suite;
  }

  private class CoverageEditorFactoryListener implements EditorFactoryListener {
    private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD, myProject);
    private final Map<Editor, Runnable> myCurrentEditors = new HashMap<Editor, Runnable>();

    public void editorCreated(@NotNull EditorFactoryEvent event) {
      synchronized (myLock) {
        if (myIsProjectClosing) return;
      }

      final Editor editor = event.getEditor();
      if (editor.getProject() != myProject) return;
      final PsiFile psiFile =
          ApplicationManager.getApplication()
              .runReadAction(
                  new Computable<PsiFile>() {
                    @Nullable
                    @Override
                    public PsiFile compute() {
                      if (myProject.isDisposed()) return null;
                      final PsiDocumentManager documentManager =
                          PsiDocumentManager.getInstance(myProject);
                      final Document document = editor.getDocument();
                      return documentManager.getPsiFile(document);
                    }
                  });

      if (psiFile != null && myCurrentSuitesBundle != null && psiFile.isPhysical()) {
        final CoverageEngine engine = myCurrentSuitesBundle.getCoverageEngine();
        if (!engine.coverageEditorHighlightingApplicableTo(psiFile)) {
          return;
        }

        SrcFileAnnotator annotator = getAnnotator(editor);
        if (annotator == null) {
          annotator = new SrcFileAnnotator(psiFile, editor);
        }

        final SrcFileAnnotator finalAnnotator = annotator;

        synchronized (ANNOTATORS_LOCK) {
          myAnnotators.put(editor, finalAnnotator);
        }

        final Runnable request =
            new Runnable() {
              @Override
              public void run() {
                if (myProject.isDisposed()) return;
                if (myCurrentSuitesBundle != null) {
                  if (engine.acceptedByFilters(psiFile, myCurrentSuitesBundle)) {
                    finalAnnotator.showCoverageInformation(myCurrentSuitesBundle);
                  }
                }
              }
            };
        myCurrentEditors.put(editor, request);
        myAlarm.addRequest(request, 100);
      }
    }

    public void editorReleased(@NotNull EditorFactoryEvent event) {
      final Editor editor = event.getEditor();
      if (editor.getProject() != myProject) return;
      try {
        final SrcFileAnnotator fileAnnotator;
        synchronized (ANNOTATORS_LOCK) {
          fileAnnotator = myAnnotators.remove(editor);
        }
        if (fileAnnotator != null) {
          Disposer.dispose(fileAnnotator);
        }
      } finally {
        final Runnable request = myCurrentEditors.remove(editor);
        if (request != null) {
          myAlarm.cancelRequest(request);
        }
      }
    }
  }
}
/** @author Konstantin Kolosovsky. */
public class TerminalProcessHandler extends SvnProcessHandler {

  private final List<InteractiveCommandListener> myInteractiveListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private final CapturingProcessAdapter terminalOutputCapturer = new CapturingProcessAdapter();

  private final StringBuilder outputLine = new StringBuilder();
  private final StringBuilder errorLine = new StringBuilder();

  public TerminalProcessHandler(@NotNull Process process, boolean forceUtf8, boolean forceBinary) {
    super(process, forceUtf8, forceBinary);
  }

  public void addInteractiveListener(@NotNull InteractiveCommandListener listener) {
    myInteractiveListeners.add(listener);
  }

  @Override
  protected boolean processHasSeparateErrorStream() {
    return false;
  }

  @Override
  protected void destroyProcessImpl() {
    final Process process = getProcess();
    process.destroy();
  }

  @Override
  protected boolean useNonBlockingRead() {
    return false;
  }

  @Override
  public void notifyTextAvailable(String text, Key outputType) {
    terminalOutputCapturer.onTextAvailable(new ProcessEvent(this, text), outputType);

    text = filterText(text);

    if (!StringUtil.isEmpty(text)) {
      StringBuilder lastLine = getLastLineFor(outputType);
      String currentLine = lastLine.append(text).toString();
      lastLine.setLength(0);

      currentLine = filterCombinedText(currentLine);

      // check if current line presents some interactive output
      boolean handled = handlePrompt(currentLine, outputType);
      if (!handled) {
        notify(currentLine, outputType, lastLine);
      }
    }
  }

  protected boolean handlePrompt(String text, Key outputType) {
    // if process has separate output and error streams => try to handle prompts only from error
    // stream output
    boolean shouldHandleWithListeners =
        !processHasSeparateErrorStream() || ProcessOutputTypes.STDERR.equals(outputType);

    return shouldHandleWithListeners && handlePromptWithListeners(text, outputType);
  }

  private boolean handlePromptWithListeners(String text, Key outputType) {
    boolean result = false;

    for (InteractiveCommandListener listener : myInteractiveListeners) {
      result |= listener.handlePrompt(text, outputType);
    }

    return result;
  }

  @NotNull
  protected String filterCombinedText(@NotNull String currentLine) {
    return currentLine;
  }

  @NotNull
  protected String filterText(@NotNull String text) {
    return text;
  }

  private void notify(
      @NotNull String text, @NotNull Key outputType, @NotNull StringBuilder lastLine) {
    // text is not more than one line - either one line or part of the line
    if (StringUtil.endsWith(text, "\n")) {
      // we have full line - notify listeners
      super.notifyTextAvailable(text, resolveOutputType(text, outputType));
    } else {
      // save line part to lastLine
      lastLine.append(text);
    }
  }

  @NotNull
  protected Key resolveOutputType(@NotNull String line, @NotNull Key outputType) {
    Key result = outputType;

    if (!ProcessOutputTypes.SYSTEM.equals(outputType)) {
      Matcher errorMatcher = SvnUtil.ERROR_PATTERN.matcher(line);
      Matcher warningMatcher = SvnUtil.WARNING_PATTERN.matcher(line);

      result =
          errorMatcher.find() || warningMatcher.find()
              ? ProcessOutputTypes.STDERR
              : ProcessOutputTypes.STDOUT;
    }

    return result;
  }

  @NotNull
  private StringBuilder getLastLineFor(Key outputType) {
    if (ProcessOutputTypes.STDERR.equals(outputType)) {
      return errorLine;
    } else if (ProcessOutputTypes.STDOUT.equals(outputType)) {
      return outputLine;
    } else {
      throw new IllegalArgumentException("Unknown process output type " + outputType);
    }
  }

  public String getTerminalOutput() {
    return terminalOutputCapturer.getOutput().getStdout();
  }
}
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;
    }
  }
}
/**
 * @author anna
 * @since 29-May-2006
 */
public abstract class MasterDetailsComponent
    implements Configurable, DetailsComponent.Facade, MasterDetails {
  protected static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.ui.MasterDetailsComponent");

  protected static final Icon COPY_ICON = PlatformIcons.COPY_ICON;

  protected NamedConfigurable myCurrentConfigurable;
  private final JBSplitter mySplitter;

  @NonNls public static final String TREE_OBJECT = "treeObject";
  @NonNls public static final String TREE_NAME = "treeName";

  protected History myHistory =
      new History(
          new Place.Navigator() {
            public void setHistory(final History history) {
              myHistory = history;
            }

            @Nullable
            public ActionCallback navigateTo(
                @Nullable final Place place, final boolean requestFocus) {
              return null;
            }

            public void queryPlace(@NotNull final Place place) {}
          });
  private JComponent myMaster;

  public void setHistory(final History history) {
    myHistory = history;
  }

  protected final MasterDetailsState myState;

  protected Runnable TREE_UPDATER;

  {
    TREE_UPDATER =
        new Runnable() {
          public void run() {
            final TreePath selectionPath = myTree.getSelectionPath();
            if (selectionPath == null) return;

            MyNode node = (MyNode) selectionPath.getLastPathComponent();
            if (node == null) return;

            myState.setLastEditedConfigurable(getNodePathString(node)); // survive after rename;
            myDetails.setText(node.getConfigurable().getBannerSlogan());
            ((DefaultTreeModel) myTree.getModel()).reload(node);
            fireItemsChangedExternally();
          }
        };
  }

  protected MyNode myRoot = new MyRootNode();
  protected Tree myTree = new Tree();

  private final DetailsComponent myDetails =
      new DetailsComponent(
          !Registry.is("ide.new.project.settings"), !Registry.is("ide.new.project.settings"));
  protected JPanel myWholePanel;
  public JPanel myNorthPanel = new JPanel(new BorderLayout());

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

  private final Set<NamedConfigurable> myInitializedConfigurables =
      new HashSet<NamedConfigurable>();

  private boolean myHasDeletedItems;
  protected AutoScrollToSourceHandler myAutoScrollHandler;

  protected boolean myToReInitWholePanel = true;

  protected MasterDetailsComponent() {
    this(new MasterDetailsState());
  }

  protected MasterDetailsComponent(MasterDetailsState state) {
    myState = state;

    mySplitter =
        isNewProjectSettings() ? new OnePixelSplitter(false, .2f) : new JBSplitter(false, .2f);
    mySplitter.setSplitterProportionKey("ProjectStructure.SecondLevelElements");
    mySplitter.setHonorComponentsMinimumSize(true);

    installAutoScroll();
    reInitWholePanelIfNeeded();
  }

  private boolean isNewProjectSettings() {
    if (!Registry.is("ide.new.project.settings")) {
      return false;
    }
    try {
      // assume that only project structure dialog uses the following base class for details:
      String name =
          "com.intellij.openapi.roots.ui.configuration.projectRoot.BaseStructureConfigurable";
      return Class.forName(name).isAssignableFrom(getClass());
    } catch (ClassNotFoundException ignored) {
      return false;
    }
  }

  protected void reInitWholePanelIfNeeded() {
    if (!myToReInitWholePanel) return;

    myWholePanel =
        new JPanel(new BorderLayout()) {
          public void addNotify() {
            super.addNotify();
            MasterDetailsComponent.this.addNotify();

            TreeModel m = myTree.getModel();
            if (m instanceof DefaultTreeModel) {
              DefaultTreeModel model = (DefaultTreeModel) m;
              for (int eachRow = 0; eachRow < myTree.getRowCount(); eachRow++) {
                TreePath eachPath = myTree.getPathForRow(eachRow);
                Object component = eachPath.getLastPathComponent();
                if (component instanceof TreeNode) {
                  model.nodeChanged((TreeNode) component);
                }
              }
            }
          }
        };
    mySplitter.setHonorComponentsMinimumSize(true);
    myWholePanel.add(mySplitter, BorderLayout.CENTER);

    JPanel left =
        new JPanel(new BorderLayout()) {
          public Dimension getMinimumSize() {
            final Dimension original = super.getMinimumSize();
            return new Dimension(Math.max(original.width, 100), original.height);
          }
        };

    if (isNewProjectSettings()) {
      ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTree);
      DefaultActionGroup group = createToolbarActionGroup();
      if (group != null) {
        decorator.setActionGroup(group);
      }
      // left.add(myNorthPanel, BorderLayout.NORTH);
      myMaster =
          decorator.setAsUsualTopToolbar().setPanelBorder(JBUI.Borders.empty()).createPanel();
      myNorthPanel.setVisible(false);
    } else {
      left.add(myNorthPanel, BorderLayout.NORTH);
      myMaster = ScrollPaneFactory.createScrollPane(myTree);
    }
    left.add(myMaster, BorderLayout.CENTER);
    mySplitter.setFirstComponent(left);

    final JPanel right = new JPanel(new BorderLayout());
    right.add(myDetails.getComponent(), BorderLayout.CENTER);
    if (!isNewProjectSettings()) {
      myWholePanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
    }

    mySplitter.setSecondComponent(right);

    GuiUtils.replaceJSplitPaneWithIDEASplitter(myWholePanel);

    myToReInitWholePanel = false;
  }

  private void installAutoScroll() {
    myAutoScrollHandler =
        new AutoScrollToSourceHandler() {
          protected boolean isAutoScrollMode() {
            return isAutoScrollEnabled();
          }

          protected void setAutoScrollMode(boolean state) {
            // do nothing
          }

          protected void scrollToSource(Component tree) {
            updateSelectionFromTree();
          }

          protected boolean needToCheckFocus() {
            return false;
          }
        };
    myAutoScrollHandler.install(myTree);
  }

  protected void addNotify() {
    updateSelectionFromTree();
  }

  private void updateSelectionFromTree() {
    TreePath[] treePaths = myTree.getSelectionPaths();
    if (treePaths != null) {
      List<NamedConfigurable> selectedConfigurables = new ArrayList<NamedConfigurable>();
      for (TreePath path : treePaths) {
        Object lastPathComponent = path.getLastPathComponent();
        if (lastPathComponent instanceof MyNode) {
          selectedConfigurables.add(((MyNode) lastPathComponent).getConfigurable());
        }
      }
      if (selectedConfigurables.size() > 1 && updateMultiSelection(selectedConfigurables)) {
        return;
      }
    }

    final TreePath path = myTree.getSelectionPath();
    if (path != null) {
      final Object lastPathComp = path.getLastPathComponent();
      if (!(lastPathComp instanceof MyNode)) return;
      final MyNode node = (MyNode) lastPathComp;
      setSelectedNode(node);
    } else {
      setSelectedNode(null);
    }
  }

  protected boolean updateMultiSelection(final List<NamedConfigurable> selectedConfigurables) {
    return false;
  }

  public DetailsComponent getDetailsComponent() {
    return myDetails;
  }

  public Splitter getSplitter() {
    return mySplitter;
  }

  protected boolean isAutoScrollEnabled() {
    return myHistory == null || !myHistory.isNavigatingNow();
  }

  protected DefaultActionGroup createToolbarActionGroup() {
    final ArrayList<AnAction> actions = createActions(false);
    if (actions != null) {
      final DefaultActionGroup group = new DefaultActionGroup();
      for (AnAction action : actions) {
        if (action instanceof ActionGroupWithPreselection) {
          group.add(new MyActionGroupWrapper((ActionGroupWithPreselection) action));
        } else {
          group.add(action);
        }
      }
      return group;
    }
    return null;
  }

  private void initToolbar() {
    if (isNewProjectSettings()) return;
    DefaultActionGroup group = createToolbarActionGroup();
    if (group != null) {
      final JComponent component =
          ActionManager.getInstance()
              .createActionToolbar(ActionPlaces.UNKNOWN, group, true)
              .getComponent();
      myNorthPanel.add(component, BorderLayout.NORTH);
    }
  }

  public void addItemsChangeListener(ItemsChangeListener l) {
    myListeners.add(l);
  }

  protected Dimension getPanelPreferredSize() {
    return JBUI.size(800, 600);
  }

  @NotNull
  public JComponent createComponent() {
    myTree.updateUI();
    reInitWholePanelIfNeeded();

    updateSelectionFromTree();

    final JPanel panel =
        new JPanel(new BorderLayout()) {
          public Dimension getPreferredSize() {
            return getPanelPreferredSize();
          }
        };
    panel.add(myWholePanel, BorderLayout.CENTER);
    return panel;
  }

  public boolean isModified() {
    if (myHasDeletedItems) return true;
    final boolean[] modified = new boolean[1];
    TreeUtil.traverseDepth(
        myRoot,
        new TreeUtil.Traverse() {
          public boolean accept(Object node) {
            if (node instanceof MyNode) {
              final NamedConfigurable configurable = ((MyNode) node).getConfigurable();
              if (isInitialized(configurable) && configurable.isModified()) {
                modified[0] = true;
                return false;
              }
            }
            return true;
          }
        });
    return modified[0];
  }

  protected boolean isInitialized(final NamedConfigurable configurable) {
    return myInitializedConfigurables.contains(configurable);
  }

  public void apply() throws ConfigurationException {
    processRemovedItems();
    final ConfigurationException[] ex = new ConfigurationException[1];
    TreeUtil.traverse(
        myRoot,
        new TreeUtil.Traverse() {
          public boolean accept(Object node) {
            if (node instanceof MyNode) {
              try {
                final NamedConfigurable configurable = ((MyNode) node).getConfigurable();
                if (isInitialized(configurable) && configurable.isModified()) {
                  configurable.apply();
                }
              } catch (ConfigurationException e) {
                ex[0] = e;
                return false;
              }
            }
            return true;
          }
        });
    if (ex[0] != null) {
      throw ex[0];
    }
    myHasDeletedItems = false;
  }

  protected abstract void processRemovedItems();

  protected abstract boolean wasObjectStored(Object editableObject);

  public void reset() {
    loadComponentState();
    myHasDeletedItems = false;
    ((DefaultTreeModel) myTree.getModel()).reload();
    // myTree.requestFocus();
    myState.getProportions().restoreSplitterProportions(myWholePanel);

    final Enumeration enumeration = myRoot.breadthFirstEnumeration();
    boolean selected = false;
    while (enumeration.hasMoreElements()) {
      final MyNode node = (MyNode) enumeration.nextElement();
      if (node instanceof MyRootNode) continue;
      final String path = getNodePathString(node);
      if (!selected && Comparing.strEqual(path, myState.getLastEditedConfigurable())) {
        TreeUtil.selectInTree(node, false, myTree);
        selected = true;
      }
    }
    if (!selected) {
      TreeUtil.selectFirstNode(myTree);
    }
    updateSelectionFromTree();
  }

  protected void loadComponentState() {
    final String key = getComponentStateKey();
    final MasterDetailsStateService stateService = getStateService();
    if (key != null && stateService != null) {
      final MasterDetailsState state = stateService.getComponentState(key, myState.getClass());
      if (state != null) {
        loadState(state);
      }
    }
  }

  private static String getNodePathString(final MyNode node) {
    StringBuilder path = new StringBuilder();
    MyNode current = node;
    while (current != null) {
      final Object userObject = current.getUserObject();
      if (!(userObject instanceof NamedConfigurable)) break;
      final String displayName = current.getDisplayName();
      if (StringUtil.isEmptyOrSpaces(displayName)) break;
      if (path.length() > 0) {
        path.append('|');
      }
      path.append(displayName);

      final TreeNode parent = current.getParent();
      if (!(parent instanceof MyNode)) break;
      current = (MyNode) parent;
    }
    return path.toString();
  }

  @Nullable
  @NonNls
  protected String getComponentStateKey() {
    return null;
  }

  @Nullable
  protected MasterDetailsStateService getStateService() {
    return null;
  }

  protected MasterDetailsState getState() {
    return myState;
  }

  protected void loadState(final MasterDetailsState object) {
    XmlSerializerUtil.copyBean(object, myState);
  }

  public void disposeUIResources() {
    myState.getProportions().saveSplitterProportions(myWholePanel);
    myAutoScrollHandler.cancelAllRequests();
    myDetails.disposeUIResources();
    myInitializedConfigurables.clear();
    clearChildren();
    final String key = getComponentStateKey();
    final MasterDetailsStateService stateService = getStateService();
    if (key != null && stateService != null) {
      stateService.setComponentState(key, getState());
    }
    myCurrentConfigurable = null;
  }

  protected void clearChildren() {
    TreeUtil.traverseDepth(
        myRoot,
        new TreeUtil.Traverse() {
          public boolean accept(Object node) {
            if (node instanceof MyNode) {
              final MyNode treeNode = ((MyNode) node);
              treeNode.getConfigurable().disposeUIResources();
              if (!(treeNode instanceof MyRootNode)) {
                treeNode.setUserObject(null);
              }
            }
            return true;
          }
        });
    myRoot.removeAllChildren();
  }

  @Nullable
  protected ArrayList<AnAction> createActions(final boolean fromPopup) {
    return null;
  }

  protected void initTree() {
    ((DefaultTreeModel) myTree.getModel()).setRoot(myRoot);
    myTree.setRootVisible(false);
    myTree.setShowsRootHandles(true);
    UIUtil.setLineStyleAngled(myTree);
    TreeUtil.installActions(myTree);
    myTree.setCellRenderer(
        new ColoredTreeCellRenderer() {
          public void customizeCellRenderer(
              JTree tree,
              Object value,
              boolean selected,
              boolean expanded,
              boolean leaf,
              int row,
              boolean hasFocus) {
            if (value instanceof MyNode) {
              final MyNode node = ((MyNode) value);
              setIcon(node.getIcon(expanded));
              final Font font = UIUtil.getTreeFont();
              if (node.isDisplayInBold()) {
                setFont(font.deriveFont(Font.BOLD));
              } else {
                setFont(font.deriveFont(Font.PLAIN));
              }
              append(
                  node.getDisplayName(),
                  node.isDisplayInBold()
                      ? SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES
                      : SimpleTextAttributes.REGULAR_ATTRIBUTES);
            }
          }
        });
    initToolbar();
    ArrayList<AnAction> actions = createActions(true);
    if (actions != null) {
      final DefaultActionGroup group = new DefaultActionGroup();
      for (AnAction action : actions) {
        group.add(action);
      }
      actions = getAdditionalActions();
      if (actions != null) {
        group.addSeparator();
        for (AnAction action : actions) {
          group.add(action);
        }
      }
      PopupHandler.installPopupHandler(
          myTree,
          group,
          ActionPlaces.UNKNOWN,
          ActionManager.getInstance()); // popup should follow the selection
    }
  }

  @Nullable
  protected ArrayList<AnAction> getAdditionalActions() {
    return null;
  }

  public void fireItemsChangeListener(final Object editableObject) {
    for (ItemsChangeListener listener : myListeners) {
      listener.itemChanged(editableObject);
    }
  }

  private void fireItemsChangedExternally() {
    for (ItemsChangeListener listener : myListeners) {
      listener.itemsExternallyChanged();
    }
  }

  private void createUIComponents() {
    myTree =
        new Tree() {
          public Dimension getPreferredScrollableViewportSize() {
            Dimension size = super.getPreferredScrollableViewportSize();
            size = new Dimension(size.width + 20, size.height);
            return size;
          }

          @SuppressWarnings({"NonStaticInitializer"})
          public JToolTip createToolTip() {
            final JToolTip toolTip =
                new JToolTip() {
                  {
                    setUI(new MultiLineTooltipUI());
                  }
                };
            toolTip.setComponent(this);
            return toolTip;
          }
        };
  }

  protected void addNode(MyNode nodeToAdd, MyNode parent) {
    int i = TreeUtil.indexedBinarySearch(parent, nodeToAdd, getNodeComparator());
    int insertionPoint = i >= 0 ? i : -i - 1;
    ((DefaultTreeModel) myTree.getModel()).insertNodeInto(nodeToAdd, parent, insertionPoint);
  }

  protected void sortDescendants(MyNode root) {
    TreeUtil.sort(root, getNodeComparator());
    ((DefaultTreeModel) myTree.getModel()).reload(root);
  }

  protected Comparator<MyNode> getNodeComparator() {
    return new Comparator<MyNode>() {
      public int compare(final MyNode o1, final MyNode o2) {
        return StringUtil.naturalCompare(o1.getDisplayName(), o2.getDisplayName());
      }
    };
  }

  public ActionCallback selectNodeInTree(final DefaultMutableTreeNode nodeToSelect) {
    return selectNodeInTree(nodeToSelect, true, false);
  }

  public ActionCallback selectNodeInTree(
      final DefaultMutableTreeNode nodeToSelect, boolean requestFocus) {
    return selectNodeInTree(nodeToSelect, true, requestFocus);
  }

  public ActionCallback selectNodeInTree(
      final DefaultMutableTreeNode nodeToSelect, boolean center, final boolean requestFocus) {
    if (requestFocus) {
      myTree.requestFocus();
    }
    if (nodeToSelect != null) {
      return TreeUtil.selectInTree(nodeToSelect, requestFocus, myTree, center);
    } else {
      return TreeUtil.selectFirstNode(myTree);
    }
  }

  @Nullable
  public Object getSelectedObject() {
    final TreePath selectionPath = myTree.getSelectionPath();
    if (selectionPath != null && selectionPath.getLastPathComponent() instanceof MyNode) {
      MyNode node = (MyNode) selectionPath.getLastPathComponent();
      final NamedConfigurable configurable = node.getConfigurable();
      LOG.assertTrue(configurable != null, "already disposed");
      return configurable.getEditableObject();
    }
    return null;
  }

  @Nullable
  public NamedConfigurable getSelectedConfigurable() {
    final TreePath selectionPath = myTree.getSelectionPath();
    if (selectionPath != null) {
      MyNode node = (MyNode) selectionPath.getLastPathComponent();
      final NamedConfigurable configurable = node.getConfigurable();
      LOG.assertTrue(configurable != null, "already disposed");
      return configurable;
    }
    return null;
  }

  public void selectNodeInTree(String displayName) {
    final MyNode nodeByName = findNodeByName(myRoot, displayName);
    selectNodeInTree(nodeByName, true);
  }

  public void selectNodeInTree(final Object object) {
    selectNodeInTree(findNodeByObject(myRoot, object), true);
  }

  @Nullable
  protected static MyNode findNodeByName(final TreeNode root, final String profileName) {
    if (profileName == null) return null; // do not suggest root node
    return findNodeByCondition(
        root,
        new Condition<NamedConfigurable>() {
          public boolean value(final NamedConfigurable configurable) {
            return Comparing.strEqual(profileName, configurable.getDisplayName());
          }
        });
  }

  @Nullable
  public static MyNode findNodeByObject(final TreeNode root, final Object editableObject) {
    if (editableObject == null) return null; // do not suggest root node
    return findNodeByCondition(
        root,
        new Condition<NamedConfigurable>() {
          public boolean value(final NamedConfigurable configurable) {
            return Comparing.equal(editableObject, configurable.getEditableObject());
          }
        });
  }

  protected static MyNode findNodeByCondition(
      final TreeNode root, final Condition<NamedConfigurable> condition) {
    final MyNode[] nodeToSelect = new MyNode[1];
    TreeUtil.traverseDepth(
        root,
        new TreeUtil.Traverse() {
          public boolean accept(Object node) {
            if (condition.value(((MyNode) node).getConfigurable())) {
              nodeToSelect[0] = (MyNode) node;
              return false;
            }
            return true;
          }
        });
    return nodeToSelect[0];
  }

  protected void setSelectedNode(@Nullable MyNode node) {
    if (node != null) {
      myState.setLastEditedConfigurable(getNodePathString(node));
    }
    updateSelection(node != null ? node.getConfigurable() : null);
  }

  protected void updateSelection(@Nullable NamedConfigurable configurable) {
    myDetails.setText(configurable != null ? configurable.getBannerSlogan() : null);

    myCurrentConfigurable = configurable;

    if (configurable != null) {
      final JComponent comp = configurable.createComponent();
      if (comp == null) {
        setEmpty();
        LOG.error("createComponent() returned null. configurable=" + configurable);
      } else {
        myDetails.setContent(comp);
        ensureInitialized(configurable);
        myHistory.pushPlaceForElement(TREE_OBJECT, configurable.getEditableObject());
      }
    } else {
      setEmpty();
    }
  }

  public void ensureInitialized(NamedConfigurable configurable) {
    if (!isInitialized(configurable)) {
      configurable.reset();
      initializeConfigurable(configurable);
    }
  }

  private void setEmpty() {
    myDetails.setContent(null);
    myDetails.setEmptyContentText(getEmptySelectionString());
  }

  public String getHelpTopic() {
    if (myCurrentConfigurable != null) {
      return myCurrentConfigurable.getHelpTopic();
    }
    return null;
  }

  protected @Nullable String getEmptySelectionString() {
    return null;
  }

  protected void initializeConfigurable(final NamedConfigurable configurable) {
    myInitializedConfigurables.add(configurable);
  }

  /** @deprecated use {@link #checkForEmptyAndDuplicatedNames(String, String, Class} instead */
  protected void checkApply(Set<MyNode> rootNodes, String prefix, String title)
      throws ConfigurationException {
    for (MyNode rootNode : rootNodes) {
      checkForEmptyAndDuplicatedNames(rootNode, prefix, title, NamedConfigurable.class, false);
    }
  }

  protected final void checkForEmptyAndDuplicatedNames(
      String prefix, String title, Class<? extends NamedConfigurable> configurableClass)
      throws ConfigurationException {
    checkForEmptyAndDuplicatedNames(myRoot, prefix, title, configurableClass, true);
  }

  private void checkForEmptyAndDuplicatedNames(
      MyNode rootNode,
      String prefix,
      String title,
      Class<? extends NamedConfigurable> configurableClass,
      boolean recursively)
      throws ConfigurationException {
    final Set<String> names = new HashSet<String>();
    for (int i = 0; i < rootNode.getChildCount(); i++) {
      final MyNode node = (MyNode) rootNode.getChildAt(i);
      final NamedConfigurable scopeConfigurable = node.getConfigurable();

      if (configurableClass.isInstance(scopeConfigurable)) {
        final String name = scopeConfigurable.getDisplayName();
        if (name.trim().length() == 0) {
          selectNodeInTree(node);
          throw new ConfigurationException("Name should contain non-space characters");
        }
        if (names.contains(name)) {
          final NamedConfigurable selectedConfigurable = getSelectedConfigurable();
          if (selectedConfigurable == null
              || !Comparing.strEqual(selectedConfigurable.getDisplayName(), name)) {
            selectNodeInTree(node);
          }
          throw new ConfigurationException(
              CommonBundle.message("smth.already.exist.error.message", prefix, name), title);
        }
        names.add(name);
      }

      if (recursively) {
        checkForEmptyAndDuplicatedNames(node, prefix, title, configurableClass, true);
      }
    }
  }

  public Tree getTree() {
    return myTree;
  }

  protected void removePaths(final TreePath... paths) {
    MyNode parentNode = null;
    int idx = -1;
    for (TreePath path : paths) {
      final MyNode node = (MyNode) path.getLastPathComponent();
      final NamedConfigurable namedConfigurable = node.getConfigurable();
      final Object editableObject = namedConfigurable.getEditableObject();
      parentNode = (MyNode) node.getParent();
      idx = parentNode.getIndex(node);
      ((DefaultTreeModel) myTree.getModel()).removeNodeFromParent(node);
      myHasDeletedItems |= wasObjectStored(editableObject);
      fireItemsChangeListener(editableObject);
      onItemDeleted(editableObject);
      namedConfigurable.disposeUIResources();
    }

    if (paths.length > 0) {
      if (parentNode != null && idx != -1) {
        DefaultMutableTreeNode toSelect = null;
        if (idx < parentNode.getChildCount()) {
          toSelect = (DefaultMutableTreeNode) parentNode.getChildAt(idx);
        } else {
          if (idx > 0 && parentNode.getChildCount() > 0) {
            if (idx - 1 < parentNode.getChildCount()) {
              toSelect = (DefaultMutableTreeNode) parentNode.getChildAt(idx - 1);
            } else {
              toSelect = (DefaultMutableTreeNode) parentNode.getFirstChild();
            }
          } else {
            if (parentNode.isRoot() && myTree.isRootVisible()) {
              toSelect = parentNode;
            } else if (parentNode.getChildCount() > 0) {
              toSelect = (DefaultMutableTreeNode) parentNode.getFirstChild();
            }
          }
        }

        if (toSelect != null) {
          TreeUtil.selectInTree(toSelect, true, myTree);
        }
      } else {
        TreeUtil.selectFirstNode(myTree);
      }
    }
  }

  protected void onItemDeleted(Object item) {}

  protected class MyDeleteAction extends AnAction implements DumbAware {
    private final Condition<Object[]> myCondition;

    public MyDeleteAction() {
      this(Conditions.<Object[]>alwaysTrue());
    }

    public MyDeleteAction(Condition<Object[]> availableCondition) {
      super(
          CommonBundle.message("button.delete"),
          CommonBundle.message("button.delete"),
          PlatformIcons.DELETE_ICON);
      registerCustomShortcutSet(CommonShortcuts.getDelete(), myTree);
      myCondition = availableCondition;
    }

    public void update(AnActionEvent e) {
      final Presentation presentation = e.getPresentation();
      presentation.setEnabled(false);
      final TreePath[] selectionPath = myTree.getSelectionPaths();
      if (selectionPath != null) {
        Object[] nodes =
            ContainerUtil.map2Array(
                selectionPath,
                new Function<TreePath, Object>() {
                  @Override
                  public Object fun(TreePath treePath) {
                    return treePath.getLastPathComponent();
                  }
                });
        if (!myCondition.value(nodes)) return;
        presentation.setEnabled(true);
      }
    }

    public void actionPerformed(AnActionEvent e) {
      removePaths(myTree.getSelectionPaths());
    }
  }

  protected static Condition<Object[]> forAll(final Condition<Object> condition) {
    return new Condition<Object[]>() {
      @Override
      public boolean value(Object[] objects) {
        for (Object object : objects) {
          if (!condition.value(object)) return false;
        }
        return true;
      }
    };
  }

  public static class MyNode extends DefaultMutableTreeNode {
    private boolean myDisplayInBold;

    public MyNode(@NotNull NamedConfigurable userObject) {
      super(userObject);
    }

    public MyNode(@NotNull NamedConfigurable userObject, boolean displayInBold) {
      super(userObject);
      myDisplayInBold = displayInBold;
    }

    @NotNull
    public String getDisplayName() {
      final NamedConfigurable configurable = ((NamedConfigurable) getUserObject());
      LOG.assertTrue(configurable != null, "Tree was already disposed");
      return configurable.getDisplayName();
    }

    public NamedConfigurable getConfigurable() {
      return (NamedConfigurable) getUserObject();
    }

    public boolean isDisplayInBold() {
      return myDisplayInBold;
    }

    public void setDisplayInBold(boolean displayInBold) {
      myDisplayInBold = displayInBold;
    }

    @Nullable
    public Icon getIcon(boolean expanded) {
      // thanks to invokeLater() in TreeUtil.showAndSelect(), we can get calls to getIcon() after
      // the tree has been disposed
      final NamedConfigurable configurable = getConfigurable();
      if (configurable != null) {
        return configurable.getIcon(expanded);
      }
      return null;
    }
  }

  @SuppressWarnings({"ConstantConditions"})
  protected static class MyRootNode extends MyNode {
    public MyRootNode() {
      super(
          new NamedConfigurable(false, null) {
            public void setDisplayName(String name) {}

            public Object getEditableObject() {
              return null;
            }

            public String getBannerSlogan() {
              return null;
            }

            public String getDisplayName() {
              return "";
            }

            @Nullable
            @NonNls
            public String getHelpTopic() {
              return null;
            }

            public JComponent createOptionsPanel() {
              return null;
            }

            public boolean isModified() {
              return false;
            }

            public void apply() throws ConfigurationException {}

            public void reset() {}

            public void disposeUIResources() {}
          },
          false);
    }
  }

  protected interface ItemsChangeListener {
    void itemChanged(@Nullable Object deletedItem);

    void itemsExternallyChanged();
  }

  public interface ActionGroupWithPreselection {
    ActionGroup getActionGroup();

    int getDefaultIndex();
  }

  protected class MyActionGroupWrapper extends AnAction implements DumbAware {
    private ActionGroup myActionGroup;
    private ActionGroupWithPreselection myPreselection;

    public MyActionGroupWrapper(final ActionGroupWithPreselection actionGroup) {
      this(actionGroup.getActionGroup());
      myPreselection = actionGroup;
    }

    public MyActionGroupWrapper(final ActionGroup actionGroup) {
      super(
          actionGroup.getTemplatePresentation().getText(),
          actionGroup.getTemplatePresentation().getDescription(),
          actionGroup.getTemplatePresentation().getIcon());
      myActionGroup = actionGroup;
      registerCustomShortcutSet(actionGroup.getShortcutSet(), myTree);
    }

    public void actionPerformed(AnActionEvent e) {
      final JBPopupFactory popupFactory = JBPopupFactory.getInstance();
      final ListPopupStep step =
          popupFactory.createActionsStep(
              myActionGroup,
              e.getDataContext(),
              false,
              false,
              myActionGroup.getTemplatePresentation().getText(),
              myTree,
              true,
              myPreselection != null ? myPreselection.getDefaultIndex() : 0,
              true);
      final ListPopup listPopup = popupFactory.createListPopup(step);
      listPopup.setHandleAutoSelectionBeforeShow(true);
      if (e instanceof AnActionButton.AnActionEventWrapper) {
        ((AnActionButton.AnActionEventWrapper) e).showPopup(listPopup);
      } else {
        listPopup.showUnderneathOf(myNorthPanel);
      }
    }
  }

  public JComponent getToolbar() {
    myToReInitWholePanel = true;
    return myNorthPanel;
  }

  public JComponent getMaster() {
    myToReInitWholePanel = true;
    return myMaster;
  }

  public DetailsComponent getDetails() {
    myToReInitWholePanel = true;
    return myDetails;
  }

  public void initUi() {
    createComponent();
  }
}
public class WebServer {
  private static final String START_TIME_PATH = "/startTime";

  private final List<ChannelFutureListener> closingListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private final ChannelGroup openChannels = new DefaultChannelGroup("web-server");

  private static final Logger LOG = Logger.getInstance(WebServer.class);

  @NonNls private static final String PROPERTY_ONLY_ANY_HOST = "rpc.onlyAnyHost";

  private final NioServerSocketChannelFactory channelFactory;

  public WebServer() {
    Executor pooledThreadExecutor = new PooledThreadExecutor();
    channelFactory =
        new NioServerSocketChannelFactory(pooledThreadExecutor, pooledThreadExecutor, 1);
  }

  public boolean isRunning() {
    return !openChannels.isEmpty();
  }

  public void start(int port, Consumer<ChannelPipeline>... pipelineConsumers) {
    start(
        port,
        new Computable.PredefinedValueComputable<Consumer<ChannelPipeline>[]>(pipelineConsumers));
  }

  public void start(int port, int portsCount, Consumer<ChannelPipeline>... pipelineConsumers) {
    start(
        port,
        portsCount,
        false,
        new Computable.PredefinedValueComputable<Consumer<ChannelPipeline>[]>(pipelineConsumers));
  }

  public void start(int port, Computable<Consumer<ChannelPipeline>[]> pipelineConsumers) {
    start(port, 1, false, pipelineConsumers);
  }

  public int start(
      int firstPort,
      int portsCount,
      boolean tryAnyPort,
      Computable<Consumer<ChannelPipeline>[]> pipelineConsumers) {
    if (isRunning()) {
      throw new IllegalStateException("server already started");
    }

    ServerBootstrap bootstrap = new ServerBootstrap(channelFactory);
    bootstrap.setOption("child.tcpNoDelay", true);
    bootstrap.setPipelineFactory(
        new ChannelPipelineFactoryImpl(pipelineConsumers, new DefaultHandler(openChannels)));
    return bind(firstPort, portsCount, tryAnyPort, bootstrap);
  }

  private static boolean checkPort(final InetSocketAddress remoteAddress) {
    final ClientBootstrap bootstrap =
        new ClientBootstrap(new OioClientSocketChannelFactory(new PooledThreadExecutor()));
    bootstrap.setOption("child.tcpNoDelay", true);

    final AtomicBoolean result = new AtomicBoolean(false);
    final Semaphore semaphore = new Semaphore();
    semaphore.down(); // must call to down() here to ensure that down was called _before_ up()
    bootstrap.setPipeline(
        pipeline(
            new HttpResponseDecoder(),
            new HttpChunkAggregator(1048576),
            new HttpRequestEncoder(),
            new SimpleChannelUpstreamHandler() {
              @Override
              public void messageReceived(ChannelHandlerContext context, MessageEvent e)
                  throws Exception {
                try {
                  if (e.getMessage() instanceof HttpResponse) {
                    HttpResponse response = (HttpResponse) e.getMessage();
                    if (response.getStatus().equals(OK)
                        && response
                            .getContent()
                            .toString(CharsetUtil.US_ASCII)
                            .equals(getApplicationStartTime())) {
                      LOG.info("port check: current OS must be marked as normal");
                      result.set(true);
                    }
                  }
                } finally {
                  semaphore.up();
                }
              }

              @Override
              public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
                  throws Exception {
                try {
                  LOG.error(e.getCause());
                } finally {
                  semaphore.up();
                }
              }
            }));

    ChannelFuture connectFuture = null;
    try {
      connectFuture = bootstrap.connect(remoteAddress);
      if (!waitComplete(connectFuture, "connect")) {
        return false;
      }
      ChannelFuture writeFuture =
          connectFuture
              .getChannel()
              .write(new DefaultHttpRequest(HTTP_1_1, HttpMethod.GET, START_TIME_PATH));
      if (!waitComplete(writeFuture, "write")) {
        return false;
      }

      try {
        // yes, 30 seconds. I always get timeout in Linux in Parallels if I set to 2 seconds.
        // In any case all work is done in pooled thread (IDE init time isn't affected)
        if (!semaphore.waitForUnsafe(30000)) {
          LOG.info("port check: semaphore down timeout");
        }
      } catch (InterruptedException e) {
        LOG.info("port check: semaphore interrupted", e);
      }
    } finally {
      if (connectFuture != null) {
        connectFuture.getChannel().close().awaitUninterruptibly();
      }
      bootstrap.releaseExternalResources();
    }
    return result.get();
  }

  private static boolean waitComplete(ChannelFuture writeFuture, String failedMessage) {
    if (!writeFuture.awaitUninterruptibly(500) || !writeFuture.isSuccess()) {
      LOG.info("port check: " + failedMessage + ", " + writeFuture.isSuccess());
      return false;
    }
    return true;
  }

  private static String getApplicationStartTime() {
    return Long.toString(ApplicationManager.getApplication().getStartTime());
  }

  // IDEA-91436 idea <121 binds to 127.0.0.1, but >=121 must be available not only from localhost
  // but if we bind only to any local port (0.0.0.0), instance of idea <121 can bind to our ports
  // and any request to us will be intercepted
  // so, we bind to 127.0.0.1 and 0.0.0.0
  private int bind(int firstPort, int portsCount, boolean tryAnyPort, ServerBootstrap bootstrap) {
    String property = System.getProperty(PROPERTY_ONLY_ANY_HOST);
    boolean onlyAnyHost =
        property == null
            ? (SystemInfo.isLinux || SystemInfo.isWindows && !SystemInfo.isWinVistaOrNewer)
            : (property.isEmpty() || Boolean.valueOf(property));
    boolean portChecked = false;
    for (int i = 0; i < portsCount; i++) {
      int port = firstPort + i;
      ChannelException channelException = null;
      try {
        openChannels.add(bootstrap.bind(new InetSocketAddress(port)));
        if (!onlyAnyHost) {
          InetSocketAddress localAddress = null;
          try {
            localAddress = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), port);
            openChannels.add(bootstrap.bind(localAddress));
          } catch (UnknownHostException e) {
            return port;
          } catch (ChannelException e) {
            channelException = e;
            if (!portChecked) {
              portChecked = true;
              assert localAddress != null;
              if (checkPortSafe(localAddress)) {
                return port;
              }
            }
          }
        }
      } catch (ChannelException e) {
        channelException = e;
      }

      if (channelException == null) {
        return port;
      } else {
        if (!openChannels.isEmpty()) {
          openChannels.close();
          openChannels.clear();
        }

        if (portsCount == 1) {
          throw channelException;
        } else if (!tryAnyPort && i == (portsCount - 1)) {
          LOG.error(channelException);
        }
      }
    }

    if (tryAnyPort) {
      LOG.info("We cannot bind to our default range, so, try to bind to any free port");
      try {
        Channel channel = bootstrap.bind(new InetSocketAddress(0));
        openChannels.add(channel);
        return ((InetSocketAddress) channel.getLocalAddress()).getPort();
      } catch (ChannelException e) {
        LOG.error(e);
      }
    }

    return -1;
  }

  private static boolean checkPortSafe(@NotNull InetSocketAddress localAddress) {
    LOG.info(
        "We have tried to bind to 127.0.0.1 host but have got exception ("
            + SystemInfo.OS_NAME
            + " "
            + SystemInfo.OS_VERSION
            + "), "
            + "so, try to check - are we really need to bind to 127.0.0.1");
    try {
      return checkPort(localAddress);
    } catch (Throwable innerE) {
      LOG.error(innerE);
      return false;
    }
  }

  public void stop() {
    try {
      for (ChannelFutureListener listener : closingListeners) {
        try {
          listener.operationComplete(null);
        } catch (Exception e) {
          LOG.error(e);
        }
      }
    } finally {
      try {
        openChannels.close().awaitUninterruptibly();
      } finally {
        channelFactory.releaseExternalResources();
      }
    }
  }

  public void addClosingListener(ChannelFutureListener listener) {
    closingListeners.add(listener);
  }

  public Runnable createShutdownTask() {
    return new Runnable() {
      @Override
      public void run() {
        if (isRunning()) {
          stop();
        }
      }
    };
  }

  public void addShutdownHook() {
    ShutDownTracker.getInstance().registerShutdownTask(createShutdownTask());
  }

  public static void removePluggableHandlers(ChannelPipeline pipeline) {
    for (String name : pipeline.getNames()) {
      if (name.startsWith("pluggable_")) {
        pipeline.remove(name);
      }
    }
  }

  private static class ChannelPipelineFactoryImpl implements ChannelPipelineFactory {
    private final Computable<Consumer<ChannelPipeline>[]> pipelineConsumers;
    private final DefaultHandler defaultHandler;

    public ChannelPipelineFactoryImpl(
        Computable<Consumer<ChannelPipeline>[]> pipelineConsumers, DefaultHandler defaultHandler) {
      this.pipelineConsumers = pipelineConsumers;
      this.defaultHandler = defaultHandler;
    }

    @Override
    public ChannelPipeline getPipeline() throws Exception {
      ChannelPipeline pipeline =
          pipeline(
              new HttpRequestDecoder(),
              new HttpChunkAggregator(1048576),
              new HttpResponseEncoder());
      for (Consumer<ChannelPipeline> consumer : pipelineConsumers.compute()) {
        try {
          consumer.consume(pipeline);
        } catch (Throwable e) {
          LOG.error(e);
        }
      }
      pipeline.addLast("defaultHandler", defaultHandler);
      return pipeline;
    }
  }

  @ChannelHandler.Sharable
  private static class DefaultHandler extends SimpleChannelUpstreamHandler {
    private final ChannelGroup openChannels;

    public DefaultHandler(ChannelGroup openChannels) {
      this.openChannels = openChannels;
    }

    @Override
    public void channelOpen(ChannelHandlerContext context, ChannelStateEvent e) {
      openChannels.add(e.getChannel());
    }

    @Override
    public void messageReceived(ChannelHandlerContext context, MessageEvent e) throws Exception {
      if (e.getMessage() instanceof HttpRequest) {
        HttpRequest message = (HttpRequest) e.getMessage();
        HttpResponse response;
        if (new QueryStringDecoder(message.getUri()).getPath().equals(START_TIME_PATH)) {
          response = new DefaultHttpResponse(HTTP_1_1, OK);
          response.setHeader("Access-Control-Allow-Origin", "*");
          response.setContent(
              ChannelBuffers.copiedBuffer(getApplicationStartTime(), CharsetUtil.US_ASCII));
        } else {
          response = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND);
        }
        context.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
      }
    }
  }
}
public class ChangeList {
  private static final Logger LOG = Logger.getInstance(ChangeList.class);
  public static final Comparator<Change> CHANGE_ORDER =
      new SimpleChange.ChangeOrder(FragmentSide.SIDE1);

  private final Project myProject;
  private final Document[] myDocuments = new Document[2];
  private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private ArrayList<Change> myChanges;
  private ArrayList<Change> myAppliedChanges;

  public ChangeList(@NotNull Document base, @NotNull Document version, @Nullable Project project) {
    myDocuments[0] = base;
    myDocuments[1] = version;
    myProject = project;
  }

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

  public void removeListener(Listener listener) {
    LOG.assertTrue(myListeners.remove(listener));
  }

  public void setChanges(@NotNull ArrayList<Change> changes) {
    if (myChanges != null) {
      HashSet<Change> newChanges = new HashSet<>(changes);
      LOG.assertTrue(newChanges.size() == changes.size());
      for (Iterator<Change> iterator = myChanges.iterator(); iterator.hasNext(); ) {
        Change oldChange = iterator.next();
        if (!newChanges.contains(oldChange)) {
          iterator.remove();
          oldChange.onRemovedFromList();
        }
      }
    }
    for (Change change : changes) {
      LOG.assertTrue(change.isValid());
    }
    myChanges = new ArrayList<>(changes);
    myAppliedChanges = new ArrayList<>();
  }

  @Nullable
  public Project getProject() {
    return myProject;
  }

  @NotNull
  public List<Change> getChanges() {
    return new ArrayList<>(myChanges);
  }

  public static ChangeList build(
      @NotNull Document base, @NotNull Document version, @NotNull Project project)
      throws FilesTooBigForDiffException {
    ChangeList result = new ChangeList(base, version, project);
    ArrayList<Change> changes = result.buildChanges();
    Collections.sort(changes, CHANGE_ORDER);
    result.setChanges(changes);
    return result;
  }

  public void setMarkup(final Editor base, final Editor version) {
    Editor[] editors = {base, version};
    for (Change change : myChanges) {
      change.addMarkup(editors);
    }
  }

  public void updateMarkup() {
    for (Change change : myChanges) {
      change.updateMarkup();
    }
  }

  @NotNull
  public Document getDocument(@NotNull FragmentSide side) {
    return myDocuments[side.getIndex()];
  }

  private ArrayList<Change> buildChanges() throws FilesTooBigForDiffException {
    Document base = getDocument(FragmentSide.SIDE1);
    DiffString[] baseLines = DiffUtil.convertToLines(base.getText());
    Document version = getDocument(FragmentSide.SIDE2);
    DiffString[] versionLines = DiffUtil.convertToLines(version.getText());
    DiffFragment[] fragments =
        ComparisonPolicy.DEFAULT.buildDiffFragmentsFromLines(baseLines, versionLines);
    final ArrayList<Change> result = new ArrayList<>();
    new DiffFragmentsEnumerator(fragments) {
      @Override
      protected void process(DiffFragment fragment) {
        if (fragment.isEqual()) return;
        Context context = getContext();
        TextRange range1 = context.createRange(FragmentSide.SIDE1);
        TextRange range2 = context.createRange(FragmentSide.SIDE2);
        result.add(
            new SimpleChange(
                ChangeType.fromDiffFragment(context.getFragment()),
                range1,
                range2,
                ChangeList.this));
      }
    }.execute();
    return result;
  }

  public Change getChange(int index) {
    return myChanges.get(index);
  }

  private abstract static class DiffFragmentsEnumerator {
    @NotNull private final DiffFragment[] myFragments;
    @NotNull private final Context myContext;

    private DiffFragmentsEnumerator(@NotNull DiffFragment[] fragments) {
      myContext = new Context();
      myFragments = fragments;
    }

    public void execute() {
      for (DiffFragment fragment : myFragments) {
        myContext.myFragment = fragment;
        process(fragment);
        DiffString text1 = fragment.getText1();
        DiffString text2 = fragment.getText2();
        myContext.myStarts[0] += StringUtil.length(text1);
        myContext.myStarts[1] += StringUtil.length(text2);
        myContext.myLines[0] += countLines(text1);
        myContext.myLines[1] += countLines(text2);
      }
    }

    private static int countLines(@Nullable DiffString text) {
      if (text == null) return 0;
      return StringUtil.countNewLines(text);
    }

    @NotNull
    protected Context getContext() {
      return myContext;
    }

    protected abstract void process(DiffFragment fragment);
  }

  public static class Context {
    private DiffFragment myFragment;
    private final int[] myStarts = {0, 0};
    private final int[] myLines = {0, 0};

    public DiffFragment getFragment() {
      return myFragment;
    }

    public int getStart(@NotNull FragmentSide side) {
      return myStarts[side.getIndex()];
    }

    public int getEnd(@NotNull FragmentSide side) {
      return getStart(side) + StringUtil.length(side.getText(myFragment));
    }

    @NotNull
    public TextRange createRange(@NotNull FragmentSide side) {
      return new TextRange(getStart(side), getEnd(side));
    }
  }

  public int getCount() {
    return myChanges.size();
  }

  public LineBlocks getNonAppliedLineBlocks() {
    ArrayList<Change> changes = new ArrayList<>(myChanges);
    return LineBlocks.fromChanges(changes);
  }

  public LineBlocks getLineBlocks() {
    ArrayList<Change> changes = new ArrayList<>(myChanges);
    changes.addAll(myAppliedChanges);
    return LineBlocks.fromChanges(changes);
  }

  public void remove(@NotNull Change change) {
    if (change.getType().isApplied()) {
      LOG.assertTrue(myAppliedChanges.remove(change), change);
    } else {
      LOG.assertTrue(myChanges.remove(change), change);
    }
    change.onRemovedFromList();
    fireOnChangeRemoved();
  }

  public void apply(@NotNull Change change) {
    LOG.assertTrue(myChanges.remove(change), change);
    myAppliedChanges.add(change);
    fireOnChangeApplied();
  }

  private void fireOnChangeRemoved() {
    for (Listener listener : myListeners) {
      listener.onChangeRemoved(this);
    }
  }

  void fireOnChangeApplied() {
    for (Listener listener : myListeners) {
      listener.onChangeApplied(this);
    }
  }

  public interface Listener {
    void onChangeRemoved(ChangeList source);

    void onChangeApplied(ChangeList source);
  }
}
public class LookupImpl extends LightweightHint implements LookupEx, Disposable, WeighingContext {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupImpl");

  private final LookupOffsets myOffsets;
  private final Project myProject;
  private final Editor myEditor;
  private final JBList myList =
      new JBList(new CollectionListModel<LookupElement>()) {
        @Override
        protected void processKeyEvent(@NotNull final KeyEvent e) {
          final char keyChar = e.getKeyChar();
          if (keyChar == KeyEvent.VK_ENTER || keyChar == KeyEvent.VK_TAB) {
            IdeFocusManager.getInstance(myProject)
                .requestFocus(myEditor.getContentComponent(), true)
                .doWhenDone(
                    new Runnable() {
                      @Override
                      public void run() {
                        IdeEventQueue.getInstance().getKeyEventDispatcher().dispatchKeyEvent(e);
                      }
                    });
            return;
          }

          super.processKeyEvent(e);
        }

        @NotNull
        @Override
        protected ExpandableItemsHandler<Integer> createExpandableItemsHandler() {
          return new CompletionExtender(this);
        }
      };
  final LookupCellRenderer myCellRenderer;

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

  private long myStampShown = 0;
  private boolean myShown = false;
  private boolean myDisposed = false;
  private boolean myHidden = false;
  private boolean mySelectionTouched;
  private FocusDegree myFocusDegree = FocusDegree.FOCUSED;
  private volatile boolean myCalculating;
  private final Advertiser myAdComponent;
  volatile int myLookupTextWidth = 50;
  private boolean myChangeGuard;
  private volatile LookupArranger myArranger;
  private LookupArranger myPresentableArranger;
  private final Map<LookupElement, PrefixMatcher> myMatchers =
      ContainerUtil.newConcurrentMap(ContainerUtil.<LookupElement>identityStrategy());
  private final Map<LookupElement, Font> myCustomFonts =
      ContainerUtil.createConcurrentWeakMap(
          10,
          0.75f,
          Runtime.getRuntime().availableProcessors(),
          ContainerUtil.<LookupElement>identityStrategy());
  private boolean myStartCompletionWhenNothingMatches;
  boolean myResizePending;
  private boolean myFinishing;
  boolean myUpdating;
  private LookupUi myUi;

  public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger) {
    super(new JPanel(new BorderLayout()));
    setForceShowAsPopup(true);
    setCancelOnClickOutside(false);
    setResizable(true);
    AbstractPopup.suppressMacCornerFor(getComponent());

    myProject = project;
    myEditor = editor;
    myArranger = arranger;
    myPresentableArranger = arranger;

    myCellRenderer = new LookupCellRenderer(this);
    myList.setCellRenderer(myCellRenderer);

    myList.setFocusable(false);
    myList.setFixedCellWidth(50);

    myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    myList.setBackground(LookupCellRenderer.BACKGROUND_COLOR);

    myList.getExpandableItemsHandler();

    myAdComponent = new Advertiser();

    myOffsets = new LookupOffsets(editor);

    final CollectionListModel<LookupElement> model = getListModel();
    addEmptyItem(model);
    updateListHeight(model);

    addListeners();
  }

  private CollectionListModel<LookupElement> getListModel() {
    //noinspection unchecked
    return (CollectionListModel<LookupElement>) myList.getModel();
  }

  public void setArranger(LookupArranger arranger) {
    myArranger = arranger;
  }

  public FocusDegree getFocusDegree() {
    return myFocusDegree;
  }

  @Override
  public boolean isFocused() {
    return getFocusDegree() == FocusDegree.FOCUSED;
  }

  public void setFocusDegree(FocusDegree focusDegree) {
    myFocusDegree = focusDegree;
  }

  public boolean isCalculating() {
    return myCalculating;
  }

  public void setCalculating(final boolean calculating) {
    myCalculating = calculating;
    if (myUi != null) {
      myUi.setCalculating(calculating);
    }
  }

  public void markSelectionTouched() {
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      ApplicationManager.getApplication().assertIsDispatchThread();
    }
    mySelectionTouched = true;
    myList.repaint();
  }

  @TestOnly
  public void setSelectionTouched(boolean selectionTouched) {
    mySelectionTouched = selectionTouched;
  }

  public void resort(boolean addAgain) {
    final List<LookupElement> items = getItems();

    synchronized (myList) {
      myPresentableArranger.prefixChanged(this);
      getListModel().removeAll();
    }

    if (addAgain) {
      for (final LookupElement item : items) {
        addItem(item, itemMatcher(item));
      }
    }
    refreshUi(true, true);
  }

  public boolean addItem(LookupElement item, PrefixMatcher matcher) {
    LookupElementPresentation presentation = renderItemApproximately(item);
    if (containsDummyIdentifier(presentation.getItemText())
        || containsDummyIdentifier(presentation.getTailText())
        || containsDummyIdentifier(presentation.getTypeText())) {
      return false;
    }

    myMatchers.put(item, matcher);
    updateLookupWidth(item, presentation);
    synchronized (myList) {
      myArranger.addElement(this, item, presentation);
    }
    return true;
  }

  private static boolean containsDummyIdentifier(@Nullable final String s) {
    return s != null && s.contains(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED);
  }

  public void updateLookupWidth(LookupElement item) {
    updateLookupWidth(item, renderItemApproximately(item));
  }

  private void updateLookupWidth(LookupElement item, LookupElementPresentation presentation) {
    final Font customFont = myCellRenderer.getFontAbleToDisplay(presentation);
    if (customFont != null) {
      myCustomFonts.put(item, customFont);
    }
    int maxWidth = myCellRenderer.updateMaximumWidth(presentation, item);
    myLookupTextWidth = Math.max(maxWidth, myLookupTextWidth);
  }

  @Nullable
  public Font getCustomFont(LookupElement item, boolean bold) {
    Font font = myCustomFonts.get(item);
    return font == null ? null : bold ? font.deriveFont(Font.BOLD) : font;
  }

  public void requestResize() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    myResizePending = true;
  }

  public Collection<LookupElementAction> getActionsFor(LookupElement element) {
    final CollectConsumer<LookupElementAction> consumer =
        new CollectConsumer<LookupElementAction>();
    for (LookupActionProvider provider : LookupActionProvider.EP_NAME.getExtensions()) {
      provider.fillActions(element, this, consumer);
    }
    if (!consumer.getResult().isEmpty()) {
      consumer.consume(new ShowHideIntentionIconLookupAction());
    }
    return consumer.getResult();
  }

  public JList getList() {
    return myList;
  }

  @Override
  public List<LookupElement> getItems() {
    synchronized (myList) {
      return ContainerUtil.findAll(
          getListModel().toList(),
          new Condition<LookupElement>() {
            @Override
            public boolean value(LookupElement element) {
              return !(element instanceof EmptyLookupItem);
            }
          });
    }
  }

  public String getAdditionalPrefix() {
    return myOffsets.getAdditionalPrefix();
  }

  void appendPrefix(char c) {
    checkValid();
    myOffsets.appendPrefix(c);
    synchronized (myList) {
      myPresentableArranger.prefixChanged(this);
    }
    requestResize();
    refreshUi(false, true);
    ensureSelectionVisible(true);
  }

  public void setStartCompletionWhenNothingMatches(boolean startCompletionWhenNothingMatches) {
    myStartCompletionWhenNothingMatches = startCompletionWhenNothingMatches;
  }

  public boolean isStartCompletionWhenNothingMatches() {
    return myStartCompletionWhenNothingMatches;
  }

  public void ensureSelectionVisible(boolean forceTopSelection) {
    if (isSelectionVisible() && !forceTopSelection) {
      return;
    }

    if (!forceTopSelection) {
      ListScrollingUtil.ensureIndexIsVisible(myList, myList.getSelectedIndex(), 1);
      return;
    }

    // selected item should be at the top of the visible list
    int top = myList.getSelectedIndex();
    if (top > 0) {
      top--; // show one element above the selected one to give the hint that there are more
             // available via scrolling
    }

    int firstVisibleIndex = myList.getFirstVisibleIndex();
    if (firstVisibleIndex == top) {
      return;
    }

    ListScrollingUtil.ensureRangeIsVisible(
        myList, top, top + myList.getLastVisibleIndex() - firstVisibleIndex);
  }

  boolean truncatePrefix(boolean preserveSelection) {
    if (!myOffsets.truncatePrefix()) {
      return false;
    }

    if (preserveSelection) {
      markSelectionTouched();
    }

    boolean shouldUpdate;
    synchronized (myList) {
      shouldUpdate = myPresentableArranger == myArranger;
      myPresentableArranger.prefixChanged(this);
    }
    requestResize();
    if (shouldUpdate) {
      refreshUi(false, true);
      ensureSelectionVisible(true);
    }

    return true;
  }

  private boolean updateList(boolean onExplicitAction, boolean reused) {
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      ApplicationManager.getApplication().assertIsDispatchThread();
    }
    checkValid();

    CollectionListModel<LookupElement> listModel = getListModel();

    Pair<List<LookupElement>, Integer> pair;
    synchronized (myList) {
      pair = myPresentableArranger.arrangeItems(this, onExplicitAction || reused);
    }

    List<LookupElement> items = pair.first;
    Integer toSelect = pair.second;
    if (toSelect == null || toSelect < 0 || items.size() > 0 && toSelect >= items.size()) {
      LOG.error(
          "Arranger "
              + myPresentableArranger
              + " returned invalid selection index="
              + toSelect
              + "; items="
              + items);
      toSelect = 0;
    }

    myOffsets.checkMinPrefixLengthChanges(items, this);
    List<LookupElement> oldModel = listModel.toList();

    listModel.removeAll();
    if (!items.isEmpty()) {
      listModel.add(items);
    } else {
      addEmptyItem(listModel);
    }

    updateListHeight(listModel);

    myList.setSelectedIndex(toSelect);
    return !ContainerUtil.equalsIdentity(oldModel, items);
  }

  private boolean isSelectionVisible() {
    return ListScrollingUtil.isIndexFullyVisible(myList, myList.getSelectedIndex());
  }

  private boolean checkReused() {
    synchronized (myList) {
      if (myPresentableArranger != myArranger) {
        myPresentableArranger = myArranger;
        myOffsets.clearAdditionalPrefix();
        myPresentableArranger.prefixChanged(this);
        return true;
      }
      return false;
    }
  }

  private void updateListHeight(ListModel model) {
    myList.setFixedCellHeight(
        myCellRenderer
            .getListCellRendererComponent(myList, model.getElementAt(0), 0, false, false)
            .getPreferredSize()
            .height);

    myList.setVisibleRowCount(
        Math.min(model.getSize(), UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT));
  }

  private void addEmptyItem(CollectionListModel<LookupElement> model) {
    LookupItem<String> item =
        new EmptyLookupItem(
            myCalculating ? " " : LangBundle.message("completion.no.suggestions"), false);
    myMatchers.put(item, new CamelHumpMatcher(""));
    model.add(item);

    updateLookupWidth(item);
    requestResize();
  }

  private static LookupElementPresentation renderItemApproximately(LookupElement item) {
    final LookupElementPresentation p = new LookupElementPresentation();
    item.renderElement(p);
    return p;
  }

  @NotNull
  @Override
  public String itemPattern(@NotNull LookupElement element) {
    String prefix = itemMatcher(element).getPrefix();
    String additionalPrefix = getAdditionalPrefix();
    return additionalPrefix.isEmpty() ? prefix : prefix + additionalPrefix;
  }

  @Override
  @NotNull
  public PrefixMatcher itemMatcher(@NotNull LookupElement item) {
    PrefixMatcher matcher = itemMatcherNullable(item);
    if (matcher == null) {
      throw new AssertionError("Item not in lookup: item=" + item + "; lookup items=" + getItems());
    }
    return matcher;
  }

  public PrefixMatcher itemMatcherNullable(LookupElement item) {
    return myMatchers.get(item);
  }

  public void finishLookup(final char completionChar) {
    finishLookup(completionChar, (LookupElement) myList.getSelectedValue());
  }

  public void finishLookup(char completionChar, @Nullable final LookupElement item) {
    //noinspection deprecation,unchecked
    if (item == null
        || item instanceof EmptyLookupItem
        || item.getObject() instanceof DeferredUserLookupValue
            && item.as(LookupItem.CLASS_CONDITION_KEY) != null
            && !((DeferredUserLookupValue) item.getObject())
                .handleUserSelection(item.as(LookupItem.CLASS_CONDITION_KEY), myProject)) {
      doHide(false, true);
      fireItemSelected(null, completionChar);
      return;
    }

    if (myDisposed) { // DeferredUserLookupValue could close us in any way
      return;
    }

    final PsiFile file = getPsiFile();
    boolean writableOk =
        file == null || FileModificationService.getInstance().prepareFileForWrite(file);
    if (myDisposed) { // ensureFilesWritable could close us by showing a dialog
      return;
    }

    if (!writableOk) {
      doHide(false, true);
      fireItemSelected(null, completionChar);
      return;
    }

    final String prefix = itemPattern(item);
    boolean plainMatch =
        ContainerUtil.or(
            item.getAllLookupStrings(),
            new Condition<String>() {
              @Override
              public boolean value(String s) {
                return StringUtil.containsIgnoreCase(s, prefix);
              }
            });
    if (!plainMatch) {
      FeatureUsageTracker.getInstance()
          .triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CAMEL_HUMPS);
    }

    myFinishing = true;
    ApplicationManager.getApplication()
        .runWriteAction(
            new Runnable() {
              public void run() {
                myEditor.getDocument().startGuardedBlockChecking();
                try {
                  insertLookupString(item, getPrefixLength(item));
                } finally {
                  myEditor.getDocument().stopGuardedBlockChecking();
                }
              }
            });

    if (myDisposed) { // any document listeners could close us
      return;
    }

    doHide(false, true);

    fireItemSelected(item, completionChar);
  }

  public int getPrefixLength(LookupElement item) {
    return myOffsets.getPrefixLength(item, this);
  }

  private void insertLookupString(LookupElement item, final int prefix) {
    final String lookupString = getCaseCorrectedLookupString(item);

    final Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor);
    hostEditor
        .getCaretModel()
        .runForEachCaret(
            new CaretAction() {
              @Override
              public void perform(Caret caret) {
                EditorModificationUtil.deleteSelectedText(hostEditor);
                final int caretOffset = hostEditor.getCaretModel().getOffset();
                int lookupStart = Math.max(caretOffset - prefix, 0);

                int len = hostEditor.getDocument().getTextLength();
                LOG.assertTrue(
                    lookupStart >= 0 && lookupStart <= len,
                    "ls: "
                        + lookupStart
                        + " caret: "
                        + caretOffset
                        + " prefix:"
                        + prefix
                        + " doc: "
                        + len);
                LOG.assertTrue(
                    caretOffset >= 0 && caretOffset <= len, "co: " + caretOffset + " doc: " + len);

                hostEditor.getDocument().replaceString(lookupStart, caretOffset, lookupString);

                int offset = lookupStart + lookupString.length();
                hostEditor.getCaretModel().moveToOffset(offset);
                hostEditor.getSelectionModel().removeSelection();
              }
            });

    myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
  }

  private String getCaseCorrectedLookupString(LookupElement item) {
    String lookupString = item.getLookupString();
    if (item.isCaseSensitive()) {
      return lookupString;
    }

    final String prefix = itemPattern(item);
    final int length = prefix.length();
    if (length == 0 || !itemMatcher(item).prefixMatches(prefix)) return lookupString;
    boolean isAllLower = true;
    boolean isAllUpper = true;
    boolean sameCase = true;
    for (int i = 0; i < length && (isAllLower || isAllUpper || sameCase); i++) {
      final char c = prefix.charAt(i);
      boolean isLower = Character.isLowerCase(c);
      boolean isUpper = Character.isUpperCase(c);
      // do not take this kind of symbols into account ('_', '@', etc.)
      if (!isLower && !isUpper) continue;
      isAllLower = isAllLower && isLower;
      isAllUpper = isAllUpper && isUpper;
      sameCase = sameCase && isLower == Character.isLowerCase(lookupString.charAt(i));
    }
    if (sameCase) return lookupString;
    if (isAllLower) return lookupString.toLowerCase();
    if (isAllUpper) return StringUtil.toUpperCase(lookupString);
    return lookupString;
  }

  @Override
  public int getLookupStart() {
    return myOffsets.getLookupStart(disposeTrace);
  }

  public int getLookupOriginalStart() {
    return myOffsets.getLookupOriginalStart();
  }

  public boolean performGuardedChange(Runnable change) {
    checkValid();
    assert !myChangeGuard : "already in change";

    myEditor.getDocument().startGuardedBlockChecking();
    myChangeGuard = true;
    boolean result;
    try {
      result = myOffsets.performGuardedChange(change);
    } finally {
      myEditor.getDocument().stopGuardedBlockChecking();
      myChangeGuard = false;
    }
    if (!result || myDisposed) {
      hide();
      return false;
    }
    if (isVisible()) {
      HintManagerImpl.updateLocation(this, myEditor, myUi.calculatePosition().getLocation());
    }
    checkValid();
    return true;
  }

  @Override
  public boolean vetoesHiding() {
    return myChangeGuard;
  }

  public boolean isAvailableToUser() {
    if (ApplicationManager.getApplication().isUnitTestMode()) {
      return myShown;
    }
    return isVisible();
  }

  public boolean isShown() {
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      ApplicationManager.getApplication().assertIsDispatchThread();
    }
    return myShown;
  }

  public boolean showLookup() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    checkValid();
    LOG.assertTrue(!myShown);
    myShown = true;
    myStampShown = System.currentTimeMillis();

    if (ApplicationManager.getApplication().isUnitTestMode()) return true;

    if (!myEditor.getContentComponent().isShowing()) {
      hide();
      return false;
    }

    myAdComponent.showRandomText();

    myUi = new LookupUi(this, myAdComponent, myList, myProject);
    myUi.setCalculating(myCalculating);
    Point p = myUi.calculatePosition().getLocation();
    try {
      HintManagerImpl.getInstanceImpl()
          .showEditorHint(
              this,
              myEditor,
              p,
              HintManager.HIDE_BY_ESCAPE | HintManager.UPDATE_BY_SCROLLING,
              0,
              false,
              HintManagerImpl.createHintHint(myEditor, p, this, HintManager.UNDER)
                  .setAwtTooltip(false));
    } catch (Exception e) {
      LOG.error(e);
    }

    if (!isVisible() || !myList.isShowing()) {
      hide();
      return false;
    }

    DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(this);

    return true;
  }

  public Advertiser getAdvertiser() {
    return myAdComponent;
  }

  public boolean mayBeNoticed() {
    return myStampShown > 0 && System.currentTimeMillis() - myStampShown > 300;
  }

  private void addListeners() {
    myEditor
        .getDocument()
        .addDocumentListener(
            new DocumentAdapter() {
              @Override
              public void documentChanged(DocumentEvent e) {
                if (!myChangeGuard && !myFinishing) {
                  hide();
                }
              }
            },
            this);

    final CaretListener caretListener =
        new CaretAdapter() {
          @Override
          public void caretPositionChanged(CaretEvent e) {
            if (!myChangeGuard && !myFinishing) {
              hide();
            }
          }
        };
    final SelectionListener selectionListener =
        new SelectionListener() {
          @Override
          public void selectionChanged(final SelectionEvent e) {
            if (!myChangeGuard && !myFinishing) {
              hide();
            }
          }
        };
    final EditorMouseListener mouseListener =
        new EditorMouseAdapter() {
          @Override
          public void mouseClicked(EditorMouseEvent e) {
            e.consume();
            hide();
          }
        };

    myEditor.getCaretModel().addCaretListener(caretListener);
    myEditor.getSelectionModel().addSelectionListener(selectionListener);
    myEditor.addEditorMouseListener(mouseListener);
    Disposer.register(
        this,
        new Disposable() {
          @Override
          public void dispose() {
            myEditor.getCaretModel().removeCaretListener(caretListener);
            myEditor.getSelectionModel().removeSelectionListener(selectionListener);
            myEditor.removeEditorMouseListener(mouseListener);
          }
        });

    JComponent editorComponent = myEditor.getContentComponent();
    if (editorComponent.isShowing()) {
      Disposer.register(
          this,
          new UiNotifyConnector(
              editorComponent,
              new Activatable() {
                @Override
                public void showNotify() {}

                @Override
                public void hideNotify() {
                  hideLookup(false);
                }
              }));
    }

    myList.addListSelectionListener(
        new ListSelectionListener() {
          private LookupElement oldItem = null;

          @Override
          public void valueChanged(@NotNull ListSelectionEvent e) {
            if (!myUpdating) {
              final LookupElement item = getCurrentItem();
              fireCurrentItemChanged(oldItem, item);
              oldItem = item;
            }
          }
        });

    new ClickListener() {
      @Override
      public boolean onClick(@NotNull MouseEvent e, int clickCount) {
        setFocusDegree(FocusDegree.FOCUSED);
        markSelectionTouched();

        if (clickCount == 2) {
          CommandProcessor.getInstance()
              .executeCommand(
                  myProject,
                  new Runnable() {
                    @Override
                    public void run() {
                      finishLookup(NORMAL_SELECT_CHAR);
                    }
                  },
                  "",
                  null);
        }
        return true;
      }
    }.installOn(myList);
  }

  @Override
  @Nullable
  public LookupElement getCurrentItem() {
    LookupElement item = (LookupElement) myList.getSelectedValue();
    return item instanceof EmptyLookupItem ? null : item;
  }

  @Override
  public void setCurrentItem(LookupElement item) {
    markSelectionTouched();
    myList.setSelectedValue(item, false);
  }

  @Override
  public void addLookupListener(LookupListener listener) {
    myListeners.add(listener);
  }

  @Override
  public void removeLookupListener(LookupListener listener) {
    myListeners.remove(listener);
  }

  @Override
  public Rectangle getCurrentItemBounds() {
    int index = myList.getSelectedIndex();
    if (index < 0) {
      LOG.error("No selected element, size=" + getListModel().getSize() + "; items" + getItems());
    }
    Rectangle itmBounds = myList.getCellBounds(index, index);
    if (itmBounds == null) {
      LOG.error("No bounds for " + index + "; size=" + getListModel().getSize());
      return null;
    }
    Point layeredPanePoint =
        SwingUtilities.convertPoint(myList, itmBounds.x, itmBounds.y, getComponent());
    itmBounds.x = layeredPanePoint.x;
    itmBounds.y = layeredPanePoint.y;
    return itmBounds;
  }

  public void fireItemSelected(@Nullable final LookupElement item, char completionChar) {
    PsiDocumentManager.getInstance(myProject).commitAllDocuments();

    if (!myListeners.isEmpty()) {
      LookupEvent event = new LookupEvent(this, item, completionChar);
      for (LookupListener listener : myListeners) {
        try {
          listener.itemSelected(event);
        } catch (Throwable e) {
          LOG.error(e);
        }
      }
    }
  }

  private void fireLookupCanceled(final boolean explicitly) {
    if (!myListeners.isEmpty()) {
      LookupEvent event = new LookupEvent(this, explicitly);
      for (LookupListener listener : myListeners) {
        try {
          listener.lookupCanceled(event);
        } catch (Throwable e) {
          LOG.error(e);
        }
      }
    }
  }

  private void fireCurrentItemChanged(
      @Nullable LookupElement oldItem, @Nullable LookupElement currentItem) {
    if (oldItem != currentItem && !myListeners.isEmpty()) {
      LookupEvent event = new LookupEvent(this, currentItem, (char) 0);
      for (LookupListener listener : myListeners) {
        listener.currentItemChanged(event);
      }
    }
  }

  public boolean fillInCommonPrefix(boolean explicitlyInvoked) {
    if (explicitlyInvoked) {
      setFocusDegree(FocusDegree.FOCUSED);
    }

    if (explicitlyInvoked && myCalculating) return false;
    if (!explicitlyInvoked && mySelectionTouched) return false;

    ListModel listModel = getListModel();
    if (listModel.getSize() <= 1) return false;

    if (listModel.getSize() == 0) return false;

    final LookupElement firstItem = (LookupElement) listModel.getElementAt(0);
    if (listModel.getSize() == 1 && firstItem instanceof EmptyLookupItem) return false;

    final PrefixMatcher firstItemMatcher = itemMatcher(firstItem);
    final String oldPrefix = firstItemMatcher.getPrefix();
    final String presentPrefix = oldPrefix + getAdditionalPrefix();
    String commonPrefix = getCaseCorrectedLookupString(firstItem);

    for (int i = 1; i < listModel.getSize(); i++) {
      LookupElement item = (LookupElement) listModel.getElementAt(i);
      if (item instanceof EmptyLookupItem) return false;
      if (!oldPrefix.equals(itemMatcher(item).getPrefix())) return false;

      final String lookupString = getCaseCorrectedLookupString(item);
      final int length = Math.min(commonPrefix.length(), lookupString.length());
      if (length < commonPrefix.length()) {
        commonPrefix = commonPrefix.substring(0, length);
      }

      for (int j = 0; j < length; j++) {
        if (commonPrefix.charAt(j) != lookupString.charAt(j)) {
          commonPrefix = lookupString.substring(0, j);
          break;
        }
      }

      if (commonPrefix.length() == 0 || commonPrefix.length() < presentPrefix.length()) {
        return false;
      }
    }

    if (commonPrefix.equals(presentPrefix)) {
      return false;
    }

    for (int i = 0; i < listModel.getSize(); i++) {
      LookupElement item = (LookupElement) listModel.getElementAt(i);
      if (!itemMatcher(item).cloneWithPrefix(commonPrefix).prefixMatches(item)) {
        return false;
      }
    }

    myOffsets.setInitialPrefix(presentPrefix, explicitlyInvoked);

    replacePrefix(presentPrefix, commonPrefix);
    return true;
  }

  public void replacePrefix(final String presentPrefix, final String newPrefix) {
    if (!performGuardedChange(
        new Runnable() {
          @Override
          public void run() {
            EditorModificationUtil.deleteSelectedText(myEditor);
            int offset = myEditor.getCaretModel().getOffset();
            final int start = offset - presentPrefix.length();
            myEditor.getDocument().replaceString(start, offset, newPrefix);

            Map<LookupElement, PrefixMatcher> newMatchers =
                new HashMap<LookupElement, PrefixMatcher>();
            for (LookupElement item : getItems()) {
              if (item.isValid()) {
                PrefixMatcher matcher = itemMatcher(item).cloneWithPrefix(newPrefix);
                if (matcher.prefixMatches(item)) {
                  newMatchers.put(item, matcher);
                }
              }
            }
            myMatchers.clear();
            myMatchers.putAll(newMatchers);

            myOffsets.clearAdditionalPrefix();

            myEditor.getCaretModel().moveToOffset(start + newPrefix.length());
          }
        })) {
      return;
    }
    synchronized (myList) {
      myPresentableArranger.prefixChanged(this);
    }
    refreshUi(true, true);
  }

  @Override
  @Nullable
  public PsiFile getPsiFile() {
    return PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
  }

  @Override
  public boolean isCompletion() {
    return myArranger instanceof CompletionLookupArranger;
  }

  @Override
  public PsiElement getPsiElement() {
    PsiFile file = getPsiFile();
    if (file == null) return null;

    int offset = getLookupStart();
    if (offset > 0) return file.findElementAt(offset - 1);

    return file.findElementAt(0);
  }

  @Override
  public Editor getEditor() {
    return myEditor;
  }

  @Override
  public boolean isPositionedAboveCaret() {
    return myUi != null && myUi.isPositionedAboveCaret();
  }

  @Override
  public boolean isSelectionTouched() {
    return mySelectionTouched;
  }

  @Override
  public List<String> getAdvertisements() {
    return myAdComponent.getAdvertisements();
  }

  @Override
  public void hide() {
    hideLookup(true);
  }

  public void hideLookup(boolean explicitly) {
    ApplicationManager.getApplication().assertIsDispatchThread();

    if (myHidden) return;

    doHide(true, explicitly);
  }

  private void doHide(final boolean fireCanceled, final boolean explicitly) {
    if (myDisposed) {
      LOG.error(disposeTrace);
    } else {
      myHidden = true;

      try {
        super.hide();

        Disposer.dispose(this);

        assert myDisposed;
      } catch (Throwable e) {
        LOG.error(e);
      }
    }

    if (fireCanceled) {
      fireLookupCanceled(explicitly);
    }
  }

  public void restorePrefix() {
    myOffsets.restorePrefix();
  }

  private static String staticDisposeTrace = null;
  private String disposeTrace = null;

  public static String getLastLookupDisposeTrace() {
    return staticDisposeTrace;
  }

  @Override
  public void dispose() {
    assert ApplicationManager.getApplication().isDispatchThread();
    assert myHidden;
    if (myDisposed) {
      LOG.error(disposeTrace);
      return;
    }

    myOffsets.disposeMarkers();
    myDisposed = true;
    disposeTrace = DebugUtil.currentStackTrace() + "\n============";
    //noinspection AssignmentToStaticFieldFromInstanceMethod
    staticDisposeTrace = disposeTrace;
  }

  public void refreshUi(boolean mayCheckReused, boolean onExplicitAction) {
    assert !myUpdating;
    LookupElement prevItem = getCurrentItem();
    myUpdating = true;
    try {
      final boolean reused = mayCheckReused && checkReused();
      boolean selectionVisible = isSelectionVisible();
      boolean itemsChanged = updateList(onExplicitAction, reused);
      if (isVisible()) {
        LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode());
        myUi.refreshUi(selectionVisible, itemsChanged, reused, onExplicitAction);
      }
    } finally {
      myUpdating = false;
      fireCurrentItemChanged(prevItem, getCurrentItem());
    }
  }

  public void markReused() {
    synchronized (myList) {
      myArranger = myArranger.createEmptyCopy();
    }
    requestResize();
  }

  public void addAdvertisement(@NotNull final String text, final @Nullable Color bgColor) {
    if (containsDummyIdentifier(text)) {
      return;
    }

    myAdComponent.addAdvertisement(text, bgColor);
    requestResize();
  }

  public boolean isLookupDisposed() {
    return myDisposed;
  }

  public void checkValid() {
    if (myDisposed) {
      throw new AssertionError("Disposed at: " + disposeTrace);
    }
  }

  @Override
  public void showItemPopup(JBPopup hint) {
    final Rectangle bounds = getCurrentItemBounds();
    hint.show(new RelativePoint(getComponent(), new Point(bounds.x + bounds.width, bounds.y)));
  }

  @Override
  public boolean showElementActions() {
    if (!isVisible()) return false;

    final LookupElement element = getCurrentItem();
    if (element == null) {
      return false;
    }

    final Collection<LookupElementAction> actions = getActionsFor(element);
    if (actions.isEmpty()) {
      return false;
    }

    showItemPopup(
        JBPopupFactory.getInstance()
            .createListPopup(new LookupActionsStep(actions, this, element)));
    return true;
  }

  public Map<LookupElement, StringBuilder> getRelevanceStrings() {
    synchronized (myList) {
      return myPresentableArranger.getRelevanceStrings();
    }
  }

  public enum FocusDegree {
    FOCUSED,
    SEMI_FOCUSED,
    UNFOCUSED
  }
}
public class FileTemplateConfigurable implements Configurable, Configurable.NoScroll {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.ide.fileTemplates.impl.FileTemplateConfigurable");
  @NonNls private static final String EMPTY_HTML = "<html></html>";
  @NonNls private static final String CONTENT_TYPE_PLAIN = "text/plain";

  private JPanel myMainPanel;
  private FileTemplate myTemplate;
  private PsiFile myFile;
  private Editor myTemplateEditor;
  private JTextField myNameField;
  private JTextField myExtensionField;
  private JCheckBox myAdjustBox;
  private JPanel myTopPanel;
  private JEditorPane myDescriptionComponent;
  private boolean myModified = false;
  private URL myDefaultDescriptionUrl;
  private final Project myProject;

  private final List<ChangeListener> myChangeListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();;
  private Splitter mySplitter;
  private final FileType myVelocityFileType =
      FileTypeManager.getInstance().getFileTypeByExtension("ft");
  private JPanel myDescriptionPanel;

  public FileTemplateConfigurable() {
    Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext());
    myProject = project != null ? project : ProjectManager.getInstance().getDefaultProject();
  }

  public FileTemplate getTemplate() {
    return myTemplate;
  }

  public void setTemplate(FileTemplate template, URL defaultDescription) {
    myDefaultDescriptionUrl = defaultDescription;
    myTemplate = template;
    if (myMainPanel != null) {
      reset();
      myNameField.selectAll();
      myExtensionField.selectAll();
    }
  }

  public void setShowInternalMessage(String message) {
    if (message == null) {
      myTopPanel.removeAll();
      myTopPanel.add(
          new JLabel(IdeBundle.message("label.name")),
          new GridBagConstraints(
              0,
              0,
              1,
              1,
              0.0,
              0.0,
              GridBagConstraints.CENTER,
              GridBagConstraints.NONE,
              new Insets(0, 0, 0, 2),
              0,
              0));
      myTopPanel.add(
          myNameField,
          new GridBagConstraints(
              1,
              0,
              1,
              1,
              1.0,
              0.0,
              GridBagConstraints.CENTER,
              GridBagConstraints.HORIZONTAL,
              new Insets(0, 2, 0, 2),
              0,
              0));
      myTopPanel.add(
          new JLabel(IdeBundle.message("label.extension")),
          new GridBagConstraints(
              2,
              0,
              1,
              1,
              0.0,
              0.0,
              GridBagConstraints.CENTER,
              GridBagConstraints.NONE,
              new Insets(0, 2, 0, 2),
              0,
              0));
      myTopPanel.add(
          myExtensionField,
          new GridBagConstraints(
              3,
              0,
              1,
              1,
              .3,
              0.0,
              GridBagConstraints.CENTER,
              GridBagConstraints.HORIZONTAL,
              new Insets(0, 2, 0, 0),
              0,
              0));
      myExtensionField.setColumns(7);
    } else {
      myTopPanel.removeAll();
      myTopPanel.add(
          new JLabel(message),
          new GridBagConstraints(
              0,
              0,
              4,
              1,
              1.0,
              0.0,
              GridBagConstraints.WEST,
              GridBagConstraints.HORIZONTAL,
              new Insets(0, 0, 0, 0),
              0,
              0));
      myTopPanel.add(
          Box.createVerticalStrut(myNameField.getPreferredSize().height),
          new GridBagConstraints(
              4,
              0,
              1,
              1,
              0.0,
              0.0,
              GridBagConstraints.WEST,
              GridBagConstraints.HORIZONTAL,
              new Insets(0, 0, 0, 0),
              0,
              0));
    }
    myMainPanel.revalidate();
    myTopPanel.repaint();
  }

  public void setShowAdjustCheckBox(boolean show) {
    myAdjustBox.setEnabled(show);
  }

  @Override
  public String getDisplayName() {
    return IdeBundle.message("title.file.templates");
  }

  @Override
  public String getHelpTopic() {
    return null;
  }

  @Override
  public JComponent createComponent() {
    myMainPanel = new JPanel(new GridBagLayout());
    myNameField = new JTextField();
    myExtensionField = new JTextField();
    mySplitter = new Splitter(true, 0.4f);

    myTemplateEditor = createEditor();

    myDescriptionComponent = new JEditorPane(UIUtil.HTML_MIME, EMPTY_HTML);
    myDescriptionComponent.setEditable(false);

    myAdjustBox = new JCheckBox(IdeBundle.message("checkbox.reformat.according.to.style"));
    myTopPanel = new JPanel(new GridBagLayout());

    myDescriptionPanel = new JPanel(new GridBagLayout());
    myDescriptionPanel.add(
        SeparatorFactory.createSeparator(IdeBundle.message("label.description"), null),
        new GridBagConstraints(
            0,
            0,
            1,
            1,
            0.0,
            0.0,
            GridBagConstraints.WEST,
            GridBagConstraints.HORIZONTAL,
            new Insets(0, 0, 2, 0),
            0,
            0));
    myDescriptionPanel.add(
        ScrollPaneFactory.createScrollPane(myDescriptionComponent),
        new GridBagConstraints(
            0,
            1,
            1,
            1,
            1.0,
            1.0,
            GridBagConstraints.CENTER,
            GridBagConstraints.BOTH,
            new Insets(2, 0, 0, 0),
            0,
            0));

    myMainPanel.add(
        myTopPanel,
        new GridBagConstraints(
            0,
            0,
            4,
            1,
            1.0,
            0.0,
            GridBagConstraints.CENTER,
            GridBagConstraints.HORIZONTAL,
            new Insets(0, 0, 2, 0),
            0,
            0));
    myMainPanel.add(
        myAdjustBox,
        new GridBagConstraints(
            0,
            1,
            4,
            1,
            0.0,
            0.0,
            GridBagConstraints.WEST,
            GridBagConstraints.HORIZONTAL,
            new Insets(2, 0, 2, 0),
            0,
            0));
    myMainPanel.add(
        mySplitter,
        new GridBagConstraints(
            0,
            2,
            4,
            1,
            1.0,
            1.0,
            GridBagConstraints.CENTER,
            GridBagConstraints.BOTH,
            new Insets(2, 0, 0, 0),
            0,
            0));

    mySplitter.setSecondComponent(myDescriptionPanel);
    setShowInternalMessage(null);

    myNameField.addFocusListener(
        new FocusAdapter() {
          @Override
          public void focusLost(FocusEvent e) {
            onNameChanged();
          }
        });
    myExtensionField.addFocusListener(
        new FocusAdapter() {
          @Override
          public void focusLost(FocusEvent e) {
            onNameChanged();
          }
        });
    myMainPanel.setPreferredSize(new Dimension(400, 300));
    return myMainPanel;
  }

  private Editor createEditor() {
    EditorFactory editorFactory = EditorFactory.getInstance();
    Document doc =
        myFile == null
            ? editorFactory.createDocument(myTemplate == null ? "" : myTemplate.getText())
            : PsiDocumentManager.getInstance(myFile.getProject()).getDocument(myFile);
    Editor editor =
        myProject == null
            ? editorFactory.createEditor(doc)
            : editorFactory.createEditor(doc, myProject);

    EditorSettings editorSettings = editor.getSettings();
    editorSettings.setVirtualSpace(false);
    editorSettings.setLineMarkerAreaShown(false);
    editorSettings.setIndentGuidesShown(false);
    editorSettings.setLineNumbersShown(false);
    editorSettings.setFoldingOutlineShown(false);
    editorSettings.setAdditionalColumnsCount(3);
    editorSettings.setAdditionalLinesCount(3);

    EditorColorsScheme scheme = editor.getColorsScheme();
    scheme.setColor(EditorColors.CARET_ROW_COLOR, null);

    editor
        .getDocument()
        .addDocumentListener(
            new DocumentAdapter() {
              @Override
              public void documentChanged(DocumentEvent e) {
                onTextChanged();
              }
            });

    ((EditorEx) editor).setHighlighter(createHighlighter());
    mySplitter.setFirstComponent(editor.getComponent());
    return editor;
  }

  private void onTextChanged() {
    myModified = true;
  }

  public String getNameValue() {
    return myNameField.getText();
  }

  public String getExtensionValue() {
    return myExtensionField.getText();
  }

  private void onNameChanged() {
    ChangeEvent event = new ChangeEvent(this);
    for (ChangeListener changeListener : myChangeListeners) {
      changeListener.stateChanged(event);
    }
  }

  public void addChangeListener(ChangeListener listener) {
    if (!myChangeListeners.contains(listener)) {
      myChangeListeners.add(listener);
    }
  }

  public void removeChangeListener(ChangeListener listener) {
    myChangeListeners.remove(listener);
  }

  @Override
  public boolean isModified() {
    if (myModified) {
      return true;
    }
    String name = (myTemplate == null) ? "" : myTemplate.getName();
    String extension = (myTemplate == null) ? "" : myTemplate.getExtension();
    if (!Comparing.equal(name, myNameField.getText())) {
      return true;
    }
    if (!Comparing.equal(extension, myExtensionField.getText())) {
      return true;
    }
    if (myTemplate != null) {
      if (myTemplate.isReformatCode() != myAdjustBox.isSelected()) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void apply() throws ConfigurationException {
    if (myTemplate != null) {
      myTemplate.setText(myTemplateEditor.getDocument().getText());
      String name = myNameField.getText();
      String extension = myExtensionField.getText();
      int lastDotIndex = extension.lastIndexOf(".");
      if (lastDotIndex >= 0) {
        name += extension.substring(0, lastDotIndex + 1);
        extension = extension.substring(lastDotIndex + 1);
      }
      if (name.length() == 0 || !isValidFilename(name + "." + extension)) {
        throw new ConfigurationException(
            IdeBundle.message("error.invalid.template.file.name.or.extension"));
      }
      myTemplate.setName(name);
      myTemplate.setExtension(extension);
      myTemplate.setReformatCode(myAdjustBox.isSelected());
    }
    myModified = false;
  }

  // TODO: needs to be generalized someday for other profiles
  private static boolean isValidFilename(final String filename) {
    if (filename.contains("/") || filename.contains("\\") || filename.contains(":")) {
      return false;
    }
    final File tempFile = new File(FileUtil.getTempDirectory() + File.separator + filename);
    return FileUtil.ensureCanCreateFile(tempFile);
  }

  @Override
  public void reset() {
    final String text = (myTemplate == null) ? "" : myTemplate.getText();
    String name = (myTemplate == null) ? "" : myTemplate.getName();
    String extension = (myTemplate == null) ? "" : myTemplate.getExtension();
    String description = (myTemplate == null) ? "" : myTemplate.getDescription();

    if ((description.length() == 0) && (myDefaultDescriptionUrl != null)) {
      try {
        description = UrlUtil.loadText(myDefaultDescriptionUrl);
      } catch (IOException e) {
        LOG.error(e);
      }
    }

    EditorFactory.getInstance().releaseEditor(myTemplateEditor);
    myFile = createFile(text, name);
    myTemplateEditor = createEditor();

    boolean adjust = (myTemplate != null) && myTemplate.isReformatCode();
    myNameField.setText(name);
    myExtensionField.setText(extension);
    myAdjustBox.setSelected(adjust);
    String desc = description.length() > 0 ? description : EMPTY_HTML;

    // [myakovlev] do not delete these stupid lines! Or you get Exception!
    myDescriptionComponent.setContentType(CONTENT_TYPE_PLAIN);
    myDescriptionComponent.setEditable(true);
    myDescriptionComponent.setText(desc);
    myDescriptionComponent.setContentType(UIUtil.HTML_MIME);
    myDescriptionComponent.setText(desc);
    myDescriptionComponent.setCaretPosition(0);
    myDescriptionComponent.setEditable(false);

    myDescriptionPanel.setVisible(StringUtil.isNotEmpty(description));

    myNameField.setEditable((myTemplate != null) && (!myTemplate.isDefault()));
    myExtensionField.setEditable((myTemplate != null) && (!myTemplate.isDefault()));
    myModified = false;
  }

  @Nullable
  private PsiFile createFile(final String text, final String name) {
    if (myTemplate == null || myProject == null) return null;

    final FileType fileType = myVelocityFileType;
    if (fileType == FileTypes.UNKNOWN) return null;

    final PsiFile file =
        PsiFileFactory.getInstance(myProject)
            .createFileFromText(name + ".txt.ft", fileType, text, 0, true);
    file.getViewProvider()
        .putUserData(
            FileTemplateManager.DEFAULT_TEMPLATE_PROPERTIES,
            FileTemplateManager.getInstance().getDefaultProperties(myProject));
    return file;
  }

  @Override
  public void disposeUIResources() {
    myMainPanel = null;
    if (myTemplateEditor != null) {
      EditorFactory.getInstance().releaseEditor(myTemplateEditor);
      myTemplateEditor = null;
    }
    myFile = null;
  }

  private EditorHighlighter createHighlighter() {
    if (myTemplate != null && myProject != null && myVelocityFileType != FileTypes.UNKNOWN) {
      return EditorHighlighterFactory.getInstance()
          .createEditorHighlighter(
              myProject, new LightVirtualFile("aaa." + myTemplate.getExtension() + ".ft"));
    }

    FileType fileType = null;
    if (myTemplate != null) {
      fileType = FileTypeManager.getInstance().getFileTypeByExtension(myTemplate.getExtension());
    }
    if (fileType == null) {
      fileType = FileTypes.PLAIN_TEXT;
    }
    SyntaxHighlighter originalHighlighter =
        SyntaxHighlighterFactory.getSyntaxHighlighter(fileType, null, null);
    if (originalHighlighter == null) originalHighlighter = new PlainSyntaxHighlighter();
    return new LexerEditorHighlighter(
        new TemplateHighlighter(originalHighlighter),
        EditorColorsManager.getInstance().getGlobalScheme());
  }

  private static final TokenSet TOKENS_TO_MERGE = TokenSet.create(FileTemplateTokenType.TEXT);

  private static class TemplateHighlighter extends SyntaxHighlighterBase {
    private final Lexer myLexer;
    private final SyntaxHighlighter myOriginalHighlighter;

    public TemplateHighlighter(SyntaxHighlighter original) {
      myOriginalHighlighter = original;
      Lexer originalLexer = original.getHighlightingLexer();
      Lexer templateLexer = new FlexAdapter(new FileTemplateTextLexer());
      templateLexer = new MergingLexerAdapter(templateLexer, TOKENS_TO_MERGE);

      myLexer =
          new CompositeLexer(originalLexer, templateLexer) {
            @Override
            protected IElementType getCompositeTokenType(IElementType type1, IElementType type2) {
              if (type2 == FileTemplateTokenType.MACRO
                  || type2 == FileTemplateTokenType.DIRECTIVE) {
                return type2;
              } else {
                return type1;
              }
            }
          };
    }

    @Override
    @NotNull
    public Lexer getHighlightingLexer() {
      return myLexer;
    }

    @Override
    @NotNull
    public TextAttributesKey[] getTokenHighlights(IElementType tokenType) {
      if (tokenType == FileTemplateTokenType.MACRO) {
        return pack(
            myOriginalHighlighter.getTokenHighlights(tokenType),
            TemplateColors.TEMPLATE_VARIABLE_ATTRIBUTES);
      } else if (tokenType == FileTemplateTokenType.DIRECTIVE) {
        return pack(
            myOriginalHighlighter.getTokenHighlights(tokenType),
            TemplateColors.TEMPLATE_VARIABLE_ATTRIBUTES);
      }

      return myOriginalHighlighter.getTokenHighlights(tokenType);
    }
  }

  public void focusToNameField() {
    myNameField.selectAll();
    myNameField.requestFocus();
  }

  public void focusToExtensionField() {
    myExtensionField.selectAll();
    myExtensionField.requestFocus();
  }
}
class Browser extends JPanel {
  private static final String UNDER_CONSTRUCTION =
      InspectionsBundle.message("inspection.tool.description.under.construction.text");
  private final List<ClickListener> myClickListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private RefEntity myCurrentEntity;
  private JEditorPane myHTMLViewer;
  private final InspectionResultsView myView;
  private final HyperlinkListener myHyperLinkListener;
  private CommonProblemDescriptor myCurrentDescriptor;

  public static class ClickEvent {
    public static final int REF_ELEMENT = 1;
    public static final int FILE_OFFSET = 2;
    private final VirtualFile myFile;
    private final int myStartPosition;
    private final int myEndPosition;
    private final RefElement refElement;
    private final int myEventType;

    public ClickEvent(VirtualFile myFile, int myStartPosition, int myEndPosition) {
      this.myFile = myFile;
      this.myStartPosition = myStartPosition;
      this.myEndPosition = myEndPosition;
      myEventType = FILE_OFFSET;
      refElement = null;
    }

    public int getEventType() {
      return myEventType;
    }

    public VirtualFile getFile() {
      return myFile;
    }

    public int getStartOffset() {
      return myStartPosition;
    }

    public int getEndOffset() {
      return myEndPosition;
    }

    public RefElement getClickedElement() {
      return refElement;
    }
  }

  public void dispose() {
    removeAll();
    if (myHTMLViewer != null) {
      myHTMLViewer.removeHyperlinkListener(myHyperLinkListener);
      myHTMLViewer = null;
    }
    myClickListeners.clear();
  }

  public interface ClickListener {
    void referenceClicked(ClickEvent e);
  }

  private void showPageFromHistory(RefEntity newEntity) {
    InspectionTool tool = getTool(newEntity);
    try {
      if (tool instanceof DescriptorProviderInspection
          && !(tool instanceof CommonInspectionToolWrapper)) {
        showEmpty();
      } else {
        try {
          String html = generateHTML(newEntity, tool);
          myHTMLViewer.read(new StringReader(html), null);
          setupStyle();
          myHTMLViewer.setCaretPosition(0);
        } catch (Exception e) {
          showEmpty();
        }
      }
    } finally {
      myCurrentEntity = newEntity;
      myCurrentDescriptor = null;
    }
  }

  public void showPageFor(RefEntity refEntity, CommonProblemDescriptor descriptor) {
    try {
      String html = generateHTML(refEntity, descriptor);
      myHTMLViewer.read(new StringReader(html), null);
      setupStyle();
      myHTMLViewer.setCaretPosition(0);
    } catch (Exception e) {
      showEmpty();
    } finally {
      myCurrentEntity = refEntity;
      myCurrentDescriptor = descriptor;
    }
  }

  public void showPageFor(RefEntity newEntity) {
    if (newEntity == null) {
      showEmpty();
      return;
    }
    // multiple problems for one entity -> refresh browser
    showPageFromHistory(newEntity.getRefManager().getRefinedElement(newEntity));
  }

  public Browser(InspectionResultsView view) {
    super(new BorderLayout());
    myView = view;

    myCurrentEntity = null;
    myCurrentDescriptor = null;

    myHTMLViewer =
        new JEditorPane(
            UIUtil.HTML_MIME,
            InspectionsBundle.message("inspection.offline.view.empty.browser.text"));
    myHTMLViewer.setEditable(false);
    myHyperLinkListener =
        new HyperlinkListener() {
          @Override
          public void hyperlinkUpdate(HyperlinkEvent e) {
            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
              JEditorPane pane = (JEditorPane) e.getSource();
              if (e instanceof HTMLFrameHyperlinkEvent) {
                HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent) e;
                HTMLDocument doc = (HTMLDocument) pane.getDocument();
                doc.processHTMLFrameHyperlinkEvent(evt);
              } else {
                try {
                  URL url = e.getURL();
                  @NonNls String ref = url.getRef();
                  if (ref.startsWith("pos:")) {
                    int delimeterPos = ref.indexOf(':', "pos:".length() + 1);
                    String startPosition = ref.substring("pos:".length(), delimeterPos);
                    String endPosition = ref.substring(delimeterPos + 1);
                    Integer textStartOffset = new Integer(startPosition);
                    Integer textEndOffset = new Integer(endPosition);
                    String fileURL = url.toExternalForm();
                    fileURL = fileURL.substring(0, fileURL.indexOf('#'));
                    VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(fileURL);
                    if (vFile != null) {
                      fireClickEvent(vFile, textStartOffset.intValue(), textEndOffset.intValue());
                    }
                  } else if (ref.startsWith("descr:")) {
                    if (myCurrentDescriptor instanceof ProblemDescriptor) {
                      PsiElement psiElement =
                          ((ProblemDescriptor) myCurrentDescriptor).getPsiElement();
                      if (psiElement == null) return;
                      VirtualFile vFile = psiElement.getContainingFile().getVirtualFile();
                      if (vFile != null) {
                        TextRange range =
                            ((ProblemDescriptorBase) myCurrentDescriptor).getTextRange();
                        fireClickEvent(vFile, range.getStartOffset(), range.getEndOffset());
                      }
                    }
                  } else if (ref.startsWith("invoke:")) {
                    int actionNumber = Integer.parseInt(ref.substring("invoke:".length()));
                    getTool()
                        .getQuickFixes(new RefElement[] {(RefElement) myCurrentEntity})[
                        actionNumber]
                        .doApplyFix(new RefElement[] {(RefElement) myCurrentEntity}, myView);
                  } else if (ref.startsWith("invokelocal:")) {
                    int actionNumber = Integer.parseInt(ref.substring("invokelocal:".length()));
                    if (actionNumber > -1) {
                      invokeLocalFix(actionNumber);
                    }
                  } else if (ref.startsWith("suppress:")) {
                    final SuppressActionWrapper.SuppressTreeAction[] suppressTreeActions =
                        new SuppressActionWrapper(
                                myView.getProject(),
                                getTool(),
                                myView.getTree().getSelectionPaths())
                            .getChildren(null);
                    final List<AnAction> activeActions = new ArrayList<AnAction>();
                    for (SuppressActionWrapper.SuppressTreeAction suppressTreeAction :
                        suppressTreeActions) {
                      if (suppressTreeAction.isAvailable()) activeActions.add(suppressTreeAction);
                    }
                    if (!activeActions.isEmpty()) {
                      int actionNumber = Integer.parseInt(ref.substring("suppress:".length()));
                      if (actionNumber > -1 && activeActions.size() > actionNumber) {
                        activeActions.get(actionNumber).actionPerformed(null);
                      }
                    }
                  } else {
                    int offset = Integer.parseInt(ref);
                    String fileURL = url.toExternalForm();
                    fileURL = fileURL.substring(0, fileURL.indexOf('#'));
                    VirtualFile vFile = VirtualFileManager.getInstance().findFileByUrl(fileURL);
                    if (vFile == null) {
                      vFile = VfsUtil.findFileByURL(url);
                    }
                    if (vFile != null) {
                      fireClickEvent(vFile, offset, offset);
                    }
                  }
                } catch (Throwable t) {
                  // ???
                }
              }
            }
          }
        };
    myHTMLViewer.addHyperlinkListener(myHyperLinkListener);

    final JScrollPane pane = ScrollPaneFactory.createScrollPane(myHTMLViewer);
    pane.setBorder(null);
    add(pane, BorderLayout.CENTER);
    setupStyle();
  }

  private void setupStyle() {
    Document document = myHTMLViewer.getDocument();
    if (!(document instanceof StyledDocument)) {
      return;
    }

    StyledDocument styledDocument = (StyledDocument) document;

    EditorColorsManager colorsManager = EditorColorsManager.getInstance();
    EditorColorsScheme scheme = colorsManager.getGlobalScheme();

    Style style = styledDocument.addStyle("active", null);
    StyleConstants.setFontFamily(style, scheme.getEditorFontName());
    StyleConstants.setFontSize(style, scheme.getEditorFontSize());
    styledDocument.setCharacterAttributes(0, document.getLength(), style, false);
  }

  public void addClickListener(ClickListener listener) {
    myClickListeners.add(listener);
  }

  private void fireClickEvent(VirtualFile file, int startPosition, int endPosition) {
    ClickEvent e = new ClickEvent(file, startPosition, endPosition);

    for (ClickListener listener : myClickListeners) {
      listener.referenceClicked(e);
    }
  }

  private String generateHTML(final RefEntity refEntity, final InspectionTool tool) {
    final StringBuffer buf = new StringBuffer();
    if (refEntity instanceof RefElement) {
      final Runnable action =
          new Runnable() {
            @Override
            public void run() {
              tool.getComposer().compose(buf, refEntity);
            }
          };
      ApplicationManager.getApplication().runReadAction(action);
    } else {
      tool.getComposer().compose(buf, refEntity);
    }

    uppercaseFirstLetter(buf);

    if (refEntity instanceof RefElement) {
      appendSuppressSection(buf);
    }

    insertHeaderFooter(buf);

    return buf.toString();
  }

  @SuppressWarnings({"HardCodedStringLiteral"})
  private static void insertHeaderFooter(final StringBuffer buf) {
    buf.insert(0, "<HTML><BODY>");
    buf.append("</BODY></HTML>");
  }

  private String generateHTML(final RefEntity refEntity, final CommonProblemDescriptor descriptor) {
    final StringBuffer buf = new StringBuffer();
    final Runnable action =
        new Runnable() {
          @Override
          public void run() {
            InspectionTool tool = getTool(refEntity);
            tool.getComposer().compose(buf, refEntity, descriptor);
          }
        };
    ApplicationManager.getApplication().runReadAction(action);

    uppercaseFirstLetter(buf);

    if (refEntity instanceof RefElement) {
      appendSuppressSection(buf);
    }

    insertHeaderFooter(buf);
    return buf.toString();
  }

  private InspectionTool getTool(final RefEntity refEntity) {
    InspectionTool tool = getTool();
    assert tool != null;
    final GlobalInspectionContextImpl manager = tool.getContext();
    if (manager == null) return tool;
    if (refEntity instanceof RefElement) {
      PsiElement element = ((RefElement) refEntity).getElement();
      if (element == null) return tool;
      final InspectionProfileWrapper profileWrapper =
          InspectionProjectProfileManagerImpl.getInstanceImpl(manager.getProject())
              .getProfileWrapper();
      if (profileWrapper == null) return tool;
      tool = profileWrapper.getInspectionTool(tool.getShortName(), element);
    }
    return tool;
  }

  private void appendSuppressSection(final StringBuffer buf) {
    final InspectionTool tool = getTool();
    if (tool != null) {
      final HighlightDisplayKey key = HighlightDisplayKey.find(tool.getShortName());
      if (key != null) { // dummy entry points
        final SuppressActionWrapper.SuppressTreeAction[] suppressActions =
            new SuppressActionWrapper(
                    myView.getProject(), tool, myView.getTree().getSelectionPaths())
                .getChildren(null);
        if (suppressActions.length > 0) {
          final List<AnAction> activeSuppressActions = new ArrayList<AnAction>();
          for (SuppressActionWrapper.SuppressTreeAction suppressAction : suppressActions) {
            if (suppressAction.isAvailable()) {
              activeSuppressActions.add(suppressAction);
            }
          }
          if (!activeSuppressActions.isEmpty()) {
            int idx = 0;
            @NonNls final String br = "<br>";
            buf.append(br);
            HTMLComposerImpl.appendHeading(
                buf, InspectionsBundle.message("inspection.export.results.suppress"));
            for (AnAction suppressAction : activeSuppressActions) {
              buf.append(br);
              if (idx == activeSuppressActions.size() - 1) {
                buf.append(br);
              }
              HTMLComposer.appendAfterHeaderIndention(buf);
              @NonNls
              final String href =
                  "<a HREF=\"file://bred.txt#suppress:"
                      + idx
                      + "\">"
                      + suppressAction.getTemplatePresentation().getText()
                      + "</a>";
              buf.append(href);
              idx++;
            }
          }
        }
      }
    }
  }

  private static void uppercaseFirstLetter(final StringBuffer buf) {
    if (buf.length() > 1) {
      char[] firstLetter = new char[1];
      buf.getChars(0, 1, firstLetter, 0);
      buf.setCharAt(0, Character.toUpperCase(firstLetter[0]));
    }
  }

  @SuppressWarnings({"HardCodedStringLiteral"})
  public void showEmpty() {
    myCurrentEntity = null;
    try {
      myHTMLViewer.read(new StringReader("<html><body></body></html>"), null);
    } catch (IOException e) {
      // can't be
    }
  }

  public void showDescription(InspectionTool tool) {
    if (tool.getShortName().length() == 0) {
      showEmpty();
      return;
    }
    @NonNls StringBuffer page = new StringBuffer();
    page.append("<table border='0' cellspacing='0' cellpadding='0' width='100%'>");
    page.append("<tr><td colspan='2'>");
    HTMLComposer.appendHeading(
        page, InspectionsBundle.message("inspection.tool.in.browser.id.title"));
    page.append("</td></tr>");
    page.append("<tr><td width='37'></td>" + "<td>");
    page.append(tool.getShortName());
    page.append("</td></tr>");
    page.append("<tr height='10'></tr>");
    page.append("<tr><td colspan='2'>");
    HTMLComposer.appendHeading(
        page, InspectionsBundle.message("inspection.tool.in.browser.description.title"));
    page.append("</td></tr>");
    page.append("<tr><td width='37'></td>" + "<td>");
    @NonNls final String underConstruction = "<b>" + UNDER_CONSTRUCTION + "</b></html>";
    try {
      @NonNls String description = tool.loadDescription();
      if (description == null) {
        description = underConstruction;
      }
      page.append(UIUtil.getHtmlBody(description));

      page.append("</td></tr></table>");
      myHTMLViewer.setText(XmlStringUtil.wrapInHtml(page));
      setupStyle();
    } finally {
      myCurrentEntity = null;
    }
  }

  @Nullable
  private InspectionTool getTool() {
    if (myView != null) {
      return myView.getTree().getSelectedTool();
    }
    return null;
  }

  public void invokeLocalFix(int idx) {
    if (myView.getTree().getSelectionCount() != 1) return;
    final InspectionTreeNode node =
        (InspectionTreeNode) myView.getTree().getSelectionPath().getLastPathComponent();
    if (node instanceof ProblemDescriptionNode) {
      final ProblemDescriptionNode problemNode = (ProblemDescriptionNode) node;
      final CommonProblemDescriptor descriptor = problemNode.getDescriptor();
      final RefEntity element = problemNode.getElement();
      invokeFix(element, descriptor, idx);
    } else if (node instanceof RefElementNode) {
      RefElementNode elementNode = (RefElementNode) node;
      RefEntity element = elementNode.getElement();
      CommonProblemDescriptor descriptor = elementNode.getProblem();
      if (descriptor != null) {
        invokeFix(element, descriptor, idx);
      }
    }
  }

  private void invokeFix(
      final RefEntity element, final CommonProblemDescriptor descriptor, final int idx) {
    final QuickFix[] fixes = descriptor.getFixes();
    if (fixes != null && fixes.length > idx && fixes[idx] != null) {
      if (element instanceof RefElement) {
        PsiElement psiElement = ((RefElement) element).getElement();
        if (psiElement != null && psiElement.isValid()) {
          if (!FileModificationService.getInstance().preparePsiElementForWrite(psiElement)) return;
          performFix(element, descriptor, idx, fixes[idx]);
        }
      } else {
        performFix(element, descriptor, idx, fixes[idx]);
      }
    }
  }

  private void performFix(
      final RefEntity element,
      final CommonProblemDescriptor descriptor,
      final int idx,
      final QuickFix fix) {
    final Runnable command =
        new Runnable() {
          @Override
          public void run() {
            ApplicationManager.getApplication()
                .runWriteAction(
                    new Runnable() {
                      @Override
                      public void run() {
                        final PsiModificationTracker tracker =
                            PsiManager.getInstance(myView.getProject()).getModificationTracker();
                        final long startCount = tracker.getModificationCount();
                        CommandProcessor.getInstance()
                            .markCurrentCommandAsGlobal(myView.getProject());
                        // CCE here means QuickFix was incorrectly inherited
                        fix.applyFix(myView.getProject(), descriptor);
                        if (startCount != tracker.getModificationCount()) {
                          final DescriptorProviderInspection tool =
                              ((DescriptorProviderInspection) myView.getTree().getSelectedTool());
                          if (tool != null) {
                            tool.ignoreProblem(element, descriptor, idx);
                          }
                          myView.updateView(false);
                        }
                      }
                    });
          }
        };
    CommandProcessor.getInstance()
        .executeCommand(myView.getProject(), command, fix.getName(), null);
  }
}
public class FoldingModelImpl implements FoldingModelEx, PrioritizedDocumentListener, Dumpable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.editor.impl.EditorFoldingModelImpl");

  private static final Key<LogicalPosition> SAVED_CARET_POSITION =
      Key.create("saved.position.before.folding");

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

  private boolean myIsFoldingEnabled;
  private final EditorImpl myEditor;
  private final FoldRegionsTree myFoldTree;
  private TextAttributes myFoldTextAttributes;
  private boolean myIsBatchFoldingProcessing;
  private boolean myDoNotCollapseCaret;
  private boolean myFoldRegionsProcessed;

  private int mySavedCaretShift;
  private final MultiMap<FoldingGroup, FoldRegion> myGroups =
      new MultiMap<FoldingGroup, FoldRegion>();
  private boolean myDocumentChangeProcessed = true;

  public FoldingModelImpl(EditorImpl editor) {
    myEditor = editor;
    myIsFoldingEnabled = true;
    myIsBatchFoldingProcessing = false;
    myDoNotCollapseCaret = false;
    myFoldTree =
        new FoldRegionsTree() {
          @Override
          protected boolean isFoldingEnabled() {
            return FoldingModelImpl.this.isFoldingEnabled();
          }
        };
    myFoldRegionsProcessed = false;
    refreshSettings();
  }

  @NotNull
  public List<FoldRegion> getGroupedRegions(@NotNull FoldingGroup group) {
    return (List<FoldRegion>) myGroups.get(group);
  }

  @NotNull
  public FoldRegion getFirstRegion(@NotNull FoldingGroup group, FoldRegion child) {
    final List<FoldRegion> regions = getGroupedRegions(group);
    if (regions.isEmpty()) {
      final boolean inAll = Arrays.asList(getAllFoldRegions()).contains(child);
      throw new AssertionError(
          "Folding group without children; the known child is in all: " + inAll);
    }

    FoldRegion main = regions.get(0);
    for (int i = 1; i < regions.size(); i++) {
      FoldRegion region = regions.get(i);
      if (main.getStartOffset() > region.getStartOffset()) {
        main = region;
      }
    }
    return main;
  }

  public int getEndOffset(@NotNull FoldingGroup group) {
    final List<FoldRegion> regions = getGroupedRegions(group);
    int endOffset = 0;
    for (FoldRegion region : regions) {
      if (region.isValid()) {
        endOffset = Math.max(endOffset, region.getEndOffset());
      }
    }
    return endOffset;
  }

  public void refreshSettings() {
    myFoldTextAttributes =
        myEditor.getColorsScheme().getAttributes(EditorColors.FOLDED_TEXT_ATTRIBUTES);
  }

  @Override
  public boolean isFoldingEnabled() {
    return myIsFoldingEnabled;
  }

  @Override
  public boolean isOffsetCollapsed(int offset) {
    assertReadAccess();
    return getCollapsedRegionAtOffset(offset) != null;
  }

  private boolean isOffsetInsideCollapsedRegion(int offset) {
    assertReadAccess();
    FoldRegion region = getCollapsedRegionAtOffset(offset);
    return region != null && region.getStartOffset() < offset;
  }

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

  private static void assertReadAccess() {
    ApplicationManagerEx.getApplicationEx().assertReadAccessAllowed();
  }

  @Override
  public void setFoldingEnabled(boolean isEnabled) {
    assertIsDispatchThreadForEditor();
    myIsFoldingEnabled = isEnabled;
  }

  @Override
  public FoldRegion addFoldRegion(int startOffset, int endOffset, @NotNull String placeholderText) {
    FoldRegion region = createFoldRegion(startOffset, endOffset, placeholderText, null, false);
    if (region == null) return null;
    if (!addFoldRegion(region)) {
      region.dispose();
      return null;
    }

    return region;
  }

  @Override
  public boolean addFoldRegion(@NotNull final FoldRegion region) {
    assertIsDispatchThreadForEditor();
    if (!isFoldingEnabled()) {
      return false;
    }
    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
      return false;
    }

    myFoldRegionsProcessed = true;
    if (myFoldTree.addRegion(region)) {
      final FoldingGroup group = region.getGroup();
      if (group != null) {
        myGroups.putValue(group, region);
      }
      for (FoldingListener listener : myListeners) {
        listener.onFoldRegionStateChange(region);
      }
      return true;
    }

    return false;
  }

  @Override
  public void runBatchFoldingOperation(@NotNull Runnable operation) {
    runBatchFoldingOperation(operation, false, true);
  }

  @Override
  public void runBatchFoldingOperation(@NotNull Runnable operation, boolean moveCaret) {
    runBatchFoldingOperation(operation, false, moveCaret);
  }

  private void runBatchFoldingOperation(
      final Runnable operation, final boolean dontCollapseCaret, final boolean moveCaret) {
    assertIsDispatchThreadForEditor();
    boolean oldDontCollapseCaret = myDoNotCollapseCaret;
    myDoNotCollapseCaret |= dontCollapseCaret;
    boolean oldBatchFlag = myIsBatchFoldingProcessing;
    if (!oldBatchFlag) {
      ((ScrollingModelImpl) myEditor.getScrollingModel()).finishAnimation();
      mySavedCaretShift =
          myEditor.visibleLineToY(myEditor.getCaretModel().getVisualPosition().line)
              - myEditor.getScrollingModel().getVerticalScrollOffset();
    }

    myIsBatchFoldingProcessing = true;
    try {
      operation.run();
    } finally {
      if (!oldBatchFlag) {
        if (myFoldRegionsProcessed) {
          notifyBatchFoldingProcessingDone(moveCaret);
          myFoldRegionsProcessed = false;
        }
        myIsBatchFoldingProcessing = false;
      }
      myDoNotCollapseCaret = oldDontCollapseCaret;
    }
  }

  @Override
  public void runBatchFoldingOperationDoNotCollapseCaret(@NotNull final Runnable operation) {
    runBatchFoldingOperation(operation, true, true);
  }

  /**
   * Disables caret position adjustment after batch folding operation is finished. Should be called
   * from inside batch operation runnable.
   */
  public void flushCaretShift() {
    mySavedCaretShift = -1;
  }

  @Override
  @NotNull
  public FoldRegion[] getAllFoldRegions() {
    assertReadAccess();
    return myFoldTree.fetchAllRegions();
  }

  @Override
  @Nullable
  public FoldRegion getCollapsedRegionAtOffset(int offset) {
    return myFoldTree.fetchOutermost(offset);
  }

  @Nullable
  @Override
  public FoldRegion getFoldRegion(int startOffset, int endOffset) {
    assertReadAccess();
    return myFoldTree.getRegionAt(startOffset, endOffset);
  }

  @Override
  @Nullable
  public FoldRegion getFoldingPlaceholderAt(Point p) {
    assertReadAccess();
    LogicalPosition pos = myEditor.xyToLogicalPosition(p);
    int line = pos.line;

    if (line >= myEditor.getDocument().getLineCount()) return null;

    int offset = myEditor.logicalPositionToOffset(pos);

    return myFoldTree.fetchOutermost(offset);
  }

  @Override
  public void removeFoldRegion(@NotNull final FoldRegion region) {
    assertIsDispatchThreadForEditor();

    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
    }

    region.setExpanded(true);
    final FoldingGroup group = region.getGroup();
    if (group != null) {
      myGroups.remove(group, region);
    }

    myFoldTree.removeRegion(region);
    myFoldRegionsProcessed = true;
    region.dispose();
  }

  public void dispose() {
    doClearFoldRegions();
  }

  @Override
  public void clearFoldRegions() {
    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be added or removed inside batchFoldProcessing() only.");
      return;
    }
    doClearFoldRegions();
  }

  public void doClearFoldRegions() {
    myGroups.clear();
    myFoldTree.clear();
  }

  public void expandFoldRegion(FoldRegion region) {
    assertIsDispatchThreadForEditor();
    if (region.isExpanded() || region.shouldNeverExpand()) return;

    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
    }

    for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
      LogicalPosition savedPosition = caret.getUserData(SAVED_CARET_POSITION);
      if (savedPosition != null) {
        int savedOffset = myEditor.logicalPositionToOffset(savedPosition);

        FoldRegion[] allCollapsed = myFoldTree.fetchCollapsedAt(savedOffset);
        if (allCollapsed.length == 1 && allCollapsed[0] == region) {
          caret.moveToLogicalPosition(savedPosition);
        }
      }
    }

    myFoldRegionsProcessed = true;
    ((FoldRegionImpl) region).setExpandedInternal(true);
    notifyListenersOnFoldRegionStateChange(region);
  }

  public void collapseFoldRegion(FoldRegion region) {
    assertIsDispatchThreadForEditor();
    if (!region.isExpanded()) return;

    if (!myIsBatchFoldingProcessing) {
      LOG.error("Fold regions must be collapsed or expanded inside batchFoldProcessing() only.");
    }

    List<Caret> carets = myEditor.getCaretModel().getAllCarets();
    for (Caret caret : carets) {
      LogicalPosition caretPosition = caret.getLogicalPosition();
      int caretOffset = myEditor.logicalPositionToOffset(caretPosition);

      if (FoldRegionsTree.contains(region, caretOffset)) {
        if (myDoNotCollapseCaret) return;
      }
    }
    for (Caret caret : carets) {
      LogicalPosition caretPosition = caret.getLogicalPosition();
      int caretOffset = myEditor.logicalPositionToOffset(caretPosition);

      if (FoldRegionsTree.contains(region, caretOffset)) {
        if (caret.getUserData(SAVED_CARET_POSITION) == null) {
          caret.putUserData(SAVED_CARET_POSITION, caretPosition.withoutVisualPositionInfo());
        }
      }
    }

    myFoldRegionsProcessed = true;
    ((FoldRegionImpl) region).setExpandedInternal(false);
    notifyListenersOnFoldRegionStateChange(region);
  }

  private void notifyBatchFoldingProcessingDone(final boolean moveCaretFromCollapsedRegion) {
    rebuild();

    for (FoldingListener listener : myListeners) {
      listener.onFoldProcessingEnd();
    }

    myEditor.updateCaretCursor();
    myEditor.recalculateSizeAndRepaint();
    myEditor.getGutterComponentEx().updateSize();
    myEditor.getGutterComponentEx().repaint();

    for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
      // There is a possible case that caret position is already visual position aware. But visual
      // position depends on number of folded
      // logical lines as well, hence, we can't be sure that target logical position defines correct
      // visual position because fold
      // regions have just changed. Hence, we use 'raw' logical position instead.
      LogicalPosition caretPosition = caret.getLogicalPosition().withoutVisualPositionInfo();
      int caretOffset = myEditor.logicalPositionToOffset(caretPosition);
      int selectionStart = caret.getSelectionStart();
      int selectionEnd = caret.getSelectionEnd();

      LogicalPosition positionToUse = null;
      int offsetToUse = -1;

      FoldRegion collapsed = myFoldTree.fetchOutermost(caretOffset);
      LogicalPosition savedPosition = caret.getUserData(SAVED_CARET_POSITION);
      if (savedPosition != null) {
        int savedOffset = myEditor.logicalPositionToOffset(savedPosition);
        FoldRegion collapsedAtSaved = myFoldTree.fetchOutermost(savedOffset);
        if (collapsedAtSaved == null) {
          positionToUse = savedPosition;
        } else {
          offsetToUse = collapsedAtSaved.getStartOffset();
        }
      }

      if (collapsed != null && positionToUse == null) {
        positionToUse = myEditor.offsetToLogicalPosition(collapsed.getStartOffset());
      }

      if (moveCaretFromCollapsedRegion && caret.isUpToDate()) {
        if (offsetToUse >= 0) {
          caret.moveToOffset(offsetToUse);
        } else if (positionToUse != null) {
          caret.moveToLogicalPosition(positionToUse);
        } else {
          caret.moveToLogicalPosition(caretPosition);
        }
      }

      caret.putUserData(SAVED_CARET_POSITION, savedPosition);

      if (isOffsetInsideCollapsedRegion(selectionStart)
          || isOffsetInsideCollapsedRegion(selectionEnd)) {
        caret.removeSelection();
      } else if (selectionStart < myEditor.getDocument().getTextLength()) {
        caret.setSelection(selectionStart, selectionEnd);
      }
    }

    if (mySavedCaretShift > 0) {
      final ScrollingModel scrollingModel = myEditor.getScrollingModel();
      scrollingModel.disableAnimation();
      scrollingModel.scrollVertically(
          myEditor.visibleLineToY(myEditor.getCaretModel().getVisualPosition().line)
              - mySavedCaretShift);
      scrollingModel.enableAnimation();
    }
  }

  @Override
  public void rebuild() {
    if (!myEditor.getDocument().isInBulkUpdate()) {
      myFoldTree.rebuild();
    }
  }

  private void updateCachedOffsets() {
    myFoldTree.updateCachedOffsets();
  }

  public int getFoldedLinesCountBefore(int offset) {
    if (!myDocumentChangeProcessed && myEditor.getDocument().isInEventsHandling()) {
      // There is a possible case that this method is called on document update before fold regions
      // are recalculated.
      // We return zero in such situations then.
      return 0;
    }
    return myFoldTree.getFoldedLinesCountBefore(offset);
  }

  @Override
  @Nullable
  public FoldRegion[] fetchTopLevel() {
    return myFoldTree.fetchTopLevel();
  }

  @Override
  @Nullable
  public FoldRegion fetchOutermost(int offset) {
    return myFoldTree.fetchOutermost(offset);
  }

  public FoldRegion[] fetchCollapsedAt(int offset) {
    return myFoldTree.fetchCollapsedAt(offset);
  }

  @Override
  public boolean intersectsRegion(int startOffset, int endOffset) {
    return myFoldTree.intersectsRegion(startOffset, endOffset);
  }

  public FoldRegion[] fetchVisible() {
    return myFoldTree.fetchVisible();
  }

  @Override
  public int getLastCollapsedRegionBefore(int offset) {
    return myFoldTree.getLastTopLevelIndexBefore(offset);
  }

  @Override
  public TextAttributes getPlaceholderAttributes() {
    return myFoldTextAttributes;
  }

  public void flushCaretPosition() {
    for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
      caret.putUserData(SAVED_CARET_POSITION, null);
    }
  }

  void onBulkDocumentUpdateStarted() {
    myFoldTree.clearCachedValues();
  }

  void onBulkDocumentUpdateFinished() {
    myFoldTree.rebuild();
  }

  @Override
  public void beforeDocumentChange(DocumentEvent event) {
    myDocumentChangeProcessed = false;
  }

  @Override
  public void documentChanged(DocumentEvent event) {
    try {
      if (!((DocumentEx) event.getDocument()).isInBulkUpdate()) {
        updateCachedOffsets();
      }
    } finally {
      myDocumentChangeProcessed = true;
    }
  }

  @Override
  public int getPriority() {
    return EditorDocumentPriorities.FOLD_MODEL;
  }

  @Override
  public FoldRegion createFoldRegion(
      int startOffset,
      int endOffset,
      @NotNull String placeholder,
      @Nullable FoldingGroup group,
      boolean neverExpands) {
    FoldRegionImpl region =
        new FoldRegionImpl(myEditor, startOffset, endOffset, placeholder, group, neverExpands);
    LOG.assertTrue(region.isValid());
    return region;
  }

  @Override
  public void addListener(
      @NotNull final FoldingListener listener, @NotNull Disposable parentDisposable) {
    myListeners.add(listener);
    Disposer.register(
        parentDisposable,
        new Disposable() {
          @Override
          public void dispose() {
            myListeners.remove(listener);
          }
        });
  }

  private void notifyListenersOnFoldRegionStateChange(@NotNull FoldRegion foldRegion) {
    for (FoldingListener listener : myListeners) {
      listener.onFoldRegionStateChange(foldRegion);
    }
  }

  @NotNull
  @Override
  public String dumpState() {
    return Arrays.toString(myFoldTree.fetchTopLevel());
  }

  @Override
  public String toString() {
    return dumpState();
  }
}
public class FindUsagesManager implements JDOMExternalizable {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.find.findParameterUsages.FindUsagesManager");

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

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

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

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

      final SearchData that = (SearchData) o;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return PsiUtilCore.toPsiElementArray(elements);
  }

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

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

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

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

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

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

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

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

    setOpenInNewTab(dialog.isShowInSeparateWindow());

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

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

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

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

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

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

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

    return indicator;
  }

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

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

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

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

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

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

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

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

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

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

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

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

    clearStatusBar();

    final FileEditorLocation currentLocation = fileEditor.getCurrentLocation();

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

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

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

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

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

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

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

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

    final FileSearchScope direction = dir;

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

            return true;
          }
        });

    fileEditor.putUserData(KEY_START_USAGE_AGAIN, null);

    return foundUsage.get();
  }

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

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

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

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

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

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

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

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

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

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

  @NotNull
  public static GlobalSearchScope getMaximalScope(@NotNull FindUsagesHandler handler) {
    PsiElement element = handler.getPsiElement();
    Project project = element.getProject();
    PsiFile file = element.getContainingFile();
    if (file != null
        && ProjectFileIndex.SERVICE
            .getInstance(project)
            .isInContent(file.getViewProvider().getVirtualFile())) {
      return GlobalSearchScope.projectScope(project);
    }
    return GlobalSearchScope.allScope(project);
  }
}
Exemple #29
0
/** @author nik */
public class XDebuggerTree extends DnDAwareTree implements DataProvider, Disposable {
  private final TransferToEDTQueue<Runnable> myLaterInvocator =
      TransferToEDTQueue.createRunnableMerger("XDebuggerTree later invocator", 100);

  private final ComponentListener myMoveListener =
      new ComponentAdapter() {
        @Override
        public void componentMoved(ComponentEvent e) {
          repaint(); // needed to repaint links in cell renderer on horizontal scrolling
        }
      };

  private static final DataKey<XDebuggerTree> XDEBUGGER_TREE_KEY = DataKey.create("xdebugger.tree");
  private final SingleAlarm myAlarm =
      new SingleAlarm(
          new Runnable() {
            @Override
            public void run() {
              final Editor editor =
                  FileEditorManager.getInstance(myProject).getSelectedTextEditor();
              if (editor != null) {
                editor.getContentComponent().revalidate();
                editor.getContentComponent().repaint();
              }
            }
          },
          100,
          this);

  private static final Convertor<TreePath, String> SPEED_SEARCH_CONVERTER =
      new Convertor<TreePath, String>() {
        @Override
        public String convert(TreePath o) {
          String text = null;
          if (o != null) {
            final Object node = o.getLastPathComponent();
            if (node instanceof XDebuggerTreeNode) {
              text = ((XDebuggerTreeNode) node).getText().toString();
            }
          }
          return StringUtil.notNullize(text);
        }
      };

  private static final TransferHandler DEFAULT_TRANSFER_HANDLER =
      new TransferHandler() {
        @Override
        protected Transferable createTransferable(JComponent c) {
          if (!(c instanceof XDebuggerTree)) {
            return null;
          }
          XDebuggerTree tree = (XDebuggerTree) c;
          //noinspection deprecation
          TreePath[] selectedPaths = tree.getSelectionPaths();
          if (selectedPaths == null || selectedPaths.length == 0) {
            return null;
          }

          StringBuilder plainBuf = new StringBuilder();
          StringBuilder htmlBuf = new StringBuilder();
          htmlBuf.append("<html>\n<body>\n<ul>\n");
          TextTransferable.ColoredStringBuilder coloredTextContainer =
              new TextTransferable.ColoredStringBuilder();
          for (TreePath path : selectedPaths) {
            htmlBuf.append("  <li>");
            Object node = path.getLastPathComponent();
            if (node != null) {
              if (node instanceof XDebuggerTreeNode) {
                ((XDebuggerTreeNode) node).appendToComponent(coloredTextContainer);
                coloredTextContainer.appendTo(plainBuf, htmlBuf);
              } else {
                String text = node.toString();
                plainBuf.append(text);
                htmlBuf.append(text);
              }
            }
            plainBuf.append('\n');
            htmlBuf.append("</li>\n");
          }

          // remove the last newline
          plainBuf.setLength(plainBuf.length() - 1);
          htmlBuf.append("</ul>\n</body>\n</html>");
          return new TextTransferable(htmlBuf.toString(), plainBuf.toString());
        }

        @Override
        public int getSourceActions(JComponent c) {
          return COPY;
        }
      };

  private final DefaultTreeModel myTreeModel;
  private final Project myProject;
  private final XDebuggerEditorsProvider myEditorsProvider;
  private XSourcePosition mySourcePosition;
  private final List<XDebuggerTreeListener> myListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private final XValueMarkers<?, ?> myValueMarkers;

  public XDebuggerTree(
      final @NotNull Project project,
      final @NotNull XDebuggerEditorsProvider editorsProvider,
      final @Nullable XSourcePosition sourcePosition,
      final @NotNull String popupActionGroupId,
      @Nullable XValueMarkers<?, ?> valueMarkers) {
    myValueMarkers = valueMarkers;
    myProject = project;
    myEditorsProvider = editorsProvider;
    mySourcePosition = sourcePosition;
    myTreeModel = new DefaultTreeModel(null);
    setModel(myTreeModel);
    setCellRenderer(new XDebuggerTreeRenderer());
    new TreeLinkMouseListener(new XDebuggerTreeRenderer()) {
      @Override
      protected boolean doCacheLastNode() {
        return false;
      }

      @Override
      protected void handleTagClick(@Nullable Object tag, @NotNull MouseEvent event) {
        if (tag instanceof XDebuggerTreeNodeHyperlink) {
          ((XDebuggerTreeNodeHyperlink) tag).onClick(event);
        }
      }
    }.installOn(this);
    setRootVisible(false);
    setShowsRootHandles(true);

    new DoubleClickListener() {
      @Override
      protected boolean onDoubleClick(MouseEvent e) {
        return expandIfEllipsis();
      }
    }.installOn(this);

    addKeyListener(
        new KeyAdapter() {
          @Override
          public void keyPressed(KeyEvent e) {
            int key = e.getKeyCode();
            if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT) {
              expandIfEllipsis();
            }
          }
        });

    if (Boolean.valueOf(System.getProperty("xdebugger.variablesView.rss"))) {
      new XDebuggerTreeSpeedSearch(this, SPEED_SEARCH_CONVERTER);
    } else {
      new TreeSpeedSearch(this, SPEED_SEARCH_CONVERTER);
    }

    final ActionManager actionManager = ActionManager.getInstance();
    addMouseListener(
        new PopupHandler() {
          @Override
          public void invokePopup(final Component comp, final int x, final int y) {
            ActionGroup group = (ActionGroup) actionManager.getAction(popupActionGroupId);
            actionManager
                .createActionPopupMenu(ActionPlaces.UNKNOWN, group)
                .getComponent()
                .show(comp, x, y);
          }
        });
    registerShortcuts();

    setTransferHandler(DEFAULT_TRANSFER_HANDLER);

    addComponentListener(myMoveListener);
  }

  public void updateEditor() {
    myAlarm.cancelAndRequest();
  }

  public boolean isUnderRemoteDebug() {
    // FIXME [VISTALL] we can access to RemoteRunProfile from platform - java specific
    // DataContext context = DataManager.getInstance().getDataContext(this);
    // ExecutionEnvironment env = LangDataKeys.EXECUTION_ENVIRONMENT.getData(context);
    // if (env != null && env.getRunProfile() instanceof RemoteRunProfile) {
    //  return true;
    // }
    return false;
  }

  private boolean expandIfEllipsis() {
    MessageTreeNode[] treeNodes = getSelectedNodes(MessageTreeNode.class, null);
    if (treeNodes.length == 1) {
      MessageTreeNode node = treeNodes[0];
      if (node.isEllipsis()) {
        TreeNode parent = node.getParent();
        if (parent instanceof XValueContainerNode) {
          ((XValueContainerNode) parent).startComputingChildren();
          return true;
        }
      }
    }
    return false;
  }

  public void addTreeListener(@NotNull XDebuggerTreeListener listener) {
    myListeners.add(listener);
  }

  public void removeTreeListener(@NotNull XDebuggerTreeListener listener) {
    myListeners.remove(listener);
  }

  public void setRoot(XDebuggerTreeNode root, final boolean rootVisible) {
    setRootVisible(rootVisible);
    myTreeModel.setRoot(root);
  }

  public XDebuggerTreeNode getRoot() {
    return (XDebuggerTreeNode) myTreeModel.getRoot();
  }

  @Nullable
  public XSourcePosition getSourcePosition() {
    return mySourcePosition;
  }

  public void setSourcePosition(final @Nullable XSourcePosition sourcePosition) {
    mySourcePosition = sourcePosition;
  }

  @NotNull
  public XDebuggerEditorsProvider getEditorsProvider() {
    return myEditorsProvider;
  }

  @NotNull
  public Project getProject() {
    return myProject;
  }

  @Nullable
  public XValueMarkers<?, ?> getValueMarkers() {
    return myValueMarkers;
  }

  public DefaultTreeModel getTreeModel() {
    return myTreeModel;
  }

  @Override
  @Nullable
  public Object getData(@NonNls final String dataId) {
    if (XDEBUGGER_TREE_KEY.is(dataId)) {
      return this;
    }
    if (PlatformDataKeys.PREDEFINED_TEXT.is(dataId)) {
      XValueNodeImpl[] selectedNodes = getSelectedNodes(XValueNodeImpl.class, null);
      if (selectedNodes.length == 1 && selectedNodes[0].getFullValueEvaluator() == null) {
        return DebuggerUIUtil.getNodeRawValue(selectedNodes[0]);
      }
    }
    return null;
  }

  public void rebuildAndRestore(final XDebuggerTreeState treeState) {
    Object rootNode = myTreeModel.getRoot();
    if (rootNode instanceof XDebuggerTreeNode) {
      ((XDebuggerTreeNode) rootNode).clearChildren();
      treeState.restoreState(this);
      repaint();
    }
  }

  public void childrenLoaded(
      final @NotNull XDebuggerTreeNode node,
      final @NotNull List<XValueContainerNode<?>> children,
      final boolean last) {
    for (XDebuggerTreeListener listener : myListeners) {
      listener.childrenLoaded(node, children, last);
    }
  }

  public void nodeLoaded(final @NotNull RestorableStateNode node, final @NotNull String name) {
    for (XDebuggerTreeListener listener : myListeners) {
      listener.nodeLoaded(node, name);
    }
  }

  public void markNodesObsolete() {
    Object root = myTreeModel.getRoot();
    if (root instanceof XValueContainerNode<?>) {
      markNodesObsolete((XValueContainerNode<?>) root);
    }
  }

  @Override
  public void dispose() {
    ActionManager actionManager = ActionManager.getInstance();
    actionManager.getAction(XDebuggerActions.SET_VALUE).unregisterCustomShortcutSet(this);
    actionManager.getAction(XDebuggerActions.COPY_VALUE).unregisterCustomShortcutSet(this);
    actionManager.getAction(XDebuggerActions.JUMP_TO_SOURCE).unregisterCustomShortcutSet(this);
    actionManager.getAction(XDebuggerActions.JUMP_TO_TYPE_SOURCE).unregisterCustomShortcutSet(this);
    actionManager.getAction(XDebuggerActions.MARK_OBJECT).unregisterCustomShortcutSet(this);

    // clear all possible inner fields that may still have links to debugger objects
    myTreeModel.setRoot(null);
    setCellRenderer(null);
    UIUtil.dispose(this);
    setLeadSelectionPath(null);
    setAnchorSelectionPath(null);
    removeComponentListener(myMoveListener);
  }

  private void registerShortcuts() {
    ActionManager actionManager = ActionManager.getInstance();
    actionManager
        .getAction(XDebuggerActions.SET_VALUE)
        .registerCustomShortcutSet(
            new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)), this);
    actionManager
        .getAction(XDebuggerActions.COPY_VALUE)
        .registerCustomShortcutSet(CommonShortcuts.getCopy(), this);
    actionManager
        .getAction(XDebuggerActions.JUMP_TO_SOURCE)
        .registerCustomShortcutSet(CommonShortcuts.getEditSource(), this);
    Shortcut[] editTypeShortcuts =
        KeymapManager.getInstance()
            .getActiveKeymap()
            .getShortcuts(XDebuggerActions.EDIT_TYPE_SOURCE);
    actionManager
        .getAction(XDebuggerActions.JUMP_TO_TYPE_SOURCE)
        .registerCustomShortcutSet(new CustomShortcutSet(editTypeShortcuts), this);
    actionManager
        .getAction(XDebuggerActions.MARK_OBJECT)
        .registerCustomShortcutSet(
            new CustomShortcutSet(
                KeymapManager.getInstance().getActiveKeymap().getShortcuts("ToggleBookmark")),
            this);
  }

  private static void markNodesObsolete(final XValueContainerNode<?> node) {
    node.setObsolete();
    List<? extends XValueContainerNode<?>> loadedChildren = node.getLoadedChildren();
    if (loadedChildren != null) {
      for (XValueContainerNode<?> child : loadedChildren) {
        markNodesObsolete(child);
      }
    }
  }

  @Nullable
  public static XDebuggerTree getTree(final AnActionEvent e) {
    return e.getData(XDEBUGGER_TREE_KEY);
  }

  @Nullable
  public static XDebuggerTree getTree(DataContext context) {
    return XDEBUGGER_TREE_KEY.getData(context);
  }

  public TransferToEDTQueue<Runnable> getLaterInvocator() {
    return myLaterInvocator;
  }

  public void selectNodeOnLoad(final Condition<TreeNode> nodeFilter) {
    addTreeListener(
        new XDebuggerTreeListener() {
          @Override
          public void nodeLoaded(@NotNull RestorableStateNode node, String name) {
            if (nodeFilter.value(node)) {
              setSelectionPath(node.getPath());
            }
          }

          @Override
          public void childrenLoaded(
              @NotNull XDebuggerTreeNode node,
              @NotNull List<XValueContainerNode<?>> children,
              boolean last) {}
        });
  }

  public void expandNodesOnLoad(final Condition<TreeNode> nodeFilter) {
    addTreeListener(
        new XDebuggerTreeListener() {
          @Override
          public void nodeLoaded(@NotNull RestorableStateNode node, String name) {
            if (nodeFilter.value(node) && !node.isLeaf()) {
              // cause children computing
              node.getChildCount();
            }
          }

          @Override
          public void childrenLoaded(
              @NotNull XDebuggerTreeNode node,
              @NotNull List<XValueContainerNode<?>> children,
              boolean last) {
            if (nodeFilter.value(node)) {
              expandPath(node.getPath());
            }
          }
        });
  }
}
/** @author Eugene.Kudelevsky */
public class MyDeviceChooser implements Disposable {
  private static final String[] COLUMN_TITLES =
      new String[] {"Device", "Serial Number", "State", "Compatible"};
  private static final int DEVICE_NAME_COLUMN_INDEX = 0;
  private static final int SERIAL_COLUMN_INDEX = 1;
  private static final int DEVICE_STATE_COLUMN_INDEX = 2;
  private static final int COMPATIBILITY_COLUMN_INDEX = 3;
  private static final int REFRESH_INTERVAL_MS = 500;

  public static final IDevice[] EMPTY_DEVICE_ARRAY = new IDevice[0];

  private final List<DeviceChooserListener> myListeners =
      ContainerUtil.createLockFreeCopyOnWriteList();
  private final Alarm myRefreshingAlarm;
  private final AndroidDebugBridge myBridge;

  private volatile boolean myProcessSelectionFlag = true;

  /** The current list of devices that is displayed in the table. */
  private IDevice[] myDisplayedDevices = EMPTY_DEVICE_ARRAY;

  /**
   * The current list of devices obtained from the debug bridge. This is updated in a background
   * thread. If it is different than {@link #myDisplayedDevices}, then a {@link #refreshTable}
   * invocation in the EDT thread will update the displayed list to match the detected list.
   */
  private AtomicReference<IDevice[]> myDetectedDevicesRef =
      new AtomicReference<IDevice[]>(EMPTY_DEVICE_ARRAY);

  private JComponent myPanel;
  private JBTable myDeviceTable;

  private final AndroidFacet myFacet;
  private final Condition<IDevice> myFilter;
  private final AndroidVersion myMinSdkVersion;
  private final IAndroidTarget myProjectTarget;
  private final EnumSet<IDevice.HardwareFeature> myRequiredHardwareFeatures;

  private int[] mySelectedRows;
  private boolean hadUserInteraction = false;
  @Nullable private String[] previouslySelectedSerials;

  public MyDeviceChooser(
      boolean multipleSelection,
      @NotNull final Action okAction,
      @NotNull AndroidFacet facet,
      @NotNull IAndroidTarget projectTarget,
      @Nullable Condition<IDevice> filter) {

    myFacet = facet;
    myFilter = filter;
    myMinSdkVersion = AndroidModuleInfo.get(facet).getRuntimeMinSdkVersion();
    myProjectTarget = projectTarget;
    if (new IsWatchFeatureRequiredCompat(facet).get()) {
      myRequiredHardwareFeatures = EnumSet.of(IDevice.HardwareFeature.WATCH);
    } else {
      myRequiredHardwareFeatures = EnumSet.noneOf(IDevice.HardwareFeature.class);
    }

    myDeviceTable = new JBTable();
    myPanel = ScrollPaneFactory.createScrollPane(myDeviceTable);
    myPanel.setPreferredSize(new Dimension(450, 220));

    myDeviceTable.setModel(new MyDeviceTableModel(EMPTY_DEVICE_ARRAY));
    myDeviceTable.setSelectionMode(
        multipleSelection
            ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
            : ListSelectionModel.SINGLE_SELECTION);
    myDeviceTable
        .getSelectionModel()
        .addListSelectionListener(
            new ListSelectionListener() {
              @Override
              public void valueChanged(ListSelectionEvent e) {
                if (myProcessSelectionFlag) {
                  hadUserInteraction = true;
                  fireSelectedDevicesChanged();
                }
              }
            });
    new DoubleClickListener() {
      @Override
      protected boolean onDoubleClick(MouseEvent e) {
        if (myDeviceTable.isEnabled() && okAction.isEnabled()) {
          okAction.actionPerformed(null);
          return true;
        }
        return false;
      }
    }.installOn(myDeviceTable);

    myDeviceTable.setDefaultRenderer(LaunchCompatibility.class, new LaunchCompatibilityRenderer());
    myDeviceTable.setDefaultRenderer(
        IDevice.class, new DeviceRenderer.DeviceNameRenderer(facet.getAvdManagerSilently()));
    myDeviceTable.addKeyListener(
        new KeyAdapter() {
          @Override
          public void keyPressed(KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_ENTER && okAction.isEnabled()) {
              okAction.actionPerformed(null);
            }
          }
        });

    setColumnWidth(
        myDeviceTable, DEVICE_NAME_COLUMN_INDEX, "Samsung Galaxy Nexus Android 4.1 (API 17)");
    setColumnWidth(myDeviceTable, SERIAL_COLUMN_INDEX, "0000-0000-00000");
    setColumnWidth(myDeviceTable, DEVICE_STATE_COLUMN_INDEX, "offline");
    setColumnWidth(myDeviceTable, COMPATIBILITY_COLUMN_INDEX, "yes");

    // Do not recreate columns on every model update - this should help maintain the column sizes
    // set above
    myDeviceTable.setAutoCreateColumnsFromModel(false);

    // Allow sorting by columns (in lexicographic order)
    myDeviceTable.setAutoCreateRowSorter(true);

    myRefreshingAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
    myBridge = AndroidSdkUtils.getDebugBridge(myFacet.getModule().getProject());
  }

  private static EnumSet<IDevice.HardwareFeature> getRequiredHardwareFeatures(
      List<UsesFeature> requiredFeatures) {
    // Currently, this method is hardcoded to only search if the list of required features includes
    // a watch.
    // We may not want to search the device for every possible feature, but only a small subset of
    // important
    // features, starting with hardware type watch..

    for (UsesFeature feature : requiredFeatures) {
      AndroidAttributeValue<String> name = feature.getName();
      if (name != null && UsesFeature.HARDWARE_TYPE_WATCH.equals(name.getStringValue())) {
        return EnumSet.of(IDevice.HardwareFeature.WATCH);
      }
    }

    return EnumSet.noneOf(IDevice.HardwareFeature.class);
  }

  private void setColumnWidth(JBTable deviceTable, int columnIndex, String sampleText) {
    int width = getWidth(deviceTable, sampleText);
    deviceTable.getColumnModel().getColumn(columnIndex).setPreferredWidth(width);
  }

  private int getWidth(JBTable deviceTable, String sampleText) {
    FontMetrics metrics = deviceTable.getFontMetrics(deviceTable.getFont());
    return metrics.stringWidth(sampleText);
  }

  public void init(@Nullable String[] selectedSerials) {
    previouslySelectedSerials = selectedSerials;
    updateTable();
    addUpdatingRequest();
  }

  public void init(List<String> selectedSerials) {
    init(selectedSerials.stream().toArray(String[]::new));
  }

  private void updatePreviouslySelectedSerials() {
    if (previouslySelectedSerials != null && !hadUserInteraction) {
      resetSelection(previouslySelectedSerials);
    }
  }

  private final Runnable myUpdateRequest =
      new Runnable() {
        @Override
        public void run() {
          updateTable();
          addUpdatingRequest();
        }
      };

  private void addUpdatingRequest() {
    if (myRefreshingAlarm.isDisposed()) {
      return;
    }
    myRefreshingAlarm.cancelAllRequests();
    myRefreshingAlarm.addRequest(myUpdateRequest, REFRESH_INTERVAL_MS);
  }

  private void resetSelection(@NotNull String[] selectedSerials) {
    MyDeviceTableModel model = (MyDeviceTableModel) myDeviceTable.getModel();
    Set<String> selectedSerialsSet = new HashSet<String>();
    Collections.addAll(selectedSerialsSet, selectedSerials);
    IDevice[] myDevices = model.myDevices;
    ListSelectionModel selectionModel = myDeviceTable.getSelectionModel();
    boolean cleared = false;

    for (int i = 0, n = myDevices.length; i < n; i++) {
      String serialNumber = myDevices[i].getSerialNumber();
      if (selectedSerialsSet.contains(serialNumber)) {
        if (!cleared) {
          selectionModel.clearSelection();
          cleared = true;
        }
        selectionModel.addSelectionInterval(i, i);
      }
    }
  }

  void updateTable() {
    IDevice[] devices = myBridge != null ? getFilteredDevices(myBridge) : EMPTY_DEVICE_ARRAY;
    if (devices.length > 1) {
      // sort by API level
      Arrays.sort(
          devices,
          new Comparator<IDevice>() {
            @Override
            public int compare(IDevice device1, IDevice device2) {
              int apiLevel1 = safeGetApiLevel(device1);
              int apiLevel2 = safeGetApiLevel(device2);
              return apiLevel2 - apiLevel1;
            }

            private int safeGetApiLevel(IDevice device) {
              try {
                String s = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
                return StringUtil.isNotEmpty(s) ? Integer.parseInt(s) : 0;
              } catch (Exception e) {
                return 0;
              }
            }
          });
    }

    if (!Arrays.equals(myDisplayedDevices, devices)) {
      myDetectedDevicesRef.set(devices);
      ApplicationManager.getApplication()
          .invokeLater(
              new Runnable() {
                @Override
                public void run() {
                  refreshTable();
                }
              },
              ModalityState.stateForComponent(myDeviceTable));
    }
  }

  private void refreshTable() {
    IDevice[] devices = myDetectedDevicesRef.get();
    myDisplayedDevices = devices;

    final IDevice[] selectedDevices = getSelectedDevices();
    final TIntArrayList selectedRows = new TIntArrayList();
    for (int i = 0; i < devices.length; i++) {
      if (ArrayUtil.indexOf(selectedDevices, devices[i]) >= 0) {
        selectedRows.add(i);
      }
    }

    myProcessSelectionFlag = false;
    myDeviceTable.setModel(new MyDeviceTableModel(devices));
    if (selectedRows.size() == 0 && devices.length > 0) {
      myDeviceTable.getSelectionModel().setSelectionInterval(0, 0);
    }
    for (int selectedRow : selectedRows.toNativeArray()) {
      if (selectedRow < devices.length) {
        myDeviceTable.getSelectionModel().addSelectionInterval(selectedRow, selectedRow);
      }
    }
    fireSelectedDevicesChanged();
    myProcessSelectionFlag = true;
    updatePreviouslySelectedSerials();
  }

  public boolean hasDevices() {
    return myDetectedDevicesRef.get().length > 0;
  }

  public JComponent getPreferredFocusComponent() {
    return myDeviceTable;
  }

  @Nullable
  public JComponent getPanel() {
    return myPanel;
  }

  @NotNull
  public IDevice[] getSelectedDevices() {
    int[] rows = mySelectedRows != null ? mySelectedRows : myDeviceTable.getSelectedRows();
    List<IDevice> result = new ArrayList<IDevice>();
    for (int row : rows) {
      if (row >= 0) {
        Object serial = myDeviceTable.getValueAt(row, SERIAL_COLUMN_INDEX);
        final AndroidDebugBridge bridge =
            AndroidSdkUtils.getDebugBridge(myFacet.getModule().getProject());
        if (bridge == null) {
          return EMPTY_DEVICE_ARRAY;
        }
        IDevice[] devices = getFilteredDevices(bridge);
        for (IDevice device : devices) {
          if (device.getSerialNumber().equals(serial.toString())) {
            result.add(device);
            break;
          }
        }
      }
    }
    return result.toArray(new IDevice[result.size()]);
  }

  @NotNull
  private IDevice[] getFilteredDevices(AndroidDebugBridge bridge) {
    final List<IDevice> filteredDevices = new ArrayList<IDevice>();
    for (IDevice device : bridge.getDevices()) {
      if (myFilter == null || myFilter.value(device)) {
        filteredDevices.add(device);
      }
    }
    // Do not filter launching cloud devices as they are just unselectable progress markers
    // that are replaced with the actual cloud devices as soon as they are up and the actual cloud
    // devices will be filtered above.
    return filteredDevices.toArray(new IDevice[filteredDevices.size()]);
  }

  public void finish() {
    mySelectedRows = myDeviceTable.getSelectedRows();
  }

  @Override
  public void dispose() {}

  public void setEnabled(boolean enabled) {
    myDeviceTable.setEnabled(enabled);
  }

  @NotNull
  private static String getDeviceState(@NotNull IDevice device) {
    IDevice.DeviceState state = device.getState();
    return state != null ? capitalize(state.name().toLowerCase()) : "";
  }

  public void fireSelectedDevicesChanged() {
    for (DeviceChooserListener listener : myListeners) {
      listener.selectedDevicesChanged();
    }
  }

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

  private class MyDeviceTableModel extends AbstractTableModel {
    private final IDevice[] myDevices;

    public MyDeviceTableModel(IDevice[] devices) {
      myDevices = devices;
    }

    @Override
    public String getColumnName(int column) {
      return COLUMN_TITLES[column];
    }

    @Override
    public int getRowCount() {
      return myDevices.length;
    }

    @Override
    public int getColumnCount() {
      return COLUMN_TITLES.length;
    }

    @Override
    @Nullable
    public Object getValueAt(int rowIndex, int columnIndex) {
      if (rowIndex >= myDevices.length) {
        return null;
      }
      IDevice device = myDevices[rowIndex];
      switch (columnIndex) {
        case DEVICE_NAME_COLUMN_INDEX:
          return device;
        case SERIAL_COLUMN_INDEX:
          return device.getSerialNumber();
        case DEVICE_STATE_COLUMN_INDEX:
          return getDeviceState(device);
        case COMPATIBILITY_COLUMN_INDEX:
          return new CanRunOnDeviceCompat(
                  myFacet, myMinSdkVersion, myProjectTarget, myRequiredHardwareFeatures, device)
              .get();
      }
      return null;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
      if (columnIndex == COMPATIBILITY_COLUMN_INDEX) {
        return LaunchCompatibility.class;
      } else if (columnIndex == DEVICE_NAME_COLUMN_INDEX) {
        return IDevice.class;
      } else {
        return String.class;
      }
    }
  }

  private static class LaunchCompatibilityRenderer extends ColoredTableCellRenderer {
    @Override
    protected void customizeCellRenderer(
        JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
      if (!(value instanceof LaunchCompatibility)) {
        return;
      }

      LaunchCompatibility compatibility = (LaunchCompatibility) value;
      ThreeState compatible = compatibility.isCompatible();
      if (compatible == ThreeState.YES) {
        append("Yes");
      } else {
        if (compatible == ThreeState.NO) {
          append("No", SimpleTextAttributes.ERROR_ATTRIBUTES);
        } else {
          append("Maybe");
        }
        String reason = compatibility.getReason();
        if (reason != null) {
          append(", ");
          append(reason);
        }
      }
    }
  }
}