private static final class CompileStatistics { private static final Key<CompileStatistics> KEY = Key.create("_Compile_Statistics_"); private int myClassesCount; private int myFilesCount; public int getClassesCount() { return myClassesCount; } public int incClassesCount() { return ++myClassesCount; } public int getFilesCount() { return myFilesCount; } public int incFilesCount() { return ++myFilesCount; } }
/** @author peter */ @SuppressWarnings("UseOfSystemOutOrSystemErr") public abstract class UsefulTestCase extends TestCase { public static final boolean IS_UNDER_TEAMCITY = System.getenv("TEAMCITY_VERSION") != null; public static final String IDEA_MARKER_CLASS = "com.intellij.openapi.components.impl.stores.IdeaProjectStoreImpl"; public static final String TEMP_DIR_MARKER = "unitTest_"; protected static boolean OVERWRITE_TESTDATA = false; private static final String DEFAULT_SETTINGS_EXTERNALIZED; private static final Random RNG = new SecureRandom(); private static final String ORIGINAL_TEMP_DIR = FileUtil.getTempDirectory(); protected final Disposable myTestRootDisposable = new Disposable() { @Override public void dispose() {} @Override public String toString() { String testName = getTestName(false); return UsefulTestCase.this.getClass() + (StringUtil.isEmpty(testName) ? "" : ".test" + testName); } }; protected static String ourPathToKeep = null; private CodeStyleSettings myOldCodeStyleSettings; private String myTempDir; protected static final Key<String> CREATION_PLACE = Key.create("CREATION_PLACE"); static { // Radar #5755208: Command line Java applications need a way to launch without a Dock icon. System.setProperty("apple.awt.UIElement", "true"); try { CodeInsightSettings defaultSettings = new CodeInsightSettings(); Element oldS = new Element("temp"); defaultSettings.writeExternal(oldS); DEFAULT_SETTINGS_EXTERNALIZED = JDOMUtil.writeElement(oldS, "\n"); } catch (Exception e) { throw new RuntimeException(e); } } protected boolean shouldContainTempFiles() { return true; } @Override protected void setUp() throws Exception { super.setUp(); if (shouldContainTempFiles()) { String testName = getTestName(true); if (StringUtil.isEmptyOrSpaces(testName)) testName = ""; testName = new File(testName).getName(); // in case the test name contains file separators myTempDir = FileUtil.toSystemDependentName( ORIGINAL_TEMP_DIR + "/" + TEMP_DIR_MARKER + testName + "_" + RNG.nextInt(1000)); FileUtil.resetCanonicalTempPathCache(myTempDir); } //noinspection AssignmentToStaticFieldFromInstanceMethod DocumentImpl.CHECK_DOCUMENT_CONSISTENCY = !isPerformanceTest(); } @Override protected void tearDown() throws Exception { try { Disposer.dispose(myTestRootDisposable); cleanupSwingDataStructures(); cleanupDeleteOnExitHookList(); } finally { if (shouldContainTempFiles()) { FileUtil.resetCanonicalTempPathCache(ORIGINAL_TEMP_DIR); if (ourPathToKeep != null && FileUtil.isAncestor(myTempDir, ourPathToKeep, false)) { File[] files = new File(myTempDir).listFiles(); if (files != null) { for (File file : files) { if (!FileUtil.pathsEqual(file.getPath(), ourPathToKeep)) { FileUtil.delete(file); } } } } else { FileUtil.delete(new File(myTempDir)); } } } UIUtil.removeLeakingAppleListeners(); super.tearDown(); } private static final Set<String> DELETE_ON_EXIT_HOOK_DOT_FILES; private static final Class DELETE_ON_EXIT_HOOK_CLASS; static { Class<?> aClass = null; Set<String> files = null; try { aClass = Class.forName("java.io.DeleteOnExitHook"); files = ReflectionUtil.getField(aClass, null, Set.class, "files"); } catch (Exception ignored) { } DELETE_ON_EXIT_HOOK_CLASS = aClass; DELETE_ON_EXIT_HOOK_DOT_FILES = files; } public static void cleanupDeleteOnExitHookList() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { // try to reduce file set retained by java.io.DeleteOnExitHook List<String> list; synchronized (DELETE_ON_EXIT_HOOK_CLASS) { if (DELETE_ON_EXIT_HOOK_DOT_FILES.isEmpty()) return; list = new ArrayList<String>(DELETE_ON_EXIT_HOOK_DOT_FILES); } for (int i = list.size() - 1; i >= 0; i--) { String path = list.get(i); if (FileSystemUtil.getAttributes(path) == null || new File(path).delete()) { synchronized (DELETE_ON_EXIT_HOOK_CLASS) { DELETE_ON_EXIT_HOOK_DOT_FILES.remove(path); } } } } private static void cleanupSwingDataStructures() throws Exception { Class<?> aClass = Class.forName("javax.swing.KeyboardManager"); Method get = aClass.getMethod("getCurrentManager"); get.setAccessible(true); Object manager = get.invoke(null); { Field mapF = aClass.getDeclaredField("componentKeyStrokeMap"); mapF.setAccessible(true); Object map = mapF.get(manager); ((Map) map).clear(); } { Field mapF = aClass.getDeclaredField("containerMap"); mapF.setAccessible(true); Object map = mapF.get(manager); ((Map) map).clear(); } } protected CompositeException checkForSettingsDamage() throws Exception { Application app = ApplicationManager.getApplication(); if (isPerformanceTest() || app == null || app instanceof MockApplication) { return new CompositeException(); } CodeStyleSettings oldCodeStyleSettings = myOldCodeStyleSettings; myOldCodeStyleSettings = null; return doCheckForSettingsDamage(oldCodeStyleSettings, getCurrentCodeStyleSettings()); } public static CompositeException doCheckForSettingsDamage( @NotNull CodeStyleSettings oldCodeStyleSettings, @NotNull CodeStyleSettings currentCodeStyleSettings) throws Exception { CompositeException result = new CompositeException(); final CodeInsightSettings settings = CodeInsightSettings.getInstance(); try { Element newS = new Element("temp"); settings.writeExternal(newS); Assert.assertEquals( "Code insight settings damaged", DEFAULT_SETTINGS_EXTERNALIZED, JDOMUtil.writeElement(newS, "\n")); } catch (AssertionError error) { CodeInsightSettings clean = new CodeInsightSettings(); Element temp = new Element("temp"); clean.writeExternal(temp); settings.loadState(temp); result.add(error); } currentCodeStyleSettings.getIndentOptions(StdFileTypes.JAVA); try { checkSettingsEqual( oldCodeStyleSettings, currentCodeStyleSettings, "Code style settings damaged"); } catch (AssertionError e) { result.add(e); } finally { currentCodeStyleSettings.clearCodeStyleSettings(); } try { InplaceRefactoring.checkCleared(); } catch (AssertionError e) { result.add(e); } try { StartMarkAction.checkCleared(); } catch (AssertionError e) { result.add(e); } return result; } protected void storeSettings() { if (!isPerformanceTest() && ApplicationManager.getApplication() != null) { myOldCodeStyleSettings = getCurrentCodeStyleSettings().clone(); myOldCodeStyleSettings.getIndentOptions(StdFileTypes.JAVA); } } protected CodeStyleSettings getCurrentCodeStyleSettings() { if (CodeStyleSchemes.getInstance().getCurrentScheme() == null) return new CodeStyleSettings(); return CodeStyleSettingsManager.getInstance().getCurrentSettings(); } public Disposable getTestRootDisposable() { return myTestRootDisposable; } @Override protected void runTest() throws Throwable { final Throwable[] throwables = new Throwable[1]; Runnable runnable = new Runnable() { @Override public void run() { try { UsefulTestCase.super.runTest(); } catch (InvocationTargetException e) { e.fillInStackTrace(); throwables[0] = e.getTargetException(); } catch (IllegalAccessException e) { e.fillInStackTrace(); throwables[0] = e; } catch (Throwable e) { throwables[0] = e; } } }; invokeTestRunnable(runnable); if (throwables[0] != null) { throw throwables[0]; } } protected boolean shouldRunTest() { return PlatformTestUtil.canRunTest(getClass()); } public static void edt(Runnable r) { UIUtil.invokeAndWaitIfNeeded(r); } protected void invokeTestRunnable(@NotNull Runnable runnable) throws Exception { UIUtil.invokeAndWaitIfNeeded(runnable); // runnable.run(); } protected void defaultRunBare() throws Throwable { super.runBare(); } @Override public void runBare() throws Throwable { if (!shouldRunTest()) return; if (runInDispatchThread()) { final Throwable[] exception = {null}; UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { try { defaultRunBare(); } catch (Throwable tearingDown) { if (exception[0] == null) exception[0] = tearingDown; } } }); if (exception[0] != null) throw exception[0]; } else { defaultRunBare(); } } protected boolean runInDispatchThread() { return true; } @NonNls public static String toString(Iterable<?> collection) { if (!collection.iterator().hasNext()) { return "<empty>"; } final StringBuilder builder = new StringBuilder(); for (final Object o : collection) { if (o instanceof THashSet) { builder.append(new TreeSet<Object>((THashSet) o)); } else { builder.append(o); } builder.append("\n"); } return builder.toString(); } public static <T> void assertOrderedEquals(T[] actual, T... expected) { assertOrderedEquals(Arrays.asList(actual), expected); } public static <T> void assertOrderedEquals(Iterable<T> actual, T... expected) { assertOrderedEquals(null, actual, expected); } public static void assertOrderedEquals(@NotNull byte[] actual, @NotNull byte[] expected) { assertEquals(actual.length, expected.length); for (int i = 0; i < actual.length; i++) { byte a = actual[i]; byte e = expected[i]; assertEquals("not equals at index: " + i, e, a); } } public static <T> void assertOrderedEquals( final String errorMsg, @NotNull Iterable<T> actual, @NotNull T... expected) { Assert.assertNotNull(actual); Assert.assertNotNull(expected); assertOrderedEquals(errorMsg, actual, Arrays.asList(expected)); } public static <T> void assertOrderedEquals( final Iterable<? extends T> actual, final Collection<? extends T> expected) { assertOrderedEquals(null, actual, expected); } public static <T> void assertOrderedEquals( final String erroMsg, final Iterable<? extends T> actual, final Collection<? extends T> expected) { ArrayList<T> list = new ArrayList<T>(); for (T t : actual) { list.add(t); } if (!list.equals(new ArrayList<T>(expected))) { String expectedString = toString(expected); String actualString = toString(actual); Assert.assertEquals(erroMsg, expectedString, actualString); Assert.fail( "Warning! 'toString' does not reflect the difference.\nExpected: " + expectedString + "\nActual: " + actualString); } } public static <T> void assertOrderedCollection(T[] collection, @NotNull Consumer<T>... checkers) { Assert.assertNotNull(collection); assertOrderedCollection(Arrays.asList(collection), checkers); } public static <T> void assertSameElements(T[] collection, T... expected) { assertSameElements(Arrays.asList(collection), expected); } public static <T> void assertSameElements(Collection<? extends T> collection, T... expected) { assertSameElements(collection, Arrays.asList(expected)); } public static <T> void assertSameElements( Collection<? extends T> collection, Collection<T> expected) { assertSameElements(null, collection, expected); } public static <T> void assertSameElements( String message, Collection<? extends T> collection, Collection<T> expected) { assertNotNull(collection); assertNotNull(expected); if (collection.size() != expected.size() || !new HashSet<T>(expected).equals(new HashSet<T>(collection))) { Assert.assertEquals(message, toString(expected, "\n"), toString(collection, "\n")); Assert.assertEquals(message, new HashSet<T>(expected), new HashSet<T>(collection)); } } public <T> void assertContainsOrdered(Collection<? extends T> collection, T... expected) { assertContainsOrdered(collection, Arrays.asList(expected)); } public <T> void assertContainsOrdered( Collection<? extends T> collection, Collection<T> expected) { ArrayList<T> copy = new ArrayList<T>(collection); copy.retainAll(expected); assertOrderedEquals(toString(collection), copy, expected); } public <T> void assertContainsElements(Collection<? extends T> collection, T... expected) { assertContainsElements(collection, Arrays.asList(expected)); } public <T> void assertContainsElements( Collection<? extends T> collection, Collection<T> expected) { ArrayList<T> copy = new ArrayList<T>(collection); copy.retainAll(expected); assertSameElements(toString(collection), copy, expected); } public static String toString(Object[] collection, String separator) { return toString(Arrays.asList(collection), separator); } public <T> void assertDoesntContain(Collection<? extends T> collection, T... notExpected) { assertDoesntContain(collection, Arrays.asList(notExpected)); } public <T> void assertDoesntContain( Collection<? extends T> collection, Collection<T> notExpected) { ArrayList<T> expected = new ArrayList<T>(collection); expected.removeAll(notExpected); assertSameElements(collection, expected); } public static String toString(Collection<?> collection, String separator) { List<String> list = ContainerUtil.map2List( collection, new Function<Object, String>() { @Override public String fun(final Object o) { return String.valueOf(o); } }); Collections.sort(list); StringBuilder builder = new StringBuilder(); boolean flag = false; for (final String o : list) { if (flag) { builder.append(separator); } builder.append(o); flag = true; } return builder.toString(); } public static <T> void assertOrderedCollection( Collection<? extends T> collection, Consumer<T>... checkers) { Assert.assertNotNull(collection); if (collection.size() != checkers.length) { Assert.fail(toString(collection)); } int i = 0; for (final T actual : collection) { try { checkers[i].consume(actual); } catch (AssertionFailedError e) { System.out.println(i + ": " + actual); throw e; } i++; } } public static <T> void assertUnorderedCollection(T[] collection, Consumer<T>... checkers) { assertUnorderedCollection(Arrays.asList(collection), checkers); } public static <T> void assertUnorderedCollection( Collection<? extends T> collection, Consumer<T>... checkers) { Assert.assertNotNull(collection); if (collection.size() != checkers.length) { Assert.fail(toString(collection)); } Set<Consumer<T>> checkerSet = new HashSet<Consumer<T>>(Arrays.asList(checkers)); int i = 0; Throwable lastError = null; for (final T actual : collection) { boolean flag = true; for (final Consumer<T> condition : checkerSet) { Throwable error = accepts(condition, actual); if (error == null) { checkerSet.remove(condition); flag = false; break; } else { lastError = error; } } if (flag) { lastError.printStackTrace(); Assert.fail("Incorrect element(" + i + "): " + actual); } i++; } } private static <T> Throwable accepts(final Consumer<T> condition, final T actual) { try { condition.consume(actual); return null; } catch (Throwable e) { return e; } } public static <T> T assertInstanceOf(Object o, Class<T> aClass) { Assert.assertNotNull("Expected instance of: " + aClass.getName() + " actual: " + null, o); Assert.assertTrue( "Expected instance of: " + aClass.getName() + " actual: " + o.getClass().getName(), aClass.isInstance(o)); @SuppressWarnings("unchecked") T t = (T) o; return t; } public static <T> T assertOneElement(Collection<T> collection) { Assert.assertNotNull(collection); Assert.assertEquals(toString(collection), 1, collection.size()); return collection.iterator().next(); } public static <T> T assertOneElement(T[] ts) { Assert.assertNotNull(ts); Assert.assertEquals(Arrays.asList(ts).toString(), 1, ts.length); return ts[0]; } public static <T> void assertOneOf(T value, T... values) { boolean found = false; for (T v : values) { if (value == v || value != null && value.equals(v)) { found = true; } } Assert.assertTrue(value + " should be equal to one of " + Arrays.toString(values), found); } public static void printThreadDump() { PerformanceWatcher.dumpThreadsToConsole("Thread dump:"); } public static void assertEmpty(final Object[] array) { assertOrderedEquals(array); } public static void assertNotEmpty(final Collection<?> collection) { if (collection == null) return; assertTrue(!collection.isEmpty()); } public static void assertEmpty(final Collection<?> collection) { assertEmpty(collection.toString(), collection); } public static void assertNullOrEmpty(final Collection<?> collection) { if (collection == null) return; assertEmpty(null, collection); } public static void assertEmpty(final String s) { assertTrue(s, StringUtil.isEmpty(s)); } public static <T> void assertEmpty(final String errorMsg, final Collection<T> collection) { assertOrderedEquals(errorMsg, collection); } public static void assertSize(int expectedSize, final Object[] array) { assertEquals(toString(Arrays.asList(array)), expectedSize, array.length); } public static void assertSize(int expectedSize, final Collection<?> c) { assertEquals(toString(c), expectedSize, c.size()); } protected <T extends Disposable> T disposeOnTearDown(final T disposable) { Disposer.register(myTestRootDisposable, disposable); return disposable; } public static void assertSameLines(String expected, String actual) { String expectedText = StringUtil.convertLineSeparators(expected.trim()); String actualText = StringUtil.convertLineSeparators(actual.trim()); Assert.assertEquals(expectedText, actualText); } public static void assertExists(File file) { assertTrue("File should exist " + file, file.exists()); } public static void assertDoesntExist(File file) { assertFalse("File should not exist " + file, file.exists()); } protected String getTestName(boolean lowercaseFirstLetter) { String name = getName(); return getTestName(name, lowercaseFirstLetter); } public static String getTestName(String name, boolean lowercaseFirstLetter) { if (name == null) { return ""; } name = StringUtil.trimStart(name, "test"); if (StringUtil.isEmpty(name)) { return ""; } return lowercaseFirstLetter(name, lowercaseFirstLetter); } public static String lowercaseFirstLetter(String name, boolean lowercaseFirstLetter) { if (lowercaseFirstLetter && !isAllUppercaseName(name)) { name = Character.toLowerCase(name.charAt(0)) + name.substring(1); } return name; } public static boolean isAllUppercaseName(String name) { int uppercaseChars = 0; for (int i = 0; i < name.length(); i++) { if (Character.isLowerCase(name.charAt(i))) { return false; } if (Character.isUpperCase(name.charAt(i))) { uppercaseChars++; } } return uppercaseChars >= 3; } protected String getTestDirectoryName() { final String testName = getTestName(true); return testName.replaceAll("_.*", ""); } protected static void assertSameLinesWithFile(String filePath, String actualText) { String fileText; try { if (OVERWRITE_TESTDATA) { VfsTestUtil.overwriteTestData(filePath, actualText); System.out.println("File " + filePath + " created."); } fileText = FileUtil.loadFile(new File(filePath), CharsetToolkit.UTF8); } catch (FileNotFoundException e) { VfsTestUtil.overwriteTestData(filePath, actualText); throw new AssertionFailedError("No output text found. File " + filePath + " created."); } catch (IOException e) { throw new RuntimeException(e); } String expected = StringUtil.convertLineSeparators(fileText.trim()); String actual = StringUtil.convertLineSeparators(actualText.trim()); if (!Comparing.equal(expected, actual)) { throw new FileComparisonFailure(null, expected, actual, filePath); } } public static void clearFields(final Object test) throws IllegalAccessException { Class aClass = test.getClass(); while (aClass != null) { clearDeclaredFields(test, aClass); aClass = aClass.getSuperclass(); } } public static void clearDeclaredFields(Object test, Class aClass) throws IllegalAccessException { if (aClass == null) return; for (final Field field : aClass.getDeclaredFields()) { @NonNls final String name = field.getDeclaringClass().getName(); if (!name.startsWith("junit.framework.") && !name.startsWith("com.intellij.testFramework.")) { final int modifiers = field.getModifiers(); if ((modifiers & Modifier.FINAL) == 0 && (modifiers & Modifier.STATIC) == 0 && !field.getType().isPrimitive()) { field.setAccessible(true); field.set(test, null); } } } } @SuppressWarnings("deprecation") protected static void checkSettingsEqual( JDOMExternalizable expected, JDOMExternalizable settings, String message) throws Exception { if (expected == null || settings == null) return; Element oldS = new Element("temp"); expected.writeExternal(oldS); Element newS = new Element("temp"); settings.writeExternal(newS); String newString = JDOMUtil.writeElement(newS, "\n"); String oldString = JDOMUtil.writeElement(oldS, "\n"); Assert.assertEquals(message, oldString, newString); } public boolean isPerformanceTest() { String name = getName(); return name != null && name.contains("Performance") || getClass().getName().contains("Performance"); } public static void doPostponedFormatting(final Project project) { DocumentUtil.writeInRunUndoTransparentAction( new Runnable() { @Override public void run() { PsiDocumentManager.getInstance(project).commitAllDocuments(); PostprocessReformattingAspect.getInstance(project).doPostponedFormatting(); } }); } protected static void checkAllTimersAreDisposed() { try { Class<?> aClass = Class.forName("javax.swing.TimerQueue"); Method inst = aClass.getDeclaredMethod("sharedInstance"); inst.setAccessible(true); Object queue = inst.invoke(null); Field field = aClass.getDeclaredField("firstTimer"); field.setAccessible(true); Object firstTimer = field.get(queue); if (firstTimer != null) { try { fail("Not disposed Timer: " + firstTimer.toString() + "; queue:" + queue); } finally { field.set(queue, null); } } } catch (Throwable e) { // Ignore } } /** * Checks that code block throw corresponding exception. * * @param exceptionCase Block annotated with some exception type * @throws Throwable */ protected void assertException(final AbstractExceptionCase exceptionCase) throws Throwable { assertException(exceptionCase, null); } /** * Checks that code block throw corresponding exception with expected error msg. If expected error * message is null it will not be checked. * * @param exceptionCase Block annotated with some exception type * @param expectedErrorMsg expected error messge * @throws Throwable */ protected void assertException( final AbstractExceptionCase exceptionCase, @Nullable final String expectedErrorMsg) throws Throwable { assertExceptionOccurred(true, exceptionCase, expectedErrorMsg); } /** * Checks that code block doesn't throw corresponding exception. * * @param exceptionCase Block annotated with some exception type * @throws Throwable */ protected void assertNoException(final AbstractExceptionCase exceptionCase) throws Throwable { assertExceptionOccurred(false, exceptionCase, null); } protected void assertNoThrowable(final Runnable closure) { String throwableName = null; try { closure.run(); } catch (Throwable thr) { throwableName = thr.getClass().getName(); } assertNull(throwableName); } private static void assertExceptionOccurred( boolean shouldOccur, AbstractExceptionCase exceptionCase, String expectedErrorMsg) throws Throwable { boolean wasThrown = false; try { exceptionCase.tryClosure(); } catch (Throwable e) { if (shouldOccur) { wasThrown = true; final String errorMessage = exceptionCase.getAssertionErrorMessage(); assertEquals(errorMessage, exceptionCase.getExpectedExceptionClass(), e.getClass()); if (expectedErrorMsg != null) { assertEquals("Compare error messages", expectedErrorMsg, e.getMessage()); } } else if (exceptionCase.getExpectedExceptionClass().equals(e.getClass())) { wasThrown = true; System.out.println(""); e.printStackTrace(System.out); fail("Exception isn't expected here. Exception message: " + e.getMessage()); } else { throw e; } } finally { if (shouldOccur && !wasThrown) { fail(exceptionCase.getAssertionErrorMessage()); } } } protected boolean annotatedWith(@NotNull Class annotationClass) { Class<?> aClass = getClass(); String methodName = "test" + getTestName(false); boolean methodChecked = false; while (aClass != null && aClass != Object.class) { if (aClass.getAnnotation(annotationClass) != null) return true; if (!methodChecked) { try { Method method = aClass.getDeclaredMethod(methodName); if (method.getAnnotation(annotationClass) != null) return true; methodChecked = true; } catch (NoSuchMethodException ignored) { } } aClass = aClass.getSuperclass(); } return false; } protected String getHomePath() { return PathManager.getHomePath().replace(File.separatorChar, '/'); } protected static boolean isInHeadlessEnvironment() { return GraphicsEnvironment.isHeadless(); } public static void refreshRecursively(@NotNull VirtualFile file) { VfsUtilCore.visitChildrenRecursively( file, new VirtualFileVisitor() { @Override public boolean visitFile(@NotNull VirtualFile file) { file.getChildren(); return true; } }); file.refresh(false, true); } @NotNull public static Test filteredSuite(@RegExp String regexp, @NotNull Test test) { final Pattern pattern = Pattern.compile(regexp); final TestSuite testSuite = new TestSuite(); new Processor<Test>() { @Override public boolean process(Test test) { if (test instanceof TestSuite) { for (int i = 0, len = ((TestSuite) test).testCount(); i < len; i++) { process(((TestSuite) test).testAt(i)); } } else if (pattern.matcher(test.toString()).find()) { testSuite.addTest(test); } return false; } }.process(test); return testSuite; } }
/** Author: msk */ public class EditorsSplitters extends IdePanePanel implements UISettingsListener, Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.EditorsSplitters"); private static final String PINNED = "pinned"; private static final String CURRENT_IN_TAB = "current-in-tab"; private static final Key<Object> DUMMY_KEY = Key.create("EditorsSplitters.dummy.key"); private EditorWindow myCurrentWindow; private final Set<EditorWindow> myWindows = new CopyOnWriteArraySet<EditorWindow>(); private final FileEditorManagerImpl myManager; private Element mySplittersElement; // temporarily used during initialization int myInsideChange; private final MyFocusWatcher myFocusWatcher; private final Alarm myIconUpdaterAlarm = new Alarm(); private final UIBuilder myUIBuilder = new UIBuilder(); EditorsSplitters( final FileEditorManagerImpl manager, DockManager dockManager, boolean createOwnDockableContainer) { super(new BorderLayout()); myManager = manager; myFocusWatcher = new MyFocusWatcher(); setFocusTraversalPolicy(new MyFocusTraversalPolicy()); clear(); if (createOwnDockableContainer) { DockableEditorTabbedContainer dockable = new DockableEditorTabbedContainer(myManager.getProject(), this, false); Disposer.register(manager.getProject(), dockable); dockManager.register(dockable); } KeymapManagerListener keymapListener = new KeymapManagerListener() { @Override public void activeKeymapChanged(Keymap keymap) { invalidate(); repaint(); } }; KeymapManager.getInstance().addKeymapManagerListener(keymapListener, this); } public FileEditorManagerImpl getManager() { return myManager; } public void clear() { for (EditorWindow window : myWindows) { window.dispose(); } removeAll(); myWindows.clear(); setCurrentWindow(null); repaint(); // revalidate doesn't repaint correctly after "Close All" } void startListeningFocus() { myFocusWatcher.install(this); } private void stopListeningFocus() { myFocusWatcher.deinstall(this); } @Override public void dispose() { myIconUpdaterAlarm.cancelAllRequests(); stopListeningFocus(); } @Nullable public VirtualFile getCurrentFile() { if (myCurrentWindow != null) { return myCurrentWindow.getSelectedFile(); } return null; } private boolean showEmptyText() { return myCurrentWindow == null || myCurrentWindow.getFiles().length == 0; } @Override protected void paintComponent(Graphics g) { if (showEmptyText()) { Graphics2D gg = IdeBackgroundUtil.withFrameBackground(g, this); super.paintComponent(gg); g.setColor(UIUtil.isUnderDarcula() ? UIUtil.getBorderColor() : new Color(0, 0, 0, 50)); g.drawLine(0, 0, getWidth(), 0); } } public void writeExternal(final Element element) { if (getComponentCount() != 0) { final Component comp = getComponent(0); LOG.assertTrue(comp instanceof JPanel); final JPanel panel = (JPanel) comp; if (panel.getComponentCount() != 0) { element.addContent(writePanel(panel)); } } } @SuppressWarnings("HardCodedStringLiteral") private Element writePanel(final JPanel panel) { final Component comp = panel.getComponent(0); if (comp instanceof Splitter) { final Splitter splitter = (Splitter) comp; final Element res = new Element("splitter"); res.setAttribute("split-orientation", splitter.getOrientation() ? "vertical" : "horizontal"); res.setAttribute("split-proportion", Float.toString(splitter.getProportion())); final Element first = new Element("split-first"); first.addContent(writePanel((JPanel) splitter.getFirstComponent())); final Element second = new Element("split-second"); second.addContent(writePanel((JPanel) splitter.getSecondComponent())); res.addContent(first); res.addContent(second); return res; } else if (comp instanceof JBTabs) { final Element res = new Element("leaf"); Integer limit = UIUtil.getClientProperty( ((JBTabs) comp).getComponent(), JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY); if (limit != null) { res.setAttribute(JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY.toString(), String.valueOf(limit)); } writeWindow(res, findWindowWith(comp)); return res; } else if (comp instanceof EditorWindow.TCompForTablessMode) { EditorWithProviderComposite composite = ((EditorWindow.TCompForTablessMode) comp).myEditor; Element res = new Element("leaf"); res.addContent(writeComposite(composite.getFile(), composite, false, composite)); return res; } else { LOG.error(comp != null ? comp.getClass().getName() : null); return null; } } private void writeWindow(@NotNull Element res, @Nullable EditorWindow window) { if (window != null) { EditorWithProviderComposite[] composites = window.getEditors(); for (int i = 0; i < composites.length; i++) { VirtualFile file = window.getFileAt(i); res.addContent( writeComposite( file, composites[i], window.isFilePinned(file), window.getSelectedEditor())); } } } @NotNull private Element writeComposite( VirtualFile file, EditorWithProviderComposite composite, boolean pinned, EditorWithProviderComposite selectedEditor) { Element fileElement = new Element("file"); fileElement.setAttribute("leaf-file-name", file.getName()); // TODO: all files composite.currentStateAsHistoryEntry().writeExternal(fileElement, getManager().getProject()); fileElement.setAttribute(PINNED, Boolean.toString(pinned)); fileElement.setAttribute(CURRENT_IN_TAB, Boolean.toString(composite.equals(selectedEditor))); return fileElement; } public void openFiles() { if (mySplittersElement != null) { initializeProgress(); final JPanel comp = myUIBuilder.process(mySplittersElement, getTopPanel()); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { if (comp != null) { removeAll(); add(comp, BorderLayout.CENTER); mySplittersElement = null; } // clear empty splitters for (EditorWindow window : getWindows()) { if (window.getEditors().length == 0) { for (EditorWindow sibling : window.findSiblings()) { sibling.unsplit(false); } } } } }); } } private static void initializeProgress() { ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null) { indicator.setText(IdeBundle.message("loading.editors")); } } public int getEditorsCount() { return mySplittersElement == null ? 0 : countFiles(mySplittersElement); } private double myProgressStep; public void setProgressStep(double step) { myProgressStep = step; } private void updateProgress() { ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null) { indicator.setFraction(indicator.getFraction() + myProgressStep); } } private static int countFiles(Element element) { Integer value = new ConfigTreeReader<Integer>() { @Override protected Integer processFiles( @NotNull List<Element> fileElements, @Nullable Integer context) { return fileElements.size(); } @Override protected Integer processSplitter( @NotNull Element element, @Nullable Element firstChild, @Nullable Element secondChild, @Nullable Integer context) { Integer first = process(firstChild, null); Integer second = process(secondChild, null); return (first == null ? 0 : first) + (second == null ? 0 : second); } }.process(element, null); return value == null ? 0 : value; } public void readExternal(final Element element) { mySplittersElement = element; } @NotNull public VirtualFile[] getOpenFiles() { final Set<VirtualFile> files = new ArrayListSet<VirtualFile>(); for (final EditorWindow myWindow : myWindows) { final EditorWithProviderComposite[] editors = myWindow.getEditors(); for (final EditorWithProviderComposite editor : editors) { VirtualFile file = editor.getFile(); // background thread may call this method when invalid file is being removed // do not return it here as it will quietly drop out soon if (file.isValid()) { files.add(file); } } } return VfsUtilCore.toVirtualFileArray(files); } @NotNull public VirtualFile[] getSelectedFiles() { final Set<VirtualFile> files = new ArrayListSet<VirtualFile>(); for (final EditorWindow window : myWindows) { final VirtualFile file = window.getSelectedFile(); if (file != null) { files.add(file); } } final VirtualFile[] virtualFiles = VfsUtilCore.toVirtualFileArray(files); final VirtualFile currentFile = getCurrentFile(); if (currentFile != null) { for (int i = 0; i != virtualFiles.length; ++i) { if (Comparing.equal(virtualFiles[i], currentFile)) { virtualFiles[i] = virtualFiles[0]; virtualFiles[0] = currentFile; break; } } } return virtualFiles; } @NotNull public FileEditor[] getSelectedEditors() { List<FileEditor> editors = new ArrayList<FileEditor>(); Set<EditorWindow> windows = new THashSet<EditorWindow>(myWindows); final EditorWindow currentWindow = getCurrentWindow(); if (currentWindow != null) { windows.add(currentWindow); } for (final EditorWindow window : windows) { final EditorWithProviderComposite composite = window.getSelectedEditor(); if (composite != null) { editors.add(composite.getSelectedEditor()); } } return editors.toArray(new FileEditor[editors.size()]); } public void updateFileIcon(@NotNull final VirtualFile file) { updateFileIconLater(file); } private void updateFileIconImmediately(final VirtualFile file) { final Collection<EditorWindow> windows = findWindows(file); for (EditorWindow window : windows) { window.updateFileIcon(file); } } private final Set<VirtualFile> myFilesToUpdateIconsFor = new HashSet<VirtualFile>(); private void updateFileIconLater(VirtualFile file) { myFilesToUpdateIconsFor.add(file); myIconUpdaterAlarm.cancelAllRequests(); myIconUpdaterAlarm.addRequest( () -> { if (myManager.getProject().isDisposed()) return; for (VirtualFile file1 : myFilesToUpdateIconsFor) { updateFileIconImmediately(file1); } myFilesToUpdateIconsFor.clear(); }, 200, ModalityState.stateForComponent(this)); } void updateFileColor(@NotNull final VirtualFile file) { final Collection<EditorWindow> windows = findWindows(file); for (final EditorWindow window : windows) { final int index = window.findEditorIndex(window.findFileComposite(file)); LOG.assertTrue(index != -1); window.setForegroundAt(index, getManager().getFileColor(file)); window.setWaveColor(index, getManager().isProblem(file) ? JBColor.red : null); } } public void trimToSize(final int editor_tab_limit) { for (final EditorWindow window : myWindows) { window.trimToSize(editor_tab_limit, null, true); } } public void setTabsPlacement(final int tabPlacement) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { windows[i].setTabsPlacement(tabPlacement); } } void setTabLayoutPolicy(int scrollTabLayout) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { windows[i].setTabLayoutPolicy(scrollTabLayout); } } void updateFileName(@Nullable final VirtualFile updatedFile) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { for (VirtualFile file : windows[i].getFiles()) { if (updatedFile == null || file.getName().equals(updatedFile.getName())) { windows[i].updateFileName(file); } } } Project project = myManager.getProject(); final IdeFrame frame = getFrame(project); if (frame != null) { VirtualFile file = getCurrentFile(); File ioFile = file == null ? null : new File(file.getPresentableUrl()); String fileTitle = null; if (file != null) { fileTitle = DumbService.isDumb(project) ? file.getName() : FrameTitleBuilder.getInstance().getFileTitle(project, file); } frame.setFileTitle(fileTitle, ioFile); } } protected IdeFrame getFrame(Project project) { final WindowManagerEx windowManagerEx = WindowManagerEx.getInstanceEx(); final IdeFrame frame = windowManagerEx.getFrame(project); LOG.assertTrue(ApplicationManager.getApplication().isUnitTestMode() || frame != null); return frame; } boolean isInsideChange() { return myInsideChange > 0; } private void setCurrentWindow(@Nullable final EditorWindow currentWindow) { if (currentWindow != null && !myWindows.contains(currentWindow)) { throw new IllegalArgumentException(currentWindow + " is not a member of this container"); } myCurrentWindow = currentWindow; } void updateFileBackgroundColor(@NotNull VirtualFile file) { final EditorWindow[] windows = getWindows(); for (int i = 0; i != windows.length; ++i) { windows[i].updateFileBackgroundColor(file); } } int getSplitCount() { if (getComponentCount() > 0) { JPanel panel = (JPanel) getComponent(0); return getSplitCount(panel); } return 0; } private static int getSplitCount(JComponent component) { if (component.getComponentCount() > 0) { final JComponent firstChild = (JComponent) component.getComponent(0); if (firstChild instanceof Splitter) { final Splitter splitter = (Splitter) firstChild; return getSplitCount(splitter.getFirstComponent()) + getSplitCount(splitter.getSecondComponent()); } return 1; } return 0; } protected void afterFileClosed(VirtualFile file) {} protected void afterFileOpen(VirtualFile file) {} @Nullable JBTabs getTabsAt(RelativePoint point) { Point thisPoint = point.getPoint(this); Component c = SwingUtilities.getDeepestComponentAt(this, thisPoint.x, thisPoint.y); while (c != null) { if (c instanceof JBTabs) { return (JBTabs) c; } c = c.getParent(); } return null; } boolean isEmptyVisible() { EditorWindow[] windows = getWindows(); for (EditorWindow each : windows) { if (!each.isEmptyVisible()) { return false; } } return true; } @Nullable private VirtualFile findNextFile(final VirtualFile file) { final EditorWindow[] windows = getWindows(); // TODO: use current file as base for (int i = 0; i != windows.length; ++i) { final VirtualFile[] files = windows[i].getFiles(); for (final VirtualFile fileAt : files) { if (!Comparing.equal(fileAt, file)) { return fileAt; } } } return null; } void closeFile(VirtualFile file, boolean moveFocus) { final List<EditorWindow> windows = findWindows(file); if (!windows.isEmpty()) { final VirtualFile nextFile = findNextFile(file); for (final EditorWindow window : windows) { LOG.assertTrue(window.getSelectedEditor() != null); window.closeFile(file, false, moveFocus); if (window.getTabCount() == 0 && nextFile != null && myManager.getProject().isOpen()) { EditorWithProviderComposite newComposite = myManager.newEditorComposite(nextFile); window.setEditor(newComposite, moveFocus); // newComposite can be null } } // cleanup windows with no tabs for (final EditorWindow window : windows) { if (window.isDisposed()) { // call to window.unsplit() which might make its sibling disposed continue; } if (window.getTabCount() == 0) { window.unsplit(false); } } } } @Override public void uiSettingsChanged(UISettings source) { if (!myManager.getProject().isOpen()) return; for (VirtualFile file : getOpenFiles()) { updateFileBackgroundColor(file); updateFileIcon(file); updateFileColor(file); } } private final class MyFocusTraversalPolicy extends IdeFocusTraversalPolicy { @Override public final Component getDefaultComponentImpl(final Container focusCycleRoot) { if (myCurrentWindow != null) { final EditorWithProviderComposite selectedEditor = myCurrentWindow.getSelectedEditor(); if (selectedEditor != null) { return IdeFocusTraversalPolicy.getPreferredFocusedComponent( selectedEditor.getComponent(), this); } } return IdeFocusTraversalPolicy.getPreferredFocusedComponent(EditorsSplitters.this, this); } } @Nullable public JPanel getTopPanel() { return getComponentCount() > 0 ? (JPanel) getComponent(0) : null; } public EditorWindow getCurrentWindow() { return myCurrentWindow; } public EditorWindow getOrCreateCurrentWindow(final VirtualFile file) { final List<EditorWindow> windows = findWindows(file); if (getCurrentWindow() == null) { final Iterator<EditorWindow> iterator = myWindows.iterator(); if (!windows.isEmpty()) { setCurrentWindow(windows.get(0), false); } else if (iterator.hasNext()) { setCurrentWindow(iterator.next(), false); } else { createCurrentWindow(); } } else if (!windows.isEmpty()) { if (!windows.contains(getCurrentWindow())) { setCurrentWindow(windows.get(0), false); } } return getCurrentWindow(); } void createCurrentWindow() { LOG.assertTrue(myCurrentWindow == null); setCurrentWindow(createEditorWindow()); add(myCurrentWindow.myPanel, BorderLayout.CENTER); } protected EditorWindow createEditorWindow() { return new EditorWindow(this); } /** * sets the window passed as a current ('focused') window among all splitters. All file openings * will be done inside this current window * * @param window a window to be set as current * @param requestFocus whether to request focus to the editor currently selected in this window */ void setCurrentWindow(@Nullable final EditorWindow window, final boolean requestFocus) { final EditorWithProviderComposite newEditor = window == null ? null : window.getSelectedEditor(); Runnable fireRunnable = () -> getManager().fireSelectionChanged(newEditor); setCurrentWindow(window); getManager().updateFileName(window == null ? null : window.getSelectedFile()); if (window != null) { final EditorWithProviderComposite selectedEditor = window.getSelectedEditor(); if (selectedEditor != null) { fireRunnable.run(); } if (requestFocus) { window.requestFocus(true); } } else { fireRunnable.run(); } } void addWindow(EditorWindow window) { myWindows.add(window); } void removeWindow(EditorWindow window) { myWindows.remove(window); if (myCurrentWindow == window) { myCurrentWindow = null; } } boolean containsWindow(EditorWindow window) { return myWindows.contains(window); } // --------------------------------------------------------- public EditorWithProviderComposite[] getEditorsComposites() { List<EditorWithProviderComposite> res = new ArrayList<EditorWithProviderComposite>(); for (final EditorWindow myWindow : myWindows) { final EditorWithProviderComposite[] editors = myWindow.getEditors(); ContainerUtil.addAll(res, editors); } return res.toArray(new EditorWithProviderComposite[res.size()]); } // --------------------------------------------------------- @NotNull public List<EditorWithProviderComposite> findEditorComposites(@NotNull VirtualFile file) { List<EditorWithProviderComposite> res = new ArrayList<EditorWithProviderComposite>(); for (final EditorWindow window : myWindows) { final EditorWithProviderComposite fileComposite = window.findFileComposite(file); if (fileComposite != null) { res.add(fileComposite); } } return res; } @NotNull private List<EditorWindow> findWindows(final VirtualFile file) { List<EditorWindow> res = new ArrayList<EditorWindow>(); for (final EditorWindow window : myWindows) { if (window.findFileComposite(file) != null) { res.add(window); } } return res; } @NotNull public EditorWindow[] getWindows() { return myWindows.toArray(new EditorWindow[myWindows.size()]); } @NotNull EditorWindow[] getOrderedWindows() { final List<EditorWindow> res = new ArrayList<EditorWindow>(); // Collector for windows in tree ordering: class Inner { private void collect(final JPanel panel) { final Component comp = panel.getComponent(0); if (comp instanceof Splitter) { final Splitter splitter = (Splitter) comp; collect((JPanel) splitter.getFirstComponent()); collect((JPanel) splitter.getSecondComponent()); } else if (comp instanceof JPanel || comp instanceof JBTabs) { final EditorWindow window = findWindowWith(comp); if (window != null) { res.add(window); } } } } // get root component and traverse splitters tree: if (getComponentCount() != 0) { final Component comp = getComponent(0); LOG.assertTrue(comp instanceof JPanel); final JPanel panel = (JPanel) comp; if (panel.getComponentCount() != 0) { new Inner().collect(panel); } } LOG.assertTrue(res.size() == myWindows.size()); return res.toArray(new EditorWindow[res.size()]); } @Nullable private EditorWindow findWindowWith(final Component component) { if (component != null) { for (final EditorWindow window : myWindows) { if (SwingUtilities.isDescendingFrom(component, window.myPanel)) { return window; } } } return null; } public boolean isFloating() { return false; } public boolean isPreview() { return false; } private final class MyFocusWatcher extends FocusWatcher { @Override protected void focusedComponentChanged(final Component component, final AWTEvent cause) { EditorWindow newWindow = null; if (component != null) { newWindow = findWindowWith(component); } else if (cause instanceof ContainerEvent && cause.getID() == ContainerEvent.COMPONENT_REMOVED) { // do not change current window in case of child removal as in JTable.removeEditor // otherwise Escape in a toolwindow will not focus editor with JTable content return; } setCurrentWindow(newWindow); setCurrentWindow(newWindow, false); } } private abstract static class ConfigTreeReader<T> { @Nullable public T process(@Nullable Element element, @Nullable T context) { if (element == null) { return null; } final Element splitterElement = element.getChild("splitter"); if (splitterElement != null) { final Element first = splitterElement.getChild("split-first"); final Element second = splitterElement.getChild("split-second"); return processSplitter(splitterElement, first, second, context); } final Element leaf = element.getChild("leaf"); if (leaf == null) { return null; } List<Element> fileElements = leaf.getChildren("file"); final List<Element> children = new ArrayList<Element>(fileElements.size()); // trim to EDITOR_TAB_LIMIT, ignoring CLOSE_NON_MODIFIED_FILES_FIRST policy int toRemove = fileElements.size() - UISettings.getInstance().EDITOR_TAB_LIMIT; for (Element fileElement : fileElements) { if (toRemove <= 0 || Boolean.valueOf(fileElement.getAttributeValue(PINNED)).booleanValue()) { children.add(fileElement); } else { toRemove--; } } return processFiles(children, context); } @Nullable protected abstract T processFiles(@NotNull List<Element> fileElements, @Nullable T context); @Nullable protected abstract T processSplitter( @NotNull Element element, @Nullable Element firstChild, @Nullable Element secondChild, @Nullable T context); } private class UIBuilder extends ConfigTreeReader<JPanel> { @Override protected JPanel processFiles(@NotNull List<Element> fileElements, final JPanel context) { final Ref<EditorWindow> windowRef = new Ref<EditorWindow>(); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { windowRef.set(context == null ? createEditorWindow() : findWindowWith(context)); } }); final EditorWindow window = windowRef.get(); LOG.assertTrue(window != null); VirtualFile focusedFile = null; for (int i = 0; i < fileElements.size(); i++) { final Element file = fileElements.get(i); if (i == 0) { EditorTabbedContainer tabbedPane = window.getTabbedPane(); if (tabbedPane != null) { try { int limit = Integer.parseInt( file.getParentElement() .getAttributeValue( JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY.toString(), String.valueOf(JBTabsImpl.DEFAULT_MAX_TAB_WIDTH))); UIUtil.putClientProperty( tabbedPane.getComponent(), JBTabsImpl.SIDE_TABS_SIZE_LIMIT_KEY, limit); } catch (NumberFormatException e) { // ignore } } } try { final FileEditorManagerImpl fileEditorManager = getManager(); Element historyElement = file.getChild(HistoryEntry.TAG); final HistoryEntry entry = HistoryEntry.createLight(fileEditorManager.getProject(), historyElement); final VirtualFile virtualFile = entry.getFile(); if (virtualFile == null) throw new InvalidDataException("No file exists: " + entry.getFilePointer().getUrl()); Document document = ApplicationManager.getApplication() .runReadAction( new Computable<Document>() { @Override public Document compute() { return virtualFile.isValid() ? FileDocumentManager.getInstance().getDocument(virtualFile) : null; } }); final boolean isCurrentInTab = Boolean.valueOf(file.getAttributeValue(CURRENT_IN_TAB)).booleanValue(); Boolean pin = Boolean.valueOf(file.getAttributeValue(PINNED)); fileEditorManager.openFileImpl4( window, virtualFile, entry, isCurrentInTab, isCurrentInTab, pin, i); if (isCurrentInTab) { focusedFile = virtualFile; } if (document != null) { // This is just to make sure document reference is kept on stack till this point // so that document is available for folding state deserialization in HistoryEntry // constructor // and that document will be created only once during file opening document.putUserData(DUMMY_KEY, null); } updateProgress(); } catch (InvalidDataException e) { if (ApplicationManager.getApplication().isUnitTestMode()) { LOG.error(e); } } } if (focusedFile != null) { getManager().addSelectionRecord(focusedFile, window); } return window.myPanel; } @Override protected JPanel processSplitter( @NotNull Element splitterElement, Element firstChild, Element secondChild, final JPanel context) { if (context == null) { final boolean orientation = "vertical".equals(splitterElement.getAttributeValue("split-orientation")); final float proportion = Float.valueOf(splitterElement.getAttributeValue("split-proportion")).floatValue(); final JPanel firstComponent = process(firstChild, null); final JPanel secondComponent = process(secondChild, null); final Ref<JPanel> panelRef = new Ref<JPanel>(); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { JPanel panel = new JPanel(new BorderLayout()); panel.setOpaque(false); Splitter splitter = new OnePixelSplitter(orientation, proportion, 0.1f, 0.9f); panel.add(splitter, BorderLayout.CENTER); splitter.setFirstComponent(firstComponent); splitter.setSecondComponent(secondComponent); panelRef.set(panel); } }); return panelRef.get(); } final Ref<JPanel> firstComponent = new Ref<JPanel>(); final Ref<JPanel> secondComponent = new Ref<JPanel>(); UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { if (context.getComponent(0) instanceof Splitter) { Splitter splitter = (Splitter) context.getComponent(0); firstComponent.set((JPanel) splitter.getFirstComponent()); secondComponent.set((JPanel) splitter.getSecondComponent()); } else { firstComponent.set(context); secondComponent.set(context); } } }); process(firstChild, firstComponent.get()); process(secondChild, secondComponent.get()); return context; } } }
public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingPass implements DumbAware { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.GeneralHighlightingPass"); static final String PRESENTABLE_NAME = DaemonBundle.message("pass.syntax"); private static final Key<Boolean> HAS_ERROR_ELEMENT = Key.create("HAS_ERROR_ELEMENT"); private final int myStartOffset; private final int myEndOffset; private final boolean myUpdateAll; private final ProperTextRange myPriorityRange; private final Editor myEditor; private final List<HighlightInfo> myHighlights = new ArrayList<HighlightInfo>(); protected volatile boolean myHasErrorElement; private volatile boolean myErrorFound; private static final Comparator<HighlightVisitor> VISITOR_ORDER_COMPARATOR = new Comparator<HighlightVisitor>() { @Override public int compare(final HighlightVisitor o1, final HighlightVisitor o2) { return o1.order() - o2.order(); } }; private Runnable myApplyCommand; private final EditorColorsScheme myGlobalScheme; private boolean myFailFastOnAcquireReadAction = true; public GeneralHighlightingPass( @NotNull Project project, @NotNull PsiFile file, @NotNull Document document, int startOffset, int endOffset, boolean updateAll) { this( project, file, document, startOffset, endOffset, updateAll, new ProperTextRange(0, document.getTextLength()), null); } public GeneralHighlightingPass( @NotNull Project project, @NotNull PsiFile file, @NotNull Document document, int startOffset, int endOffset, boolean updateAll, @NotNull ProperTextRange priorityRange, @Nullable Editor editor) { super(project, document, PRESENTABLE_NAME, file, true); myStartOffset = startOffset; myEndOffset = endOffset; myUpdateAll = updateAll; myPriorityRange = priorityRange; myEditor = editor; LOG.assertTrue(file.isValid()); setId(Pass.UPDATE_ALL); myHasErrorElement = !isWholeFileHighlighting() && Boolean.TRUE.equals(myFile.getUserData(HAS_ERROR_ELEMENT)); FileStatusMap fileStatusMap = ((DaemonCodeAnalyzerImpl) DaemonCodeAnalyzer.getInstance(myProject)).getFileStatusMap(); myErrorFound = !isWholeFileHighlighting() && fileStatusMap.wasErrorFound(myDocument); myApplyCommand = new Runnable() { @Override public void run() { ProperTextRange range = new ProperTextRange(myStartOffset, myEndOffset); MarkupModel model = DocumentMarkupModel.forDocument(myDocument, myProject, true); UpdateHighlightersUtil.cleanFileLevelHighlights(myProject, Pass.UPDATE_ALL, myFile); final EditorColorsScheme colorsScheme = getColorsScheme(); UpdateHighlightersUtil.setHighlightersInRange( myProject, myDocument, range, colorsScheme, myHighlights, (MarkupModelEx) model, Pass.UPDATE_ALL); } }; // initial guess to show correct progress in the traffic light icon setProgressLimit(document.getTextLength() / 2); // approx number of PSI elements = file length/2 myGlobalScheme = EditorColorsManager.getInstance().getGlobalScheme(); } private static final Key<AtomicInteger> HIGHLIGHT_VISITOR_INSTANCE_COUNT = new Key<AtomicInteger>("HIGHLIGHT_VISITOR_INSTANCE_COUNT"); @NotNull private HighlightVisitor[] getHighlightVisitors() { int oldCount = incVisitorUsageCount(1); HighlightVisitor[] highlightVisitors = createHighlightVisitors(); if (oldCount != 0) { HighlightVisitor[] clones = new HighlightVisitor[highlightVisitors.length]; for (int i = 0; i < highlightVisitors.length; i++) { HighlightVisitor highlightVisitor = highlightVisitors[i]; clones[i] = highlightVisitor.clone(); } highlightVisitors = clones; } return highlightVisitors; } protected HighlightVisitor[] createHighlightVisitors() { return Extensions.getExtensions(HighlightVisitor.EP_HIGHLIGHT_VISITOR, myProject); } // returns old value private int incVisitorUsageCount(int delta) { AtomicInteger count = myProject.getUserData(HIGHLIGHT_VISITOR_INSTANCE_COUNT); if (count == null) { count = ((UserDataHolderEx) myProject) .putUserDataIfAbsent(HIGHLIGHT_VISITOR_INSTANCE_COUNT, new AtomicInteger(0)); } int old = count.getAndAdd(delta); assert old + delta >= 0 : old + ";" + delta; return old; } @Override protected void collectInformationWithProgress(final ProgressIndicator progress) { final Set<HighlightInfo> gotHighlights = new THashSet<HighlightInfo>(100); final Set<HighlightInfo> outsideResult = new THashSet<HighlightInfo>(100); DaemonCodeAnalyzer daemonCodeAnalyzer = DaemonCodeAnalyzer.getInstance(myProject); HighlightVisitor[] highlightVisitors = getHighlightVisitors(); final HighlightVisitor[] filteredVisitors = filterVisitors(highlightVisitors, myFile); final List<PsiElement> inside = new ArrayList<PsiElement>(); final List<PsiElement> outside = new ArrayList<PsiElement>(); try { Divider.divideInsideAndOutside( myFile, myStartOffset, myEndOffset, myPriorityRange, inside, outside, HighlightLevelUtil.AnalysisLevel.HIGHLIGHT, false); setProgressLimit((long) (inside.size() + outside.size())); final boolean forceHighlightParents = forceHighlightParents(); if (!isDumbMode()) { highlightTodos( myFile, myDocument.getCharsSequence(), myStartOffset, myEndOffset, progress, myPriorityRange, gotHighlights, outsideResult); } collectHighlights( inside, new Runnable() { @Override public void run() { // all infos for the "injected fragment for the host which is inside" are indeed // inside // but some of the infos for the "injected fragment for the host which is outside" can // be still inside Set<HighlightInfo> injectedResult = new THashSet<HighlightInfo>(); final Set<PsiFile> injected = new THashSet<PsiFile>(); getInjectedPsiFiles(inside, outside, progress, injected); if (!addInjectedPsiHighlights( injected, progress, Collections.synchronizedSet(injectedResult))) throw new ProcessCanceledException(); final List<HighlightInfo> injectionsOutside = new ArrayList<HighlightInfo>(gotHighlights.size()); Set<HighlightInfo> result; synchronized (injectedResult) { // sync here because all writes happened in another thread result = injectedResult; } for (HighlightInfo info : result) { if (myPriorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) { gotHighlights.add(info); } else { // nonconditionally apply injected results regardless whether they are in // myStartOffset,myEndOffset injectionsOutside.add(info); } } if (outsideResult.isEmpty() && injectionsOutside.isEmpty()) { return; // apply only result (by default apply command) and only within inside } final ProperTextRange priorityIntersection = myPriorityRange.intersection(new TextRange(myStartOffset, myEndOffset)); if ((!inside.isEmpty() || !gotHighlights.isEmpty()) && priorityIntersection != null) { // do not apply when there were no elements to highlight // clear infos found in visible area to avoid applying them twice final List<HighlightInfo> toApplyInside = new ArrayList<HighlightInfo>(gotHighlights); myHighlights.addAll(toApplyInside); gotHighlights.clear(); gotHighlights.addAll(outsideResult); final long modificationStamp = myDocument.getModificationStamp(); UIUtil.invokeLaterIfNeeded( new Runnable() { @Override public void run() { if (myProject.isDisposed() || modificationStamp != myDocument.getModificationStamp()) return; MarkupModel markupModel = DocumentMarkupModel.forDocument(myDocument, myProject, true); UpdateHighlightersUtil.setHighlightersInRange( myProject, myDocument, priorityIntersection, getColorsScheme(), toApplyInside, (MarkupModelEx) markupModel, Pass.UPDATE_ALL); if (myEditor != null) { new ShowAutoImportPass(myProject, myFile, myEditor) .applyInformationToEditor(); } } }); } myApplyCommand = new Runnable() { @Override public void run() { ProperTextRange range = new ProperTextRange(myStartOffset, myEndOffset); List<HighlightInfo> toApply = new ArrayList<HighlightInfo>(); for (HighlightInfo info : gotHighlights) { if (!range.containsRange(info.getStartOffset(), info.getEndOffset())) continue; if (!myPriorityRange.containsRange( info.getStartOffset(), info.getEndOffset())) { toApply.add(info); } } toApply.addAll(injectionsOutside); UpdateHighlightersUtil.setHighlightersOutsideRange( myProject, myDocument, toApply, getColorsScheme(), myStartOffset, myEndOffset, myPriorityRange, Pass.UPDATE_ALL); } }; } }, outside, progress, filteredVisitors, gotHighlights, forceHighlightParents); if (myUpdateAll) { ((DaemonCodeAnalyzerImpl) daemonCodeAnalyzer) .getFileStatusMap() .setErrorFoundFlag(myDocument, myErrorFound); } } finally { incVisitorUsageCount(-1); } myHighlights.addAll(gotHighlights); } private void getInjectedPsiFiles( @NotNull final List<PsiElement> elements1, @NotNull final List<PsiElement> elements2, @NotNull final ProgressIndicator progress, @NotNull final Set<PsiFile> outInjected) { List<DocumentWindow> injected = InjectedLanguageUtil.getCachedInjectedDocuments(myFile); Collection<PsiElement> hosts = new THashSet<PsiElement>(elements1.size() + elements2.size() + injected.size()); // rehighlight all injected PSI regardless the range, // since change in one place can lead to invalidation of injected PSI in (completely) other // place. for (DocumentWindow documentRange : injected) { progress.checkCanceled(); if (!documentRange.isValid()) continue; PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(documentRange); if (file == null) continue; PsiElement context = file.getContext(); if (context != null && context.isValid() && !file.getProject().isDisposed() && (myUpdateAll || new ProperTextRange(myStartOffset, myEndOffset) .intersects(context.getTextRange()))) { hosts.add(context); } } hosts.addAll(elements1); hosts.addAll(elements2); final PsiLanguageInjectionHost.InjectedPsiVisitor visitor = new PsiLanguageInjectionHost.InjectedPsiVisitor() { @Override public void visit( @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { synchronized (outInjected) { outInjected.add(injectedPsi); } } }; if (!JobUtil.invokeConcurrentlyUnderProgress( new ArrayList<PsiElement>(hosts), progress, false, new Processor<PsiElement>() { @Override public boolean process(PsiElement element) { progress.checkCanceled(); InjectedLanguageUtil.enumerate(element, myFile, false, visitor); return true; } })) throw new ProcessCanceledException(); } // returns false if canceled private boolean addInjectedPsiHighlights( @NotNull final Set<PsiFile> injectedFiles, @NotNull final ProgressIndicator progress, @NotNull final Collection<HighlightInfo> outInfos) { if (injectedFiles.isEmpty()) return true; final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(myProject); final TextAttributes injectedAttributes = myGlobalScheme.getAttributes(EditorColors.INJECTED_LANGUAGE_FRAGMENT); return JobUtil.invokeConcurrentlyUnderProgress( new ArrayList<PsiFile>(injectedFiles), progress, myFailFastOnAcquireReadAction, new Processor<PsiFile>() { @Override public boolean process(final PsiFile injectedPsi) { DocumentWindow documentWindow = (DocumentWindow) PsiDocumentManager.getInstance(myProject).getCachedDocument(injectedPsi); if (documentWindow == null) return true; Place places = InjectedLanguageUtil.getShreds(injectedPsi); for (PsiLanguageInjectionHost.Shred place : places) { TextRange textRange = place.getRangeInsideHost().shiftRight(place.host.getTextRange().getStartOffset()); if (textRange.isEmpty()) continue; String desc = injectedPsi.getLanguage().getDisplayName() + ": " + injectedPsi.getText(); HighlightInfo info = HighlightInfo.createHighlightInfo( HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT, textRange, null, desc, injectedAttributes); info.fromInjection = true; outInfos.add(info); } HighlightInfoHolder holder = createInfoHolder(injectedPsi); runHighlightVisitorsForInjected(injectedPsi, holder, progress); for (int i = 0; i < holder.size(); i++) { HighlightInfo info = holder.get(i); final int startOffset = documentWindow.injectedToHost(info.startOffset); final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset); addPatchedInfos( info, injectedPsi, documentWindow, injectedLanguageManager, fixedTextRange, outInfos); } holder.clear(); highlightInjectedSyntax(injectedPsi, holder); for (int i = 0; i < holder.size(); i++) { HighlightInfo info = holder.get(i); final int startOffset = info.startOffset; final TextRange fixedTextRange = getFixedTextRange(documentWindow, startOffset); if (fixedTextRange == null) { info.fromInjection = true; outInfos.add(info); } else { HighlightInfo patched = new HighlightInfo( info.forcedTextAttributes, info.forcedTextAttributesKey, info.type, fixedTextRange.getStartOffset(), fixedTextRange.getEndOffset(), info.description, info.toolTip, info.type.getSeverity(null), info.isAfterEndOfLine, null, false); patched.fromInjection = true; outInfos.add(patched); } } if (!isDumbMode()) { List<HighlightInfo> todos = new ArrayList<HighlightInfo>(); highlightTodos( injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), progress, myPriorityRange, todos, todos); for (HighlightInfo info : todos) { addPatchedInfos( info, injectedPsi, documentWindow, injectedLanguageManager, null, outInfos); } } return true; } }); } private static TextRange getFixedTextRange( @NotNull DocumentWindow documentWindow, int startOffset) { final TextRange fixedTextRange; TextRange textRange = documentWindow.getHostRange(startOffset); if (textRange == null) { // todo[cdr] check this fix. prefix/suffix code annotation case textRange = findNearestTextRange(documentWindow, startOffset); final boolean isBefore = startOffset < textRange.getStartOffset(); fixedTextRange = new ProperTextRange( isBefore ? textRange.getStartOffset() - 1 : textRange.getEndOffset(), isBefore ? textRange.getStartOffset() : textRange.getEndOffset() + 1); } else { fixedTextRange = null; } return fixedTextRange; } private static void addPatchedInfos( @NotNull HighlightInfo info, @NotNull PsiFile injectedPsi, @NotNull DocumentWindow documentWindow, @NotNull InjectedLanguageManager injectedLanguageManager, @Nullable TextRange fixedTextRange, @NotNull Collection<HighlightInfo> out) { ProperTextRange textRange = new ProperTextRange(info.startOffset, info.endOffset); List<TextRange> editables = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, textRange); for (TextRange editable : editables) { TextRange hostRange = fixedTextRange == null ? documentWindow.injectedToHost(editable) : fixedTextRange; boolean isAfterEndOfLine = info.isAfterEndOfLine; if (isAfterEndOfLine) { // convert injected afterEndOfLine to either host' afterEndOfLine or not-afterEndOfLine // highlight of the injected fragment boundary int hostEndOffset = hostRange.getEndOffset(); int lineNumber = documentWindow.getDelegate().getLineNumber(hostEndOffset); int hostLineEndOffset = documentWindow.getDelegate().getLineEndOffset(lineNumber); if (hostEndOffset < hostLineEndOffset) { // convert to non-afterEndOfLine isAfterEndOfLine = false; hostRange = new ProperTextRange(hostRange.getStartOffset(), hostEndOffset + 1); } } HighlightInfo patched = new HighlightInfo( info.forcedTextAttributes, info.forcedTextAttributesKey, info.type, hostRange.getStartOffset(), hostRange.getEndOffset(), info.description, info.toolTip, info.type.getSeverity(null), isAfterEndOfLine, null, false); patched.setHint(info.hasHint()); patched.setGutterIconRenderer(info.getGutterIconRenderer()); if (info.quickFixActionRanges != null) { for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair : info.quickFixActionRanges) { TextRange quickfixTextRange = pair.getSecond(); List<TextRange> editableQF = injectedLanguageManager.intersectWithAllEditableFragments( injectedPsi, quickfixTextRange); for (TextRange editableRange : editableQF) { HighlightInfo.IntentionActionDescriptor descriptor = pair.getFirst(); if (patched.quickFixActionRanges == null) patched.quickFixActionRanges = new ArrayList<Pair<HighlightInfo.IntentionActionDescriptor, TextRange>>(); TextRange hostEditableRange = documentWindow.injectedToHost(editableRange); patched.quickFixActionRanges.add(Pair.create(descriptor, hostEditableRange)); } } } patched.fromInjection = true; out.add(patched); } } // finds the first nearest text range private static TextRange findNearestTextRange( final DocumentWindow documentWindow, final int startOffset) { TextRange textRange = null; for (RangeMarker marker : documentWindow.getHostRanges()) { TextRange curRange = ProperTextRange.create(marker); if (curRange.getStartOffset() > startOffset && textRange != null) break; textRange = curRange; } assert textRange != null; return textRange; } private void runHighlightVisitorsForInjected( @NotNull PsiFile injectedPsi, @NotNull final HighlightInfoHolder holder, @NotNull final ProgressIndicator progress) { HighlightVisitor[] visitors = getHighlightVisitors(); try { HighlightVisitor[] filtered = filterVisitors(visitors, injectedPsi); final List<PsiElement> elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength()); for (final HighlightVisitor visitor : filtered) { visitor.analyze( injectedPsi, true, holder, new Runnable() { @Override public void run() { for (PsiElement element : elements) { progress.checkCanceled(); visitor.visit(element); } } }); } } finally { incVisitorUsageCount(-1); } } private void highlightInjectedSyntax(final PsiFile injectedPsi, HighlightInfoHolder holder) { List<Trinity<IElementType, PsiLanguageInjectionHost, TextRange>> tokens = InjectedLanguageUtil.getHighlightTokens(injectedPsi); if (tokens == null) return; final Language injectedLanguage = injectedPsi.getLanguage(); Project project = injectedPsi.getProject(); SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter( injectedLanguage, project, injectedPsi.getVirtualFile()); final TextAttributes defaultAttrs = myGlobalScheme.getAttributes(HighlighterColors.TEXT); for (Trinity<IElementType, PsiLanguageInjectionHost, TextRange> token : tokens) { ProgressManager.checkCanceled(); IElementType tokenType = token.getFirst(); PsiLanguageInjectionHost injectionHost = token.getSecond(); TextRange textRange = token.getThird(); TextAttributesKey[] keys = syntaxHighlighter.getTokenHighlights(tokenType); if (textRange.getLength() == 0) continue; TextRange annRange = textRange.shiftRight(injectionHost.getTextRange().getStartOffset()); // force attribute colors to override host' ones TextAttributes attributes = null; for (TextAttributesKey key : keys) { TextAttributes attrs2 = myGlobalScheme.getAttributes(key); if (attrs2 != null) { attributes = attributes == null ? attrs2 : TextAttributes.merge(attributes, attrs2); } } TextAttributes forcedAttributes; if (attributes == null || attributes.isEmpty() || attributes.equals(defaultAttrs)) { forcedAttributes = TextAttributes.ERASE_MARKER; } else { Color back = attributes.getBackgroundColor() == null ? myGlobalScheme.getDefaultBackground() : attributes.getBackgroundColor(); Color fore = attributes.getForegroundColor() == null ? myGlobalScheme.getDefaultForeground() : attributes.getForegroundColor(); forcedAttributes = new TextAttributes( fore, back, attributes.getEffectColor(), attributes.getEffectType(), attributes.getFontType()); } HighlightInfo info = HighlightInfo.createHighlightInfo( HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT, annRange, null, null, forcedAttributes); holder.add(info); } } private boolean isWholeFileHighlighting() { return myUpdateAll && myStartOffset == 0 && myEndOffset == myDocument.getTextLength(); } @Override protected void applyInformationWithProgress() { myFile.putUserData(HAS_ERROR_ELEMENT, myHasErrorElement); myApplyCommand.run(); if (myUpdateAll) { reportErrorsToWolf(); } } @Override @NotNull public List<HighlightInfo> getInfos() { return new ArrayList<HighlightInfo>(myHighlights); } private void collectHighlights( @NotNull final List<PsiElement> elements1, @NotNull final Runnable after1, @NotNull final List<PsiElement> elements2, @NotNull final ProgressIndicator progress, @NotNull final HighlightVisitor[] visitors, @NotNull final Set<HighlightInfo> gotHighlights, final boolean forceHighlightParents) { final Set<PsiElement> skipParentsSet = new THashSet<PsiElement>(); // TODO - add color scheme to holder final HighlightInfoHolder holder = createInfoHolder(myFile); final int chunkSize = Math.max(1, (elements1.size() + elements2.size()) / 100); // one percent precision is enough final Runnable action = new Runnable() { @Override public void run() { //noinspection unchecked boolean failed = false; for (List<PsiElement> elements : new List[] {elements1, elements2}) { int nextLimit = chunkSize; for (int i = 0; i < elements.size(); i++) { PsiElement element = elements.get(i); progress.checkCanceled(); if (element != myFile && !skipParentsSet.isEmpty() && element.getFirstChild() != null && skipParentsSet.contains(element)) { skipParentsSet.add(element.getParent()); continue; } if (element instanceof PsiErrorElement) { myHasErrorElement = true; } holder.clear(); for (final HighlightVisitor visitor : visitors) { try { visitor.visit(element); } catch (ProcessCanceledException e) { throw e; } catch (IndexNotReadyException e) { throw e; } catch (WolfTheProblemSolverImpl.HaveGotErrorException e) { throw e; } catch (Exception e) { if (!failed) { LOG.error(e); } failed = true; } } if (i == nextLimit) { advanceProgress(chunkSize); nextLimit = i + chunkSize; } //noinspection ForLoopReplaceableByForEach for (int j = 0; j < holder.size(); j++) { final HighlightInfo info = holder.get(j); assert info != null; // have to filter out already obtained highlights if (!gotHighlights.add(info)) continue; boolean isError = info.getSeverity() == HighlightSeverity.ERROR; if (isError) { if (!forceHighlightParents) { skipParentsSet.add(element.getParent()); } myErrorFound = true; } myTransferToEDTQueue.offer(Pair.create(info, progress)); } } advanceProgress(elements.size() - (nextLimit - chunkSize)); if (elements == elements1) after1.run(); } } }; analyzeByVisitors(progress, visitors, holder, 0, action); } // private void collectHighlights(@NotNull final List<PsiElement> elements1, // @NotNull final Runnable after1, // @NotNull final List<PsiElement> elements2, // @NotNull final ProgressIndicator progress, // @NotNull final HighlightVisitor[] visitors, // @NotNull final Set<HighlightInfo> gotHighlights, // final boolean forceHighlightParents) { // final HighlightInfoHolder holder = createInfoHolder(myFile); // // final int chunkSize = Math.max(1, (elements1.size()+elements2.size()) / 100); // one percent // precision is enough // // final Runnable action = new Runnable() { // public void run() { // //noinspection unchecked // boolean failed = false; // PsiNodeTask task = createPsiTask(myFile); // JobSchedulerImpl.submitTask(task); // // for (List<PsiElement> elements : new List[]{elements1, elements2}) { // int nextLimit = chunkSize; // for (int i = 0; i < elements.size(); i++) { // PsiElement element = elements.get(i); // progress.checkCanceled(); // // if (element != myFile && !skipParentsSet.isEmpty() && element.getFirstChild() != null // && skipParentsSet.contains(element)) { // skipParentsSet.add(element.getParent()); // continue; // } // // if (element instanceof PsiErrorElement) { // myHasErrorElement = true; // } // kjlhkjh // if (i == nextLimit) { // advanceProgress(chunkSize); // nextLimit = i + chunkSize; // } // // } // advanceProgress(elements.size() - (nextLimit-chunkSize)); // if (elements == elements1) after1.run(); // } // } // // private PsiNodeTask createPsiTask(@NotNull PsiElement root) { // return new PsiNodeTask(root) { // @Override // public void onEnter(@NotNull PsiElement element) { // if (element instanceof PsiErrorElement) { // myHasErrorElement = true; // } // super.onEnter(element); // } // // @Override // protected PsiNodeTask forkNode(@NotNull PsiElement child) { // return createPsiTask(child); // } // // @Override // protected boolean highlight(PsiElement element) { // holder.clear(); // // for (final HighlightVisitor visitor : visitors) { // try { // visitor.visit(element); // } // catch (ProcessCanceledException e) { // throw e; // } // catch (IndexNotReadyException e) { // throw e; // } // catch (WolfTheProblemSolverImpl.HaveGotErrorException e) { // throw e; // } // catch (Exception e) { // LOG.error(e); // } // } // // //noinspection ForLoopReplaceableByForEach // for (int j = 0; j < holder.size(); j++) { // final HighlightInfo info = holder.get(j); // assert info != null; // // have to filter out already obtained highlights // if (!gotHighlights.add(info)) continue; // boolean isError = info.getSeverity() == HighlightSeverity.ERROR; // if (isError) { // if (!forceHighlightParents) { // skipParentsSet.add(element.getParent()); // } // myErrorFound = true; // } // myTransferToEDTQueue.offer(Pair.create(info, progress)); // } // return true; // } // }; // } // }; // // analyzeByVisitors(progress, visitors, holder, 0, action); // } private final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<TextRange, RangeMarker>(); private final TransferToEDTQueue<Pair<HighlightInfo, ProgressIndicator>> myTransferToEDTQueue = new TransferToEDTQueue<Pair<HighlightInfo, ProgressIndicator>>( "Apply highlighting results", new Processor<Pair<HighlightInfo, ProgressIndicator>>() { @Override public boolean process(Pair<HighlightInfo, ProgressIndicator> pair) { ApplicationManager.getApplication().assertIsDispatchThread(); ProgressIndicator indicator = pair.getSecond(); if (indicator.isCanceled()) { return false; } HighlightInfo info = pair.getFirst(); final EditorColorsScheme colorsScheme = getColorsScheme(); UpdateHighlightersUtil.addHighlighterToEditorIncrementally( myProject, myDocument, myFile, myStartOffset, myEndOffset, info, colorsScheme, Pass.UPDATE_ALL, ranges2markersCache); return true; } }, myProject.getDisposed(), 200); private void analyzeByVisitors( @NotNull final ProgressIndicator progress, @NotNull final HighlightVisitor[] visitors, @NotNull final HighlightInfoHolder holder, final int i, @NotNull final Runnable action) { if (i == visitors.length) { action.run(); } else { if (!visitors[i].analyze( myFile, myUpdateAll, holder, new Runnable() { @Override public void run() { analyzeByVisitors(progress, visitors, holder, i + 1, action); } })) { cancelAndRestartDaemonLater(progress, myProject, this); } } } private static HighlightVisitor[] filterVisitors( HighlightVisitor[] highlightVisitors, final PsiFile file) { final List<HighlightVisitor> visitors = new ArrayList<HighlightVisitor>(highlightVisitors.length); List<HighlightVisitor> list = Arrays.asList(highlightVisitors); for (HighlightVisitor visitor : DumbService.getInstance(file.getProject()).filterByDumbAwareness(list)) { if (visitor.suitableForFile(file)) visitors.add(visitor); } LOG.assertTrue(!visitors.isEmpty(), list); HighlightVisitor[] visitorArray = visitors.toArray(new HighlightVisitor[visitors.size()]); Arrays.sort(visitorArray, VISITOR_ORDER_COMPARATOR); return visitorArray; } static Void cancelAndRestartDaemonLater( ProgressIndicator progress, final Project project, TextEditorHighlightingPass pass) { PassExecutorService.log(progress, pass, "Cancel and restart"); progress.cancel(); ApplicationManager.getApplication() .invokeLater( new Runnable() { @Override public void run() { try { Thread.sleep(new Random().nextInt(100)); } catch (InterruptedException e) { LOG.error(e); } DaemonCodeAnalyzer.getInstance(project).restart(); } }, project.getDisposed()); throw new ProcessCanceledException(); } private boolean forceHighlightParents() { boolean forceHighlightParents = false; for (HighlightRangeExtension extension : Extensions.getExtensions(HighlightRangeExtension.EP_NAME)) { if (extension.isForceHighlightParents(myFile)) { forceHighlightParents = true; break; } } return forceHighlightParents; } protected HighlightInfoHolder createInfoHolder(final PsiFile file) { final HighlightInfoFilter[] filters = ApplicationManager.getApplication().getExtensions(HighlightInfoFilter.EXTENSION_POINT_NAME); return new HighlightInfoHolder(file, getColorsScheme(), filters); } private static void highlightTodos( @NotNull PsiFile file, @NotNull CharSequence text, int startOffset, int endOffset, @NotNull ProgressIndicator progress, @NotNull ProperTextRange priorityRange, @NotNull Collection<HighlightInfo> result, @NotNull Collection<HighlightInfo> outsideResult) { PsiSearchHelper helper = PsiSearchHelper.SERVICE.getInstance(file.getProject()); TodoItem[] todoItems = helper.findTodoItems(file, startOffset, endOffset); if (todoItems.length == 0) return; for (TodoItem todoItem : todoItems) { progress.checkCanceled(); TextRange range = todoItem.getTextRange(); String description = text.subSequence(range.getStartOffset(), range.getEndOffset()).toString(); TextAttributes attributes = todoItem.getPattern().getAttributes().getTextAttributes(); HighlightInfo info = HighlightInfo.createHighlightInfo( HighlightInfoType.TODO, range, description, description, attributes); assert info != null; if (priorityRange.containsRange(info.getStartOffset(), info.getEndOffset())) { result.add(info); } else { outsideResult.add(info); } } } private void reportErrorsToWolf() { if (!myFile.getViewProvider().isPhysical()) return; // e.g. errors in evaluate expression Project project = myFile.getProject(); if (!PsiManager.getInstance(project).isInProject(myFile)) return; // do not report problems in libraries VirtualFile file = myFile.getVirtualFile(); if (file == null) return; List<Problem> problems = convertToProblems(getInfos(), file, myHasErrorElement); WolfTheProblemSolver wolf = WolfTheProblemSolver.getInstance(project); boolean hasErrors = DaemonCodeAnalyzerImpl.hasErrors(project, getDocument()); if (!hasErrors || isWholeFileHighlighting()) { wolf.reportProblems(file, problems); } else { wolf.weHaveGotProblems(file, problems); } } @Override public double getProgress() { // do not show progress of visible highlighters update return myUpdateAll ? super.getProgress() : -1; } private static List<Problem> convertToProblems( final Collection<HighlightInfo> infos, final VirtualFile file, final boolean hasErrorElement) { List<Problem> problems = new SmartList<Problem>(); for (HighlightInfo info : infos) { if (info.getSeverity() == HighlightSeverity.ERROR) { Problem problem = new ProblemImpl(file, info, hasErrorElement); problems.add(problem); } } return problems; } @Override public String toString() { return super.toString() + " updateAll=" + myUpdateAll + " range=(" + myStartOffset + "," + myEndOffset + ")"; } public void setFailFastOnAcquireReadAction(boolean failFastOnAcquireReadAction) { myFailFastOnAcquireReadAction = failFastOnAcquireReadAction; } }
public abstract class PsiDocumentManagerBase extends PsiDocumentManager implements DocumentListener { static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl"); private static final Key<Document> HARD_REF_TO_DOCUMENT = Key.create("HARD_REFERENCE_TO_DOCUMENT"); private static final Key<PsiFile> HARD_REF_TO_PSI = Key.create("HARD_REFERENCE_TO_PSI"); private static final Key<List<Runnable>> ACTION_AFTER_COMMIT = Key.create("ACTION_AFTER_COMMIT"); protected final Project myProject; private final PsiManager myPsiManager; private final DocumentCommitProcessor myDocumentCommitProcessor; protected final Set<Document> myUncommittedDocuments = ContainerUtil.newConcurrentSet(); private final Map<Document, Pair<CharSequence, Long>> myLastCommittedTexts = ContainerUtil.newConcurrentMap(); protected boolean myStopTrackingDocuments; protected boolean myPerformBackgroundCommit = true; private volatile boolean myIsCommitInProgress; private final PsiToDocumentSynchronizer mySynchronizer; private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); protected PsiDocumentManagerBase( @NotNull final Project project, @NotNull PsiManager psiManager, @NotNull MessageBus bus, @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) { myProject = project; myPsiManager = psiManager; myDocumentCommitProcessor = documentCommitProcessor; mySynchronizer = new PsiToDocumentSynchronizer(this, bus); myPsiManager.addPsiTreeChangeListener(mySynchronizer); bus.connect() .subscribe( PsiDocumentTransactionListener.TOPIC, new PsiDocumentTransactionListener() { @Override public void transactionStarted(@NotNull Document document, @NotNull PsiFile file) { myUncommittedDocuments.remove(document); } @Override public void transactionCompleted(@NotNull Document document, @NotNull PsiFile file) {} }); } @Override @Nullable public PsiFile getPsiFile(@NotNull Document document) { final PsiFile userData = document.getUserData(HARD_REF_TO_PSI); if (userData != null) return userData; PsiFile psiFile = getCachedPsiFile(document); if (psiFile != null) return psiFile; final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; psiFile = getPsiFile(virtualFile); if (psiFile == null) return null; fireFileCreated(document, psiFile); return psiFile; } public static void cachePsi(@NotNull Document document, @Nullable PsiFile file) { document.putUserData(HARD_REF_TO_PSI, file); } @Override public PsiFile getCachedPsiFile(@NotNull Document document) { final PsiFile userData = document.getUserData(HARD_REF_TO_PSI); if (userData != null) return userData; final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; return getCachedPsiFile(virtualFile); } @Nullable FileViewProvider getCachedViewProvider(@NotNull Document document) { final VirtualFile virtualFile = getVirtualFile(document); if (virtualFile == null) return null; return getCachedViewProvider(virtualFile); } private FileViewProvider getCachedViewProvider(@NotNull VirtualFile virtualFile) { return ((PsiManagerEx) myPsiManager).getFileManager().findCachedViewProvider(virtualFile); } private static VirtualFile getVirtualFile(@NotNull Document document) { final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; return virtualFile; } @Nullable PsiFile getCachedPsiFile(@NotNull VirtualFile virtualFile) { return ((PsiManagerEx) myPsiManager).getFileManager().getCachedPsiFile(virtualFile); } @Nullable private PsiFile getPsiFile(@NotNull VirtualFile virtualFile) { return ((PsiManagerEx) myPsiManager).getFileManager().findFile(virtualFile); } @Nullable @Override public Document getDocument(@NotNull PsiFile file) { if (file instanceof PsiBinaryFile) return null; Document document = getCachedDocument(file); if (document != null) { if (!file.getViewProvider().isPhysical() && document.getUserData(HARD_REF_TO_PSI) == null) { PsiUtilCore.ensureValid(file); cachePsi(document, file); } return document; } FileViewProvider viewProvider = file.getViewProvider(); if (!viewProvider.isEventSystemEnabled()) return null; document = FileDocumentManager.getInstance().getDocument(viewProvider.getVirtualFile()); if (document != null) { if (document.getTextLength() != file.getTextLength()) { String message = "Document/PSI mismatch: " + file + " (" + file.getClass() + "); physical=" + viewProvider.isPhysical(); if (document.getTextLength() + file.getTextLength() < 8096) { message += "\n=== document ===\n" + document.getText() + "\n=== PSI ===\n" + file.getText(); } throw new AssertionError(message); } if (!viewProvider.isPhysical()) { PsiUtilCore.ensureValid(file); cachePsi(document, file); file.putUserData(HARD_REF_TO_DOCUMENT, document); } } return document; } @Override public Document getCachedDocument(@NotNull PsiFile file) { if (!file.isPhysical()) return null; VirtualFile vFile = file.getViewProvider().getVirtualFile(); return FileDocumentManager.getInstance().getCachedDocument(vFile); } @Override public void commitAllDocuments() { ApplicationManager.getApplication().assertIsDispatchThread(); if (myUncommittedDocuments.isEmpty()) return; final Document[] documents = getUncommittedDocuments(); for (Document document : documents) { commitDocument(document); } LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments); } @Override public void performForCommittedDocument( @NotNull final Document doc, @NotNull final Runnable action) { final Document document = doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc; if (isCommitted(document)) { action.run(); } else { addRunOnCommit(document, action); } } private final Map<Object, Runnable> actionsWhenAllDocumentsAreCommitted = new LinkedHashMap<Object, Runnable>(); // accessed from EDT only private static final Object PERFORM_ALWAYS_KEY = new Object() { @Override @NonNls public String toString() { return "PERFORM_ALWAYS"; } }; /** * Cancel previously registered action and schedules (new) action to be executed when all * documents are committed. * * @param key the (unique) id of the action. * @param action The action to be executed after automatic commit. This action will overwrite any * action which was registered under this key earlier. The action will be executed in EDT. * @return true if action has been run immediately, or false if action was scheduled for execution * later. */ public boolean cancelAndRunWhenAllCommitted( @NonNls @NotNull Object key, @NotNull final Runnable action) { ApplicationManager.getApplication().assertIsDispatchThread(); if (myProject.isDisposed()) { action.run(); return true; } if (myUncommittedDocuments.isEmpty()) { action.run(); if (!hasUncommitedDocuments()) { assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted; } return true; } actionsWhenAllDocumentsAreCommitted.put(key, action); return false; } public static void addRunOnCommit(@NotNull Document document, @NotNull Runnable action) { synchronized (ACTION_AFTER_COMMIT) { List<Runnable> list = document.getUserData(ACTION_AFTER_COMMIT); if (list == null) { document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<Runnable>()); } list.add(action); } } @Override public void commitDocument(@NotNull final Document doc) { final Document document = doc instanceof DocumentWindow ? ((DocumentWindow) doc).getDelegate() : doc; if (!isCommitted(document)) { doCommit(document); } } // public for Upsource public boolean finishCommit( @NotNull final Document document, @NotNull final List<Processor<Document>> finishProcessors, final boolean synchronously, @NotNull final Object reason) { assert !myProject.isDisposed() : "Already disposed"; final boolean[] ok = {true}; ApplicationManager.getApplication() .runWriteAction( new CommitToPsiFileAction(document, myProject) { @Override public void run() { ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously); } }); if (ok[0]) { // otherwise changes maybe not synced to the document yet, and injectors will crash if (!mySynchronizer.isDocumentAffectedByTransactions(document)) { InjectedLanguageManager.getInstance(myProject).startRunInjectors(document, synchronously); } // run after commit actions outside write action runAfterCommitActions(document); if (DebugUtil.DO_EXPENSIVE_CHECKS && !ApplicationInfoImpl.isInPerformanceTest()) { checkAllElementsValid(document, reason); } } return ok[0]; } protected boolean finishCommitInWriteAction( @NotNull final Document document, @NotNull final List<Processor<Document>> finishProcessors, final boolean synchronously) { if (myProject.isDisposed()) return false; assert !(document instanceof DocumentWindow); myIsCommitInProgress = true; boolean success = true; try { final FileViewProvider viewProvider = getCachedViewProvider(document); if (viewProvider != null) { for (Processor<Document> finishRunnable : finishProcessors) { success = finishRunnable.process(document); if (synchronously) { assert success : finishRunnable + " in " + finishProcessors; } if (!success) { break; } } if (success) { myLastCommittedTexts.remove(document); viewProvider.contentsSynchronized(); } } else { handleCommitWithoutPsi(document); } } finally { myDocumentCommitProcessor.log( "in PDI.finishDoc: ", null, synchronously, success, myUncommittedDocuments); if (success) { myUncommittedDocuments.remove(document); myDocumentCommitProcessor.log( "in PDI.finishDoc: removed doc", null, synchronously, success, myUncommittedDocuments); } myIsCommitInProgress = false; myDocumentCommitProcessor.log( "in PDI.finishDoc: exit", null, synchronously, success, myUncommittedDocuments); } return success; } private void checkAllElementsValid(@NotNull Document document, @NotNull final Object reason) { final PsiFile psiFile = getCachedPsiFile(document); if (psiFile != null) { psiFile.accept( new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { if (!element.isValid()) { throw new AssertionError( "Commit to '" + psiFile.getVirtualFile() + "' has led to invalid element: " + element + "; Reason: '" + reason + "'"); } } }); } } private void doCommit(@NotNull final Document document) { assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener"; ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged() if (getSynchronizer().isDocumentAffectedByTransactions(document)) return; myIsCommitInProgress = true; try { myDocumentCommitProcessor.commitSynchronously(document, myProject); } finally { myIsCommitInProgress = false; } assert !isInUncommittedSet(document) : "Document :" + document; } }); } @Override public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) { final Ref<T> ref = Ref.create(null); commitAndRunReadAction( new Runnable() { @Override public void run() { ref.set(computation.compute()); } }); return ref.get(); } @Override public void reparseFiles(@NotNull Collection<VirtualFile> files, boolean includeOpenFiles) { FileContentUtilCore.reparseFiles(files); } @Override public void commitAndRunReadAction(@NotNull final Runnable runnable) { final Application application = ApplicationManager.getApplication(); if (SwingUtilities.isEventDispatchThread()) { commitAllDocuments(); runnable.run(); } else { if (ApplicationManager.getApplication().isReadAccessAllowed()) { LOG.error( "Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock otherwise. " + Thread.currentThread()); } final Semaphore s1 = new Semaphore(); final Semaphore s2 = new Semaphore(); final boolean[] committed = {false}; application.runReadAction( new Runnable() { @Override public void run() { if (myUncommittedDocuments.isEmpty()) { runnable.run(); committed[0] = true; } else { s1.down(); s2.down(); final Runnable commitRunnable = new Runnable() { @Override public void run() { commitAllDocuments(); s1.up(); s2.waitFor(); } }; final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator(); if (progressIndicator == null) { ApplicationManager.getApplication().invokeLater(commitRunnable); } else { ApplicationManager.getApplication() .invokeLater(commitRunnable, progressIndicator.getModalityState()); } } } }); if (!committed[0]) { s1.waitFor(); application.runReadAction( new Runnable() { @Override public void run() { s2.up(); runnable.run(); } }); } } } /** * Schedules action to be executed when all documents are committed. * * @return true if action has been run immediately, or false if action was scheduled for execution * later. */ @Override public boolean performWhenAllCommitted(@NotNull final Runnable action) { ApplicationManager.getApplication().assertIsDispatchThread(); assert !myProject.isDisposed() : "Already disposed: " + myProject; if (myUncommittedDocuments.isEmpty()) { action.run(); return true; } CompositeRunnable actions = (CompositeRunnable) actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY); if (actions == null) { actions = new CompositeRunnable(); actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions); } actions.add(action); myDocumentCommitProcessor.log( "PDI: added performWhenAllCommitted", null, false, action, myUncommittedDocuments); return false; } private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable { @Override public void run() { for (Runnable runnable : this) { runnable.run(); } } } private void runAfterCommitActions(@NotNull Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); List<Runnable> list; synchronized (ACTION_AFTER_COMMIT) { list = document.getUserData(ACTION_AFTER_COMMIT); if (list != null) { list = new ArrayList<Runnable>(list); document.putUserData(ACTION_AFTER_COMMIT, null); } } if (list != null) { for (final Runnable runnable : list) { runnable.run(); } } if (!hasUncommitedDocuments() && !actionsWhenAllDocumentsAreCommitted.isEmpty()) { List<Object> keys = new ArrayList<Object>(actionsWhenAllDocumentsAreCommitted.keySet()); for (Object key : keys) { try { Runnable action = actionsWhenAllDocumentsAreCommitted.remove(key); myDocumentCommitProcessor.log( "Running after commit runnable: ", null, false, key, action); action.run(); } catch (Throwable e) { LOG.error(e); } } } } @Override public void addListener(@NotNull Listener listener) { myListeners.add(listener); } @Override public void removeListener(@NotNull Listener listener) { myListeners.remove(listener); } @Override public boolean isDocumentBlockedByPsi(@NotNull Document doc) { return false; } @Override public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) {} void fireDocumentCreated(@NotNull Document document, PsiFile file) { for (Listener listener : myListeners) { listener.documentCreated(document, file); } } private void fireFileCreated(@NotNull Document document, @NotNull PsiFile file) { for (Listener listener : myListeners) { listener.fileCreated(file, document); } } @Override @NotNull public CharSequence getLastCommittedText(@NotNull Document document) { Pair<CharSequence, Long> pair = myLastCommittedTexts.get(document); return pair != null ? pair.first : document.getImmutableCharSequence(); } @Override public long getLastCommittedStamp(@NotNull Document document) { Pair<CharSequence, Long> pair = myLastCommittedTexts.get(document); return pair != null ? pair.second : document.getModificationStamp(); } @Override @NotNull public Document[] getUncommittedDocuments() { ApplicationManager.getApplication().assertIsDispatchThread(); Document[] documents = myUncommittedDocuments.toArray(new Document[myUncommittedDocuments.size()]); return ArrayUtil.stripTrailingNulls(documents); } boolean isInUncommittedSet(@NotNull Document document) { if (document instanceof DocumentWindow) return isInUncommittedSet(((DocumentWindow) document).getDelegate()); return myUncommittedDocuments.contains(document); } @Override public boolean isUncommited(@NotNull Document document) { return !isCommitted(document); } @Override public boolean isCommitted(@NotNull Document document) { if (document instanceof DocumentWindow) return isCommitted(((DocumentWindow) document).getDelegate()); if (getSynchronizer().isInSynchronization(document)) return true; return !((DocumentEx) document).isInEventsHandling() && !isInUncommittedSet(document); } @Override public boolean hasUncommitedDocuments() { return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty(); } @Override public void beforeDocumentChange(@NotNull DocumentEvent event) { if (myStopTrackingDocuments) return; final Document document = event.getDocument(); if (!(document instanceof DocumentWindow) && !myLastCommittedTexts.containsKey(document)) { myLastCommittedTexts.put( document, Pair.create(document.getImmutableCharSequence(), document.getModificationStamp())); } VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); boolean isRelevant = virtualFile != null && isRelevant(virtualFile); final FileViewProvider viewProvider = getCachedViewProvider(document); boolean inMyProject = viewProvider != null && viewProvider.getManager() == myPsiManager; if (!isRelevant || !inMyProject) { return; } final List<PsiFile> files = viewProvider.getAllFiles(); PsiFile psiCause = null; for (PsiFile file : files) { if (file == null) { throw new AssertionError( "View provider " + viewProvider + " (" + viewProvider.getClass() + ") returned null in its files array: " + files + " for file " + viewProvider.getVirtualFile()); } if (mySynchronizer.isInsideAtomicChange(file)) { psiCause = file; } } if (psiCause == null) { beforeDocumentChangeOnUnlockedDocument(viewProvider); } ((SingleRootFileViewProvider) viewProvider).beforeDocumentChanged(psiCause); } protected void beforeDocumentChangeOnUnlockedDocument( @NotNull final FileViewProvider viewProvider) {} @Override public void documentChanged(DocumentEvent event) { if (myStopTrackingDocuments) return; final Document document = event.getDocument(); VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); boolean isRelevant = virtualFile != null && isRelevant(virtualFile); final FileViewProvider viewProvider = getCachedViewProvider(document); if (viewProvider == null) { handleCommitWithoutPsi(document); return; } boolean inMyProject = viewProvider.getManager() == myPsiManager; if (!isRelevant || !inMyProject) { myLastCommittedTexts.remove(document); return; } ApplicationManager.getApplication().assertWriteAccessAllowed(); final List<PsiFile> files = viewProvider.getAllFiles(); boolean commitNecessary = true; for (PsiFile file : files) { if (mySynchronizer.isInsideAtomicChange(file)) { commitNecessary = false; continue; } assert file instanceof PsiFileImpl || "mock.file".equals(file.getName()) && ApplicationManager.getApplication().isUnitTestMode() : event + "; file=" + file + "; allFiles=" + files + "; viewProvider=" + viewProvider; } boolean forceCommit = ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class) && (SystemProperties.getBooleanProperty("idea.force.commit.on.external.change", false) || ApplicationManager.getApplication().isHeadlessEnvironment() && !ApplicationManager.getApplication().isUnitTestMode()); // Consider that it's worth to perform complete re-parse instead of merge if the whole document // text is replaced and // current document lines number is roughly above 5000. This makes sense in situations when // external change is performed // for the huge file (that causes the whole document to be reloaded and 'merge' way takes a // while to complete). if (event.isWholeTextReplaced() && document.getTextLength() > 100000) { document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE); } if (commitNecessary) { assert !(document instanceof DocumentWindow); myUncommittedDocuments.add(document); myDocumentCommitProcessor.log( "added uncommitted doc", null, false, myProject, document, ((DocumentEx) document).isInBulkUpdate()); if (forceCommit) { commitDocument(document); } else if (!((DocumentEx) document).isInBulkUpdate() && myPerformBackgroundCommit) { myDocumentCommitProcessor.commitAsynchronously(myProject, document, event); } } else { myLastCommittedTexts.remove(document); } } void handleCommitWithoutPsi(@NotNull Document document) { final Pair<CharSequence, Long> prevPair = myLastCommittedTexts.remove(document); if (prevPair == null) { return; } if (!myProject.isInitialized() || myProject.isDisposed()) { return; } myUncommittedDocuments.remove(document); VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !FileIndexFacade.getInstance(myProject).isInContent(virtualFile)) { return; } final PsiFile psiFile = getPsiFile(document); if (psiFile == null) { return; } // we can end up outside write action here if the document has forUseInNonAWTThread=true ApplicationManager.getApplication() .runWriteAction( new ExternalChangeAction() { @Override public void run() { psiFile.getViewProvider().beforeContentsSynchronized(); synchronized (PsiLock.LOCK) { final int oldLength = prevPair.first.length(); PsiManagerImpl manager = (PsiManagerImpl) psiFile.getManager(); BlockSupportImpl.sendBeforeChildrenChangeEvent(manager, psiFile, true); BlockSupportImpl.sendBeforeChildrenChangeEvent(manager, psiFile, false); if (psiFile instanceof PsiFileImpl) { ((PsiFileImpl) psiFile).onContentReload(); } BlockSupportImpl.sendAfterChildrenChangedEvent( manager, psiFile, oldLength, false); BlockSupportImpl.sendAfterChildrenChangedEvent(manager, psiFile, oldLength, true); } psiFile.getViewProvider().contentsSynchronized(); } }); } private boolean isRelevant(@NotNull VirtualFile virtualFile) { return !virtualFile.getFileType().isBinary() && !myProject.isDisposed(); } public static boolean checkConsistency(@NotNull PsiFile psiFile, @NotNull Document document) { // todo hack if (psiFile.getVirtualFile() == null) return true; CharSequence editorText = document.getCharsSequence(); int documentLength = document.getTextLength(); if (psiFile.textMatches(editorText)) { LOG.assertTrue(psiFile.getTextLength() == documentLength); return true; } char[] fileText = psiFile.textToCharArray(); @SuppressWarnings("NonConstantStringShouldBeStringBuffer") @NonNls String error = "File '" + psiFile.getName() + "' text mismatch after reparse. " + "File length=" + fileText.length + "; Doc length=" + documentLength + "\n"; int i = 0; for (; i < documentLength; i++) { if (i >= fileText.length) { error += "editorText.length > psiText.length i=" + i + "\n"; break; } if (i >= editorText.length()) { error += "editorText.length > psiText.length i=" + i + "\n"; break; } if (editorText.charAt(i) != fileText[i]) { error += "first unequal char i=" + i + "\n"; break; } } // error += "*********************************************" + "\n"; // if (i <= 500){ // error += "Equal part:" + editorText.subSequence(0, i) + "\n"; // } // else{ // error += "Equal part start:\n" + editorText.subSequence(0, 200) + "\n"; // error += "................................................" + "\n"; // error += "................................................" + "\n"; // error += "................................................" + "\n"; // error += "Equal part end:\n" + editorText.subSequence(i - 200, i) + "\n"; // } error += "*********************************************" + "\n"; error += "Editor Text tail:(" + (documentLength - i) + ")\n"; // + editorText.subSequence(i, Math.min(i + 300, documentLength)) + "\n"; error += "*********************************************" + "\n"; error += "Psi Text tail:(" + (fileText.length - i) + ")\n"; error += "*********************************************" + "\n"; if (document instanceof DocumentWindow) { error += "doc: '" + document.getText() + "'\n"; error += "psi: '" + psiFile.getText() + "'\n"; error += "ast: '" + psiFile.getNode().getText() + "'\n"; error += psiFile.getLanguage() + "\n"; PsiElement context = InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile); if (context != null) { error += "context: " + context + "; text: '" + context.getText() + "'\n"; error += "context file: " + context.getContainingFile() + "\n"; } error += "document window ranges: " + Arrays.asList(((DocumentWindow) document).getHostRanges()) + "\n"; } LOG.error(error); // document.replaceString(0, documentLength, psiFile.getText()); return false; } @TestOnly public void clearUncommittedDocuments() { myLastCommittedTexts.clear(); myUncommittedDocuments.clear(); mySynchronizer.cleanupForNextTest(); } @TestOnly public void disableBackgroundCommit(@NotNull Disposable parentDisposable) { assert myPerformBackgroundCommit; myPerformBackgroundCommit = false; Disposer.register( parentDisposable, new Disposable() { @Override public void dispose() { myPerformBackgroundCommit = true; } }); } @NotNull public PsiToDocumentSynchronizer getSynchronizer() { return mySynchronizer; } }
public class JavaCompletionUtil { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.JavaCompletionUtil"); public static final Key<PairFunction<PsiExpression, CompletionParameters, PsiType>> DYNAMIC_TYPE_EVALUATOR = Key.create("DYNAMIC_TYPE_EVALUATOR"); private static final Key<PsiType> QUALIFIER_TYPE_ATTR = Key.create("qualifierType"); // SmartPsiElementPointer to PsiType of "qualifier" public static final OffsetKey LPAREN_OFFSET = OffsetKey.create("lparen"); public static final OffsetKey RPAREN_OFFSET = OffsetKey.create("rparen"); public static final OffsetKey ARG_LIST_END_OFFSET = OffsetKey.create("argListEnd"); static final NullableLazyKey<ExpectedTypeInfo[], CompletionLocation> EXPECTED_TYPES = NullableLazyKey.create( "expectedTypes", new NullableFunction<CompletionLocation, ExpectedTypeInfo[]>() { @Override @Nullable public ExpectedTypeInfo[] fun(final CompletionLocation location) { if (PsiJavaPatterns.psiElement() .beforeLeaf(PsiJavaPatterns.psiElement().withText(".")) .accepts(location.getCompletionParameters().getPosition())) { return ExpectedTypeInfo.EMPTY_ARRAY; } return JavaSmartCompletionContributor.getExpectedTypes( location.getCompletionParameters()); } }); private static final ElementPattern<PsiElement> LEFT_PAREN = psiElement(JavaTokenType.LPARENTH) .andOr( psiElement().withParent(PsiExpressionList.class), psiElement().afterLeaf(".", PsiKeyword.NEW)); public static final Key<Boolean> SUPER_METHOD_PARAMETERS = Key.create("SUPER_METHOD_PARAMETERS"); @Nullable public static Set<PsiType> getExpectedTypes(final CompletionParameters parameters) { final PsiExpression expr = PsiTreeUtil.getContextOfType(parameters.getPosition(), PsiExpression.class, true); if (expr != null) { final Set<PsiType> set = new THashSet<PsiType>(); for (final ExpectedTypeInfo expectedInfo : JavaSmartCompletionContributor.getExpectedTypes(parameters)) { set.add(expectedInfo.getType()); } return set; } return null; } private static final Key<List<SmartPsiElementPointer<PsiMethod>>> ALL_METHODS_ATTRIBUTE = Key.create("allMethods"); public static PsiType getQualifierType(LookupItem item) { return item.getUserData(QUALIFIER_TYPE_ATTR); } public static void completeVariableNameForRefactoring( Project project, Set<LookupElement> set, String prefix, PsiType varType, VariableKind varKind) { final CamelHumpMatcher camelHumpMatcher = new CamelHumpMatcher(prefix); JavaMemberNameCompletionContributor.completeVariableNameForRefactoring( project, set, camelHumpMatcher, varType, varKind, true, false); } public static void putAllMethods(LookupElement item, List<PsiMethod> methods) { item.putUserData( ALL_METHODS_ATTRIBUTE, ContainerUtil.map( methods, new Function<PsiMethod, SmartPsiElementPointer<PsiMethod>>() { @Override public SmartPsiElementPointer<PsiMethod> fun(PsiMethod method) { return SmartPointerManager.getInstance(method.getProject()) .createSmartPsiElementPointer(method); } })); } public static List<PsiMethod> getAllMethods(LookupElement item) { List<SmartPsiElementPointer<PsiMethod>> pointers = item.getUserData(ALL_METHODS_ATTRIBUTE); if (pointers == null) return null; return ContainerUtil.mapNotNull( pointers, new Function<SmartPsiElementPointer<PsiMethod>, PsiMethod>() { @Override public PsiMethod fun(SmartPsiElementPointer<PsiMethod> pointer) { return pointer.getElement(); } }); } public static String[] completeVariableNameForRefactoring( JavaCodeStyleManager codeStyleManager, @Nullable final PsiType varType, final VariableKind varKind, SuggestedNameInfo suggestedNameInfo) { return JavaMemberNameCompletionContributor.completeVariableNameForRefactoring( codeStyleManager, new CamelHumpMatcher(""), varType, varKind, suggestedNameInfo, true, false); } public static boolean isInExcludedPackage( @NotNull final PsiMember member, boolean allowInstanceInnerClasses) { final String name = PsiUtil.getMemberQualifiedName(member); if (name == null) return false; if (!member.hasModifierProperty(PsiModifier.STATIC)) { if (member instanceof PsiMethod || member instanceof PsiField) { return false; } if (allowInstanceInnerClasses && member instanceof PsiClass && member.getContainingClass() != null) { return false; } } return ProjectCodeInsightSettings.getSettings(member.getProject()).isExcluded(name); } @SuppressWarnings({"unchecked"}) @NotNull public static <T extends PsiType> T originalize(@NotNull T type) { if (!type.isValid()) { return type; } T result = new PsiTypeMapper() { private final Set<PsiClassType> myVisited = ContainerUtil.newIdentityTroveSet(); @Override public PsiType visitClassType(final PsiClassType classType) { if (!myVisited.add(classType)) return classType; final PsiClassType.ClassResolveResult classResolveResult = classType.resolveGenerics(); final PsiClass psiClass = classResolveResult.getElement(); final PsiSubstitutor substitutor = classResolveResult.getSubstitutor(); if (psiClass == null) return classType; return new PsiImmediateClassType( CompletionUtil.getOriginalOrSelf(psiClass), originalizeSubstitutor(substitutor)); } private PsiSubstitutor originalizeSubstitutor(final PsiSubstitutor substitutor) { PsiSubstitutor originalSubstitutor = PsiSubstitutor.EMPTY; for (final Map.Entry<PsiTypeParameter, PsiType> entry : substitutor.getSubstitutionMap().entrySet()) { final PsiType value = entry.getValue(); originalSubstitutor = originalSubstitutor.put( CompletionUtil.getOriginalOrSelf(entry.getKey()), value == null ? null : mapType(value)); } return originalSubstitutor; } @Override public PsiType visitType(PsiType type) { return type; } }.mapType(type); if (result == null) { throw new AssertionError("Null result for type " + type + " of class " + type.getClass()); } return result; } public static void initOffsets(final PsiFile file, final OffsetMap offsetMap) { int offset = Math.max( offsetMap.getOffset(CompletionInitializationContext.SELECTION_END_OFFSET), offsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET)); PsiElement element = file.findElementAt(offset); if (element instanceof PsiWhiteSpace && (!element.textContains('\n') || CodeStyleSettingsManager.getSettings(file.getProject()) .getCommonSettings(JavaLanguage.INSTANCE) .METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE)) { element = file.findElementAt(element.getTextRange().getEndOffset()); } if (element == null) return; if (LEFT_PAREN.accepts(element)) { offsetMap.addOffset(LPAREN_OFFSET, element.getTextRange().getStartOffset()); PsiElement list = element.getParent(); PsiElement last = list.getLastChild(); if (last instanceof PsiJavaToken && ((PsiJavaToken) last).getTokenType() == JavaTokenType.RPARENTH) { offsetMap.addOffset(RPAREN_OFFSET, last.getTextRange().getStartOffset()); } offsetMap.addOffset(ARG_LIST_END_OFFSET, list.getTextRange().getEndOffset()); } } public static void resetParensInfo(final OffsetMap offsetMap) { offsetMap.removeOffset(LPAREN_OFFSET); offsetMap.removeOffset(RPAREN_OFFSET); offsetMap.removeOffset(ARG_LIST_END_OFFSET); offsetMap.removeOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET); } @Nullable public static List<? extends PsiElement> getAllPsiElements(final LookupElement item) { List<PsiMethod> allMethods = getAllMethods(item); if (allMethods != null) return allMethods; if (item.getObject() instanceof PsiElement) return Collections.singletonList((PsiElement) item.getObject()); return null; } @Nullable public static PsiType getLookupElementType(final LookupElement element) { TypedLookupItem typed = element.as(TypedLookupItem.CLASS_CONDITION_KEY); return typed != null ? typed.getType() : null; } @Nullable public static PsiType getQualifiedMemberReferenceType( @Nullable PsiType qualifierType, @NotNull final PsiMember member) { final Ref<PsiSubstitutor> subst = Ref.create(PsiSubstitutor.EMPTY); class MyProcessor extends BaseScopeProcessor implements NameHint, ElementClassHint { @Override public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) { if (element == member) { subst.set(state.get(PsiSubstitutor.KEY)); } return true; } @Override public String getName(@NotNull ResolveState state) { return member.getName(); } @Override public boolean shouldProcess(DeclarationKind kind) { return member instanceof PsiEnumConstant ? kind == DeclarationKind.ENUM_CONST : member instanceof PsiField ? kind == DeclarationKind.FIELD : kind == DeclarationKind.METHOD; } @Override public <T> T getHint(@NotNull Key<T> hintKey) { return hintKey == NameHint.KEY || hintKey == ElementClassHint.KEY ? (T) this : null; } } PsiScopesUtil.processTypeDeclarations(qualifierType, member, new MyProcessor()); PsiType rawType = member instanceof PsiField ? ((PsiField) member).getType() : member instanceof PsiMethod ? ((PsiMethod) member).getReturnType() : JavaPsiFacade.getElementFactory(member.getProject()) .createType((PsiClass) member); return subst.get().substitute(rawType); } public static Set<LookupElement> processJavaReference( PsiElement element, PsiJavaReference javaReference, ElementFilter elementFilter, JavaCompletionProcessor.Options options, final PrefixMatcher matcher, CompletionParameters parameters) { final Set<LookupElement> set = new LinkedHashSet<LookupElement>(); final Condition<String> nameCondition = new Condition<String>() { @Override public boolean value(String s) { return matcher.prefixMatches(s); } }; PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class); boolean checkInitialized = parameters.getInvocationCount() <= 1 && call != null && PsiKeyword.SUPER.equals(call.getMethodExpression().getText()); final JavaCompletionProcessor processor = new JavaCompletionProcessor( element, elementFilter, options.withInitialized(checkInitialized), nameCondition); final PsiType plainQualifier = processor.getQualifierType(); PsiType qualifierType = plainQualifier; PsiType runtimeQualifier = getQualifierCastType(javaReference, parameters); if (runtimeQualifier != null) { PsiType composite = qualifierType == null ? runtimeQualifier : PsiIntersectionType.createIntersection(qualifierType, runtimeQualifier); PsiElement ctx = createContextWithXxxVariable(element, composite); javaReference = (PsiReferenceExpression) JavaPsiFacade.getElementFactory(element.getProject()) .createExpressionFromText("xxx.xxx", ctx); qualifierType = runtimeQualifier; processor.setQualifierType(qualifierType); } javaReference.processVariants(processor); final PsiTypeLookupItem castItem = runtimeQualifier == null ? null : PsiTypeLookupItem.createLookupItem( runtimeQualifier, (PsiReferenceExpression) javaReference); final boolean pkgContext = inSomePackage(element); final Set<PsiMember> mentioned = new THashSet<PsiMember>(); for (CompletionElement completionElement : processor.getResults()) { for (LookupElement item : createLookupElements(completionElement, javaReference)) { item.putUserData(QUALIFIER_TYPE_ATTR, qualifierType); final Object o = item.getObject(); if (o instanceof PsiClass && !isSourceLevelAccessible(element, (PsiClass) o, pkgContext)) { continue; } if (o instanceof PsiMember) { if (isInExcludedPackage((PsiMember) o, true)) { continue; } mentioned.add(CompletionUtil.getOriginalOrSelf((PsiMember) o)); } set.add( highlightIfNeeded( qualifierType, castQualifier(item, castItem, plainQualifier, processor), o, element)); } } if (javaReference instanceof PsiJavaCodeReferenceElement && !((PsiJavaCodeReferenceElement) javaReference).isQualified()) { final StaticMemberProcessor memberProcessor = new JavaStaticMemberProcessor(parameters); memberProcessor.processMembersOfRegisteredClasses( matcher, new PairConsumer<PsiMember, PsiClass>() { @Override public void consume(PsiMember member, PsiClass psiClass) { if (!mentioned.contains(member) && processor.satisfies(member, ResolveState.initial())) { set.add(memberProcessor.createLookupElement(member, psiClass, true)); } } }); } return set; } @Nullable private static PsiType getQualifierCastType( PsiJavaReference javaReference, CompletionParameters parameters) { if (javaReference instanceof PsiReferenceExpression) { final PsiReferenceExpression refExpr = (PsiReferenceExpression) javaReference; final PsiExpression qualifier = refExpr.getQualifierExpression(); if (qualifier != null) { final Project project = qualifier.getProject(); PsiType type = null; final PairFunction<PsiExpression, CompletionParameters, PsiType> evaluator = refExpr.getContainingFile().getCopyableUserData(DYNAMIC_TYPE_EVALUATOR); if (evaluator != null) { type = evaluator.fun(qualifier, parameters); } if (type == null) { type = GuessManager.getInstance(project).getControlFlowExpressionType(qualifier); } return type; } } return null; } @NotNull private static LookupElement castQualifier( @NotNull LookupElement item, @Nullable final PsiTypeLookupItem castTypeItem, @Nullable PsiType plainQualifier, JavaCompletionProcessor processor) { if (castTypeItem == null) { return item; } if (plainQualifier != null) { Object o = item.getObject(); if (o instanceof PsiMethod) { PsiType castType = castTypeItem.getType(); if (plainQualifier instanceof PsiClassType && castType instanceof PsiClassType) { PsiMethod method = (PsiMethod) o; PsiClassType.ClassResolveResult plainResult = ((PsiClassType) plainQualifier).resolveGenerics(); PsiClass plainClass = plainResult.getElement(); if (plainClass != null && plainClass.findMethodBySignature(method, true) != null) { PsiClass castClass = ((PsiClassType) castType).resolveGenerics().getElement(); if (castClass == null || !castClass.isInheritor(plainClass, true)) { return item; } PsiSubstitutor plainSub = plainResult.getSubstitutor(); PsiSubstitutor castSub = TypeConversionUtil.getSuperClassSubstitutor(plainClass, (PsiClassType) castType); PsiType returnType = method.getReturnType(); if (method.getSignature(plainSub).equals(method.getSignature(castSub))) { PsiType typeAfterCast = toRaw(castSub.substitute(returnType)); PsiType typeDeclared = toRaw(plainSub.substitute(returnType)); if (typeAfterCast != null && typeDeclared != null && typeAfterCast.isAssignableFrom(typeDeclared) && processor.isAccessible(plainClass.findMethodBySignature(method, true))) { return item; } } } } } else if (containsMember(plainQualifier, o)) { return item; } } return LookupElementDecorator.withInsertHandler( item, new InsertHandlerDecorator<LookupElement>() { @Override public void handleInsert( InsertionContext context, LookupElementDecorator<LookupElement> item) { final Document document = context.getEditor().getDocument(); context.commitDocument(); final PsiFile file = context.getFile(); final PsiJavaCodeReferenceElement ref = PsiTreeUtil.findElementOfClassAtOffset( file, context.getStartOffset(), PsiJavaCodeReferenceElement.class, false); if (ref != null) { final PsiElement qualifier = ref.getQualifier(); if (qualifier != null) { final CommonCodeStyleSettings settings = context.getCodeStyleSettings(); final String parenSpace = settings.SPACE_WITHIN_PARENTHESES ? " " : ""; document.insertString(qualifier.getTextRange().getEndOffset(), parenSpace + ")"); final String spaceWithin = settings.SPACE_WITHIN_CAST_PARENTHESES ? " " : ""; final String prefix = "(" + parenSpace + "(" + spaceWithin; final String spaceAfter = settings.SPACE_AFTER_TYPE_CAST ? " " : ""; final int exprStart = qualifier.getTextRange().getStartOffset(); document.insertString(exprStart, prefix + spaceWithin + ")" + spaceAfter); CompletionUtil.emulateInsertion(context, exprStart + prefix.length(), castTypeItem); PsiDocumentManager.getInstance(file.getProject()) .doPostponedOperationsAndUnblockDocument(document); context.getEditor().getCaretModel().moveToOffset(context.getTailOffset()); } } item.getDelegate().handleInsert(context); } }); } @Nullable private static PsiType toRaw(@Nullable PsiType type) { return type instanceof PsiClassType ? ((PsiClassType) type).rawType() : type; } @NotNull public static LookupElement highlightIfNeeded( @Nullable PsiType qualifierType, @NotNull LookupElement item, @NotNull Object object, @NotNull PsiElement place) { if (shouldMarkRed(object, place)) { return PrioritizedLookupElement.withExplicitProximity( LookupElementDecorator.withRenderer( item, new LookupElementRenderer<LookupElementDecorator<LookupElement>>() { @Override public void renderElement( LookupElementDecorator<LookupElement> element, LookupElementPresentation presentation) { element.getDelegate().renderElement(presentation); presentation.setItemTextForeground(JBColor.RED); } }), -1); } if (containsMember(qualifierType, object)) { LookupElementRenderer<LookupElementDecorator<LookupElement>> boldRenderer = new LookupElementRenderer<LookupElementDecorator<LookupElement>>() { @Override public void renderElement( LookupElementDecorator<LookupElement> element, LookupElementPresentation presentation) { element.getDelegate().renderElement(presentation); presentation.setItemTextBold(true); } }; return PrioritizedLookupElement.withExplicitProximity( LookupElementDecorator.withRenderer(item, boldRenderer), 1); } return item; } private static boolean shouldMarkRed(@NotNull Object object, @NotNull PsiElement place) { if (!(object instanceof PsiMember)) return false; if (Java15APIUsageInspectionBase.isForbiddenApiUsage( (PsiMember) object, PsiUtil.getLanguageLevel(place))) return true; if (object instanceof PsiEnumConstant) { return findConstantsUsedInSwitch(place) .contains(CompletionUtil.getOriginalOrSelf((PsiEnumConstant) object)); } return false; } public static boolean containsMember(@Nullable PsiType qualifierType, @NotNull Object object) { if (qualifierType instanceof PsiArrayType && object instanceof PsiMember) { // length and clone() PsiFile file = ((PsiMember) object).getContainingFile(); if (file == null || file.getVirtualFile() == null) { // yes, they're a bit dummy return true; } } else if (qualifierType instanceof PsiClassType) { PsiClass qualifierClass = ((PsiClassType) qualifierType).resolve(); if (qualifierClass == null) return false; if (object instanceof PsiMethod && qualifierClass.findMethodBySignature((PsiMethod) object, false) != null) { return true; } if (object instanceof PsiMember) { return qualifierClass.equals(((PsiMember) object).getContainingClass()); } } return false; } private static List<? extends LookupElement> createLookupElements( CompletionElement completionElement, PsiJavaReference reference) { Object completion = completionElement.getElement(); assert !(completion instanceof LookupElement); if (reference instanceof PsiJavaCodeReferenceElement) { if (completion instanceof PsiMethod && ((PsiJavaCodeReferenceElement) reference).getParent() instanceof PsiImportStaticStatement) { return Collections.singletonList( JavaLookupElementBuilder.forMethod((PsiMethod) completion, PsiSubstitutor.EMPTY)); } if (completion instanceof PsiClass) { return JavaClassNameCompletionContributor.createClassLookupItems( (PsiClass) completion, JavaClassNameCompletionContributor.AFTER_NEW.accepts(reference), JavaClassNameInsertHandler.JAVA_CLASS_INSERT_HANDLER, Conditions.<PsiClass>alwaysTrue()); } } if (reference instanceof PsiMethodReferenceExpression && completion instanceof PsiMethod && ((PsiMethod) completion).isConstructor()) { return Collections.singletonList( JavaLookupElementBuilder.forMethod( (PsiMethod) completion, "new", PsiSubstitutor.EMPTY, null)); } LookupElement _ret = LookupItemUtil.objectToLookupItem(completion); if (_ret instanceof LookupItem) { final PsiSubstitutor substitutor = completionElement.getSubstitutor(); if (substitutor != null) { ((LookupItem<?>) _ret).setAttribute(LookupItem.SUBSTITUTOR, substitutor); } } return Collections.singletonList(_ret); } public static boolean hasAccessibleConstructor(PsiType type) { if (type instanceof PsiArrayType) return true; final PsiClass psiClass = PsiUtil.resolveClassInType(type); if (psiClass == null || psiClass.isEnum() || psiClass.isAnnotationType()) return false; if (!(psiClass instanceof PsiCompiledElement)) return true; final PsiMethod[] methods = psiClass.getConstructors(); if (methods.length == 0) return true; for (final PsiMethod method : methods) { if (!method.hasModifierProperty(PsiModifier.PRIVATE)) return true; } return false; } public static LookupItem qualify(final LookupItem ret) { return ret.forceQualify(); } public static Set<String> getAllLookupStrings(@NotNull PsiMember member) { Set<String> allLookupStrings = ContainerUtil.newLinkedHashSet(); String name = member.getName(); allLookupStrings.add(name); PsiClass containingClass = member.getContainingClass(); while (containingClass != null) { final String className = containingClass.getName(); if (className == null) { break; } name = className + "." + name; allLookupStrings.add(name); final PsiElement parent = containingClass.getParent(); if (!(parent instanceof PsiClass)) { break; } containingClass = (PsiClass) parent; } return allLookupStrings; } public static LookupItem setShowFQN(final LookupItem ret) { ret.setAttribute( JavaPsiClassReferenceElement.PACKAGE_NAME, PsiFormatUtil.getPackageDisplayName((PsiClass) ret.getObject())); return ret; } public static boolean mayHaveSideEffects(@Nullable final PsiElement element) { return element instanceof PsiExpression && SideEffectChecker.mayHaveSideEffects((PsiExpression) element); } public static void insertClassReference( @NotNull PsiClass psiClass, @NotNull PsiFile file, int offset) { insertClassReference(psiClass, file, offset, offset); } public static int insertClassReference( PsiClass psiClass, PsiFile file, int startOffset, int endOffset) { final Project project = file.getProject(); PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); documentManager.commitAllDocuments(); final PsiManager manager = file.getManager(); final Document document = FileDocumentManager.getInstance().getDocument(file.getViewProvider().getVirtualFile()); final PsiReference reference = file.findReferenceAt(startOffset); if (reference != null) { final PsiElement resolved = reference.resolve(); if (resolved instanceof PsiClass) { if (((PsiClass) resolved).getQualifiedName() == null || manager.areElementsEquivalent(psiClass, resolved)) { return endOffset; } } } String name = psiClass.getName(); if (name == null) { return endOffset; } assert document != null; document.replaceString(startOffset, endOffset, name); int newEndOffset = startOffset + name.length(); final RangeMarker toDelete = insertTemporary(newEndOffset, document, " "); documentManager.commitAllDocuments(); PsiElement element = file.findElementAt(startOffset); if (element instanceof PsiIdentifier) { PsiElement parent = element.getParent(); if (parent instanceof PsiJavaCodeReferenceElement && !((PsiJavaCodeReferenceElement) parent).isQualified() && !(parent.getParent() instanceof PsiPackageStatement)) { PsiJavaCodeReferenceElement ref = (PsiJavaCodeReferenceElement) parent; if (psiClass.isValid() && !psiClass.getManager().areElementsEquivalent(psiClass, resolveReference(ref))) { final boolean staticImport = ref instanceof PsiImportStaticReferenceElement; PsiElement newElement; try { newElement = staticImport ? ((PsiImportStaticReferenceElement) ref).bindToTargetClass(psiClass) : ref.bindToElement(psiClass); } catch (IncorrectOperationException e) { return endOffset; // can happen if fqn contains reserved words, for example } final RangeMarker rangeMarker = document.createRangeMarker(newElement.getTextRange()); documentManager.doPostponedOperationsAndUnblockDocument(document); documentManager.commitDocument(document); newElement = CodeInsightUtilCore.findElementInRange( file, rangeMarker.getStartOffset(), rangeMarker.getEndOffset(), PsiJavaCodeReferenceElement.class, JavaLanguage.INSTANCE); rangeMarker.dispose(); if (newElement != null) { newEndOffset = newElement.getTextRange().getEndOffset(); if (!(newElement instanceof PsiReferenceExpression)) { PsiReferenceParameterList parameterList = ((PsiJavaCodeReferenceElement) newElement).getParameterList(); if (parameterList != null) { newEndOffset = parameterList.getTextRange().getStartOffset(); } } if (!staticImport && !psiClass .getManager() .areElementsEquivalent(psiClass, resolveReference((PsiReference) newElement)) && !PsiUtil.isInnerClass(psiClass)) { final String qName = psiClass.getQualifiedName(); if (qName != null) { document.replaceString( newElement.getTextRange().getStartOffset(), newEndOffset, qName); newEndOffset = newElement.getTextRange().getStartOffset() + qName.length(); } } } } } } if (toDelete.isValid()) { document.deleteString(toDelete.getStartOffset(), toDelete.getEndOffset()); } return newEndOffset; } @Nullable static PsiElement resolveReference(final PsiReference psiReference) { if (psiReference instanceof PsiPolyVariantReference) { final ResolveResult[] results = ((PsiPolyVariantReference) psiReference).multiResolve(true); if (results.length == 1) return results[0].getElement(); } return psiReference.resolve(); } public static RangeMarker insertTemporary( final int endOffset, final Document document, final String temporary) { final CharSequence chars = document.getCharsSequence(); final int length = chars.length(); final RangeMarker toDelete; if (endOffset < length && Character.isJavaIdentifierPart(chars.charAt(endOffset))) { document.insertString(endOffset, temporary); toDelete = document.createRangeMarker(endOffset, endOffset + 1); } else if (endOffset >= length) { toDelete = document.createRangeMarker(length, length); } else { toDelete = document.createRangeMarker(endOffset, endOffset); } toDelete.setGreedyToLeft(true); toDelete.setGreedyToRight(true); return toDelete; } public static void insertParentheses( final InsertionContext context, final LookupElement item, boolean overloadsMatter, boolean hasParams) { insertParentheses(context, item, overloadsMatter, hasParams, false); } public static void insertParentheses( final InsertionContext context, final LookupElement item, boolean overloadsMatter, boolean hasParams, final boolean forceClosingParenthesis) { final Editor editor = context.getEditor(); final char completionChar = context.getCompletionChar(); final PsiFile file = context.getFile(); final TailType tailType = completionChar == '(' ? TailType.NONE : completionChar == ':' ? TailType.COND_EXPR_COLON : LookupItem.handleCompletionChar(context.getEditor(), item, completionChar); final boolean hasTail = tailType != TailType.NONE && tailType != TailType.UNKNOWN; final boolean smart = completionChar == Lookup.COMPLETE_STATEMENT_SELECT_CHAR; if (completionChar == '(' || completionChar == '.' || completionChar == ',' || completionChar == ';' || completionChar == ':' || completionChar == ' ') { context.setAddCompletionChar(false); } if (hasTail) { hasParams = false; } final boolean needRightParenth = forceClosingParenthesis || !smart && (CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET || !hasParams && completionChar != '('); context.commitDocument(); final CommonCodeStyleSettings styleSettings = context.getCodeStyleSettings(); final PsiElement elementAt = file.findElementAt(context.getStartOffset()); if (elementAt == null || !(elementAt.getParent() instanceof PsiMethodReferenceExpression)) { ParenthesesInsertHandler.getInstance( hasParams, styleSettings.SPACE_BEFORE_METHOD_CALL_PARENTHESES, styleSettings.SPACE_WITHIN_METHOD_CALL_PARENTHESES && hasParams, needRightParenth, styleSettings.METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE) .handleInsert(context, item); } if (hasParams) { // Invoke parameters popup AutoPopupController.getInstance(file.getProject()) .autoPopupParameterInfo(editor, overloadsMatter ? null : (PsiElement) item.getObject()); } if (smart || !needRightParenth || !insertTail(context, item, tailType, hasTail)) { return; } if (completionChar == '.') { AutoPopupController.getInstance(file.getProject()) .autoPopupMemberLookup(context.getEditor(), null); } else if (completionChar == ',') { AutoPopupController.getInstance(file.getProject()) .autoPopupParameterInfo(context.getEditor(), null); } } public static boolean insertTail( InsertionContext context, LookupElement item, TailType tailType, boolean hasTail) { TailType toInsert = tailType; LookupItem<?> lookupItem = item.as(LookupItem.CLASS_CONDITION_KEY); if (lookupItem == null || lookupItem.getAttribute(LookupItem.TAIL_TYPE_ATTR) != TailType.UNKNOWN) { if (!hasTail && item.getObject() instanceof PsiMethod && ((PsiMethod) item.getObject()).getReturnType() == PsiType.VOID) { PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments(); if (psiElement() .beforeLeaf(psiElement().withText(".")) .accepts(context.getFile().findElementAt(context.getTailOffset() - 1))) { return false; } boolean insertAdditionalSemicolon = true; final PsiReferenceExpression referenceExpression = PsiTreeUtil.getTopmostParentOfType( context.getFile().findElementAt(context.getStartOffset()), PsiReferenceExpression.class); if (referenceExpression instanceof PsiMethodReferenceExpression && LambdaHighlightingUtil.insertSemicolon(referenceExpression.getParent())) { insertAdditionalSemicolon = false; } else if (referenceExpression != null) { PsiElement parent = referenceExpression.getParent(); if (parent instanceof PsiMethodCallExpression) { parent = parent.getParent(); } if (parent instanceof PsiLambdaExpression && !LambdaHighlightingUtil.insertSemicolonAfter((PsiLambdaExpression) parent)) { insertAdditionalSemicolon = false; } } if (insertAdditionalSemicolon) { toInsert = TailType.SEMICOLON; } } } toInsert.processTail(context.getEditor(), context.getTailOffset()); return true; } // need to shorten references in type argument list public static void shortenReference(final PsiFile file, final int offset) throws IncorrectOperationException { Project project = file.getProject(); final PsiDocumentManager manager = PsiDocumentManager.getInstance(project); Document document = manager.getDocument(file); if (document == null) { PsiUtilCore.ensureValid(file); LOG.error("No document for " + file); return; } manager.commitDocument(document); final PsiReference ref = file.findReferenceAt(offset); if (ref != null) { PsiElement element = ref.getElement(); if (element != null) { JavaCodeStyleManager.getInstance(project).shortenClassReferences(element); PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document); } } } public static boolean inSomePackage(PsiElement context) { PsiFile contextFile = context.getContainingFile(); return contextFile instanceof PsiClassOwner && StringUtil.isNotEmpty(((PsiClassOwner) contextFile).getPackageName()); } public static boolean isSourceLevelAccessible( PsiElement context, PsiClass psiClass, final boolean pkgContext) { if (!JavaPsiFacade.getInstance(psiClass.getProject()) .getResolveHelper() .isAccessible(psiClass, context, null)) { return false; } if (pkgContext) { PsiClass topLevel = PsiUtil.getTopLevelClass(psiClass); if (topLevel != null) { String fqName = topLevel.getQualifiedName(); if (fqName != null && StringUtil.isEmpty(StringUtil.getPackageName(fqName))) { return false; } } } return true; } public static boolean promptTypeArgs(InsertionContext context, int offset) { if (offset < 0) { return false; } OffsetKey key = context.trackOffset(offset, false); PostprocessReformattingAspect.getInstance(context.getProject()).doPostponedFormatting(); offset = context.getOffset(key); if (offset < 0) { return false; } String open = escapeXmlIfNeeded(context, "<"); context.getDocument().insertString(offset, open); context.getEditor().getCaretModel().moveToOffset(offset + open.length()); context.getDocument().insertString(offset + open.length(), escapeXmlIfNeeded(context, ">")); context.setAddCompletionChar(false); return true; } public static FakePsiElement createContextWithXxxVariable( final PsiElement place, final PsiType varType) { return new FakePsiElement() { @Override public boolean processDeclarations( @NotNull PsiScopeProcessor processor, @NotNull ResolveState state, PsiElement lastParent, @NotNull PsiElement place) { return processor.execute( new LightVariableBuilder("xxx", varType, place), ResolveState.initial()); } @Override public PsiElement getParent() { return place; } }; } public static String escapeXmlIfNeeded(InsertionContext context, String generics) { if (context.getFile().getViewProvider().getBaseLanguage() == StdLanguages.JSPX) { return StringUtil.escapeXml(generics); } return generics; } }
class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.folding.impl.DocumentFoldingInfo"); private static final Key<FoldingInfo> FOLDING_INFO_KEY = Key.create("FOLDING_INFO"); @NotNull private final Project myProject; private final VirtualFile myFile; private static class SerializedPsiElement { private final String mySerializedElement; private final FoldingInfo myFoldingInfo; public SerializedPsiElement(@NotNull String serialized, @NotNull FoldingInfo foldingInfo) { mySerializedElement = serialized; myFoldingInfo = foldingInfo; } } @NotNull private final List<SmartPsiElementPointer<PsiElement>> myPsiElements = ContainerUtil.createLockFreeCopyOnWriteList(); @NotNull private final List<SerializedPsiElement> mySerializedElements = ContainerUtil.createLockFreeCopyOnWriteList(); @NotNull private final List<RangeMarker> myRangeMarkers = ContainerUtil.createLockFreeCopyOnWriteList(); private static final String DEFAULT_PLACEHOLDER = "..."; @NonNls private static final String ELEMENT_TAG = "element"; @NonNls private static final String SIGNATURE_ATT = "signature"; @NonNls private static final String EXPANDED_ATT = "expanded"; @NonNls private static final String MARKER_TAG = "marker"; @NonNls private static final String DATE_ATT = "date"; @NonNls private static final String PLACEHOLDER_ATT = "placeholder"; DocumentFoldingInfo(@NotNull Project project, @NotNull Document document) { myProject = project; myFile = FileDocumentManager.getInstance().getFile(document); } void loadFromEditor(@NotNull Editor editor) { assertDispatchThread(); LOG.assertTrue(!editor.isDisposed()); clear(); PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject); documentManager.commitDocument(editor.getDocument()); PsiFile file = documentManager.getPsiFile(editor.getDocument()); SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject); EditorFoldingInfo info = EditorFoldingInfo.get(editor); FoldRegion[] foldRegions = editor.getFoldingModel().getAllFoldRegions(); for (FoldRegion region : foldRegions) { if (!region.isValid()) continue; PsiElement element = info.getPsiElement(region); boolean expanded = region.isExpanded(); boolean collapseByDefault = element != null && FoldingPolicy.isCollapseByDefault(element) && !FoldingUtil.caretInsideRange(editor, TextRange.create(region)); if (collapseByDefault == expanded || element == null) { FoldingInfo fi = new FoldingInfo(region.getPlaceholderText(), expanded); if (element != null) { myPsiElements.add(smartPointerManager.createSmartPsiElementPointer(element, file)); element.putUserData(FOLDING_INFO_KEY, fi); } else if (region.isValid()) { myRangeMarkers.add(region); region.putUserData(FOLDING_INFO_KEY, fi); } } } } private static void assertDispatchThread() { ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(); } @Override public void setToEditor(@NotNull final Editor editor) { assertDispatchThread(); final PsiManager psiManager = PsiManager.getInstance(myProject); if (psiManager.isDisposed()) return; if (!myFile.isValid()) return; final PsiFile psiFile = psiManager.findFile(myFile); if (psiFile == null) return; if (!mySerializedElements.isEmpty()) { // Restore postponed state assert myPsiElements.isEmpty() : "Sequential deserialization"; for (SerializedPsiElement entry : mySerializedElements) { PsiElement restoredElement = FoldingPolicy.restoreBySignature(psiFile, entry.mySerializedElement); if (restoredElement != null && restoredElement.isValid()) { myPsiElements.add( SmartPointerManager.getInstance(myProject) .createSmartPsiElementPointer(restoredElement)); restoredElement.putUserData(FOLDING_INFO_KEY, entry.myFoldingInfo); } } mySerializedElements.clear(); } Map<PsiElement, FoldingDescriptor> ranges = null; for (SmartPsiElementPointer<PsiElement> ptr : myPsiElements) { PsiElement element = ptr.getElement(); if (element == null || !element.isValid()) { continue; } if (ranges == null) { ranges = buildRanges(editor, psiFile); } FoldingDescriptor descriptor = ranges.get(element); if (descriptor == null) { continue; } TextRange range = descriptor.getRange(); FoldRegion region = FoldingUtil.findFoldRegion(editor, range.getStartOffset(), range.getEndOffset()); if (region != null) { FoldingInfo fi = element.getUserData(FOLDING_INFO_KEY); boolean state = fi != null && fi.expanded; region.setExpanded(state); } } for (RangeMarker marker : myRangeMarkers) { if (!marker.isValid()) { continue; } FoldRegion region = FoldingUtil.findFoldRegion(editor, marker.getStartOffset(), marker.getEndOffset()); FoldingInfo info = marker.getUserData(FOLDING_INFO_KEY); if (region == null) { if (info != null) { region = editor .getFoldingModel() .addFoldRegion(marker.getStartOffset(), marker.getEndOffset(), info.placeHolder); } if (region == null) { return; } } boolean state = info != null && info.expanded; region.setExpanded(state); } } @NotNull private static Map<PsiElement, FoldingDescriptor> buildRanges( @NotNull Editor editor, @NotNull PsiFile psiFile) { final FoldingBuilder foldingBuilder = LanguageFolding.INSTANCE.forLanguage(psiFile.getLanguage()); final ASTNode node = psiFile.getNode(); if (node == null) return Collections.emptyMap(); final FoldingDescriptor[] descriptors = LanguageFolding.buildFoldingDescriptors( foldingBuilder, psiFile, editor.getDocument(), true); Map<PsiElement, FoldingDescriptor> ranges = new HashMap<PsiElement, FoldingDescriptor>(); for (FoldingDescriptor descriptor : descriptors) { final ASTNode ast = descriptor.getElement(); final PsiElement psi = ast.getPsi(); if (psi != null) { ranges.put(psi, descriptor); } } return ranges; } void clear() { myPsiElements.clear(); for (RangeMarker marker : myRangeMarkers) { if (!(marker instanceof FoldRegion)) marker.dispose(); } myRangeMarkers.clear(); mySerializedElements.clear(); } @Override public void writeExternal(Element element) throws WriteExternalException { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); if (myPsiElements.isEmpty() && myRangeMarkers.isEmpty() && mySerializedElements.isEmpty()) { throw new WriteExternalException(); } if (mySerializedElements.isEmpty()) { for (SmartPsiElementPointer<PsiElement> ptr : myPsiElements) { PsiElement psiElement = ptr.getElement(); if (psiElement == null || !psiElement.isValid()) { continue; } FoldingInfo fi = psiElement.getUserData(FOLDING_INFO_KEY); boolean state = fi != null && fi.expanded; String signature = FoldingPolicy.getSignature(psiElement); if (signature == null) { continue; } PsiFile containingFile = psiElement.getContainingFile(); PsiElement restoredElement = FoldingPolicy.restoreBySignature(containingFile, signature); if (!psiElement.equals(restoredElement)) { StringBuilder trace = new StringBuilder(); PsiElement restoredAgain = FoldingPolicy.restoreBySignature(containingFile, signature, trace); LOG.error( "element: " + psiElement + "(" + psiElement.getText() + "); restoredElement: " + restoredElement + "; signature: '" + signature + "'; file: " + containingFile + "; injected: " + InjectedLanguageManager.getInstance(myProject) .isInjectedFragment(containingFile) + "; languages: " + containingFile.getViewProvider().getLanguages() + "; restored again: " + restoredAgain + "; restore produces same results: " + (restoredAgain == restoredElement) + "; trace:\n" + trace); } Element e = new Element(ELEMENT_TAG); e.setAttribute(SIGNATURE_ATT, signature); e.setAttribute(EXPANDED_ATT, Boolean.toString(state)); element.addContent(e); } } else { // get back postponed state (before folding initialization) for (SerializedPsiElement entry : mySerializedElements) { Element e = new Element(ELEMENT_TAG); e.setAttribute(SIGNATURE_ATT, entry.mySerializedElement); e.setAttribute(EXPANDED_ATT, Boolean.toString(entry.myFoldingInfo.getExpanded())); element.addContent(e); } } String date = null; for (RangeMarker marker : myRangeMarkers) { FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY); boolean state = fi != null && fi.expanded; Element e = new Element(MARKER_TAG); if (date == null) { date = getTimeStamp(); } if (date.isEmpty()) { continue; } e.setAttribute(DATE_ATT, date); e.setAttribute(EXPANDED_ATT, Boolean.toString(state)); String signature = Integer.valueOf(marker.getStartOffset()) + ":" + Integer.valueOf(marker.getEndOffset()); e.setAttribute(SIGNATURE_ATT, signature); String placeHolderText = fi == null ? DEFAULT_PLACEHOLDER : fi.placeHolder; e.setAttribute(PLACEHOLDER_ATT, placeHolderText); element.addContent(e); } } @Override public void readExternal(final Element element) { ApplicationManager.getApplication() .runReadAction( new Runnable() { @Override public void run() { clear(); if (!myFile.isValid()) return; final Document document = FileDocumentManager.getInstance().getDocument(myFile); if (document == null) return; PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); if (psiFile == null || !psiFile.getViewProvider().isPhysical()) return; String date = null; boolean canRestoreElement = !DumbService.getInstance(myProject).isDumb() || FoldingUpdate.supportsDumbModeFolding(psiFile); for (final Object o : element.getChildren()) { Element e = (Element) o; Boolean expanded = Boolean.valueOf(e.getAttributeValue(EXPANDED_ATT)); if (ELEMENT_TAG.equals(e.getName())) { String signature = e.getAttributeValue(SIGNATURE_ATT); if (signature == null) { continue; } FoldingInfo fi = new FoldingInfo(DEFAULT_PLACEHOLDER, expanded); if (canRestoreElement) { PsiElement restoredElement = FoldingPolicy.restoreBySignature(psiFile, signature); if (restoredElement != null && restoredElement.isValid()) { myPsiElements.add( SmartPointerManager.getInstance(myProject) .createSmartPsiElementPointer(restoredElement)); restoredElement.putUserData(FOLDING_INFO_KEY, fi); } } else { // Postponed initialization mySerializedElements.add(new SerializedPsiElement(signature, fi)); } } else if (MARKER_TAG.equals(e.getName())) { if (date == null) { date = getTimeStamp(); } if (date.isEmpty()) continue; if (!date.equals(e.getAttributeValue(DATE_ATT)) || FileDocumentManager.getInstance().isDocumentUnsaved(document)) continue; StringTokenizer tokenizer = new StringTokenizer(e.getAttributeValue(SIGNATURE_ATT), ":"); try { int start = Integer.valueOf(tokenizer.nextToken()).intValue(); int end = Integer.valueOf(tokenizer.nextToken()).intValue(); if (start < 0 || end >= document.getTextLength() || start > end) continue; RangeMarker marker = document.createRangeMarker(start, end); myRangeMarkers.add(marker); String placeHolderText = e.getAttributeValue(PLACEHOLDER_ATT); if (placeHolderText == null) placeHolderText = DEFAULT_PLACEHOLDER; FoldingInfo fi = new FoldingInfo(placeHolderText, expanded); marker.putUserData(FOLDING_INFO_KEY, fi); } catch (NoSuchElementException exc) { LOG.error(exc); } } else { throw new IllegalStateException("unknown tag: " + e.getName()); } } } }); } private String getTimeStamp() { if (!myFile.isValid()) return ""; return Long.toString(myFile.getTimeStamp()); } @Override public int hashCode() { int result = myProject.hashCode(); result = 31 * result + (myFile != null ? myFile.hashCode() : 0); result = 31 * result + myPsiElements.hashCode(); result = 31 * result + myRangeMarkers.hashCode(); result = 31 * result + mySerializedElements.hashCode(); return result; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DocumentFoldingInfo info = (DocumentFoldingInfo) o; if (myFile != null ? !myFile.equals(info.myFile) : info.myFile != null) { return false; } if (!myProject.equals(info.myProject) || !myPsiElements.equals(info.myPsiElements) || !mySerializedElements.equals(info.mySerializedElements)) { return false; } if (myRangeMarkers.size() != info.myRangeMarkers.size()) return false; for (int i = 0; i < myRangeMarkers.size(); i++) { RangeMarker marker = myRangeMarkers.get(i); RangeMarker other = info.myRangeMarkers.get(i); if (marker == other || !marker.isValid() || !other.isValid()) { continue; } if (!TextRange.areSegmentsEqual(marker, other)) return false; FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY); FoldingInfo ofi = other.getUserData(FOLDING_INFO_KEY); if (!Comparing.equal(fi, ofi)) return false; } return true; } private static class FoldingInfo { private final String placeHolder; private final boolean expanded; private FoldingInfo(@NotNull String placeHolder, boolean expanded) { this.placeHolder = placeHolder; this.expanded = expanded; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } FoldingInfo info = (FoldingInfo) o; return expanded == info.expanded && placeHolder.equals(info.placeHolder); } @Override public int hashCode() { int result = placeHolder.hashCode(); result = 31 * result + (expanded ? 1 : 0); return result; } public boolean getExpanded() { return expanded; } } }
public class ProjectManagerImpl extends ProjectManagerEx implements NamedJDOMExternalizable, ExportableApplicationComponent { private static final Logger LOG = Logger.getInstance("#com.intellij.project.impl.ProjectManagerImpl"); public static final int CURRENT_FORMAT_VERSION = 4; private static final Key<List<ProjectManagerListener>> LISTENERS_IN_PROJECT_KEY = Key.create("LISTENERS_IN_PROJECT_KEY"); @NonNls private static final String ELEMENT_DEFAULT_PROJECT = "defaultProject"; @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) private ProjectImpl myDefaultProject; // Only used asynchronously in save and dispose, which itself are // synchronized. @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) private Element myDefaultProjectRootElement; // Only used asynchronously in save and dispose, which itself are // synchronized. private final List<Project> myOpenProjects = new ArrayList<Project>(); private Project[] myOpenProjectsArrayCache = {}; private final List<ProjectManagerListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private final Set<Project> myTestProjects = new THashSet<Project>(); private final Map<VirtualFile, byte[]> mySavedCopies = new HashMap<VirtualFile, byte[]>(); private final TObjectLongHashMap<VirtualFile> mySavedTimestamps = new TObjectLongHashMap<VirtualFile>(); private final Map<Project, List<Pair<VirtualFile, StateStorage>>> myChangedProjectFiles = new HashMap<Project, List<Pair<VirtualFile, StateStorage>>>(); private final Alarm myChangedFilesAlarm = new Alarm(); private final List<Pair<VirtualFile, StateStorage>> myChangedApplicationFiles = new ArrayList<Pair<VirtualFile, StateStorage>>(); private final AtomicInteger myReloadBlockCount = new AtomicInteger(0); @SuppressWarnings("FieldCanBeLocal") private final Map<Project, String> myProjects = new WeakHashMap<Project, String>(); private static final int MAX_LEAKY_PROJECTS = 42; private final ProgressManager myProgressManager; private volatile boolean myDefaultProjectWasDisposed = false; @NotNull private static List<ProjectManagerListener> getListeners(Project project) { List<ProjectManagerListener> array = project.getUserData(LISTENERS_IN_PROJECT_KEY); if (array == null) return Collections.emptyList(); return array; } /** @noinspection UnusedParameters */ public ProjectManagerImpl( VirtualFileManager virtualFileManager, RecentProjectsManagerBase recentProjectsManager, ProgressManager progressManager) { myProgressManager = progressManager; Application app = ApplicationManager.getApplication(); MessageBus messageBus = app.getMessageBus(); MessageBusConnection connection = messageBus.connect(app); connection.subscribe( StateStorage.STORAGE_TOPIC, new StateStorage.Listener() { @Override public void storageFileChanged( @NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) { VirtualFile file = event.getFile(); if (!file.isDirectory() && !(event.getRequestor() instanceof StateStorage.SaveSession)) { saveChangedProjectFile(file, null, storage); } } }); final ProjectManagerListener busPublisher = messageBus.syncPublisher(TOPIC); addProjectManagerListener( new ProjectManagerListener() { @Override public void projectOpened(final Project project) { MessageBus messageBus = project.getMessageBus(); MessageBusConnection connection = messageBus.connect(project); connection.subscribe( StateStorage.STORAGE_TOPIC, new StateStorage.Listener() { @Override public void storageFileChanged( @NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) { VirtualFile file = event.getFile(); if (!file.isDirectory() && !(event.getRequestor() instanceof StateStorage.SaveSession)) { saveChangedProjectFile(file, project, storage); } } }); busPublisher.projectOpened(project); for (ProjectManagerListener listener : getListeners(project)) { listener.projectOpened(project); } } @Override public void projectClosed(Project project) { busPublisher.projectClosed(project); for (ProjectManagerListener listener : getListeners(project)) { listener.projectClosed(project); } } @Override public boolean canCloseProject(Project project) { for (ProjectManagerListener listener : getListeners(project)) { if (!listener.canCloseProject(project)) { return false; } } return true; } @Override public void projectClosing(Project project) { busPublisher.projectClosing(project); for (ProjectManagerListener listener : getListeners(project)) { listener.projectClosing(project); } } }); registerExternalProjectFileListener(virtualFileManager); } @Override public void disposeComponent() { ApplicationManager.getApplication().assertWriteAccessAllowed(); Disposer.dispose(myChangedFilesAlarm); if (myDefaultProject != null) { Disposer.dispose(myDefaultProject); myDefaultProject = null; myDefaultProjectWasDisposed = true; } } @Override public void initComponent() {} private static final boolean LOG_PROJECT_LEAKAGE_IN_TESTS = false; @Override @Nullable public Project newProject( final String projectName, @NotNull String filePath, boolean useDefaultProjectSettings, boolean isDummy) { filePath = toCanonicalName(filePath); //noinspection ConstantConditions if (LOG_PROJECT_LEAKAGE_IN_TESTS && ApplicationManager.getApplication().isUnitTestMode()) { for (int i = 0; i < 42; i++) { if (myProjects.size() < MAX_LEAKY_PROJECTS) break; System.gc(); TimeoutUtil.sleep(100); System.gc(); } if (myProjects.size() >= MAX_LEAKY_PROJECTS) { List<Project> copy = new ArrayList<Project>(myProjects.keySet()); myProjects.clear(); throw new TooManyProjectLeakedException(copy); } } ProjectImpl project = createProject( projectName, filePath, false, ApplicationManager.getApplication().isUnitTestMode()); try { initProject(project, useDefaultProjectSettings ? (ProjectImpl) getDefaultProject() : null); if (LOG_PROJECT_LEAKAGE_IN_TESTS) { myProjects.put(project, null); } return project; } catch (final Exception e) { LOG.info(e); Messages.showErrorDialog(message(e), ProjectBundle.message("project.load.default.error")); } return null; } @NonNls private static String message(Throwable e) { String message = e.getMessage(); if (message != null) return message; message = e.getLocalizedMessage(); if (message != null) return message; message = e.toString(); Throwable cause = e.getCause(); if (cause != null) { String causeMessage = message(cause); return message + " (cause: " + causeMessage + ")"; } return message; } private void initProject(@NotNull ProjectImpl project, @Nullable ProjectImpl template) throws IOException { final ProgressIndicator indicator = myProgressManager.getProgressIndicator(); if (indicator != null && !project.isDefault()) { indicator.setText(ProjectBundle.message("loading.components.for", project.getName())); indicator.setIndeterminate(true); } ApplicationManager.getApplication() .getMessageBus() .syncPublisher(ProjectLifecycleListener.TOPIC) .beforeProjectLoaded(project); try { if (template != null) { project.getStateStore().loadProjectFromTemplate(template); } else { project.getStateStore().load(); } project.loadProjectComponents(); project.init(); } catch (IOException e) { scheduleDispose(project); throw e; } catch (ProcessCanceledException e) { scheduleDispose(project); throw e; } } private ProjectImpl createProject( @Nullable String projectName, @NotNull String filePath, boolean isDefault, boolean isOptimiseTestLoadSpeed) { return isDefault ? new DefaultProject(this, "", isOptimiseTestLoadSpeed) : new ProjectImpl( this, new File(filePath).getAbsolutePath(), isOptimiseTestLoadSpeed, projectName); } private static void scheduleDispose(final ProjectImpl project) { if (project.isDefault()) { return; } ApplicationManager.getApplication() .invokeLater( new Runnable() { @Override public void run() { ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { if (!project.isDisposed()) { Disposer.dispose(project); } } }); } }); } @Override @Nullable public Project loadProject(@NotNull String filePath) throws IOException, JDOMException, InvalidDataException { try { ProjectImpl project = createProject(null, filePath, false, false); initProject(project, null); return project; } catch (StateStorageException e) { throw new IOException(e.getMessage()); } } @NotNull private static String toCanonicalName(@NotNull final String filePath) { try { return FileUtil.resolveShortWindowsName(filePath); } catch (IOException e) { // OK. File does not yet exist so it's canonical path will be equal to its original path. } return filePath; } @TestOnly public synchronized boolean isDefaultProjectInitialized() { return myDefaultProject != null; } @Override @NotNull public synchronized Project getDefaultProject() { LOG.assertTrue(!myDefaultProjectWasDisposed, "Default project has been already disposed!"); if (myDefaultProject == null) { try { myDefaultProject = createProject(null, "", true, ApplicationManager.getApplication().isUnitTestMode()); initProject(myDefaultProject, null); myDefaultProjectRootElement = null; } catch (IOException e) { LOG.error(e); } catch (StateStorageException e) { LOG.error(e); } } return myDefaultProject; } public Element getDefaultProjectRootElement() { return myDefaultProjectRootElement; } @Override @NotNull public Project[] getOpenProjects() { synchronized (myOpenProjects) { if (myOpenProjectsArrayCache.length != myOpenProjects.size()) { LOG.error( "Open projects: " + myOpenProjects + "; cache: " + Arrays.asList(myOpenProjectsArrayCache)); } if (myOpenProjectsArrayCache.length > 0 && myOpenProjectsArrayCache[0] != myOpenProjects.get(0)) { LOG.error( "Open projects cache corrupted. Open projects: " + myOpenProjects + "; cache: " + Arrays.asList(myOpenProjectsArrayCache)); } if (ApplicationManager.getApplication().isUnitTestMode()) { Project[] testProjects = myTestProjects.toArray(new Project[myTestProjects.size()]); for (Project testProject : testProjects) { assert !testProject.isDisposed() : testProject; } return ArrayUtil.mergeArrays(myOpenProjectsArrayCache, testProjects); } return myOpenProjectsArrayCache; } } @Override public boolean isProjectOpened(Project project) { synchronized (myOpenProjects) { return ApplicationManager.getApplication().isUnitTestMode() && myTestProjects.contains(project) || myOpenProjects.contains(project); } } @Override public boolean openProject(final Project project) { if (isLight(project)) { throw new AssertionError("must not open light project"); } final Application application = ApplicationManager.getApplication(); if (!application.isUnitTestMode() && !((ProjectEx) project).getStateStore().checkVersion()) { return false; } synchronized (myOpenProjects) { if (myOpenProjects.contains(project)) { return false; } myOpenProjects.add(project); cacheOpenProjects(); } fireProjectOpened(project); final StartupManagerImpl startupManager = (StartupManagerImpl) StartupManager.getInstance(project); waitForFileWatcher(project); boolean ok = myProgressManager.runProcessWithProgressSynchronously( new Runnable() { @Override public void run() { startupManager.runStartupActivities(); // dumb mode should start before post-startup activities // only when startCacheUpdate is called from UI thread, we can guarantee that // when the method returns, the application has entered dumb mode UIUtil.invokeAndWaitIfNeeded( new Runnable() { @Override public void run() { startupManager.startCacheUpdate(); } }); startupManager.runPostStartupActivitiesFromExtensions(); UIUtil.invokeLaterIfNeeded( new Runnable() { @Override public void run() { startupManager.runPostStartupActivities(); } }); } }, ProjectBundle.message("project.load.progress"), true, project); if (!ok) { closeProject(project, false, false, true); notifyProjectOpenFailed(); return false; } if (!application.isHeadlessEnvironment() && !application.isUnitTestMode()) { // should be invoked last startupManager.runWhenProjectIsInitialized( new Runnable() { @Override public void run() { final TrackingPathMacroSubstitutor macroSubstitutor = ((ProjectEx) project) .getStateStore() .getStateStorageManager() .getMacroSubstitutor(); if (macroSubstitutor != null) { StorageUtil.notifyUnknownMacros(macroSubstitutor, project, null); } } }); } return true; } private void cacheOpenProjects() { myOpenProjectsArrayCache = myOpenProjects.toArray(new Project[myOpenProjects.size()]); } private void waitForFileWatcher(@NotNull Project project) { LocalFileSystem fs = LocalFileSystem.getInstance(); if (!(fs instanceof LocalFileSystemImpl)) return; final FileWatcher watcher = ((LocalFileSystemImpl) fs).getFileWatcher(); if (!watcher.isOperational() || !watcher.isSettingRoots()) return; LOG.info("FW/roots waiting started"); Task.Modal task = new Task.Modal(project, ProjectBundle.message("project.load.progress"), true) { @Override public void run(@NotNull ProgressIndicator indicator) { indicator.setIndeterminate(true); indicator.setText(ProjectBundle.message("project.load.waiting.watcher")); if (indicator instanceof ProgressWindow) { ((ProgressWindow) indicator).setCancelButtonText(CommonBundle.message("button.skip")); } while (watcher.isSettingRoots() && !indicator.isCanceled()) { TimeoutUtil.sleep(10); } LOG.info("FW/roots waiting finished"); } }; myProgressManager.run(task); } @Override public Project loadAndOpenProject(@NotNull final String filePath) throws IOException { final Project project = convertAndLoadProject(filePath); if (project == null) { WelcomeFrame.showIfNoProjectOpened(); return null; } // todo unify this logic with PlatformProjectOpenProcessor if (!openProject(project)) { WelcomeFrame.showIfNoProjectOpened(); ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { Disposer.dispose(project); } }); } return project; } /** * Converts and loads the project at the specified path. * * @param filePath the path to open the project. * @return the project, or null if the user has cancelled opening the project. */ @Override @Nullable public Project convertAndLoadProject(String filePath) throws IOException { final String fp = toCanonicalName(filePath); final ConversionResult conversionResult = ConversionService.getInstance().convert(fp); if (conversionResult.openingIsCanceled()) { return null; } final Project project = loadProjectWithProgress(filePath); if (project == null) return null; if (!conversionResult.conversionNotNeeded()) { StartupManager.getInstance(project) .registerPostStartupActivity( new Runnable() { @Override public void run() { conversionResult.postStartupActivity(project); } }); } return project; } /** * Opens the project at the specified path. * * @param filePath the path to open the project. * @return the project, or null if the user has cancelled opening the project. */ @Nullable private Project loadProjectWithProgress(@NotNull final String filePath) throws IOException { final ProjectImpl project = createProject(null, toCanonicalName(filePath), false, false); try { myProgressManager.runProcessWithProgressSynchronously( new ThrowableComputable<Project, IOException>() { @Override @Nullable public Project compute() throws IOException { initProject(project, null); return project; } }, ProjectBundle.message("project.load.progress"), true, project); } catch (StateStorageException e) { throw new IOException(e); } catch (ProcessCanceledException ignore) { return null; } return project; } private static void notifyProjectOpenFailed() { ApplicationManager.getApplication() .getMessageBus() .syncPublisher(AppLifecycleListener.TOPIC) .projectOpenFailed(); WelcomeFrame.showIfNoProjectOpened(); } private void registerExternalProjectFileListener(VirtualFileManager virtualFileManager) { virtualFileManager.addVirtualFileManagerListener( new VirtualFileManagerListener() { @Override public void beforeRefreshStart(boolean asynchronous) {} @Override public void afterRefreshFinish(boolean asynchronous) { scheduleReloadApplicationAndProject(); } }); } private void askToReloadProjectIfConfigFilesChangedExternally() { LOG.debug("[RELOAD] myReloadBlockCount = " + myReloadBlockCount.get()); if (myReloadBlockCount.get() == 0) { Set<Project> projects; synchronized (myChangedProjectFiles) { if (myChangedProjectFiles.isEmpty()) return; projects = new HashSet<Project>(myChangedProjectFiles.keySet()); } List<Project> projectsToReload = new ArrayList<Project>(); for (Project project : projects) { if (shouldReloadProject(project)) { projectsToReload.add(project); } } for (final Project projectToReload : projectsToReload) { reloadProjectImpl(projectToReload, false); } } } private boolean tryToReloadApplication() { try { final Application app = ApplicationManager.getApplication(); if (app.isDisposed()) return false; final HashSet<Pair<VirtualFile, StateStorage>> causes = new HashSet<Pair<VirtualFile, StateStorage>>(myChangedApplicationFiles); if (causes.isEmpty()) return true; final boolean[] reloadOk = {false}; final LinkedHashSet<String> components = new LinkedHashSet<String>(); ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { try { reloadOk[0] = ((ApplicationImpl) app).getStateStore().reload(causes, components); } catch (StateStorageException e) { Messages.showWarningDialog( ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } catch (IOException e) { Messages.showWarningDialog( ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } } }); if (!reloadOk[0] && !components.isEmpty()) { String message = "Application components were changed externally and cannot be reloaded:\n"; for (String component : components) { message += component + "\n"; } final boolean canRestart = ApplicationManager.getApplication().isRestartCapable(); message += "Would you like to " + (canRestart ? "restart " : "shutdown "); message += ApplicationNamesInfo.getInstance().getProductName() + "?"; if (Messages.showYesNoDialog( message, "Application Configuration Reload", Messages.getQuestionIcon()) == Messages.YES) { for (Pair<VirtualFile, StateStorage> cause : causes) { StateStorage stateStorage = cause.getSecond(); if (stateStorage instanceof XmlElementStorage) { ((XmlElementStorage) stateStorage).disableSaving(); } } ApplicationManagerEx.getApplicationEx().restart(true); } } return reloadOk[0]; } finally { myChangedApplicationFiles.clear(); } } private boolean shouldReloadProject(final Project project) { if (project.isDisposed()) return false; final HashSet<Pair<VirtualFile, StateStorage>> causes = new HashSet<Pair<VirtualFile, StateStorage>>(); synchronized (myChangedProjectFiles) { final List<Pair<VirtualFile, StateStorage>> changes = myChangedProjectFiles.remove(project); if (changes != null) { causes.addAll(changes); } if (causes.isEmpty()) return false; } final boolean[] reloadOk = {false}; ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { try { LOG.debug("[RELOAD] Reloading project/components..."); reloadOk[0] = ((ProjectEx) project).getStateStore().reload(causes); } catch (StateStorageException e) { Messages.showWarningDialog( ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } catch (IOException e) { Messages.showWarningDialog( ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } } }); if (reloadOk[0]) return false; String message; if (causes.size() == 1) { message = ProjectBundle.message( "project.reload.external.change.single", causes.iterator().next().first.getPresentableUrl()); } else { StringBuilder filesBuilder = new StringBuilder(); boolean first = true; Set<String> alreadyShown = new HashSet<String>(); for (Pair<VirtualFile, StateStorage> cause : causes) { String url = cause.first.getPresentableUrl(); if (!alreadyShown.contains(url)) { if (alreadyShown.size() > 10) { filesBuilder .append("\n" + "and ") .append(causes.size() - alreadyShown.size()) .append(" more"); break; } if (!first) filesBuilder.append("\n"); first = false; filesBuilder.append(url); alreadyShown.add(url); } } message = ProjectBundle.message("project.reload.external.change.multiple", filesBuilder.toString()); } return Messages.showTwoStepConfirmationDialog( message, ProjectBundle.message("project.reload.external.change.title"), "Reload project", Messages.getQuestionIcon()) == 0; } @Override public boolean isFileSavedToBeReloaded(VirtualFile candidate) { return mySavedCopies.containsKey(candidate); } @Override public void blockReloadingProjectOnExternalChanges() { myReloadBlockCount.incrementAndGet(); } @Override public void unblockReloadingProjectOnExternalChanges() { if (myReloadBlockCount.decrementAndGet() == 0) scheduleReloadApplicationAndProject(); } private void scheduleReloadApplicationAndProject() { // todo: commented due to "IDEA-61938 Libraries configuration is kept if switching branches" // because of save which may happen _before_ project reload ;( // ApplicationManager.getApplication().invokeLater(new Runnable() { // public void run() { // IdeEventQueue.getInstance().addIdleListener(new Runnable() { // @Override // public void run() { // IdeEventQueue.getInstance().removeIdleListener(this); ApplicationManager.getApplication() .invokeLater( new Runnable() { @Override public void run() { if (!tryToReloadApplication()) return; askToReloadProjectIfConfigFilesChangedExternally(); } }, ModalityState.NON_MODAL); // } // }, 2000); // } // }, ModalityState.NON_MODAL); } @Override public void openTestProject(@NotNull final Project project) { synchronized (myOpenProjects) { assert ApplicationManager.getApplication().isUnitTestMode(); assert !project.isDisposed() : "Must not open already disposed project"; myTestProjects.add(project); } } @Override public Collection<Project> closeTestProject(@NotNull Project project) { synchronized (myOpenProjects) { assert ApplicationManager.getApplication().isUnitTestMode(); myTestProjects.remove(project); return myTestProjects; } } @Override public void saveChangedProjectFile(final VirtualFile file, final Project project) { if (file.exists()) { copyToTemp(file); } registerProjectToReload(project, file, null); } private void saveChangedProjectFile( final VirtualFile file, @Nullable final Project project, final StateStorage storage) { if (file.exists()) { copyToTemp(file); } registerProjectToReload(project, file, storage); } private void registerProjectToReload( @Nullable final Project project, final VirtualFile cause, @Nullable final StateStorage storage) { if (LOG.isDebugEnabled()) { LOG.debug("[RELOAD] Registering project to reload: " + cause, new Exception()); } if (project != null) { synchronized (myChangedProjectFiles) { List<Pair<VirtualFile, StateStorage>> changedProjectFiles = myChangedProjectFiles.get(project); if (changedProjectFiles == null) { changedProjectFiles = new ArrayList<Pair<VirtualFile, StateStorage>>(); myChangedProjectFiles.put(project, changedProjectFiles); } changedProjectFiles.add(new Pair<VirtualFile, StateStorage>(cause, storage)); } } else { myChangedApplicationFiles.add(new Pair<VirtualFile, StateStorage>(cause, storage)); } myChangedFilesAlarm.cancelAllRequests(); myChangedFilesAlarm.addRequest( new Runnable() { @Override public void run() { LOG.debug( "[RELOAD] Scheduling reload application & project, myReloadBlockCount = " + myReloadBlockCount); if (myReloadBlockCount.get() == 0) { scheduleReloadApplicationAndProject(); } } }, 444); } private void copyToTemp(VirtualFile file) { try { final byte[] bytes = file.contentsToByteArray(); mySavedCopies.put(file, bytes); mySavedTimestamps.put(file, file.getTimeStamp()); } catch (IOException e) { LOG.error(e); } } private void restoreCopy(VirtualFile file) { try { if (file == null) return; // Externally deleted actually. if (!file.isWritable()) return; // IDEA was unable to save it as well. So no need to restore. final byte[] bytes = mySavedCopies.get(file); if (bytes != null) { try { file.setBinaryContent(bytes, -1, mySavedTimestamps.get(file)); } catch (IOException e) { Messages.showWarningDialog( ProjectBundle.message("project.reload.write.failed", file.getPresentableUrl()), ProjectBundle.message("project.reload.write.failed.title")); } } } finally { mySavedCopies.remove(file); mySavedTimestamps.remove(file); } } @Override public void reloadProject(@NotNull final Project p) { reloadProjectImpl(p, true); } public void reloadProjectImpl(@NotNull final Project p, final boolean clearCopyToRestore) { if (clearCopyToRestore) { mySavedCopies.clear(); mySavedTimestamps.clear(); } final Project[] project = {p}; ProjectReloadState.getInstance(project[0]).onBeforeAutomaticProjectReload(); final Application application = ApplicationManager.getApplication(); application.invokeLater( new Runnable() { @Override public void run() { LOG.debug("Reloading project."); ProjectImpl projectImpl = (ProjectImpl) project[0]; if (projectImpl.isDisposed()) return; IProjectStore projectStore = projectImpl.getStateStore(); final String location = projectImpl.getPresentableUrl(); final List<IFile> original; try { final IComponentStore.SaveSession saveSession = projectStore.startSave(); original = saveSession.getAllStorageFiles(true); saveSession.finishSave(); } catch (IOException e) { LOG.error(e); return; } if (project[0].isDisposed() || ProjectUtil.closeAndDispose(project[0])) { application.runWriteAction( new Runnable() { @Override public void run() { for (final IFile originalFile : original) { restoreCopy( LocalFileSystem.getInstance().refreshAndFindFileByIoFile(originalFile)); } } }); project[0] = null; // Let it go. ProjectUtil.openProject(location, null, true); } } }, ModalityState.NON_MODAL); } @Override public boolean closeProject(@NotNull final Project project) { return closeProject(project, true, false, true); } public boolean closeProject( @NotNull final Project project, final boolean save, final boolean dispose, boolean checkCanClose) { if (isLight(project)) { throw new AssertionError("must not close light project"); } if (!isProjectOpened(project)) return true; if (checkCanClose && !canClose(project)) return false; final ShutDownTracker shutDownTracker = ShutDownTracker.getInstance(); shutDownTracker.registerStopperThread(Thread.currentThread()); try { if (save) { FileDocumentManager.getInstance().saveAllDocuments(); project.save(); } if (checkCanClose && !ensureCouldCloseIfUnableToSave(project)) { return false; } fireProjectClosing(project); // somebody can start progress here, do not wrap in write action ApplicationManager.getApplication() .runWriteAction( new Runnable() { @Override public void run() { synchronized (myOpenProjects) { myOpenProjects.remove(project); cacheOpenProjects(); myTestProjects.remove(project); } myChangedProjectFiles.remove(project); fireProjectClosed(project); if (dispose) { Disposer.dispose(project); } } }); } finally { shutDownTracker.unregisterStopperThread(Thread.currentThread()); } return true; } public static boolean isLight(@NotNull Project project) { return ApplicationManager.getApplication().isUnitTestMode() && project.toString().contains("light_temp_"); } @Override public boolean closeAndDispose(@NotNull final Project project) { return closeProject(project, true, true, true); } private void fireProjectClosing(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("enter: fireProjectClosing()"); } for (ProjectManagerListener listener : myListeners) { try { listener.projectClosing(project); } catch (Exception e) { LOG.error(e); } } } @Override public void addProjectManagerListener(@NotNull ProjectManagerListener listener) { myListeners.add(listener); } @Override public void addProjectManagerListener( @NotNull final ProjectManagerListener listener, @NotNull Disposable parentDisposable) { addProjectManagerListener(listener); Disposer.register( parentDisposable, new Disposable() { @Override public void dispose() { removeProjectManagerListener(listener); } }); } @Override public void removeProjectManagerListener(@NotNull ProjectManagerListener listener) { boolean removed = myListeners.remove(listener); LOG.assertTrue(removed); } @Override public void addProjectManagerListener( @NotNull Project project, @NotNull ProjectManagerListener listener) { List<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY); if (listeners == null) { listeners = ((UserDataHolderEx) project) .putUserDataIfAbsent( LISTENERS_IN_PROJECT_KEY, ContainerUtil.<ProjectManagerListener>createLockFreeCopyOnWriteList()); } listeners.add(listener); } @Override public void removeProjectManagerListener( @NotNull Project project, @NotNull ProjectManagerListener listener) { List<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY); LOG.assertTrue(listeners != null); boolean removed = listeners.remove(listener); LOG.assertTrue(removed); } private void fireProjectOpened(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("projectOpened"); } for (ProjectManagerListener listener : myListeners) { try { listener.projectOpened(project); } catch (Exception e) { LOG.error(e); } } } private void fireProjectClosed(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("projectClosed"); } for (ProjectManagerListener listener : myListeners) { try { listener.projectClosed(project); } catch (Exception e) { LOG.error(e); } } } @Override public boolean canClose(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("enter: canClose()"); } for (ProjectManagerListener listener : myListeners) { try { if (!listener.canCloseProject(project)) return false; } catch (Throwable e) { LOG.warn(e); // DO NOT LET ANY PLUGIN to prevent closing due to exception } } return true; } private static boolean ensureCouldCloseIfUnableToSave(@NotNull final Project project) { final ProjectImpl.UnableToSaveProjectNotification[] notifications = NotificationsManager.getNotificationsManager() .getNotificationsOfType(ProjectImpl.UnableToSaveProjectNotification.class, project); if (notifications.length == 0) return true; final String fileNames = StringUtil.join(notifications[0].getFileNames(), "\n"); final String msg = String.format( "%s was unable to save some project files,\nare you sure you want to close this project anyway?", ApplicationNamesInfo.getInstance().getProductName()); return Messages.showDialog( project, msg, "Unsaved Project", "Read-only files:\n\n" + fileNames, new String[] {"Yes", "No"}, 0, 1, Messages.getWarningIcon()) == 0; } @Override public void writeExternal(Element parentNode) throws WriteExternalException { if (myDefaultProject != null) { myDefaultProject.save(); } if (myDefaultProjectRootElement == null) { // read external isn't called if config folder is absent myDefaultProjectRootElement = new Element(ELEMENT_DEFAULT_PROJECT); } myDefaultProjectRootElement.detach(); parentNode.addContent(myDefaultProjectRootElement); } public void setDefaultProjectRootElement(final Element defaultProjectRootElement) { myDefaultProjectRootElement = defaultProjectRootElement; } @Override public void readExternal(Element parentNode) throws InvalidDataException { myDefaultProjectRootElement = parentNode.getChild(ELEMENT_DEFAULT_PROJECT); if (myDefaultProjectRootElement == null) { myDefaultProjectRootElement = new Element(ELEMENT_DEFAULT_PROJECT); } myDefaultProjectRootElement.detach(); } @Override public String getExternalFileName() { return "project.default"; } @Override @NotNull public String getComponentName() { return "ProjectManager"; } @Override @NotNull public File[] getExportFiles() { return new File[] {PathManager.getOptionsFile(this)}; } @Override @NotNull public String getPresentableName() { return ProjectBundle.message("project.default.settings"); } }
/** This class also controls the auto-reparse and auto-hints. */ public class DaemonCodeAnalyzerImpl extends DaemonCodeAnalyzer implements JDOMExternalizable, ProjectComponent { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl"); private static final Key<List<LineMarkerInfo>> MARKERS_IN_EDITOR_DOCUMENT_KEY = Key.create("MARKERS_IN_EDITOR_DOCUMENT"); private final Project myProject; private final DaemonCodeAnalyzerSettings mySettings; private final EditorTracker myEditorTracker; private DaemonProgressIndicator myUpdateProgress = new DaemonProgressIndicator(); // guarded by this private final Runnable myUpdateRunnable = createUpdateRunnable(); private final Alarm myAlarm = new Alarm(); private boolean myUpdateByTimerEnabled = true; private final Collection<VirtualFile> myDisabledHintsFiles = new THashSet<VirtualFile>(); private final Collection<PsiFile> myDisabledHighlightingFiles = new THashSet<PsiFile>(); private final FileStatusMap myFileStatusMap; private DaemonCodeAnalyzerSettings myLastSettings; private IntentionHintComponent myLastIntentionHint; // guarded by this private volatile boolean myDisposed; // the only possible transition: false -> true private volatile boolean myInitialized; // the only possible transition: false -> true @NonNls private static final String DISABLE_HINTS_TAG = "disable_hints"; @NonNls private static final String FILE_TAG = "file"; @NonNls private static final String URL_ATT = "url"; private DaemonListeners myDaemonListeners; private final PassExecutorService myPassExecutorService; private int myModificationCount = 0; private volatile boolean allowToInterrupt = true; private StatusBarUpdater myStatusBarUpdater; public DaemonCodeAnalyzerImpl( Project project, DaemonCodeAnalyzerSettings daemonCodeAnalyzerSettings, EditorTracker editorTracker) { myProject = project; mySettings = daemonCodeAnalyzerSettings; myEditorTracker = editorTracker; myLastSettings = (DaemonCodeAnalyzerSettings) mySettings.clone(); myFileStatusMap = new FileStatusMap(myProject); myPassExecutorService = new PassExecutorService(myProject) { protected void afterApplyInformationToEditor( final TextEditorHighlightingPass pass, final FileEditor fileEditor, final ProgressIndicator updateProgress) { if (fileEditor instanceof TextEditor) { log(updateProgress, pass, "Apply "); Editor editor = ((TextEditor) fileEditor).getEditor(); repaintErrorStripeRenderer(editor); } } protected boolean isDisposed() { return myDisposed || super.isDisposed(); } }; Disposer.register(project, myPassExecutorService); Disposer.register(project, myFileStatusMap); } static boolean hasErrors(Project project, Document document) { return !processHighlights( document, project, HighlightSeverity.ERROR, 0, document.getTextLength(), CommonProcessors.<HighlightInfo>alwaysFalse()); } @NotNull @TestOnly public static List<HighlightInfo> getHighlights( Document document, HighlightSeverity minSeverity, Project project) { List<HighlightInfo> infos = new ArrayList<HighlightInfo>(); processHighlights( document, project, minSeverity, 0, document.getTextLength(), new CommonProcessors.CollectProcessor<HighlightInfo>(infos)); return infos; } public List<HighlightInfo> runMainPasses( @NotNull PsiFile psiFile, @NotNull Document document, @NotNull final ProgressIndicator progress) { final List<HighlightInfo> result = new ArrayList<HighlightInfo>(); final VirtualFile virtualFile = psiFile.getVirtualFile(); if (virtualFile != null && !virtualFile.getFileType().isBinary()) { final List<TextEditorHighlightingPass> passes = TextEditorHighlightingPassRegistrarEx.getInstanceEx(myProject) .instantiateMainPasses(psiFile, document); Collections.sort( passes, new Comparator<TextEditorHighlightingPass>() { @Override public int compare(TextEditorHighlightingPass o1, TextEditorHighlightingPass o2) { if (o1 instanceof GeneralHighlightingPass) return -1; if (o2 instanceof GeneralHighlightingPass) return 1; return 0; } }); for (TextEditorHighlightingPass pass : passes) { pass.doCollectInformation(progress); result.addAll(pass.getInfos()); } } return result; } @TestOnly public List<HighlightInfo> runPasses( @NotNull PsiFile file, @NotNull Document document, @NotNull TextEditor textEditor, @NotNull int[] toIgnore, boolean canChangeDocument, @Nullable Runnable callbackWhileWaiting) { assert myInitialized; assert !myDisposed; Application application = ApplicationManager.getApplication(); application.assertIsDispatchThread(); assert !application.isWriteAccessAllowed(); // pump first so that queued event do not interfere UIUtil.dispatchAllInvocationEvents(); UIUtil.dispatchAllInvocationEvents(); Project project = file.getProject(); setUpdateByTimerEnabled(false); FileStatusMap fileStatusMap = getFileStatusMap(); for (int ignoreId : toIgnore) { fileStatusMap.markFileUpToDate(document, ignoreId); } fileStatusMap.allowDirt(canChangeDocument); TextEditorBackgroundHighlighter highlighter = (TextEditorBackgroundHighlighter) textEditor.getBackgroundHighlighter(); final List<TextEditorHighlightingPass> passes = highlighter.getPasses(toIgnore); HighlightingPass[] array = passes.toArray(new HighlightingPass[passes.size()]); final DaemonProgressIndicator progress = createUpdateProgress(); progress.setDebug(LOG.isDebugEnabled()); myPassExecutorService.submitPasses( Collections.singletonMap((FileEditor) textEditor, array), progress, Job.DEFAULT_PRIORITY); try { while (progress.isRunning()) { try { if (progress.isCanceled() && progress.isRunning()) { // write action sneaked in the AWT. restart waitForTermination(); Throwable savedException = PassExecutorService.getSavedException(progress); if (savedException != null) throw savedException; return runPasses( file, document, textEditor, toIgnore, canChangeDocument, callbackWhileWaiting); } if (callbackWhileWaiting != null) { callbackWhileWaiting.run(); } progress.waitFor(100); UIUtil.dispatchAllInvocationEvents(); Throwable savedException = PassExecutorService.getSavedException(progress); if (savedException != null) throw savedException; } catch (RuntimeException e) { throw e; } catch (Error e) { e.printStackTrace(); throw e; } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException(e); } } UIUtil.dispatchAllInvocationEvents(); UIUtil.dispatchAllInvocationEvents(); return getHighlights(document, null, project); } finally { fileStatusMap.allowDirt(true); waitForTermination(); } } @TestOnly public void prepareForTest() { if (!myInitialized) { projectOpened(); } setUpdateByTimerEnabled(false); waitForTermination(); } @TestOnly public void cleanupAfterTest(boolean dispose) { if (!myProject.isOpen()) return; stopProcess(false); if (dispose) { projectClosed(); Disposer.dispose(myStatusBarUpdater); myStatusBarUpdater = null; Disposer.dispose(myDaemonListeners); myDaemonListeners = null; } setUpdateByTimerEnabled(false); waitForTermination(); } private void waitForTermination() { myPassExecutorService.cancelAll(true); } @NotNull public String getComponentName() { return "DaemonCodeAnalyzer"; } public void initComponent() {} public void disposeComponent() {} @Override public void projectOpened() { assert !myInitialized : "Double Initializing"; myStatusBarUpdater = new StatusBarUpdater(myProject); Disposer.register(myProject, myStatusBarUpdater); myDaemonListeners = new DaemonListeners(myProject, this, myEditorTracker); Disposer.register(myProject, myDaemonListeners); reloadScopes(); myInitialized = true; myDisposed = false; myFileStatusMap.markAllFilesDirty(); } public void projectClosed() { assert myInitialized : "Disposing not initialized component"; assert !myDisposed : "Double dispose"; stopProcess(false); myDisposed = true; myLastSettings = null; } void repaintErrorStripeRenderer(Editor editor) { if (!myProject.isInitialized()) return; final Document document = editor.getDocument(); final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); final EditorMarkupModel markup = (EditorMarkupModel) editor.getMarkupModel(); markup.setErrorPanelPopupHandler(new DaemonEditorPopup(psiFile)); markup.setErrorStripTooltipRendererProvider(new DaemonTooltipRendererProvider(myProject)); markup.setMinMarkHeight(DaemonCodeAnalyzerSettings.getInstance().ERROR_STRIPE_MARK_MIN_HEIGHT); TrafficLightRenderer.setOrRefreshErrorStripeRenderer(markup, myProject, document, psiFile); } private final List<Pair<NamedScope, NamedScopesHolder>> myScopes = ContainerUtil.createEmptyCOWList(); void reloadScopes() { ApplicationManager.getApplication().assertIsDispatchThread(); List<Pair<NamedScope, NamedScopesHolder>> scopeList = new ArrayList<Pair<NamedScope, NamedScopesHolder>>(); final DependencyValidationManager dependencyValidationManager = DependencyValidationManager.getInstance(myProject); addScopesToList(scopeList, NamedScopeManager.getInstance(myProject)); addScopesToList(scopeList, dependencyValidationManager); myScopes.clear(); myScopes.addAll(scopeList); dependencyValidationManager.reloadRules(); } private static void addScopesToList( final List<Pair<NamedScope, NamedScopesHolder>> scopeList, final NamedScopesHolder holder) { NamedScope[] scopes = holder.getScopes(); for (NamedScope scope : scopes) { scopeList.add(Pair.create(scope, holder)); } } @NotNull public List<Pair<NamedScope, NamedScopesHolder>> getScopeBasedHighlightingCachedScopes() { return myScopes; } public void settingsChanged() { DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance(); if (settings.isCodeHighlightingChanged(myLastSettings)) { restart(); } myLastSettings = (DaemonCodeAnalyzerSettings) settings.clone(); } public void updateVisibleHighlighters(@NotNull Editor editor) { ApplicationManager.getApplication().assertIsDispatchThread(); // no need, will not work anyway } public void setUpdateByTimerEnabled(boolean value) { myUpdateByTimerEnabled = value; stopProcess(value); } public boolean isUpdateByTimerEnabled() { return myUpdateByTimerEnabled; } public void setImportHintsEnabled(@NotNull PsiFile file, boolean value) { VirtualFile vFile = file.getVirtualFile(); if (value) { myDisabledHintsFiles.remove(vFile); stopProcess(true); } else { myDisabledHintsFiles.add(vFile); HintManager.getInstance().hideAllHints(); } } public void resetImportHintsEnabledForProject() { myDisabledHintsFiles.clear(); } public void setHighlightingEnabled(@NotNull PsiFile file, boolean value) { if (value) { myDisabledHighlightingFiles.remove(file); } else { myDisabledHighlightingFiles.add(file); } } public boolean isHighlightingAvailable(PsiFile file) { if (myDisabledHighlightingFiles.contains(file)) return false; if (file == null || !file.isPhysical()) return false; if (file instanceof PsiCompiledElement) return false; final FileType fileType = file.getFileType(); if (fileType == StdFileTypes.GUI_DESIGNER_FORM) { return true; } // To enable T.O.D.O. highlighting return !fileType.isBinary(); } public boolean isImportHintsEnabled(@NotNull PsiFile file) { return isAutohintsAvailable(file) && !myDisabledHintsFiles.contains(file.getVirtualFile()); } public boolean isAutohintsAvailable(PsiFile file) { return isHighlightingAvailable(file) && !(file instanceof PsiCompiledElement); } public void restart() { myFileStatusMap.markAllFilesDirty(); stopProcess(true); } @Override public void restart(@NotNull PsiFile file) { Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file); if (document == null) return; myFileStatusMap.markFileScopeDirty( document, new TextRange(0, document.getTextLength()), file.getTextLength()); stopProcess(true); } public List<TextEditorHighlightingPass> getPassesToShowProgressFor(Document document) { List<TextEditorHighlightingPass> allPasses = myPassExecutorService.getAllSubmittedPasses(); List<TextEditorHighlightingPass> result = new ArrayList<TextEditorHighlightingPass>(allPasses.size()); for (TextEditorHighlightingPass pass : allPasses) { if (pass.getDocument() == document || pass.getDocument() == null) { result.add(pass); } } return result; } public boolean isAllAnalysisFinished(@NotNull PsiFile file) { if (myDisposed) return false; Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file); return document != null && document.getModificationStamp() == file.getModificationStamp() && myFileStatusMap.allDirtyScopesAreNull(document); } public boolean isErrorAnalyzingFinished(PsiFile file) { if (myDisposed) return false; Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file); return document != null && document.getModificationStamp() == file.getModificationStamp() && myFileStatusMap.getFileDirtyScope(document, Pass.UPDATE_ALL) == null; } public FileStatusMap getFileStatusMap() { return myFileStatusMap; } public synchronized int getModificationCount() { return myModificationCount; } public synchronized boolean isRunning() { return myUpdateProgress != null && !myUpdateProgress.isCanceled(); } synchronized void stopProcess(boolean toRestartAlarm) { if (!allowToInterrupt) throw new RuntimeException("Cannot interrupt daemon"); cancelUpdateProgress(toRestartAlarm, "by Stop process"); myAlarm.cancelAllRequests(); boolean restart = toRestartAlarm && !myDisposed && myInitialized; if (restart) { myAlarm.addRequest(myUpdateRunnable, mySettings.AUTOREPARSE_DELAY); } } private synchronized void cancelUpdateProgress(final boolean start, @NonNls String reason) { PassExecutorService.log(myUpdateProgress, null, reason, start); myModificationCount++; if (myUpdateProgress != null) { myUpdateProgress.cancel(); myPassExecutorService.cancelAll(false); myUpdateProgress = null; } } public static boolean processHighlights( @NotNull Document document, @NotNull Project project, @Nullable("null means all") final HighlightSeverity minSeverity, final int startOffset, final int endOffset, @NotNull final Processor<HighlightInfo> processor) { LOG.assertTrue(ApplicationManager.getApplication().isReadAccessAllowed()); final SeverityRegistrar severityRegistrar = SeverityRegistrar.getInstance(project); MarkupModelEx model = (MarkupModelEx) DocumentMarkupModel.forDocument(document, project, true); return model.processRangeHighlightersOverlappingWith( startOffset, endOffset, new Processor<RangeHighlighterEx>() { public boolean process(RangeHighlighterEx marker) { Object tt = marker.getErrorStripeTooltip(); if (!(tt instanceof HighlightInfo)) return true; HighlightInfo info = (HighlightInfo) tt; return minSeverity != null && severityRegistrar.compare(info.getSeverity(), minSeverity) < 0 || info.highlighter == null || processor.process(info); } }); } public static boolean processHighlightsOverlappingOutside( @NotNull Document document, @NotNull Project project, @Nullable("null means all") final HighlightSeverity minSeverity, final int startOffset, final int endOffset, @NotNull final Processor<HighlightInfo> processor) { LOG.assertTrue(ApplicationManager.getApplication().isReadAccessAllowed()); final SeverityRegistrar severityRegistrar = SeverityRegistrar.getInstance(project); MarkupModelEx model = (MarkupModelEx) DocumentMarkupModel.forDocument(document, project, true); return model.processRangeHighlightersOutside( startOffset, endOffset, new Processor<RangeHighlighterEx>() { public boolean process(RangeHighlighterEx marker) { Object tt = marker.getErrorStripeTooltip(); if (!(tt instanceof HighlightInfo)) return true; HighlightInfo info = (HighlightInfo) tt; return minSeverity != null && severityRegistrar.compare(info.getSeverity(), minSeverity) < 0 || info.highlighter == null || processor.process(info); } }); } public static boolean processHighlightsNearOffset( @NotNull Document document, @NotNull Project project, @NotNull final HighlightSeverity minSeverity, final int offset, final boolean includeFixRange, @NotNull final Processor<HighlightInfo> processor) { return processHighlights( document, project, null, 0, document.getTextLength(), new Processor<HighlightInfo>() { public boolean process(HighlightInfo info) { if (!isOffsetInsideHighlightInfo(offset, info, includeFixRange)) return true; int compare = info.getSeverity().compareTo(minSeverity); return compare < 0 || processor.process(info); } }); } @Nullable public HighlightInfo findHighlightByOffset( Document document, final int offset, final boolean includeFixRange) { final List<HighlightInfo> foundInfoList = new SmartList<HighlightInfo>(); processHighlightsNearOffset( document, myProject, HighlightSeverity.INFORMATION, offset, includeFixRange, new Processor<HighlightInfo>() { public boolean process(HighlightInfo info) { if (!foundInfoList.isEmpty()) { HighlightInfo foundInfo = foundInfoList.get(0); int compare = foundInfo.getSeverity().compareTo(info.getSeverity()); if (compare < 0) { foundInfoList.clear(); } else if (compare > 0) { return true; } } foundInfoList.add(info); return true; } }); if (foundInfoList.isEmpty()) return null; if (foundInfoList.size() == 1) return foundInfoList.get(0); return new HighlightInfoComposite(foundInfoList); } private static boolean isOffsetInsideHighlightInfo( int offset, HighlightInfo info, boolean includeFixRange) { RangeHighlighterEx highlighter = info.highlighter; if (highlighter == null || !highlighter.isValid()) return false; int startOffset = highlighter.getStartOffset(); int endOffset = highlighter.getEndOffset(); if (startOffset <= offset && offset <= endOffset) { return true; } if (!includeFixRange) return false; RangeMarker fixMarker = info.fixMarker; if (fixMarker != null) { // null means its range is the same as highlighter if (!fixMarker.isValid()) return false; startOffset = fixMarker.getStartOffset(); endOffset = fixMarker.getEndOffset(); return startOffset <= offset && offset <= endOffset; } return false; } @Nullable public static List<LineMarkerInfo> getLineMarkers(Document document, Project project) { ApplicationManager.getApplication().assertIsDispatchThread(); MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true); return markup.getUserData(MARKERS_IN_EDITOR_DOCUMENT_KEY); } public static void setLineMarkers( @NotNull Document document, List<LineMarkerInfo> lineMarkers, Project project) { ApplicationManager.getApplication().assertIsDispatchThread(); MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true); markup.putUserData(MARKERS_IN_EDITOR_DOCUMENT_KEY, lineMarkers); } public synchronized void setLastIntentionHint( Project project, PsiFile file, Editor editor, ShowIntentionsPass.IntentionsInfo intentions, boolean hasToRecreate) { ApplicationManager.getApplication().assertIsDispatchThread(); hideLastIntentionHint(); IntentionHintComponent hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, intentions, false); if (hasToRecreate) { hintComponent.recreate(); } myLastIntentionHint = hintComponent; } public synchronized void hideLastIntentionHint() { if (myLastIntentionHint != null && myLastIntentionHint.isVisible()) { myLastIntentionHint.hide(); myLastIntentionHint = null; } } public synchronized IntentionHintComponent getLastIntentionHint() { return myLastIntentionHint; } public void writeExternal(Element parentNode) throws WriteExternalException { Element disableHintsElement = new Element(DISABLE_HINTS_TAG); parentNode.addContent(disableHintsElement); List<String> array = new ArrayList<String>(); for (VirtualFile file : myDisabledHintsFiles) { if (file.isValid()) { array.add(file.getUrl()); } } Collections.sort(array); for (String url : array) { Element fileElement = new Element(FILE_TAG); fileElement.setAttribute(URL_ATT, url); disableHintsElement.addContent(fileElement); } } public void readExternal(Element parentNode) throws InvalidDataException { myDisabledHintsFiles.clear(); Element element = parentNode.getChild(DISABLE_HINTS_TAG); if (element != null) { for (Object o : element.getChildren(FILE_TAG)) { Element e = (Element) o; String url = e.getAttributeValue(URL_ATT); if (url != null) { VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url); if (file != null) { myDisabledHintsFiles.add(file); } } } } } private Runnable createUpdateRunnable() { return new Runnable() { public void run() { if (myDisposed || !myProject.isInitialized()) return; if (PowerSaveMode.isEnabled()) return; Editor activeEditor = FileEditorManager.getInstance(myProject).getSelectedTextEditor(); Runnable runnable = new Runnable() { public void run() { PassExecutorService.log( myUpdateProgress, null, "Update Runnable. myUpdateByTimerEnabled:", myUpdateByTimerEnabled, " something disposed:", PowerSaveMode.isEnabled() || myDisposed || !myProject.isInitialized(), " activeEditors:", myProject.isDisposed() ? null : myDaemonListeners.getSelectedEditors()); if (!myUpdateByTimerEnabled) return; if (myDisposed) return; ApplicationManager.getApplication().assertIsDispatchThread(); final Collection<FileEditor> activeEditors = myDaemonListeners.getSelectedEditors(); if (activeEditors.isEmpty()) return; ApplicationManager.getApplication().assertIsDispatchThread(); if (ApplicationManager.getApplication().isWriteAccessAllowed()) { // makes no sense to start from within write action, will cancel anyway // we'll restart when write action finish return; } if (PsiDocumentManager.getInstance(myProject).hasUncommitedDocuments()) { ((PsiDocumentManagerImpl) PsiDocumentManager.getInstance(myProject)) .cancelAndRunWhenAllCommitted("restart daemon when all committed", this); return; } Map<FileEditor, HighlightingPass[]> passes = new THashMap<FileEditor, HighlightingPass[]>(activeEditors.size()); for (FileEditor fileEditor : activeEditors) { BackgroundEditorHighlighter highlighter = fileEditor.getBackgroundHighlighter(); if (highlighter != null) { HighlightingPass[] highlightingPasses = highlighter.createPassesForEditor(); passes.put(fileEditor, highlightingPasses); } } // cancel all after calling createPasses() since there are perverts {@link // com.intellij.util.xml.ui.DomUIFactoryImpl} who are changing PSI there cancelUpdateProgress(true, "Cancel by alarm"); myAlarm.cancelAllRequests(); DaemonProgressIndicator progress = createUpdateProgress(); myPassExecutorService.submitPasses(passes, progress, Job.DEFAULT_PRIORITY); } }; if (activeEditor == null) { runnable.run(); } else { ((PsiDocumentManagerImpl) PsiDocumentManager.getInstance(myProject)) .cancelAndRunWhenAllCommitted("start daemon when all committed", runnable); } } }; } private synchronized DaemonProgressIndicator createUpdateProgress() { DaemonProgressIndicator progress = new DaemonProgressIndicator() { @Override public void stopIfRunning() { super.stopIfRunning(); myProject.getMessageBus().syncPublisher(DAEMON_EVENT_TOPIC).daemonFinished(); } }; progress.start(); myUpdateProgress = progress; return progress; } public boolean canChangeFileSilently(PsiFileSystemItem file) { return myDaemonListeners.canChangeFileSilently(file); } public void autoImportReferenceAtCursor(@NotNull Editor editor, @NotNull PsiFile file) { for (ReferenceImporter importer : Extensions.getExtensions(ReferenceImporter.EP_NAME)) { if (importer.autoImportReferenceAtCursor(editor, file)) break; } } @NotNull @TestOnly public static List<HighlightInfo> getFileLevelHighlights(Project project, PsiFile file) { return UpdateHighlightersUtil.getFileLeveleHighlights(project, file); } @TestOnly public void allowToInterrupt(boolean can) { allowToInterrupt = can; } @TestOnly public synchronized DaemonProgressIndicator getUpdateProgress() { return myUpdateProgress; } }
/** @author cdr */ public class InjectedLanguageManagerImpl extends InjectedLanguageManager implements Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl"); private final Project myProject; private final DumbService myDumbService; private final AtomicReference<MultiHostInjector> myPsiManagerRegisteredInjectorsAdapter = new AtomicReference<MultiHostInjector>(); private volatile DaemonProgressIndicator myProgress; public static InjectedLanguageManagerImpl getInstanceImpl(Project project) { return (InjectedLanguageManagerImpl) InjectedLanguageManager.getInstance(project); } public InjectedLanguageManagerImpl(Project project, DumbService dumbService) { myProject = project; myDumbService = dumbService; final ExtensionPoint<MultiHostInjector> multiPoint = Extensions.getArea(project).getExtensionPoint(MultiHostInjector.MULTIHOST_INJECTOR_EP_NAME); multiPoint.addExtensionPointListener( new ExtensionPointListener<MultiHostInjector>() { @Override public void extensionAdded( @NotNull MultiHostInjector injector, @Nullable PluginDescriptor pluginDescriptor) { registerMultiHostInjector(injector); } @Override public void extensionRemoved( @NotNull MultiHostInjector injector, @Nullable PluginDescriptor pluginDescriptor) { unregisterMultiHostInjector(injector); } }, this); final ExtensionPointListener<LanguageInjector> myListener = new ExtensionPointListener<LanguageInjector>() { @Override public void extensionAdded( @NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) { psiManagerInjectorsChanged(); } @Override public void extensionRemoved( @NotNull LanguageInjector extension, @Nullable PluginDescriptor pluginDescriptor) { psiManagerInjectorsChanged(); } }; final ExtensionPoint<LanguageInjector> psiManagerPoint = Extensions.getRootArea().getExtensionPoint(LanguageInjector.EXTENSION_POINT_NAME); psiManagerPoint.addExtensionPointListener(myListener, this); myProgress = new DaemonProgressIndicator(); project .getMessageBus() .connect(this) .subscribe( DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC, new DaemonCodeAnalyzer.DaemonListener() { @Override public void daemonFinished() {} @Override public void daemonCancelEventOccurred() { myProgress.cancel(); } }); } @Override public void dispose() {} public void startRunInjectors(@NotNull final Document hostDocument, final boolean synchronously) { if (myProject.isDisposed()) return; if (!synchronously && ApplicationManager.getApplication().isWriteAccessAllowed()) return; // use cached to avoid recreate PSI in alien project final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject); final PsiFile hostPsiFile = documentManager.getCachedPsiFile(hostDocument); if (hostPsiFile == null) return; final CopyOnWriteArrayList<DocumentWindow> injected = (CopyOnWriteArrayList<DocumentWindow>) InjectedLanguageUtil.getCachedInjectedDocuments(hostPsiFile); if (injected.isEmpty()) return; if (myProgress.isCanceled()) { myProgress = new DaemonProgressIndicator(); } final Processor<DocumentWindow> commitProcessor = new Processor<DocumentWindow>() { @Override public boolean process(DocumentWindow documentWindow) { if (myProject.isDisposed()) return false; ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (indicator != null && indicator.isCanceled()) return false; if (documentManager.isUncommited(hostDocument) || !hostPsiFile.isValid()) return false; // will be committed later Segment[] ranges = documentWindow.getHostRanges(); Segment rangeMarker = ranges.length > 0 ? ranges[0] : null; PsiElement element = rangeMarker == null ? null : hostPsiFile.findElementAt(rangeMarker.getStartOffset()); if (element == null) { synchronized (PsiLock.LOCK) { injected.remove(documentWindow); } return true; } final DocumentWindow[] stillInjectedDocument = {null}; // it is here where the reparse happens and old file contents replaced InjectedLanguageUtil.enumerate( element, hostPsiFile, true, new PsiLanguageInjectionHost.InjectedPsiVisitor() { @Override public void visit( @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { stillInjectedDocument[0] = (DocumentWindow) injectedPsi.getViewProvider().getDocument(); PsiDocumentManagerImpl.checkConsistency(injectedPsi, stillInjectedDocument[0]); } }); synchronized (PsiLock.LOCK) { if (stillInjectedDocument[0] == null) { injected.remove(documentWindow); } else if (stillInjectedDocument[0] != documentWindow) { injected.remove(documentWindow); injected.addIfAbsent(stillInjectedDocument[0]); } } return true; } }; final Runnable commitInjectionsRunnable = new Runnable() { @Override public void run() { if (myProgress.isCanceled()) return; JobLauncher.getInstance() .invokeConcurrentlyUnderProgress( new ArrayList<DocumentWindow>(injected), myProgress, !synchronously, commitProcessor); } }; if (synchronously) { if (Thread.holdsLock(PsiLock.LOCK)) { // hack for the case when docCommit was called from within PSI modification, e.g. in // formatter. // we can't spawn threads to do injections there, otherwise a deadlock is imminent ContainerUtil.process(new ArrayList<DocumentWindow>(injected), commitProcessor); } else { commitInjectionsRunnable.run(); } } else { JobLauncher.getInstance() .submitToJobThread( Job.DEFAULT_PRIORITY, new Runnable() { @Override public void run() { ApplicationManagerEx.getApplicationEx() .tryRunReadAction(commitInjectionsRunnable); } }); } } public void psiManagerInjectorsChanged() { LanguageInjector[] extensions = Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME); if (extensions.length == 0) { MultiHostInjector prev = myPsiManagerRegisteredInjectorsAdapter.getAndSet(null); if (prev != null) { unregisterMultiHostInjector(prev); } } else { PsiManagerRegisteredInjectorsAdapter adapter = new PsiManagerRegisteredInjectorsAdapter(); if (myPsiManagerRegisteredInjectorsAdapter.compareAndSet(null, adapter)) { registerMultiHostInjector(adapter); } } } @Override public PsiLanguageInjectionHost getInjectionHost(@NotNull PsiElement element) { final PsiFile file = element.getContainingFile(); final VirtualFile virtualFile = file == null ? null : file.getVirtualFile(); if (virtualFile instanceof VirtualFileWindow) { PsiElement host = FileContextUtil.getFileContext( file); // use utility method in case the file's overridden getContext() if (host instanceof PsiLanguageInjectionHost) { return (PsiLanguageInjectionHost) host; } } return null; } @Override @NotNull public TextRange injectedToHost( @NotNull PsiElement injectedContext, @NotNull TextRange injectedTextRange) { ProperTextRange.assertProperRange(injectedTextRange); PsiFile file = injectedContext.getContainingFile(); if (file == null) return injectedTextRange; Document document = PsiDocumentManager.getInstance(file.getProject()).getCachedDocument(file); if (!(document instanceof DocumentWindowImpl)) return injectedTextRange; DocumentWindowImpl documentWindow = (DocumentWindowImpl) document; return documentWindow.injectedToHost(injectedTextRange); } @Override public int injectedToHost(@NotNull PsiElement element, int offset) { PsiFile file = element.getContainingFile(); if (file == null) return offset; Document document = PsiDocumentManager.getInstance(file.getProject()).getCachedDocument(file); if (!(document instanceof DocumentWindowImpl)) return offset; DocumentWindowImpl documentWindow = (DocumentWindowImpl) document; return documentWindow.injectedToHost(offset); } private final ConcurrentMap<Class, MultiHostInjector[]> injectors = new ConcurrentHashMap<Class, MultiHostInjector[]>(); private final ClassMapCachingNulls<MultiHostInjector> cachedInjectors = new ClassMapCachingNulls<MultiHostInjector>(injectors, new MultiHostInjector[0]); @Override public void registerMultiHostInjector(@NotNull MultiHostInjector injector) { for (Class<? extends PsiElement> place : injector.elementsToInjectIn()) { LOG.assertTrue(place != null, injector); while (true) { MultiHostInjector[] injectors = this.injectors.get(place); if (injectors == null) { if (this.injectors.putIfAbsent(place, new MultiHostInjector[] {injector}) == null) break; } else { MultiHostInjector[] newInfos = ArrayUtil.append(injectors, injector); if (this.injectors.replace(place, injectors, newInfos)) break; } } } cachedInjectors.clearCache(); } @Override public boolean unregisterMultiHostInjector(@NotNull MultiHostInjector injector) { boolean removed = false; Iterator<Map.Entry<Class, MultiHostInjector[]>> iterator = injectors.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Class, MultiHostInjector[]> entry = iterator.next(); MultiHostInjector[] infos = entry.getValue(); int i = ArrayUtil.find(infos, injector); if (i != -1) { MultiHostInjector[] newInfos = ArrayUtil.remove(infos, i); if (newInfos.length == 0) { iterator.remove(); } else { injectors.put(entry.getKey(), newInfos); } removed = true; } } cachedInjectors.clearCache(); return removed; } static final Key<String> UNESCAPED_TEXT = Key.create("INJECTED_UNESCAPED_TEXT"); @Override public String getUnescapedText(@NotNull final PsiElement injectedNode) { final StringBuilder text = new StringBuilder(injectedNode.getTextLength()); // gather text from (patched) leaves injectedNode.accept( new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { String unescaped = element.getCopyableUserData(UNESCAPED_TEXT); if (unescaped != null) { text.append(unescaped); return; } if (element.getFirstChild() == null) { text.append(element.getText()); return; } super.visitElement(element); } }); return text.toString(); } /** * intersection may spread over several injected fragments * * @param rangeToEdit range in encoded(raw) PSI * @return list of ranges in encoded (raw) PSI */ @Override @SuppressWarnings({"ConstantConditions", "unchecked"}) @NotNull public List<TextRange> intersectWithAllEditableFragments( @NotNull PsiFile injectedPsi, @NotNull TextRange rangeToEdit) { Place shreds = InjectedLanguageUtil.getShreds(injectedPsi); if (shreds == null) return Collections.emptyList(); Object result = null; // optimization: TextRange or ArrayList int count = 0; int offset = 0; for (PsiLanguageInjectionHost.Shred shred : shreds) { TextRange encodedRange = TextRange.from( offset + shred.getPrefix().length(), shred.getRangeInsideHost().getLength()); TextRange intersection = encodedRange.intersection(rangeToEdit); if (intersection != null) { count++; if (count == 1) { result = intersection; } else if (count == 2) { TextRange range = (TextRange) result; if (range.isEmpty()) { result = intersection; count = 1; } else if (intersection.isEmpty()) { count = 1; } else { List<TextRange> list = new ArrayList<TextRange>(); list.add(range); list.add(intersection); result = list; } } else if (intersection.isEmpty()) { count--; } else { ((List<TextRange>) result).add(intersection); } } offset += shred.getPrefix().length() + shred.getRangeInsideHost().getLength() + shred.getSuffix().length(); } return count == 0 ? Collections.<TextRange>emptyList() : count == 1 ? Collections.singletonList((TextRange) result) : (List<TextRange>) result; } @Override public boolean isInjectedFragment(final PsiFile file) { return file.getViewProvider() instanceof InjectedFileViewProvider; } @Override public PsiElement findInjectedElementAt(@NotNull PsiFile hostFile, int hostDocumentOffset) { return InjectedLanguageUtil.findInjectedElementNoCommit(hostFile, hostDocumentOffset); } @Override public void dropFileCaches(@NotNull PsiFile file) { InjectedLanguageUtil.clearCachedInjectedFragmentsForFile(file); } private final Map<Class, MultiHostInjector[]> myInjectorsClone = new HashMap<Class, MultiHostInjector[]>(); @TestOnly public static void pushInjectors(@NotNull Project project) { InjectedLanguageManagerImpl cachedManager = (InjectedLanguageManagerImpl) project.getUserData(INSTANCE_CACHE); if (cachedManager == null) return; try { assert cachedManager.myInjectorsClone.isEmpty() : cachedManager.myInjectorsClone; } finally { cachedManager.myInjectorsClone.clear(); } cachedManager.myInjectorsClone.putAll(cachedManager.injectors); } @TestOnly public static void checkInjectorsAreDisposed(@NotNull Project project) { InjectedLanguageManagerImpl cachedManager = (InjectedLanguageManagerImpl) project.getUserData(INSTANCE_CACHE); if (cachedManager == null) return; try { for (Map.Entry<Class, MultiHostInjector[]> entry : cachedManager.injectors.entrySet()) { Class key = entry.getKey(); if (cachedManager.myInjectorsClone.isEmpty()) return; MultiHostInjector[] oldInjectors = cachedManager.myInjectorsClone.get(key); for (MultiHostInjector injector : entry.getValue()) { if (!ArrayUtil.contains(injector, oldInjectors)) { throw new AssertionError("Injector was not disposed: " + key + " -> " + injector); } } } } finally { cachedManager.myInjectorsClone.clear(); } } public interface InjProcessor { boolean process(PsiElement element, MultiHostInjector injector); } public void processInPlaceInjectorsFor( @NotNull PsiElement element, @NotNull InjProcessor processor) { MultiHostInjector[] infos = cachedInjectors.get(element.getClass()); if (infos != null) { final boolean dumb = myDumbService.isDumb(); for (MultiHostInjector injector : infos) { if (dumb && !DumbService.isDumbAware(injector)) { continue; } if (!processor.process(element, injector)) return; } } } @Override @Nullable public List<Pair<PsiElement, TextRange>> getInjectedPsiFiles(@NotNull final PsiElement host) { if (!(host instanceof PsiLanguageInjectionHost) || !((PsiLanguageInjectionHost) host).isValidHost()) { return null; } final PsiElement inTree = InjectedLanguageUtil.loadTree(host, host.getContainingFile()); final List<Pair<PsiElement, TextRange>> result = new SmartList<Pair<PsiElement, TextRange>>(); InjectedLanguageUtil.enumerate( inTree, new PsiLanguageInjectionHost.InjectedPsiVisitor() { @Override public void visit( @NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) { for (PsiLanguageInjectionHost.Shred place : places) { if (place.getHost() == inTree) { result.add( new Pair<PsiElement, TextRange>(injectedPsi, place.getRangeInsideHost())); } } } }); return result.isEmpty() ? null : result; } private static class PsiManagerRegisteredInjectorsAdapter implements MultiHostInjector { @Override public void getLanguagesToInject( @NotNull final MultiHostRegistrar injectionPlacesRegistrar, @NotNull PsiElement context) { final PsiLanguageInjectionHost host = (PsiLanguageInjectionHost) context; InjectedLanguagePlaces placesRegistrar = new InjectedLanguagePlaces() { @Override public void addPlace( @NotNull Language language, @NotNull TextRange rangeInsideHost, @NonNls @Nullable String prefix, @NonNls @Nullable String suffix) { ProperTextRange.assertProperRange(rangeInsideHost); injectionPlacesRegistrar .startInjecting(language) .addPlace(prefix, suffix, host, rangeInsideHost) .doneInjecting(); } }; for (LanguageInjector injector : Extensions.getExtensions(LanguageInjector.EXTENSION_POINT_NAME)) { injector.getLanguagesToInject(host, placesRegistrar); } } @Override @NotNull public List<? extends Class<? extends PsiElement>> elementsToInjectIn() { return Arrays.asList(PsiLanguageInjectionHost.class); } } }
public class PostprocessReformattingAspect implements PomModelAspect { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.PostprocessReformattingAspect"); private final Project myProject; private final PsiManager myPsiManager; private final TreeAspect myTreeAspect; private final Map<FileViewProvider, List<ASTNode>> myReformatElements = new HashMap<FileViewProvider, List<ASTNode>>(); private volatile int myDisabledCounter = 0; private final Set<FileViewProvider> myUpdatedProviders = new HashSet<FileViewProvider>(); private final AtomicInteger myPostponedCounter = new AtomicInteger(); private static final Key<Throwable> REFORMAT_ORIGINATOR = Key.create("REFORMAT_ORIGINATOR"); private static final boolean STORE_REFORMAT_ORIGINATOR_STACKTRACE = ApplicationManager.getApplication().isInternal(); public PostprocessReformattingAspect( Project project, PsiManager psiManager, TreeAspect treeAspect, final CommandProcessor processor) { myProject = project; myPsiManager = psiManager; myTreeAspect = treeAspect; PomManager.getModel(psiManager.getProject()) .registerAspect( PostprocessReformattingAspect.class, this, Collections.singleton((PomModelAspect) treeAspect)); ApplicationListener applicationListener = new ApplicationAdapter() { @Override public void writeActionStarted(final Object action) { if (processor != null) { final Project project = processor.getCurrentCommandProject(); if (project == myProject) { incrementPostponedCounter(); } } } @Override public void writeActionFinished(final Object action) { if (processor != null) { final Project project = processor.getCurrentCommandProject(); if (project == myProject) { decrementPostponedCounter(); } } } }; ApplicationManager.getApplication().addApplicationListener(applicationListener, project); } public void disablePostprocessFormattingInside(@NotNull final Runnable runnable) { disablePostprocessFormattingInside( new NullableComputable<Object>() { @Override public Object compute() { runnable.run(); return null; } }); } public <T> T disablePostprocessFormattingInside(@NotNull Computable<T> computable) { try { myDisabledCounter++; return computable.compute(); } finally { myDisabledCounter--; LOG.assertTrue(myDisabledCounter > 0 || !isDisabled()); } } public void postponeFormattingInside(@NotNull final Runnable runnable) { postponeFormattingInside( new NullableComputable<Object>() { @Override public Object compute() { runnable.run(); return null; } }); } public <T> T postponeFormattingInside(@NotNull Computable<T> computable) { Application application = ApplicationManager.getApplication(); application.assertIsDispatchThread(); try { incrementPostponedCounter(); return computable.compute(); } finally { decrementPostponedCounter(); } } private void incrementPostponedCounter() { myPostponedCounter.incrementAndGet(); } private void decrementPostponedCounter() { Application application = ApplicationManager.getApplication(); application.assertIsDispatchThread(); if (myPostponedCounter.decrementAndGet() == 0) { if (application.isWriteAccessAllowed()) { doPostponedFormatting(); } else { application.runWriteAction( new Runnable() { @Override public void run() { doPostponedFormatting(); } }); } } } private static void atomic(@NotNull Runnable r) { ProgressManager.getInstance().executeNonCancelableSection(r); } @Override public void update(@NotNull final PomModelEvent event) { atomic( new Runnable() { @Override public void run() { if (isDisabled() || myPostponedCounter.get() == 0 && !ApplicationManager.getApplication().isUnitTestMode()) return; final TreeChangeEvent changeSet = (TreeChangeEvent) event.getChangeSet(myTreeAspect); if (changeSet == null) return; final PsiElement psiElement = changeSet.getRootElement().getPsi(); if (psiElement == null) return; PsiFile containingFile = InjectedLanguageManager.getInstance(psiElement.getProject()) .getTopLevelFile(psiElement); final FileViewProvider viewProvider = containingFile.getViewProvider(); if (!viewProvider.isEventSystemEnabled()) return; myUpdatedProviders.add(viewProvider); for (final ASTNode node : changeSet.getChangedElements()) { final TreeChange treeChange = changeSet.getChangesByElement(node); for (final ASTNode affectedChild : treeChange.getAffectedChildren()) { final ChangeInfo childChange = treeChange.getChangeByChild(affectedChild); switch (childChange.getChangeType()) { case ChangeInfo.ADD: case ChangeInfo.REPLACE: postponeFormatting(viewProvider, affectedChild); break; case ChangeInfo.CONTENTS_CHANGED: if (!CodeEditUtil.isNodeGenerated(affectedChild)) { ((TreeElement) affectedChild) .acceptTree( new RecursiveTreeElementWalkingVisitor() { @Override protected void visitNode(TreeElement element) { if (CodeEditUtil.isNodeGenerated(element) && CodeEditUtil.isSuspendedNodesReformattingAllowed()) { postponeFormatting(viewProvider, element); return; } super.visitNode(element); } }); } break; } } } } }); } public void doPostponedFormatting() { atomic( new Runnable() { @Override public void run() { if (isDisabled()) return; try { FileViewProvider[] viewProviders = myUpdatedProviders.toArray(new FileViewProvider[myUpdatedProviders.size()]); for (final FileViewProvider viewProvider : viewProviders) { doPostponedFormatting(viewProvider); } } catch (Exception e) { LOG.error(e); } finally { LOG.assertTrue(myReformatElements.isEmpty(), myReformatElements); } } }); } public void postponedFormatting(@NotNull FileViewProvider viewProvider) { postponedFormattingImpl(viewProvider, true); } public void doPostponedFormatting(@NotNull FileViewProvider viewProvider) { postponedFormattingImpl(viewProvider, false); } private void postponedFormattingImpl( @NotNull final FileViewProvider viewProvider, final boolean check) { atomic( new Runnable() { @Override public void run() { if (isDisabled() || check && !myUpdatedProviders.contains(viewProvider)) return; try { disablePostprocessFormattingInside( new Runnable() { @Override public void run() { doPostponedFormattingInner(viewProvider); } }); } finally { myUpdatedProviders.remove(viewProvider); myReformatElements.remove(viewProvider); viewProvider.putUserData(REFORMAT_ORIGINATOR, null); } } }); } public boolean isViewProviderLocked(@NotNull FileViewProvider fileViewProvider) { return myReformatElements.containsKey(fileViewProvider); } public void beforeDocumentChanged(@NotNull FileViewProvider viewProvider) { if (isViewProviderLocked(viewProvider)) { Throwable cause = viewProvider.getUserData(REFORMAT_ORIGINATOR); @NonNls String message = "Document is locked by write PSI operations. " + "Use PsiDocumentManager.doPostponedOperationsAndUnblockDocument() to commit PSI changes to the document." + (cause == null ? "" : " See cause stacktrace for the reason to lock."); throw cause == null ? new RuntimeException(message) : new RuntimeException(message, cause); } postponedFormatting(viewProvider); } public static PostprocessReformattingAspect getInstance(Project project) { return project.getComponent(PostprocessReformattingAspect.class); } private void postponeFormatting(@NotNull FileViewProvider viewProvider, @NotNull ASTNode child) { if (!CodeEditUtil.isNodeGenerated(child) && child.getElementType() != TokenType.WHITE_SPACE) { final int oldIndent = CodeEditUtil.getOldIndentation(child); LOG.assertTrue( oldIndent >= 0, "for not generated items old indentation must be defined: element=" + child + ", text=" + child.getText()); } List<ASTNode> list = myReformatElements.get(viewProvider); if (list == null) { list = new ArrayList<ASTNode>(); myReformatElements.put(viewProvider, list); if (STORE_REFORMAT_ORIGINATOR_STACKTRACE) { viewProvider.putUserData(REFORMAT_ORIGINATOR, new Throwable()); } } list.add(child); } private void doPostponedFormattingInner(@NotNull FileViewProvider key) { final List<ASTNode> astNodes = myReformatElements.remove(key); final Document document = key.getDocument(); // Sort ranges by end offsets so that we won't need any offset adjustment after reformat or // reindent if (document == null) return; final VirtualFile virtualFile = key.getVirtualFile(); if (!virtualFile.isValid()) return; final TreeSet<PostprocessFormattingTask> postProcessTasks = new TreeSet<PostprocessFormattingTask>(); Collection<Disposable> toDispose = ContainerUtilRt.newArrayList(); try { // process all roots in viewProvider to find marked for reformat before elements and create // appropriate range markers handleReformatMarkers(key, postProcessTasks); toDispose.addAll(postProcessTasks); // then we create ranges by changed nodes. One per node. There ranges can intersect. Ranges // are sorted by end offset. if (astNodes != null) createActionsMap(astNodes, key, postProcessTasks); if (Boolean.getBoolean("check.psi.is.valid") && ApplicationManager.getApplication().isUnitTestMode()) { checkPsiIsCorrect(key); } while (!postProcessTasks.isEmpty()) { // now we have to normalize actions so that they not intersect and ordered in most // appropriate way // (free reformatting -> reindent -> formatting under reindent) final List<PostponedAction> normalizedActions = normalizeAndReorderPostponedActions(postProcessTasks, document); toDispose.addAll(normalizedActions); // only in following loop real changes in document are made for (final PostponedAction normalizedAction : normalizedActions) { CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject()); boolean old = settings.ENABLE_JAVADOC_FORMATTING; settings.ENABLE_JAVADOC_FORMATTING = false; try { normalizedAction.execute(key); } finally { settings.ENABLE_JAVADOC_FORMATTING = old; } } } } finally { for (Disposable disposable : toDispose) { //noinspection SSBasedInspection disposable.dispose(); } } } private void checkPsiIsCorrect(@NotNull FileViewProvider key) { PsiFile actualPsi = key.getPsi(key.getBaseLanguage()); PsiTreeDebugBuilder treeDebugBuilder = new PsiTreeDebugBuilder().setShowErrorElements(false).setShowWhiteSpaces(false); String actualPsiTree = treeDebugBuilder.psiToString(actualPsi); String fileName = key.getVirtualFile().getName(); PsiFile psi = PsiFileFactory.getInstance(myProject) .createFileFromText( fileName, FileTypeManager.getInstance().getFileTypeByFileName(fileName), actualPsi.getNode().getText(), LocalTimeCounter.currentTime(), false); if (actualPsi.getClass().equals(psi.getClass())) { String expectedPsi = treeDebugBuilder.psiToString(psi); if (!expectedPsi.equals(actualPsiTree)) { myReformatElements.clear(); assert expectedPsi.equals(actualPsiTree) : "Refactored psi should be the same as result of parsing"; } } } @NotNull private List<PostponedAction> normalizeAndReorderPostponedActions( @NotNull Set<PostprocessFormattingTask> rangesToProcess, @NotNull Document document) { final List<PostprocessFormattingTask> freeFormattingActions = new ArrayList<PostprocessFormattingTask>(); final List<ReindentTask> indentActions = new ArrayList<ReindentTask>(); PostprocessFormattingTask accumulatedTask = null; Iterator<PostprocessFormattingTask> iterator = rangesToProcess.iterator(); while (iterator.hasNext()) { final PostprocessFormattingTask currentTask = iterator.next(); if (accumulatedTask == null) { accumulatedTask = currentTask; iterator.remove(); } else if (accumulatedTask.getStartOffset() > currentTask.getEndOffset() || accumulatedTask.getStartOffset() == currentTask.getEndOffset() && !canStickActionsTogether(accumulatedTask, currentTask)) { // action can be pushed if (accumulatedTask instanceof ReindentTask) { indentActions.add((ReindentTask) accumulatedTask); } else { freeFormattingActions.add(accumulatedTask); } accumulatedTask = currentTask; iterator.remove(); } else if (accumulatedTask instanceof ReformatTask && currentTask instanceof ReindentTask) { // split accumulated reformat range into two if (accumulatedTask.getStartOffset() < currentTask.getStartOffset()) { final RangeMarker endOfRange = document.createRangeMarker( accumulatedTask.getStartOffset(), currentTask.getStartOffset()); // add heading reformat part rangesToProcess.add(new ReformatTask(endOfRange)); // and manage heading whitespace because formatter does not edit it in previous action iterator = rangesToProcess.iterator(); //noinspection StatementWithEmptyBody while (iterator.next().getRange() != currentTask.getRange()) ; } final RangeMarker rangeToProcess = document.createRangeMarker(currentTask.getEndOffset(), accumulatedTask.getEndOffset()); freeFormattingActions.add(new ReformatWithHeadingWhitespaceTask(rangeToProcess)); accumulatedTask = currentTask; iterator.remove(); } else { if (!(accumulatedTask instanceof ReindentTask)) { iterator.remove(); boolean withLeadingWhitespace = accumulatedTask instanceof ReformatWithHeadingWhitespaceTask; if (accumulatedTask instanceof ReformatTask && currentTask instanceof ReformatWithHeadingWhitespaceTask && accumulatedTask.getStartOffset() == currentTask.getStartOffset()) { withLeadingWhitespace = true; } else if (accumulatedTask instanceof ReformatWithHeadingWhitespaceTask && currentTask instanceof ReformatTask && accumulatedTask.getStartOffset() < currentTask.getStartOffset()) { withLeadingWhitespace = false; } int newStart = Math.min(accumulatedTask.getStartOffset(), currentTask.getStartOffset()); int newEnd = Math.max(accumulatedTask.getEndOffset(), currentTask.getEndOffset()); RangeMarker rangeMarker; if (accumulatedTask.getStartOffset() == newStart && accumulatedTask.getEndOffset() == newEnd) { rangeMarker = accumulatedTask.getRange(); } else if (currentTask.getStartOffset() == newStart && currentTask.getEndOffset() == newEnd) { rangeMarker = currentTask.getRange(); } else { rangeMarker = document.createRangeMarker(newStart, newEnd); } if (withLeadingWhitespace) { accumulatedTask = new ReformatWithHeadingWhitespaceTask(rangeMarker); } else { accumulatedTask = new ReformatTask(rangeMarker); } } else if (currentTask instanceof ReindentTask) { iterator.remove(); } // TODO[ik]: need to be fixed to correctly process indent inside indent } } if (accumulatedTask != null) { if (accumulatedTask instanceof ReindentTask) { indentActions.add((ReindentTask) accumulatedTask); } else { freeFormattingActions.add(accumulatedTask); } } final List<PostponedAction> result = new ArrayList<PostponedAction>(); Collections.reverse(freeFormattingActions); Collections.reverse(indentActions); if (!freeFormattingActions.isEmpty()) { FormatTextRanges ranges = new FormatTextRanges(); for (PostprocessFormattingTask action : freeFormattingActions) { TextRange range = TextRange.create(action); ranges.add(range, action instanceof ReformatWithHeadingWhitespaceTask); } result.add(new ReformatRangesAction(ranges)); } if (!indentActions.isEmpty()) { ReindentRangesAction reindentRangesAction = new ReindentRangesAction(); for (ReindentTask action : indentActions) { reindentRangesAction.add(action.getRange(), action.getOldIndent()); } result.add(reindentRangesAction); } return result; } private static boolean canStickActionsTogether( final PostprocessFormattingTask currentTask, final PostprocessFormattingTask nextTask) { // empty reformat markers can't be stuck together with any action if (nextTask instanceof ReformatWithHeadingWhitespaceTask && nextTask.getStartOffset() == nextTask.getEndOffset()) return false; if (currentTask instanceof ReformatWithHeadingWhitespaceTask && currentTask.getStartOffset() == currentTask.getEndOffset()) { return false; } // reindent actions can't be be stuck at all return !(currentTask instanceof ReindentTask); } private static void createActionsMap( @NotNull List<ASTNode> astNodes, @NotNull FileViewProvider provider, @NotNull final TreeSet<PostprocessFormattingTask> rangesToProcess) { final Set<ASTNode> nodesToProcess = new HashSet<ASTNode>(astNodes); final Document document = provider.getDocument(); if (document == null) { return; } for (final ASTNode node : astNodes) { nodesToProcess.remove(node); final FileElement fileElement = TreeUtil.getFileElement((TreeElement) node); if (fileElement == null || ((PsiFile) fileElement.getPsi()).getViewProvider() != provider) continue; final boolean isGenerated = CodeEditUtil.isNodeGenerated(node); ((TreeElement) node) .acceptTree( new RecursiveTreeElementVisitor() { boolean inGeneratedContext = !isGenerated; @Override protected boolean visitNode(TreeElement element) { if (nodesToProcess.contains(element)) return false; final boolean currentNodeGenerated = CodeEditUtil.isNodeGenerated(element); CodeEditUtil.setNodeGenerated(element, false); if (currentNodeGenerated && !inGeneratedContext) { rangesToProcess.add( new ReformatTask(document.createRangeMarker(element.getTextRange()))); inGeneratedContext = true; } if (!currentNodeGenerated && inGeneratedContext) { if (element.getElementType() == TokenType.WHITE_SPACE) return false; final int oldIndent = CodeEditUtil.getOldIndentation(element); CodeEditUtil.setOldIndentation(element, -1); LOG.assertTrue( oldIndent >= 0, "for not generated items old indentation must be defined: element " + element); for (TextRange indentRange : getEnabledRanges(element.getPsi())) { rangesToProcess.add( new ReindentTask(document.createRangeMarker(indentRange), oldIndent)); } inGeneratedContext = false; } return true; } private Iterable<TextRange> getEnabledRanges(@NotNull PsiElement element) { List<TextRange> disabledRanges = new ArrayList<TextRange>(); for (DisabledIndentRangesProvider rangesProvider : DisabledIndentRangesProvider.EP_NAME.getExtensions()) { Collection<TextRange> providedDisabledRanges = rangesProvider.getDisabledIndentRanges(element); if (providedDisabledRanges != null) { disabledRanges.addAll(providedDisabledRanges); } } return TextRangeUtil.excludeRanges(element.getTextRange(), disabledRanges); } @Override public void visitComposite(CompositeElement composite) { boolean oldGeneratedContext = inGeneratedContext; super.visitComposite(composite); inGeneratedContext = oldGeneratedContext; } @Override public void visitLeaf(LeafElement leaf) { boolean oldGeneratedContext = inGeneratedContext; super.visitLeaf(leaf); inGeneratedContext = oldGeneratedContext; } }); } } private static void handleReformatMarkers( @NotNull final FileViewProvider key, @NotNull final Set<PostprocessFormattingTask> rangesToProcess) { final Document document = key.getDocument(); if (document == null) { return; } for (final FileElement fileElement : ((SingleRootFileViewProvider) key).getKnownTreeRoots()) { fileElement.acceptTree( new RecursiveTreeElementWalkingVisitor() { @Override protected void visitNode(TreeElement element) { if (CodeEditUtil.isMarkedToReformatBefore(element)) { CodeEditUtil.markToReformatBefore(element, false); rangesToProcess.add( new ReformatWithHeadingWhitespaceTask( document.createRangeMarker( element.getStartOffset(), element.getStartOffset()))); } else if (CodeEditUtil.isMarkedToReformat(element)) { CodeEditUtil.markToReformat(element, false); rangesToProcess.add( new ReformatWithHeadingWhitespaceTask( document.createRangeMarker( element.getStartOffset(), element.getStartOffset() + element.getTextLength()))); } super.visitNode(element); } }); } } private static void adjustIndentationInRange( @NotNull PsiFile file, @NotNull Document document, @NotNull TextRange[] indents, final int indentAdjustment) { final CharSequence charsSequence = document.getCharsSequence(); for (final TextRange indent : indents) { final String oldIndentStr = charsSequence.subSequence(indent.getStartOffset() + 1, indent.getEndOffset()).toString(); final int oldIndent = IndentHelperImpl.getIndent(file.getProject(), file.getFileType(), oldIndentStr, true); final String newIndentStr = IndentHelperImpl.fillIndent( file.getProject(), file.getFileType(), Math.max(oldIndent + indentAdjustment, 0)); document.replaceString(indent.getStartOffset() + 1, indent.getEndOffset(), newIndentStr); } } private static int getNewIndent(@NotNull PsiFile psiFile, final int firstWhitespace) { final Document document = psiFile.getViewProvider().getDocument(); assert document != null; final int startOffset = document.getLineStartOffset(document.getLineNumber(firstWhitespace)); int endOffset = startOffset; final CharSequence charsSequence = document.getCharsSequence(); //noinspection StatementWithEmptyBody while (Character.isWhitespace(charsSequence.charAt(endOffset++))) ; final String newIndentStr = charsSequence.subSequence(startOffset, endOffset - 1).toString(); return IndentHelperImpl.getIndent( psiFile.getProject(), psiFile.getFileType(), newIndentStr, true); } public boolean isDisabled() { return myDisabledCounter > 0; } @NotNull private CodeFormatterFacade getFormatterFacade(@NotNull FileViewProvider viewProvider) { final CodeStyleSettings styleSettings = CodeStyleSettingsManager.getSettings(myPsiManager.getProject()); final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject()); final Document document = viewProvider.getDocument(); assert document != null; final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(styleSettings); documentManager.commitDocument(document); return codeFormatter; } private abstract static class PostprocessFormattingTask implements Comparable<PostprocessFormattingTask>, Segment, Disposable { @NotNull private final RangeMarker myRange; public PostprocessFormattingTask(@NotNull RangeMarker rangeMarker) { myRange = rangeMarker; } @Override public int compareTo(@NotNull PostprocessFormattingTask o) { RangeMarker o1 = myRange; RangeMarker o2 = o.myRange; if (o1.equals(o2)) return 0; final int diff = o2.getEndOffset() - o1.getEndOffset(); if (diff == 0) { if (o1.getStartOffset() == o2.getStartOffset()) return 0; if (o1.getStartOffset() == o1.getEndOffset()) return -1; // empty ranges first if (o2.getStartOffset() == o2.getEndOffset()) return 1; // empty ranges first return o1.getStartOffset() - o2.getStartOffset(); } return diff; } @NotNull public RangeMarker getRange() { return myRange; } @Override public int getStartOffset() { return myRange.getStartOffset(); } @Override public int getEndOffset() { return myRange.getEndOffset(); } @Override public void dispose() { if (myRange.isValid()) { myRange.dispose(); } } } private static class ReformatTask extends PostprocessFormattingTask { public ReformatTask(@NotNull RangeMarker rangeMarker) { super(rangeMarker); } } private static class ReformatWithHeadingWhitespaceTask extends PostprocessFormattingTask { public ReformatWithHeadingWhitespaceTask(@NotNull RangeMarker rangeMarker) { super(rangeMarker); } } private static class ReindentTask extends PostprocessFormattingTask { private final int myOldIndent; public ReindentTask(@NotNull RangeMarker rangeMarker, int oldIndent) { super(rangeMarker); myOldIndent = oldIndent; } public int getOldIndent() { return myOldIndent; } } private interface PostponedAction extends Disposable { void execute(@NotNull FileViewProvider viewProvider); } private class ReformatRangesAction implements PostponedAction { private final FormatTextRanges myRanges; public ReformatRangesAction(@NotNull FormatTextRanges ranges) { myRanges = ranges; } @Override public void execute(@NotNull FileViewProvider viewProvider) { final CodeFormatterFacade codeFormatter = getFormatterFacade(viewProvider); codeFormatter.processText( viewProvider.getPsi(viewProvider.getBaseLanguage()), myRanges.ensureNonEmpty(), false); } @Override public void dispose() {} } private static class ReindentRangesAction implements PostponedAction { private final List<Pair<Integer, RangeMarker>> myRangesToReindent = new ArrayList<Pair<Integer, RangeMarker>>(); public void add(@NotNull RangeMarker rangeMarker, int oldIndent) { myRangesToReindent.add(new Pair<Integer, RangeMarker>(oldIndent, rangeMarker)); } @Override public void execute(@NotNull FileViewProvider viewProvider) { final Document document = viewProvider.getDocument(); assert document != null; final PsiFile psiFile = viewProvider.getPsi(viewProvider.getBaseLanguage()); for (Pair<Integer, RangeMarker> integerRangeMarkerPair : myRangesToReindent) { RangeMarker marker = integerRangeMarkerPair.second; final CharSequence charsSequence = document.getCharsSequence().subSequence(marker.getStartOffset(), marker.getEndOffset()); final int oldIndent = integerRangeMarkerPair.first; final TextRange[] whitespaces = CharArrayUtil.getIndents(charsSequence, marker.getStartOffset()); final int indentAdjustment = getNewIndent(psiFile, marker.getStartOffset()) - oldIndent; if (indentAdjustment != 0) adjustIndentationInRange(psiFile, document, whitespaces, indentAdjustment); } } @Override public void dispose() { for (Pair<Integer, RangeMarker> pair : myRangesToReindent) { RangeMarker marker = pair.second; if (marker.isValid()) { marker.dispose(); } } } } @TestOnly public void clear() { myReformatElements.clear(); } }
/** @author ik Date: 24.10.2003 */ public class PsiClassImplUtil { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiClassImplUtil"); private static final Key<ParameterizedCachedValue<MembersMap, PsiClass>> MAP_IN_CLASS_KEY = Key.create("MAP_KEY"); private PsiClassImplUtil() {} public static void cacheEverything(PsiClass aClass) { getValues(aClass).getValue(aClass); } @NotNull public static PsiField[] getAllFields(@NotNull PsiClass aClass) { List<PsiField> map = getAllByMap(aClass, MemberType.FIELD); return map.toArray(new PsiField[map.size()]); } @NotNull public static PsiMethod[] getAllMethods(@NotNull PsiClass aClass) { List<PsiMethod> methods = getAllByMap(aClass, MemberType.METHOD); return methods.toArray(new PsiMethod[methods.size()]); } @NotNull public static PsiClass[] getAllInnerClasses(@NotNull PsiClass aClass) { List<PsiClass> classes = getAllByMap(aClass, MemberType.CLASS); return classes.toArray(new PsiClass[classes.size()]); } @Nullable public static PsiField findFieldByName( @NotNull PsiClass aClass, String name, boolean checkBases) { List<PsiMember> byMap = findByMap(aClass, name, checkBases, MemberType.FIELD); return byMap.isEmpty() ? null : (PsiField) byMap.get(0); } @NotNull public static PsiMethod[] findMethodsByName( @NotNull PsiClass aClass, String name, boolean checkBases) { List<PsiMember> methods = findByMap(aClass, name, checkBases, MemberType.METHOD); //noinspection SuspiciousToArrayCall return methods.toArray(new PsiMethod[methods.size()]); } @Nullable public static PsiMethod findMethodBySignature( @NotNull PsiClass aClass, @NotNull PsiMethod patternMethod, final boolean checkBases) { final List<PsiMethod> result = findMethodsBySignature(aClass, patternMethod, checkBases, true); return result.isEmpty() ? null : result.get(0); } // ----------------------------- findMethodsBySignature ----------------------------------- @NotNull public static PsiMethod[] findMethodsBySignature( @NotNull PsiClass aClass, @NotNull PsiMethod patternMethod, final boolean checkBases) { List<PsiMethod> methods = findMethodsBySignature(aClass, patternMethod, checkBases, false); return methods.toArray(new PsiMethod[methods.size()]); } @NotNull private static List<PsiMethod> findMethodsBySignature( @NotNull PsiClass aClass, @NotNull PsiMethod patternMethod, boolean checkBases, boolean stopOnFirst) { final PsiMethod[] methodsByName = aClass.findMethodsByName(patternMethod.getName(), checkBases); if (methodsByName.length == 0) return Collections.emptyList(); final List<PsiMethod> methods = new SmartList<PsiMethod>(); final MethodSignature patternSignature = patternMethod.getSignature(PsiSubstitutor.EMPTY); for (final PsiMethod method : methodsByName) { final PsiClass superClass = method.getContainingClass(); final PsiSubstitutor substitutor; if (checkBases && !aClass.equals(superClass)) { substitutor = TypeConversionUtil.getSuperClassSubstitutor(superClass, aClass, PsiSubstitutor.EMPTY); } else { substitutor = PsiSubstitutor.EMPTY; } final MethodSignature signature = method.getSignature(substitutor); if (signature.equals(patternSignature)) { methods.add(method); if (stopOnFirst) { break; } } } return methods; } // ---------------------------------------------------------------------------------------- @Nullable public static PsiClass findInnerByName( @NotNull PsiClass aClass, String name, boolean checkBases) { List<PsiMember> byMap = findByMap(aClass, name, checkBases, MemberType.CLASS); return byMap.isEmpty() ? null : (PsiClass) byMap.get(0); } @NotNull private static List<PsiMember> findByMap( @NotNull PsiClass aClass, String name, boolean checkBases, @NotNull MemberType type) { if (name == null) return Collections.emptyList(); if (checkBases) { Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allMethodsMap = getMap(aClass, type); List<Pair<PsiMember, PsiSubstitutor>> list = allMethodsMap.get(name); if (list == null) return Collections.emptyList(); List<PsiMember> ret = new ArrayList<PsiMember>(list.size()); for (final Pair<PsiMember, PsiSubstitutor> info : list) { ret.add(info.getFirst()); } return ret; } else { PsiMember[] members = null; switch (type) { case METHOD: members = aClass.getMethods(); break; case CLASS: members = aClass.getInnerClasses(); break; case FIELD: members = aClass.getFields(); break; } List<PsiMember> list = new ArrayList<PsiMember>(); for (PsiMember member : members) { if (name.equals(member.getName())) { list.add(member); } } return list; } } @NotNull public static <T extends PsiMember> List<Pair<T, PsiSubstitutor>> getAllWithSubstitutorsByMap( @NotNull PsiClass aClass, @NotNull MemberType type) { Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allMap = getMap(aClass, type); //noinspection unchecked return (List) allMap.get(ALL); } @NotNull private static <T extends PsiMember> List<T> getAllByMap( @NotNull PsiClass aClass, @NotNull MemberType type) { List<Pair<T, PsiSubstitutor>> pairs = getAllWithSubstitutorsByMap(aClass, type); final List<T> ret = new ArrayList<T>(pairs.size()); //noinspection ForLoopReplaceableByForEach for (int i = 0; i < pairs.size(); i++) { Pair<T, PsiSubstitutor> pair = pairs.get(i); T t = pair.getFirst(); LOG.assertTrue(t != null, aClass); ret.add(t); } return ret; } @NonNls private static final String ALL = "Intellij-IDEA-ALL"; public enum MemberType { CLASS, FIELD, METHOD } @NotNull private static MembersMap buildAllMaps(@NotNull PsiClass psiClass) { final List<Pair<PsiMember, PsiSubstitutor>> classes = new ArrayList<Pair<PsiMember, PsiSubstitutor>>(); final List<Pair<PsiMember, PsiSubstitutor>> fields = new ArrayList<Pair<PsiMember, PsiSubstitutor>>(); final List<Pair<PsiMember, PsiSubstitutor>> methods = new ArrayList<Pair<PsiMember, PsiSubstitutor>>(); FilterScopeProcessor<MethodCandidateInfo> processor = new FilterScopeProcessor<MethodCandidateInfo>( new OrFilter( ElementClassFilter.METHOD, ElementClassFilter.FIELD, ElementClassFilter.CLASS)) { @Override protected void add(PsiElement element, PsiSubstitutor substitutor) { if (element instanceof PsiMethod) { methods.add(Pair.create((PsiMember) element, substitutor)); } else if (element instanceof PsiField) { fields.add(Pair.create((PsiMember) element, substitutor)); } else if (element instanceof PsiClass) { classes.add(Pair.create((PsiMember) element, substitutor)); } } }; processDeclarationsInClassNotCached( psiClass, processor, ResolveState.initial(), null, null, psiClass, false, PsiUtil.getLanguageLevel(psiClass)); MembersMap result = new MembersMap(MemberType.class); result.put(MemberType.CLASS, generateMapByList(classes)); result.put(MemberType.METHOD, generateMapByList(methods)); result.put(MemberType.FIELD, generateMapByList(fields)); return result; } @NotNull private static Map<String, List<Pair<PsiMember, PsiSubstitutor>>> generateMapByList( @NotNull final List<Pair<PsiMember, PsiSubstitutor>> list) { Map<String, List<Pair<PsiMember, PsiSubstitutor>>> map = new THashMap<String, List<Pair<PsiMember, PsiSubstitutor>>>(); map.put(ALL, list); for (final Pair<PsiMember, PsiSubstitutor> info : list) { PsiMember element = info.getFirst(); String currentName = element.getName(); List<Pair<PsiMember, PsiSubstitutor>> listByName = map.get(currentName); if (listByName == null) { listByName = new ArrayList<Pair<PsiMember, PsiSubstitutor>>(1); map.put(currentName, listByName); } listByName.add(info); } return map; } private static Map<String, List<Pair<PsiMember, PsiSubstitutor>>> getMap( @NotNull PsiClass aClass, @NotNull MemberType type) { ParameterizedCachedValue<MembersMap, PsiClass> value = getValues(aClass); return value.getValue(aClass).get(type); } @NotNull private static ParameterizedCachedValue<MembersMap, PsiClass> getValues( @NotNull PsiClass aClass) { ParameterizedCachedValue<MembersMap, PsiClass> value = aClass.getUserData(MAP_IN_CLASS_KEY); if (value == null) { value = CachedValuesManager.getManager(aClass.getProject()) .createParameterizedCachedValue(ByNameCachedValueProvider.INSTANCE, false); // Do not cache for nonphysical elements if (aClass.isPhysical()) { value = ((UserDataHolderEx) aClass).putUserDataIfAbsent(MAP_IN_CLASS_KEY, value); } } return value; } private static class ClassIconRequest { @NotNull private final PsiClass psiClass; private final int flags; private final Icon symbolIcon; private ClassIconRequest(@NotNull PsiClass psiClass, int flags, Icon symbolIcon) { this.psiClass = psiClass; this.flags = flags; this.symbolIcon = symbolIcon; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ClassIconRequest)) return false; ClassIconRequest that = (ClassIconRequest) o; return flags == that.flags && psiClass.equals(that.psiClass); } @Override public int hashCode() { int result = psiClass.hashCode(); result = 31 * result + flags; return result; } } private static final Function<ClassIconRequest, Icon> FULL_ICON_EVALUATOR = new NullableFunction<ClassIconRequest, Icon>() { @Override public Icon fun(ClassIconRequest r) { if (!r.psiClass.isValid() || r.psiClass.getProject().isDisposed()) return null; final boolean isLocked = (r.flags & Iconable.ICON_FLAG_READ_STATUS) != 0 && !r.psiClass.isWritable(); Icon symbolIcon = r.symbolIcon != null ? r.symbolIcon : ElementPresentationUtil.getClassIconOfKind( r.psiClass, ElementPresentationUtil.getClassKind(r.psiClass)); RowIcon baseIcon = ElementPresentationUtil.createLayeredIcon(symbolIcon, r.psiClass, isLocked); return ElementPresentationUtil.addVisibilityIcon(r.psiClass, r.flags, baseIcon); } }; public static Icon getClassIcon(final int flags, @NotNull PsiClass aClass) { return getClassIcon(flags, aClass, null); } public static Icon getClassIcon(int flags, @NotNull PsiClass aClass, @Nullable Icon symbolIcon) { Icon base = Iconable.LastComputedIcon.get(aClass, flags); if (base == null) { if (symbolIcon == null) { symbolIcon = ElementPresentationUtil.getClassIconOfKind( aClass, ElementPresentationUtil.getBasicClassKind(aClass)); } RowIcon baseIcon = ElementBase.createLayeredIcon(aClass, symbolIcon, 0); base = ElementPresentationUtil.addVisibilityIcon(aClass, flags, baseIcon); } return IconDeferrer.getInstance() .defer(base, new ClassIconRequest(aClass, flags, symbolIcon), FULL_ICON_EVALUATOR); } @NotNull public static SearchScope getClassUseScope(@NotNull PsiClass aClass) { if (aClass instanceof PsiAnonymousClass) { return new LocalSearchScope(aClass); } final GlobalSearchScope maximalUseScope = ResolveScopeManager.getElementUseScope(aClass); PsiFile file = aClass.getContainingFile(); if (PsiImplUtil.isInServerPage(file)) return maximalUseScope; final PsiClass containingClass = aClass.getContainingClass(); if (aClass.hasModifierProperty(PsiModifier.PUBLIC) || aClass.hasModifierProperty(PsiModifier.PROTECTED)) { return containingClass == null ? maximalUseScope : containingClass.getUseScope(); } else if (aClass.hasModifierProperty(PsiModifier.PRIVATE) || aClass instanceof PsiTypeParameter) { PsiClass topClass = PsiUtil.getTopLevelClass(aClass); return new LocalSearchScope(topClass == null ? aClass.getContainingFile() : topClass); } else { PsiPackage aPackage = null; if (file instanceof PsiJavaFile) { aPackage = JavaPsiFacade.getInstance(aClass.getProject()) .findPackage(((PsiJavaFile) file).getPackageName()); } if (aPackage == null) { PsiDirectory dir = file.getContainingDirectory(); if (dir != null) { aPackage = JavaDirectoryService.getInstance().getPackage(dir); } } if (aPackage != null) { SearchScope scope = PackageScope.packageScope(aPackage, false); scope = scope.intersectWith(maximalUseScope); return scope; } return new LocalSearchScope(file); } } public static boolean isMainOrPremainMethod(@NotNull PsiMethod method) { if (!PsiType.VOID.equals(method.getReturnType())) return false; String name = method.getName(); if (!("main".equals(name) || "premain".equals(name))) return false; PsiElementFactory factory = JavaPsiFacade.getInstance(method.getProject()).getElementFactory(); MethodSignature signature = method.getSignature(PsiSubstitutor.EMPTY); try { MethodSignature main = createSignatureFromText(factory, "void main(String[] args);"); if (MethodSignatureUtil.areSignaturesEqual(signature, main)) return true; MethodSignature premain = createSignatureFromText( factory, "void premain(String args, java.lang.instrument.Instrumentation i);"); if (MethodSignatureUtil.areSignaturesEqual(signature, premain)) return true; } catch (IncorrectOperationException e) { LOG.error(e); } return false; } @NotNull private static MethodSignature createSignatureFromText( @NotNull PsiElementFactory factory, @NotNull String text) { return factory.createMethodFromText(text, null).getSignature(PsiSubstitutor.EMPTY); } private static class MembersMap extends EnumMap<MemberType, Map<String, List<Pair<PsiMember, PsiSubstitutor>>>> { public MembersMap(@NotNull Class<MemberType> keyType) { super(keyType); } } private static class ByNameCachedValueProvider implements ParameterizedCachedValueProvider<MembersMap, PsiClass> { private static final ByNameCachedValueProvider INSTANCE = new ByNameCachedValueProvider(); @Override public CachedValueProvider.Result<MembersMap> compute(@NotNull PsiClass myClass) { MembersMap map = buildAllMaps(myClass); return new CachedValueProvider.Result<MembersMap>( map, PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT); } } public static boolean processDeclarationsInClass( @NotNull PsiClass aClass, @NotNull final PsiScopeProcessor processor, @NotNull ResolveState state, @Nullable Set<PsiClass> visited, PsiElement last, @NotNull PsiElement place, boolean isRaw) { if (last instanceof PsiTypeParameterList || last instanceof PsiModifierList) { return true; // TypeParameterList and ModifierList do not see our declarations } if (visited != null && visited.contains(aClass)) return true; PsiSubstitutor substitutor = state.get(PsiSubstitutor.KEY); isRaw = isRaw || PsiUtil.isRawSubstitutor(aClass, substitutor); ParameterizedCachedValue<MembersMap, PsiClass> cache = getValues(aClass); // aClass.getUserData(MAP_IN_CLASS_KEY); boolean upToDate = cache.hasUpToDateValue(); LanguageLevel languageLevel = PsiUtil.getLanguageLevel(place); if ( /*true || */ upToDate) { final NameHint nameHint = processor.getHint(NameHint.KEY); if (nameHint != null) { String name = nameHint.getName(state); return processCachedMembersByName( aClass, processor, state, visited, last, place, isRaw, substitutor, cache.getValue(aClass), name, languageLevel); } } return processDeclarationsInClassNotCached( aClass, processor, state, visited, last, place, isRaw, languageLevel); } private static boolean processCachedMembersByName( @NotNull PsiClass aClass, @NotNull PsiScopeProcessor processor, @NotNull ResolveState state, @Nullable Set<PsiClass> visited, PsiElement last, @NotNull PsiElement place, boolean isRaw, @NotNull PsiSubstitutor substitutor, @NotNull MembersMap value, String name, @NotNull LanguageLevel languageLevel) { final ElementClassHint classHint = processor.getHint(ElementClassHint.KEY); PsiElementFactory factory = JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory(); if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.FIELD)) { final PsiField fieldByName = aClass.findFieldByName(name, false); if (fieldByName != null) { processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass); if (!processor.execute(fieldByName, state)) return false; } else { final Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allFieldsMap = value.get(MemberType.FIELD); final List<Pair<PsiMember, PsiSubstitutor>> list = allFieldsMap.get(name); if (list != null) { for (final Pair<PsiMember, PsiSubstitutor> candidate : list) { PsiMember candidateField = candidate.getFirst(); PsiSubstitutor finalSubstitutor = obtainFinalSubstitutor( candidateField.getContainingClass(), candidate.getSecond(), aClass, substitutor, factory, languageLevel); processor.handleEvent( PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, candidateField.getContainingClass()); if (!processor.execute(candidateField, state.put(PsiSubstitutor.KEY, finalSubstitutor))) return false; } } } } if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.CLASS)) { if (last != null && last.getParent() == aClass) { if (last instanceof PsiClass) { if (!processor.execute(last, state)) return false; } // Parameters final PsiTypeParameterList list = aClass.getTypeParameterList(); if (list != null && !list.processDeclarations(processor, state, last, place)) return false; } if (!(last instanceof PsiReferenceList)) { final PsiClass classByName = aClass.findInnerClassByName(name, false); if (classByName != null) { processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass); if (!processor.execute(classByName, state)) return false; } else { Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allClassesMap = value.get(MemberType.CLASS); List<Pair<PsiMember, PsiSubstitutor>> list = allClassesMap.get(name); if (list != null) { for (final Pair<PsiMember, PsiSubstitutor> candidate : list) { PsiMember inner = candidate.getFirst(); PsiClass containingClass = inner.getContainingClass(); if (containingClass != null) { PsiSubstitutor finalSubstitutor = obtainFinalSubstitutor( containingClass, candidate.getSecond(), aClass, substitutor, factory, languageLevel); processor.handleEvent( PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, containingClass); if (!processor.execute(inner, state.put(PsiSubstitutor.KEY, finalSubstitutor))) return false; } } } } } } if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.METHOD)) { if (processor instanceof MethodResolverProcessor) { final MethodResolverProcessor methodResolverProcessor = (MethodResolverProcessor) processor; if (methodResolverProcessor.isConstructor()) { final PsiMethod[] constructors = aClass.getConstructors(); methodResolverProcessor.handleEvent( PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass); for (PsiMethod constructor : constructors) { if (!methodResolverProcessor.execute(constructor, state)) return false; } return true; } } Map<String, List<Pair<PsiMember, PsiSubstitutor>>> allMethodsMap = value.get(MemberType.METHOD); List<Pair<PsiMember, PsiSubstitutor>> list = allMethodsMap.get(name); if (list != null) { for (final Pair<PsiMember, PsiSubstitutor> candidate : list) { ProgressIndicatorProvider.checkCanceled(); PsiMethod candidateMethod = (PsiMethod) candidate.getFirst(); if (processor instanceof MethodResolverProcessor) { if (candidateMethod.isConstructor() != ((MethodResolverProcessor) processor).isConstructor()) continue; } final PsiClass containingClass = candidateMethod.getContainingClass(); if (visited != null && visited.contains(candidateMethod.getContainingClass())) { continue; } PsiSubstitutor finalSubstitutor = obtainFinalSubstitutor( containingClass, candidate.getSecond(), aClass, substitutor, factory, languageLevel); finalSubstitutor = checkRaw(isRaw, factory, candidateMethod, finalSubstitutor); processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, containingClass); if (!processor.execute(candidateMethod, state.put(PsiSubstitutor.KEY, finalSubstitutor))) return false; } if (visited != null) { for (Pair<PsiMember, PsiSubstitutor> aList : list) { visited.add(aList.getFirst().getContainingClass()); } } } } return true; } private static PsiSubstitutor checkRaw( boolean isRaw, @NotNull PsiElementFactory factory, @NotNull PsiMethod candidateMethod, @NotNull PsiSubstitutor substitutor) { if (isRaw && !candidateMethod.hasModifierProperty( PsiModifier.STATIC)) { // static methods are not erased due to raw overriding PsiTypeParameter[] methodTypeParameters = candidateMethod.getTypeParameters(); substitutor = factory.createRawSubstitutor(substitutor, methodTypeParameters); } return substitutor; } public static PsiSubstitutor obtainFinalSubstitutor( @NotNull PsiClass candidateClass, @NotNull PsiSubstitutor candidateSubstitutor, @NotNull PsiClass aClass, @NotNull PsiSubstitutor substitutor, @NotNull PsiElementFactory elementFactory, @NotNull LanguageLevel languageLevel) { if (PsiUtil.isRawSubstitutor(aClass, substitutor)) { return elementFactory.createRawSubstitutor(candidateClass); } final PsiType containingType = elementFactory.createType(candidateClass, candidateSubstitutor, languageLevel); PsiType type = substitutor.substitute(containingType); if (!(type instanceof PsiClassType)) return candidateSubstitutor; return ((PsiClassType) type).resolveGenerics().getSubstitutor(); } private static boolean processDeclarationsInClassNotCached( @NotNull PsiClass aClass, @NotNull PsiScopeProcessor processor, @NotNull ResolveState state, @Nullable Set<PsiClass> visited, PsiElement last, @NotNull PsiElement place, boolean isRaw, @NotNull LanguageLevel languageLevel) { if (visited == null) visited = new THashSet<PsiClass>(); if (!visited.add(aClass)) return true; processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, aClass); final ElementClassHint classHint = processor.getHint(ElementClassHint.KEY); final NameHint nameHint = processor.getHint(NameHint.KEY); if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.FIELD)) { if (nameHint != null) { final PsiField fieldByName = aClass.findFieldByName(nameHint.getName(state), false); if (fieldByName != null && !processor.execute(fieldByName, state)) return false; } else { final PsiField[] fields = aClass.getFields(); for (final PsiField field : fields) { if (!processor.execute(field, state)) return false; } } } PsiElementFactory factory = JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory(); if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.METHOD)) { PsiSubstitutor baseSubstitutor = state.get(PsiSubstitutor.KEY); final PsiMethod[] methods = nameHint != null ? aClass.findMethodsByName(nameHint.getName(state), false) : aClass.getMethods(); for (final PsiMethod method : methods) { PsiSubstitutor finalSubstitutor = checkRaw(isRaw, factory, method, baseSubstitutor); ResolveState methodState = finalSubstitutor == baseSubstitutor ? state : state.put(PsiSubstitutor.KEY, finalSubstitutor); if (!processor.execute(method, methodState)) return false; } } if (classHint == null || classHint.shouldProcess(ElementClassHint.DeclarationKind.CLASS)) { if (last != null && last.getParent() == aClass) { // Parameters final PsiTypeParameterList list = aClass.getTypeParameterList(); if (list != null && !list.processDeclarations(processor, ResolveState.initial(), last, place)) return false; } if (!(last instanceof PsiReferenceList) && !(last instanceof PsiModifierList)) { // Inners if (nameHint != null) { final PsiClass inner = aClass.findInnerClassByName(nameHint.getName(state), false); if (inner != null) { if (!processor.execute(inner, state)) return false; } } else { final PsiClass[] inners = aClass.getInnerClasses(); for (final PsiClass inner : inners) { if (!processor.execute(inner, state)) return false; } } } } return last instanceof PsiReferenceList || processSuperTypes( aClass, processor, visited, last, place, state, isRaw, factory, languageLevel); } private static boolean processSuperTypes( @NotNull PsiClass aClass, @NotNull PsiScopeProcessor processor, @Nullable Set<PsiClass> visited, PsiElement last, @NotNull PsiElement place, @NotNull ResolveState state, boolean isRaw, @NotNull PsiElementFactory factory, @NotNull LanguageLevel languageLevel) { boolean resolved = false; for (final PsiClassType superType : aClass.getSuperTypes()) { final PsiClassType.ClassResolveResult superTypeResolveResult = superType.resolveGenerics(); PsiClass superClass = superTypeResolveResult.getElement(); if (superClass == null) continue; PsiSubstitutor finalSubstitutor = obtainFinalSubstitutor( superClass, superTypeResolveResult.getSubstitutor(), aClass, state.get(PsiSubstitutor.KEY), factory, languageLevel); if (aClass instanceof PsiTypeParameter && PsiUtil.isRawSubstitutor(superClass, finalSubstitutor)) { finalSubstitutor = PsiSubstitutor.EMPTY; } if (!processDeclarationsInClass( superClass, processor, state.put(PsiSubstitutor.KEY, finalSubstitutor), visited, last, place, isRaw)) { resolved = true; } } return !resolved; } @Nullable public static PsiClass getSuperClass(@NotNull PsiClass psiClass) { PsiManager manager = psiClass.getManager(); GlobalSearchScope resolveScope = psiClass.getResolveScope(); final JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject()); if (psiClass.isInterface()) { return facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope); } if (psiClass.isEnum()) { return facade.findClass(CommonClassNames.JAVA_LANG_ENUM, resolveScope); } if (psiClass instanceof PsiAnonymousClass) { PsiClassType baseClassReference = ((PsiAnonymousClass) psiClass).getBaseClassType(); PsiClass baseClass = baseClassReference.resolve(); if (baseClass == null || baseClass.isInterface()) return facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope); return baseClass; } if (CommonClassNames.JAVA_LANG_OBJECT.equals(psiClass.getQualifiedName())) return null; final PsiClassType[] referenceElements = psiClass.getExtendsListTypes(); if (referenceElements.length == 0) return facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope); PsiClass psiResoved = referenceElements[0].resolve(); return psiResoved == null ? facade.findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope) : psiResoved; } @NotNull public static PsiClass[] getSupers(@NotNull PsiClass psiClass) { final PsiClass[] supers = getSupersInner(psiClass); for (final PsiClass aSuper : supers) { LOG.assertTrue(aSuper != null); } return supers; } @NotNull private static PsiClass[] getSupersInner(@NotNull PsiClass psiClass) { PsiClassType[] extendsListTypes = psiClass.getExtendsListTypes(); PsiClassType[] implementsListTypes = psiClass.getImplementsListTypes(); if (psiClass.isInterface()) { return resolveClassReferenceList( extendsListTypes, psiClass.getManager(), psiClass.getResolveScope(), true); } if (psiClass instanceof PsiAnonymousClass) { PsiAnonymousClass psiAnonymousClass = (PsiAnonymousClass) psiClass; PsiClassType baseClassReference = psiAnonymousClass.getBaseClassType(); PsiClass baseClass = baseClassReference.resolve(); if (baseClass != null) { if (baseClass.isInterface()) { PsiClass objectClass = JavaPsiFacade.getInstance(psiClass.getProject()) .findClass(CommonClassNames.JAVA_LANG_OBJECT, psiClass.getResolveScope()); return objectClass != null ? new PsiClass[] {objectClass, baseClass} : new PsiClass[] {baseClass}; } return new PsiClass[] {baseClass}; } PsiClass objectClass = JavaPsiFacade.getInstance(psiClass.getProject()) .findClass(CommonClassNames.JAVA_LANG_OBJECT, psiClass.getResolveScope()); return objectClass != null ? new PsiClass[] {objectClass} : PsiClass.EMPTY_ARRAY; } if (psiClass instanceof PsiTypeParameter) { if (extendsListTypes.length == 0) { final PsiClass objectClass = JavaPsiFacade.getInstance(psiClass.getProject()) .findClass(CommonClassNames.JAVA_LANG_OBJECT, psiClass.getResolveScope()); return objectClass != null ? new PsiClass[] {objectClass} : PsiClass.EMPTY_ARRAY; } return resolveClassReferenceList( extendsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false); } PsiClass[] interfaces = resolveClassReferenceList( implementsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false); PsiClass superClass = getSuperClass(psiClass); if (superClass == null) return interfaces; PsiClass[] types = new PsiClass[interfaces.length + 1]; types[0] = superClass; System.arraycopy(interfaces, 0, types, 1, interfaces.length); return types; } @NotNull public static PsiClassType[] getSuperTypes(@NotNull PsiClass psiClass) { if (psiClass instanceof PsiAnonymousClass) { PsiClassType baseClassType = ((PsiAnonymousClass) psiClass).getBaseClassType(); PsiClass baseClass = baseClassType.resolve(); if (baseClass == null || !baseClass.isInterface()) { return new PsiClassType[] {baseClassType}; } else { PsiClassType objectType = PsiType.getJavaLangObject(psiClass.getManager(), psiClass.getResolveScope()); return new PsiClassType[] {objectType, baseClassType}; } } PsiClassType[] extendsTypes = psiClass.getExtendsListTypes(); PsiClassType[] implementsTypes = psiClass.getImplementsListTypes(); boolean hasExtends = extendsTypes.length != 0; int extendsListLength = extendsTypes.length + (hasExtends ? 0 : 1); PsiClassType[] result = new PsiClassType[extendsListLength + implementsTypes.length]; System.arraycopy(extendsTypes, 0, result, 0, extendsTypes.length); if (!hasExtends) { if (CommonClassNames.JAVA_LANG_OBJECT.equals(psiClass.getQualifiedName())) { return PsiClassType.EMPTY_ARRAY; } PsiManager manager = psiClass.getManager(); PsiClassType objectType = PsiType.getJavaLangObject(manager, psiClass.getResolveScope()); result[0] = objectType; } System.arraycopy(implementsTypes, 0, result, extendsListLength, implementsTypes.length); for (int i = 0; i < result.length; i++) { PsiClassType type = result[i]; result[i] = (PsiClassType) PsiUtil.captureToplevelWildcards(type, psiClass); } return result; } @NotNull private static PsiClassType getAnnotationSuperType( @NotNull PsiClass psiClass, @NotNull PsiElementFactory factory) { return factory.createTypeByFQClassName( "java.lang.annotation.Annotation", psiClass.getResolveScope()); } private static PsiClassType getEnumSuperType( @NotNull PsiClass psiClass, @NotNull PsiElementFactory factory) { PsiClassType superType; final PsiManager manager = psiClass.getManager(); final PsiClass enumClass = JavaPsiFacade.getInstance(manager.getProject()) .findClass("java.lang.Enum", psiClass.getResolveScope()); if (enumClass == null) { try { superType = (PsiClassType) factory.createTypeFromText("java.lang.Enum", null); } catch (IncorrectOperationException e) { superType = null; } } else { final PsiTypeParameter[] typeParameters = enumClass.getTypeParameters(); PsiSubstitutor substitutor = PsiSubstitutor.EMPTY; if (typeParameters.length == 1) { substitutor = substitutor.put(typeParameters[0], factory.createType(psiClass)); } superType = new PsiImmediateClassType(enumClass, substitutor); } return superType; } @NotNull public static PsiClass[] getInterfaces(@NotNull PsiTypeParameter typeParameter) { final PsiClassType[] referencedTypes = typeParameter.getExtendsListTypes(); if (referencedTypes.length == 0) { return PsiClass.EMPTY_ARRAY; } final List<PsiClass> result = new ArrayList<PsiClass>(referencedTypes.length); for (PsiClassType referencedType : referencedTypes) { final PsiClass psiClass = referencedType.resolve(); if (psiClass != null && psiClass.isInterface()) { result.add(psiClass); } } return result.toArray(new PsiClass[result.size()]); } @NotNull public static PsiClass[] getInterfaces(@NotNull PsiClass psiClass) { if (psiClass.isInterface()) { final PsiClassType[] extendsListTypes = psiClass.getExtendsListTypes(); return resolveClassReferenceList( extendsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false); } if (psiClass instanceof PsiAnonymousClass) { PsiClassType baseClassReference = ((PsiAnonymousClass) psiClass).getBaseClassType(); PsiClass baseClass = baseClassReference.resolve(); return baseClass != null && baseClass.isInterface() ? new PsiClass[] {baseClass} : PsiClass.EMPTY_ARRAY; } final PsiClassType[] implementsListTypes = psiClass.getImplementsListTypes(); return resolveClassReferenceList( implementsListTypes, psiClass.getManager(), psiClass.getResolveScope(), false); } @NotNull private static PsiClass[] resolveClassReferenceList( @NotNull PsiClassType[] listOfTypes, @NotNull PsiManager manager, @NotNull GlobalSearchScope resolveScope, boolean includeObject) { PsiClass objectClass = JavaPsiFacade.getInstance(manager.getProject()) .findClass(CommonClassNames.JAVA_LANG_OBJECT, resolveScope); if (objectClass == null) includeObject = false; if (listOfTypes.length == 0) { if (includeObject) return new PsiClass[] {objectClass}; return PsiClass.EMPTY_ARRAY; } int referenceCount = listOfTypes.length; if (includeObject) referenceCount++; PsiClass[] resolved = new PsiClass[referenceCount]; int resolvedCount = 0; if (includeObject) resolved[resolvedCount++] = objectClass; for (PsiClassType reference : listOfTypes) { PsiClass refResolved = reference.resolve(); if (refResolved != null) resolved[resolvedCount++] = refResolved; } if (resolvedCount < referenceCount) { PsiClass[] shorter = new PsiClass[resolvedCount]; System.arraycopy(resolved, 0, shorter, 0, resolvedCount); resolved = shorter; } return resolved; } @NotNull public static List<Pair<PsiMethod, PsiSubstitutor>> findMethodsAndTheirSubstitutorsByName( @NotNull PsiClass psiClass, String name, boolean checkBases) { if (!checkBases) { final PsiMethod[] methodsByName = psiClass.findMethodsByName(name, false); final List<Pair<PsiMethod, PsiSubstitutor>> ret = new ArrayList<Pair<PsiMethod, PsiSubstitutor>>(methodsByName.length); for (final PsiMethod method : methodsByName) { ret.add(new Pair<PsiMethod, PsiSubstitutor>(method, PsiSubstitutor.EMPTY)); } return ret; } Map<String, List<Pair<PsiMember, PsiSubstitutor>>> map = getMap(psiClass, MemberType.METHOD); @SuppressWarnings("unchecked") List<Pair<PsiMethod, PsiSubstitutor>> list = (List) map.get(name); return list == null ? Collections.<Pair<PsiMethod, PsiSubstitutor>>emptyList() : Collections.unmodifiableList(list); } @NotNull public static PsiClassType[] getExtendsListTypes(@NotNull PsiClass psiClass) { if (psiClass.isEnum()) { PsiClassType enumSuperType = getEnumSuperType( psiClass, JavaPsiFacade.getInstance(psiClass.getProject()).getElementFactory()); return enumSuperType == null ? PsiClassType.EMPTY_ARRAY : new PsiClassType[] {enumSuperType}; } if (psiClass.isAnnotationType()) { return new PsiClassType[] { getAnnotationSuperType( psiClass, JavaPsiFacade.getInstance(psiClass.getProject()).getElementFactory()) }; } final PsiReferenceList extendsList = psiClass.getExtendsList(); if (extendsList != null) { return extendsList.getReferencedTypes(); } return PsiClassType.EMPTY_ARRAY; } @NotNull public static PsiClassType[] getImplementsListTypes(@NotNull PsiClass psiClass) { final PsiReferenceList extendsList = psiClass.getImplementsList(); if (extendsList != null) { return extendsList.getReferencedTypes(); } return PsiClassType.EMPTY_ARRAY; } public static boolean isClassEquivalentTo(@NotNull PsiClass aClass, PsiElement another) { if (aClass == another) return true; if (!(another instanceof PsiClass)) return false; String name1 = aClass.getName(); if (name1 == null) return false; if (!another.isValid()) return false; String name2 = ((PsiClass) another).getName(); if (name2 == null) return false; if (name1.hashCode() != name2.hashCode()) return false; if (!name1.equals(name2)) return false; String qName1 = aClass.getQualifiedName(); String qName2 = ((PsiClass) another).getQualifiedName(); if (qName1 == null || qName2 == null) { //noinspection StringEquality if (qName1 != qName2) return false; if (aClass instanceof PsiTypeParameter && another instanceof PsiTypeParameter) { PsiTypeParameter p1 = (PsiTypeParameter) aClass; PsiTypeParameter p2 = (PsiTypeParameter) another; return p1.getIndex() == p2.getIndex() && aClass.getManager().areElementsEquivalent(p1.getOwner(), p2.getOwner()); } else { return false; } } if (qName1.hashCode() != qName2.hashCode() || !qName1.equals(qName2)) { return false; } if (originalElement(aClass).equals(originalElement((PsiClass) another))) { return true; } final PsiFile file1 = aClass.getContainingFile().getOriginalFile(); final PsiFile file2 = another.getContainingFile().getOriginalFile(); // see com.intellij.openapi.vcs.changes.PsiChangeTracker // see com.intellij.psi.impl.PsiFileFactoryImpl#createFileFromText(CharSequence,PsiFile) final PsiFile original1 = file1.getUserData(PsiFileFactory.ORIGINAL_FILE); final PsiFile original2 = file2.getUserData(PsiFileFactory.ORIGINAL_FILE); if (original1 == original2 && original1 != null || original1 == file2 || original2 == file1 || file1 == file2) { return compareClassSeqNumber(aClass, (PsiClass) another); } final FileIndexFacade fileIndex = ServiceManager.getService(file1.getProject(), FileIndexFacade.class); final VirtualFile vfile1 = file1.getViewProvider().getVirtualFile(); final VirtualFile vfile2 = file2.getViewProvider().getVirtualFile(); boolean lib1 = fileIndex.isInLibraryClasses(vfile1); boolean lib2 = fileIndex.isInLibraryClasses(vfile2); return (fileIndex.isInSource(vfile1) || lib1) && (fileIndex.isInSource(vfile2) || lib2); } private static boolean compareClassSeqNumber( @NotNull PsiClass aClass, @NotNull PsiClass another) { // there may be several classes in one file, they must not be equal int index1 = getSeqNumber(aClass); if (index1 == -1) return true; int index2 = getSeqNumber(another); return index1 == index2; } private static int getSeqNumber(@NotNull PsiClass aClass) { // sequence number of this class among its parent' child classes named the same PsiElement parent = aClass.getParent(); if (parent == null) return -1; int seqNo = 0; for (PsiElement child : parent.getChildren()) { if (child == aClass) return seqNo; if (child instanceof PsiClass && Comparing.strEqual(aClass.getName(), ((PsiClass) child).getName())) { seqNo++; } } return -1; } @NotNull private static PsiElement originalElement(@NotNull PsiClass aClass) { final PsiElement originalElement = aClass.getOriginalElement(); ASTNode node = originalElement.getNode(); if (node != null) { final PsiCompiledElement compiled = node.getUserData(ClsElementImpl.COMPILED_ELEMENT); if (compiled != null) { return compiled; } } return originalElement; } public static boolean isFieldEquivalentTo(@NotNull PsiField field, PsiElement another) { if (!(another instanceof PsiField)) return false; String name1 = field.getName(); if (name1 == null) return false; if (!another.isValid()) return false; String name2 = ((PsiField) another).getName(); if (!name1.equals(name2)) return false; PsiClass aClass1 = field.getContainingClass(); PsiClass aClass2 = ((PsiField) another).getContainingClass(); return aClass1 != null && aClass2 != null && field.getManager().areElementsEquivalent(aClass1, aClass2); } public static boolean isMethodEquivalentTo(@NotNull PsiMethod method1, PsiElement another) { if (method1 == another) return true; if (!(another instanceof PsiMethod)) return false; PsiMethod method2 = (PsiMethod) another; if (!another.isValid()) return false; if (!method1.getName().equals(method2.getName())) return false; PsiClass aClass1 = method1.getContainingClass(); PsiClass aClass2 = method2.getContainingClass(); PsiManager manager = method1.getManager(); if (!(aClass1 != null && aClass2 != null && manager.areElementsEquivalent(aClass1, aClass2))) return false; PsiParameter[] parameters1 = method1.getParameterList().getParameters(); PsiParameter[] parameters2 = method2.getParameterList().getParameters(); if (parameters1.length != parameters2.length) return false; for (int i = 0; i < parameters1.length; i++) { PsiParameter parameter1 = parameters1[i]; PsiParameter parameter2 = parameters2[i]; PsiType type1 = parameter1.getType(); PsiType type2 = parameter2.getType(); if (!compareParamTypes(manager, type1, type2)) return false; } return true; } private static boolean compareParamTypes( @NotNull PsiManager manager, @NotNull PsiType type1, @NotNull PsiType type2) { if (type1 instanceof PsiArrayType) { return type2 instanceof PsiArrayType && compareParamTypes( manager, ((PsiArrayType) type1).getComponentType(), ((PsiArrayType) type2).getComponentType()); } if (!(type1 instanceof PsiClassType) || !(type2 instanceof PsiClassType)) { return type1.equals(type2); } PsiClass class1 = ((PsiClassType) type1).resolve(); PsiClass class2 = ((PsiClassType) type2).resolve(); if (class1 instanceof PsiTypeParameter && class2 instanceof PsiTypeParameter) { return Comparing.equal(class1.getName(), class2.getName()) && ((PsiTypeParameter) class1).getIndex() == ((PsiTypeParameter) class2).getIndex(); } return manager.areElementsEquivalent(class1, class2); } }