diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonAnnotatorsRespondToChangesTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonAnnotatorsRespondToChangesTest.java index 93e288336a01..5f570ae2d242 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonAnnotatorsRespondToChangesTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonAnnotatorsRespondToChangesTest.java @@ -22,6 +22,7 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorMouseHoverPopupManager; import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.editor.ex.MarkupModelEx; import com.intellij.openapi.editor.impl.DocumentMarkupModel; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.editor.markup.MarkupModel; @@ -36,6 +37,7 @@ import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl; import com.intellij.openapi.util.ProperTextRange; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; @@ -80,6 +82,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase @Override protected void tearDown() throws Exception { + MyRecordingAnnotator.clearAll(); try { if (myEditor != null) { Document document = myEditor.getDocument(); @@ -169,7 +172,6 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase } public void testAddRemoveHighlighterRaceInIncorrectAnnotatorsWhichUseFileRecursiveVisit() { - //System.out.println("i = " + i); useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new MyRecordingAnnotator[]{new MyIncorrectlyRecursiveAnnotator()}, () -> { @Language("JAVA") String text1 = """ @@ -188,7 +190,6 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase assertEquals("XXX", assertOneElement(doHighlighting(HighlightSeverity.WARNING)).getDescription()); for (int i = 0; i < 100; i++) { - //System.out.println("i = " + i); myDaemonCodeAnalyzer.restart(); List infos = doHighlighting(HighlightSeverity.WARNING); assertEquals("XXX", assertOneElement(infos).getDescription()); @@ -295,6 +296,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase TimeoutUtil.sleep(100); } } + LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt()); } } public static class MyFastAnnotator extends MyRecordingAnnotator { @@ -306,6 +308,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase holder.newAnnotation(HighlightSeverity.ERROR, SWEARING).range(element).create(); iDidIt(); } + LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt()); } } @@ -316,6 +319,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase holder.newAnnotation(HighlightSeverity.INFORMATION, "comment").create(); iDidIt(); } + LOG.debug(getClass()+".annotate("+element+") = "+didIDoIt()); } } @@ -356,7 +360,9 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase MarkupModel markupModel = DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); TestTimeOut n = TestTimeOut.setTimeout(100, TimeUnit.SECONDS); AtomicInteger called = new AtomicInteger(); + AtomicBoolean success = new AtomicBoolean(); Runnable checkHighlighted = () -> { + if (success.get()) return; called.incrementAndGet(); UIUtil.dispatchAllInvocationEvents(); long highlighted = Arrays.stream(markupModel.getAllHighlighters()) @@ -366,26 +372,25 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase .count(); if (highlighted != 0) { toSleepMs.set(0); - throw new DebugException(); // sorry for that, had to differentiate from failure + success.set(true); + //throw new DebugException(); // sorry for that, had to differentiate from failure } if (n.timedOut()) { toSleepMs.set(0); throw new RuntimeException(new TimeoutException(ThreadDumper.dumpThreadsToString())); } }; - try { - CodeInsightTestFixtureImpl.ensureIndexesUpToDate(getProject()); - TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor()); - PsiDocumentManager.getInstance(myProject).commitAllDocuments(); - long start = System.currentTimeMillis(); - myDaemonCodeAnalyzer.runPasses(getFile(), getEditor().getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, false, checkHighlighted); + CodeInsightTestFixtureImpl.ensureIndexesUpToDate(getProject()); + TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(getEditor()); + PsiDocumentManager.getInstance(myProject).commitAllDocuments(); + long start = System.currentTimeMillis(); + myDaemonCodeAnalyzer.runPasses(getFile(), getEditor().getDocument(), textEditor, ArrayUtilRt.EMPTY_INT_ARRAY, false, checkHighlighted); + if (!success.get()) { List errors = ContainerUtil.filter(markupModel.getAllHighlighters(), highlighter -> HighlightInfo.fromRangeHighlighter(highlighter) != null && HighlightInfo.fromRangeHighlighter(highlighter).getSeverity() == HighlightSeverity.ERROR); long elapsed = System.currentTimeMillis() - start; fail("should have been interrupted. toSleepMs: " + toSleepMs + "; highlights: " + errors + "; called: " + called+"; highlighted in "+elapsed+"ms"); } - catch (DebugException ignored) { - } } public static class MyNewBuilderAnnotator extends MyRecordingAnnotator { @@ -393,21 +398,21 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { if (element instanceof PsiComment && element.getText().equals("//XXX")) { holder.newAnnotation(HighlightSeverity.ERROR, MyFastAnnotator.SWEARING).create(); + iDidIt(); + } + else if (didIDoIt()) { // sleep after creating annotation to emulate a very big annotator which does a great amount of work after registering annotation - // use this contrived form to be able to bail out immediately by modifying toSleepMs in the other thread while (toSleepMs.addAndGet(-100) > 0) { TimeoutUtil.sleep(100); } - iDidIt(); } } } public void testAddAnnotationViaBuilderEntailsCreatingCorrespondingRangeHighlighterImmediately() { PlatformTestUtil.assumeEnoughParallelism(); - useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new MyRecordingAnnotator[]{new MyNewBuilderAnnotator()}, - this::checkSwearingHighlightIsVisibleImmediately); + useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new MyRecordingAnnotator[]{new MyNewBuilderAnnotator()}, this::checkSwearingHighlightIsVisibleImmediately); } private static final AtomicBoolean annotated = new AtomicBoolean(); @@ -436,7 +441,7 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase } } - public void test_SerializeCodeInsightPasses_SecretSettingDoesWork() { + public void _test_SerializeCodeInsightPasses_SecretSettingDoesWork() { PlatformTestUtil.assumeEnoughParallelism(); TextEditorHighlightingPassRegistrarImpl registrar = @@ -538,24 +543,17 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase } private void checkFirstAnnotation() { - AtomicReference reported = new AtomicReference<>(); + AtomicReference firstStatistics = new AtomicReference<>(); getProject().getMessageBus().connect(getTestRootDisposable()).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC, - new DaemonCodeAnalyzer.DaemonListener() { - @Override - public void daemonAnnotatorStatisticsGenerated(@NotNull AnnotationSession session, - @NotNull Collection statistics, - @NotNull PsiFile file) { - AnnotatorStatistics stat = assertOneElement(ContainerUtil.filter(statistics, stat1 -> stat1.annotator instanceof MyInfoAnnotator)); - Throwable old = reported.getAndSet(new Throwable()); - assertNull(old==null? null: ExceptionUtil.getMessage(old), old); - assertEquals("Annotation(message='comment', severity='INFORMATION', toolTip='comment')", stat.firstAnnotation.toString()); - assertSame(stat.firstAnnotation, stat.lastAnnotation); - assertTrue(stat.annotatorStartStamp > 0); - assertTrue(stat.firstAnnotationStamp >= stat.annotatorStartStamp); - assertTrue(stat.lastAnnotationStamp >= stat.firstAnnotationStamp); - assertTrue(stat.annotatorFinishStamp >= stat.lastAnnotationStamp); - } - }); + new DaemonCodeAnalyzer.DaemonListener() { + @Override + public void daemonAnnotatorStatisticsGenerated(@NotNull AnnotationSession session, + @NotNull Collection statistics, + @NotNull PsiFile file) { + AnnotatorStatistics stat = assertOneElement(ContainerUtil.filter(statistics, stat1 -> stat1.annotator instanceof MyInfoAnnotator)); + firstStatistics.compareAndExchange(null, stat); + } + }); @Language("JAVA") String text = """ @@ -566,7 +564,15 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase configureByText(JavaFileType.INSTANCE, text); doHighlighting(); - assertNotNull(reported.get()); + + DaemonCodeAnalyzer.DaemonListener.AnnotatorStatistics stat = firstStatistics.get(); + assertNotNull(stat); + assertEquals("Annotation(message='comment', severity='INFORMATION', toolTip='comment')", stat.firstAnnotation.toString()); + assertSame(stat.firstAnnotation, stat.lastAnnotation); + assertTrue(stat.annotatorStartStamp > 0); + assertTrue(stat.firstAnnotationStamp >= stat.annotatorStartStamp); + assertTrue(stat.lastAnnotationStamp >= stat.firstAnnotationStamp); + assertTrue(stat.annotatorFinishStamp >= stat.lastAnnotationStamp); } private static final String wordToAnnotate = "annotate_here"; @@ -642,4 +648,215 @@ public class DaemonAnnotatorsRespondToChangesTest extends DaemonAnalyzerTestCase expectedVisibleRange = new TextRange(0, editor.getDocument().getTextLength()); useAnnotatorsIn(PlainTextLanguage.INSTANCE, new MyRecordingAnnotator[]{new CheckVisibleRangeAnnotator()}, ()-> assertEmpty(doHighlighting())); } + + // highlight each field, stall every other element + static class MyFieldSlowAnnotator extends MyRecordingAnnotator { + static final AtomicReference fieldWarningText = new AtomicReference<>(); + static final AtomicInteger stallMs = new AtomicInteger(); + static final AtomicBoolean finished = new AtomicBoolean(); + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (element instanceof PsiField) { + holder.newAnnotation(HighlightSeverity.WARNING, fieldWarningText.get()).range(element).create(); + iDidIt(); + } + else if (element instanceof PsiFile) { + finished.set(true); + } + else { + // stall every other element to exacerbate latency problems if the order is wrong + TimeoutUtil.sleep(stallMs.get()); + } + } + } + // highlights all "xxx" comments, but only when there are no comments after it + static class MyCommentFastAnnotator extends MyRecordingAnnotator { + static final AtomicBoolean finished = new AtomicBoolean(); + static final String fastToolText = "blah.MyCommentFastAnnotator"; + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (element instanceof PsiComment) { + if (element.getText().contains("xxx") && !element.getContainingFile().getText().substring(element.getTextOffset()+2).contains("//")) { + holder.newAnnotation(HighlightSeverity.WARNING, fastToolText).range(element).create(); + iDidIt(); + } + } + else if (element instanceof PsiFile) { + finished.set(true); + iDidIt(); + } + } + } + + public void testAnnotatorMustRemoveItsObsoleteHighlightsImmediatelyAfterFinished() { + @Language("JAVA") + String text = """ + class LQF { + // xxx + int f; + }"""; + configureByText(JavaFileType.INSTANCE, text); + DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way + UIUtil.markAsFocused(getEditor().getContentComponent(), true); // to make ShowIntentionPass call its collectInformation() + SeverityRegistrar.getSeverityRegistrar(getProject()); //preload inspection profile + MyFieldSlowAnnotator.fieldWarningText.set("1st run"); + MyFieldSlowAnnotator.finished.set(false); + MyFieldSlowAnnotator.stallMs.set(0); + + MyCommentFastAnnotator.finished.set(false); + + Map annotatorsByLanguage = new HashMap<>(); + annotatorsByLanguage.put(JavaLanguage.INSTANCE, new MyRecordingAnnotator[]{new MyFieldSlowAnnotator(), new MyCommentFastAnnotator()}); + MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); + + // both annos should produce their results + useAnnotatorsIn(annotatorsByLanguage, () -> { + List infos = doHighlighting(HighlightSeverity.WARNING); + assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(MyFieldSlowAnnotator.fieldWarningText.get()))); + assertTrue(infos.toString(), ContainerUtil.exists(infos, i -> i.getDescription().equals(MyCommentFastAnnotator.fastToolText))); + RangeHighlighter[] markers = model.getAllHighlighters(); + assertTrue(Arrays.toString(markers), ContainerUtil.exists(markers, i -> HighlightInfo.fromRangeHighlighter(i) != null && MyFieldSlowAnnotator.fieldWarningText.get().equals(HighlightInfo.fromRangeHighlighter(i).getDescription()))); + assertTrue(Arrays.toString(markers), ContainerUtil.exists(markers, i -> HighlightInfo.fromRangeHighlighter(i) != null && MyCommentFastAnnotator.fastToolText.equals(HighlightInfo.fromRangeHighlighter(i).getDescription()))); + }); + + MyFieldSlowAnnotator.fieldWarningText.set("Aha, field, finally!"); + MyFieldSlowAnnotator.stallMs.set(100); + // type another comment which will cause the warning about the first comment (by MyCommentFastAnnotator) to disappear + // and check that as soon as MyCommentFastAnnotator is finished, it removed its own obsolete warnings, whereas MyFieldSlowAnnotator continues to run + type("// another comment"); + MyCommentFastAnnotator.finished.set(false); + MyFieldSlowAnnotator.finished.set(false); + DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way + useAnnotatorsIn(annotatorsByLanguage, () -> { + // now when the highlighting is restarted, we should get back our inspection result very fast, despite very slow processing of every other element + long deadline = System.currentTimeMillis() + 10_000; + while (!DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) { + PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); + if (System.currentTimeMillis() > deadline) { + fail("Too long waiting for daemon to start"); + } + } + try { + boolean fastToolFinishedFaster = false; + while (DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) { + if (System.currentTimeMillis() > deadline) { + fail("Too long waiting for daemon to finish\n" + ThreadDumper.dumpThreadsToString()); + } + PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); + if (MyCommentFastAnnotator.finished.get() && !MyFieldSlowAnnotator.finished.get()) { + boolean fastToolWarningFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0, + myEditor.getDocument().getTextLength(), + info -> !MyCommentFastAnnotator.fastToolText.equals(info.getDescription())); + fastToolFinishedFaster = true; + if (fastToolWarningFound) { + fail("Annotator must have removed its own obsolete highlights as soon as it's finished, but got:" + + StringUtil.join(model.getAllHighlighters(), Object::toString, "\n ") + "; thread dump:\n" + ThreadDumper.dumpThreadsToString()); + } + } + } + assertTrue("Fast inspection must have finished faster than the slow one, but it didn't", fastToolFinishedFaster); + } + finally { + MyFieldSlowAnnotator.stallMs.set(0); + } + }); + } + + static class MyComment1Annotator extends MyRecordingAnnotator { + static final AtomicBoolean stall1 = new AtomicBoolean(); + static final String comment1Text = "comment1Text"; + public MyComment1Annotator() { + } + + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + while (stall1.get()) { + Thread.yield(); + //ProgressManager.checkCanceled(); + } + if (element instanceof PsiComment) { + if (element.getText().contains("xxx")) { + holder.newAnnotation(HighlightSeverity.WARNING, comment1Text).range(element).create(); + iDidIt(); + //stall1.set(true); // stall right after producing annotation + } + } + } + } + static class MyComment2Annotator extends MyRecordingAnnotator { + static final AtomicBoolean stall2 = new AtomicBoolean(); + static final String comment2Text = "comment2Text"; + public MyComment2Annotator() { + } + + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + while (stall2.get()) { + Thread.yield(); + //ProgressManager.checkCanceled(); + } + if (element instanceof PsiComment) { + if (element.getText().contains("xxx")) { + holder.newAnnotation(HighlightSeverity.WARNING, comment2Text).range(element).create(); + iDidIt(); + //stall2.set(true); // stall right after producing annotation + } + } + } + } + + public void testAnnotatorsMustNotWaitForEachOther() { + PlatformTestUtil.assumeEnoughParallelism(); + @Language("JAVA") + String text = """ + class LQF { + // xxx + int f; + }"""; + configureByText(JavaFileType.INSTANCE, text); + DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way + UIUtil.markAsFocused(getEditor().getContentComponent(), true); // to make ShowIntentionPass call its collectInformation() + + Map annotatorsByLanguage = new HashMap<>(); + annotatorsByLanguage.put(JavaLanguage.INSTANCE, new MyRecordingAnnotator[]{new MyComment1Annotator(), new MyComment2Annotator()}); + MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(getEditor().getDocument(), getProject(), true); + + // both annos should produce their results + myDaemonCodeAnalyzer.restart(); + DaemonRespondToChangesTest.makeWholeEditorWindowVisible((EditorImpl)myEditor); // get "visible area first" optimization out of the way + useAnnotatorsIn(annotatorsByLanguage, () -> { + long deadline = System.currentTimeMillis() + 20_000; + while (!DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) { + PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); + if (System.currentTimeMillis() > deadline) { + fail("Too long waiting for daemon to start"); + } + } + boolean tool1AnnoFound = false; + boolean tool2AnnoFound = false; + while (!tool1AnnoFound || !tool2AnnoFound) { + if (System.currentTimeMillis() > deadline) { + fail("Too long waiting for daemon to finish\n" + ThreadDumper.dumpThreadsToString()); + } + PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); + tool1AnnoFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0, + myEditor.getDocument().getTextLength(), + info -> !MyComment1Annotator.comment1Text.equals(info.getDescription())); + tool2AnnoFound = !DaemonCodeAnalyzerEx.processHighlights(model, getProject(), HighlightSeverity.WARNING, 0, + myEditor.getDocument().getTextLength(), + info -> !MyComment2Annotator.comment2Text.equals(info.getDescription())); + } + MyComment1Annotator.stall1.set(false); + MyComment2Annotator.stall2.set(false); + while (DaemonRespondToChangesTest.daemonIsWorkingOrPending(myProject, myEditor.getDocument())) { + PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue(); + if (System.currentTimeMillis() > deadline+1_000) { + fail("Too long waiting for daemon to finish; stall1="+MyComment1Annotator.stall1+"; stall2="+MyComment2Annotator.stall2 + + "\n" + ThreadDumper.dumpThreadsToString()); + } + Thread.yield(); + } + }); + } + } diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonRespondToChangesTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonRespondToChangesTest.java index 048c35084003..891bba20e6ea 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonRespondToChangesTest.java +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/DaemonRespondToChangesTest.java @@ -2632,4 +2632,17 @@ public class DaemonRespondToChangesTest extends DaemonAnalyzerTestCase { assertEmpty(highlightErrors()); } + + public void testTypingErrorElementMustHighlightIt() { + ThreadingAssertions.assertEventDispatchThread(); + configureByText(JavaFileType.INSTANCE, "class X { void f() { } }"); + assertEmpty(highlightErrors()); + makeEditorWindowVisible(new Point(0, 1000), myEditor); + + type("/"); + waitForDaemon(myProject, myEditor.getDocument()); + List errors = DaemonCodeAnalyzerImpl.getHighlights(getEditor().getDocument(), HighlightSeverity.ERROR, getProject()); + assertNotEmpty(errors); + assertTrue(errors.toString().contains("'class' or 'interface' expected")); + } } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAnnotatorHighlightingTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAnnotatorHighlightingTest.java index 8ea1658cf888..945b40f5cf2b 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAnnotatorHighlightingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/LightAnnotatorHighlightingTest.java @@ -48,6 +48,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; +import java.util.Comparator; import java.util.List; import java.util.function.Function; @@ -70,15 +71,14 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase List fileLevel = ((DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(getProject())).getFileLevelHighlights(getProject(), getFile()); HighlightInfo info = assertOneElement(fileLevel); - assertEquals("top level", info.getDescription()); + assertTrue(MyFileLevelAnnotator.isMy(info)); type("\n\n"); assertEmpty(highlightErrors()); fileLevel = ((DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(getProject())).getFileLevelHighlights(getProject(), getFile()); info = assertOneElement(fileLevel); - assertEquals("top level", info.getDescription()); - + assertTrue(MyFileLevelAnnotator.isMy(info)); type("//xxx"); //disable top level annotation List warnings = doHighlighting(HighlightSeverity.WARNING); assertEmpty(warnings); @@ -121,6 +121,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase iDidIt(); } } + static boolean isMy(HighlightInfo info) { + return HighlightSeverity.WARNING.equals(info.getSeverity()) && "top level".equals(info.getDescription()); + } } public void testAnnotatorMustNotSpecifyCrazyRangeForCreatedAnnotation() { @@ -464,23 +467,25 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyStupidRepetitiveAnnotator()}, () -> runMyAnnotators()); } - public void testAnnotatorTryingToHighlightWarningAndErrorToTheSameElementMustFilterOutWarning() { + public void testDifferentAnnotatorsTryingToHighlightWarningAndErrorToTheSameElementMustNotInterfere() { DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyErrorAnnotator(), new MyWarningAnnotator()}, () -> { configureFromFileText("My.java", "class My {}"); ((EditorEx)getEditor()).getScrollPane().getViewport().setSize(new Dimension(1000,1000)); // whole file fit onscreen - List infos = doHighlighting(HighlightSeverity.WARNING); - HighlightInfo info = assertOneElement(infos); - MyErrorAnnotator.assertMy(info); + List infos = ContainerUtil.sorted(doHighlighting(HighlightSeverity.WARNING), Comparator.comparing(HighlightInfo::getSeverity)); + assertEquals(2, infos.size()); + assertTrue(MyWarningAnnotator.isMy(infos.get(0))); + assertTrue(MyErrorAnnotator.isMy(infos.get(1))); }); } - public void testAnnotatorTryingToHighlightInformationAndErrorToTheSameElementMustFilterOutInformation() { + public void testDifferentAnnotatorsTryingToHighlightInformationAndErrorToTheSameElementMustNotInterfere() { DaemonAnnotatorsRespondToChangesTest.useAnnotatorsIn(JavaFileType.INSTANCE.getLanguage(), new DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator[]{new MyErrorAnnotator(), new MyInfoAnnotator()}, () -> { configureFromFileText("My.java", "class My {}"); ((EditorEx)getEditor()).getScrollPane().getViewport().setSize(new Dimension(1000,1000)); // whole file fit onscreen - List infos = doHighlighting(HighlightSeverity.INFORMATION); - HighlightInfo info = assertOneElement(infos); - MyErrorAnnotator.assertMy(info); + List infos = ContainerUtil.sorted(doHighlighting(HighlightSeverity.INFORMATION), Comparator.comparing(HighlightInfo::getSeverity)); + assertEquals(2, infos.size()); + assertTrue(MyInfoAnnotator.isMy(infos.get(0))); + assertTrue(MyErrorAnnotator.isMy(infos.get(1))); }); } @@ -489,8 +494,8 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase configureFromFileText("My.java", "class My {}"); ((EditorEx)getEditor()).getScrollPane().getViewport().setSize(new Dimension(1000,1000)); // whole file fit onscreen List infos = doHighlighting(HighlightInfoType.SYMBOL_TYPE_SEVERITY); - assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightSeverity.ERROR) && info.getDescription().equals("error2"))); - assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightInfoType.SYMBOL_TYPE_SEVERITY) && info.getDescription().equals("symbol2"))); + assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightSeverity.ERROR) && MyErrorAnnotator.isMy(info))); + assertTrue(infos.toString(), ContainerUtil.exists(infos, info -> info.getSeverity().equals(HighlightInfoType.SYMBOL_TYPE_SEVERITY) && MySymbolAnnotator.isMy(info))); }); } @@ -502,9 +507,8 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase iDidIt(); } } - static void assertMy(HighlightInfo info) { - assertEquals(HighlightSeverity.ERROR, info.getSeverity()); - assertEquals("error2", info.getDescription()); + static boolean isMy(HighlightInfo info) { + return HighlightSeverity.ERROR.equals(info.getSeverity()) && "error2".equals(info.getDescription()); } } public static class MyWarningAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator { @@ -515,6 +519,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase iDidIt(); } } + static boolean isMy(HighlightInfo info) { + return HighlightSeverity.WARNING.equals(info.getSeverity()) && "warn2".equals(info.getDescription()); + } } public static class MyInfoAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator { @Override @@ -524,6 +531,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase iDidIt(); } } + static boolean isMy(HighlightInfo info) { + return HighlightSeverity.INFORMATION.equals(info.getSeverity()) && "info2".equals(info.getDescription()); + } } public static class MySymbolAnnotator extends DaemonAnnotatorsRespondToChangesTest.MyRecordingAnnotator { @Override @@ -533,6 +543,9 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase iDidIt(); } } + static boolean isMy(HighlightInfo info) { + return HighlightInfoType.SYMBOL_TYPE_SEVERITY.equals(info.getSeverity()) && "symbol2".equals(info.getDescription()); + } } /** @@ -563,7 +576,7 @@ public class LightAnnotatorHighlightingTest extends LightDaemonAnalyzerTestCase private static volatile boolean FIX_ENABLED; @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - if (element.getText().equals("hello")) { + if (element.getText().equals("hello") && !(element instanceof PsiFile)) { holder.newAnnotation(HighlightSeverity.ERROR, "i hate it") .newFix(new DeleteElementFix(element) { @Override diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotationHolderImpl.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotationHolderImpl.java index 6ba7335d88d6..ff713ea94448 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotationHolderImpl.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotationHolderImpl.java @@ -196,11 +196,16 @@ public class AnnotationHolderImpl extends SmartList implements Annot @Override public @NotNull AnnotationBuilder newAnnotation(@NotNull HighlightSeverity severity, @NotNull @Nls String message) { - return new B(this, severity, message, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator)); + return createBuilder(severity, message, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator)); } @Override public @NotNull AnnotationBuilder newSilentAnnotation(@NotNull HighlightSeverity severity) { - return new B(this, severity, null, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator)); + return createBuilder(severity, null, myCurrentElement, ObjectUtils.chooseNotNull(myCurrentAnnotator, myExternalAnnotator)); + } + + @NotNull + protected B createBuilder(@NotNull HighlightSeverity severity, @Nls String message, PsiElement currentElement, Object currentAnnotator) { + return new B(this, severity, message, currentElement, currentAnnotator); } PsiElement myCurrentElement; diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotatorRunner.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotatorRunner.java new file mode 100644 index 000000000000..dbc2e6ee8f1f --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/AnnotatorRunner.java @@ -0,0 +1,262 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.daemon.impl; + +import com.intellij.codeInsight.daemon.AnnotatorStatisticsCollector; +import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder; +import com.intellij.codeInspection.ex.GlobalInspectionContextBase; +import com.intellij.concurrency.JobLauncher; +import com.intellij.diagnostic.PluginException; +import com.intellij.injected.editor.DocumentWindow; +import com.intellij.lang.Language; +import com.intellij.lang.LanguageAnnotators; +import com.intellij.lang.annotation.Annotation; +import com.intellij.lang.annotation.Annotator; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.ApplicationManagerEx; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.ProperTextRange; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.util.PairProcessor; +import com.intellij.util.ReflectionUtil; +import com.intellij.util.containers.CollectionFactory; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.HashingStrategy; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +final class AnnotatorRunner { + private static final Logger LOG = Logger.getInstance(AnnotatorRunner.class); + private static final Annotator TOMB_STONE = (__, __1) -> { }; + private final Project myProject; + private final PsiFile myPsiFile; + private final HighlightInfoHolder myHighlightInfoHolder; + private final HighlightingSession myHighlightingSession; + private final DumbService myDumbService; + private final boolean myBatchMode; + private boolean myDumb; + private final AnnotatorStatisticsCollector myAnnotatorStatisticsCollector = new AnnotatorStatisticsCollector(); + private final List results = Collections.synchronizedList(new ArrayList<>()); + + AnnotatorRunner(@NotNull PsiFile psiFile, boolean batchMode, @NotNull HighlightInfoHolder holder, @NotNull HighlightingSession highlightingSession) { + myProject = psiFile.getProject(); + myPsiFile = psiFile; + myHighlightInfoHolder = holder; + myHighlightingSession = highlightingSession; + myDumbService = DumbService.getInstance(myProject); + myBatchMode = batchMode; + } + + boolean runAnnotatorsAsync(@NotNull List inside, @NotNull List outside, @NotNull BooleanSupplier runnable) { + ApplicationManager.getApplication().assertIsNonDispatchThread(); + DaemonProgressIndicator indicator = GlobalInspectionContextBase.assertUnderDaemonProgress(); + + myDumb = myDumbService.isDumb(); + + // TODO move inside Divider to calc only once + List insideThenOutside = ContainerUtil.concat(inside, outside); + Map> supportedLanguages = calcSupportedLanguages(insideThenOutside); + PairProcessor> processor = (annotator, __) -> + ApplicationManagerEx.getApplicationEx().tryRunReadAction(() -> runAnnotator(annotator, insideThenOutside, supportedLanguages)); + boolean result = JobLauncher.getInstance().procInOrderAsync(indicator, supportedLanguages.size(), processor, addToQueue -> { + for (Annotator annotator : supportedLanguages.keySet()) { + addToQueue.enqueue(annotator); + } + addToQueue.finish(); + return runnable.getAsBoolean(); + }); + myAnnotatorStatisticsCollector.reportAnalysisFinished(myProject, myHighlightInfoHolder.getAnnotationSession(), myPsiFile); + return result; + } + + @NotNull + private Map> calcSupportedLanguages(@NotNull List elements) { + Map> map = CollectionFactory.createCustomHashingStrategyMap(new HashingStrategy<>() { + @Override + public int hashCode(Annotator object) { + return object.getClass().hashCode(); + } + + @Override + public boolean equals(Annotator o1, Annotator o2) { + return o1 == null || o2 == null ? o1==o2 : o1.getClass().equals(o2.getClass()); + } + }); + Set languages = new HashSet<>(); + for (PsiElement element : elements) { + Language language = element.getLanguage(); + addDialects(language, languages); + } + for (Language language : languages) { + List templates = LanguageAnnotators.INSTANCE.allForLanguageOrAny(language); + for (Annotator template : templates) { + Set supportedLanguages = map.get(template); + if (supportedLanguages == null) { + supportedLanguages = new HashSet<>(); + map.put(cloneTemplate(template), supportedLanguages); + } + supportedLanguages.add(language); + } + } + return map; + } + private static void addDialects(@NotNull Language language, @NotNull Set outProcessedLanguages) { + if (outProcessedLanguages.add(language)) { + Collection dialects = language.getTransitiveDialects(); + outProcessedLanguages.addAll(dialects); + } + } + + private void runAnnotator(@NotNull Annotator annotator, + @NotNull List insideThenOutside, + @NotNull Map> supportedLanguages) { + Set supported = supportedLanguages.get(annotator); + if (supported.isEmpty()) { + return; + } + AtomicReference currentElement = new AtomicReference<>(); + // create AnnotationHolderImpl for each Annotator to make it immutable thread-safe converter to the corresponding HighlightInfo + AnnotationHolderImpl annotationHolder = new AnnotationHolderImpl(myHighlightInfoHolder.getAnnotationSession(), myBatchMode) { + @Override + public boolean add(Annotation annotation) { + myAnnotatorStatisticsCollector.reportAnnotationProduced(annotator, annotation); + super.add(annotation); + return true; + } + + @Override + protected @NotNull B createBuilder(@NotNull HighlightSeverity severity, @Nls String message, PsiElement __, Object ___) { + return super.createBuilder(severity, message, currentElement.get(), annotator); + } + }; + HighlightersRecycler emptyElementRecycler = new HighlightersRecycler(); // no need to call incinerate/release because it's always empty + for (PsiElement element : insideThenOutside) { + if (!supported.contains(element.getLanguage())) { + continue; + } + if (myDumb && !DumbService.isDumbAware(annotator)) { + continue; + } + ProgressManager.checkCanceled(); + currentElement.set(element); + int sizeBefore = annotationHolder.size(); + annotator.annotate(element, annotationHolder); + int sizeAfter = annotationHolder.size(); + + List newInfos; + if (sizeBefore == sizeAfter) { + newInfos = List.of(); + } + else { + newInfos = new ArrayList<>(sizeAfter - sizeBefore); + for (int i = sizeBefore; i < sizeAfter; i++) { + Annotation annotation = annotationHolder.get(i); + HighlightInfo info = HighlightInfo.fromAnnotation(annotator.getClass(), annotation, myBatchMode); + info.setGroup(-1); // prevent DefaultHighlightProcessor from removing this info, we want to control it ourselves via `psiElementVisited` below + addConvertedToHostInfo(info, newInfos); + if (LOG.isDebugEnabled()) { + LOG.debug("runAnnotator annotation="+annotation+" -> "+newInfos); + } + } + results.addAll(newInfos); + } + Document hostDocument = myPsiFile.getFileDocument(); + if (hostDocument instanceof DocumentWindow w) hostDocument = w.getDelegate(); + HighlightInfoUpdater.getInstance(myProject).psiElementVisited(annotator.getClass(), element, newInfos, hostDocument, myPsiFile, myProject, + emptyElementRecycler, myHighlightingSession); + } + } + + private static void addPatchedInfos(@NotNull HighlightInfo info, + @NotNull PsiFile injectedPsi, + @NotNull DocumentWindow documentWindow, + @NotNull InjectedLanguageManager injectedLanguageManager, + @NotNull Consumer outInfos) { + ProperTextRange infoRange = new ProperTextRange(info.startOffset, info.endOffset); + List editables = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, infoRange); + for (TextRange editable : editables) { + TextRange hostRange = documentWindow.injectedToHost(editable); + + boolean isAfterEndOfLine = info.isAfterEndOfLine(); + if (isAfterEndOfLine) { + // convert injected afterEndOfLine to either host's 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.getDescription(), info.getToolTip(), info.getSeverity(), isAfterEndOfLine, null, + false, 0, info.getProblemGroup(), info.toolId, info.getGutterIconRenderer(), info.getGroup(), info.unresolvedReference); + patched.setHint(info.hasHint()); + + info.findRegisteredQuickFix((descriptor, quickfixTextRange) -> { + List editableQF = injectedLanguageManager.intersectWithAllEditableFragments(injectedPsi, quickfixTextRange); + for (TextRange editableRange : editableQF) { + TextRange hostEditableRange = documentWindow.injectedToHost(editableRange); + patched.registerFix(descriptor.getAction(), descriptor.myOptions, descriptor.getDisplayName(), hostEditableRange, descriptor.myKey); + } + return null; + }); + patched.markFromInjection(); + outInfos.accept(patched); + } + } + + private void addConvertedToHostInfo(@NotNull HighlightInfo info, @NotNull List newInfos) { + Document document = myPsiFile.getFileDocument(); + if (document instanceof DocumentWindow window) { + addPatchedInfos(info, myPsiFile, window, InjectedLanguageManager.getInstance(myProject), patched -> newInfos.add(patched)); + } + else { + newInfos.add(info); + } + } + + private @NotNull List cloneTemplates(@NotNull Collection templates) { + List result = new ArrayList<>(templates.size()); + for (Annotator template : templates) { + Annotator annotator = cloneTemplate(template); + if (annotator == null) continue; + result.add(annotator); + myAnnotatorStatisticsCollector.reportNewAnnotatorCreated(annotator); + } + return result; + } + + private static Annotator cloneTemplate(@NotNull Annotator template) { + Annotator annotator; + try { + annotator = ReflectionUtil.newInstance(template.getClass()); + } + catch (Exception e) { + LOG.error(PluginException.createByClass(e, template.getClass())); + return null; + } + return annotator; + } + + @NotNull + List getResults() { + return results; + } +} \ No newline at end of file diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/BackgroundUpdateHighlightersUtil.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/BackgroundUpdateHighlightersUtil.java index dd54b362b4af..fa5c6ee41077 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/BackgroundUpdateHighlightersUtil.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/BackgroundUpdateHighlightersUtil.java @@ -126,7 +126,7 @@ public final class BackgroundUpdateHighlightersUtil { Set infoSet = new HashSet<>(filteredInfos); Processor processor = info -> { - if (info.getGroup() == group) { + if (info.getGroup() == group && !info.isFromAnnotator()) { // ignore annotators, they are applied via HighlightInfoUpdater RangeHighlighterEx highlighter = info.getHighlighter(); int hiStart = highlighter.getStartOffset(); int hiEnd = highlighter.getEndOffset(); @@ -165,7 +165,7 @@ public final class BackgroundUpdateHighlightersUtil { if (UpdateHighlightersUtil.isWarningCoveredByError(info, severityRegistrar, overlappingIntervals)) { return true; } - if (info.getStartOffset() < priorityRange.getStartOffset() || info.getEndOffset() > priorityRange.getEndOffset()) { + if ((info.getStartOffset() < priorityRange.getStartOffset() || info.getEndOffset() > priorityRange.getEndOffset()) && !info.isFromAnnotator()) { // have to create RangeHighlighter later, to avoid exposing them to the markup model immediately, // thus messing the HighlightInfo.getStartOffset() leading to "sweep generator supplied infos in a wrong order" exception infosToCreateHighlightersFor.add(info); @@ -242,6 +242,10 @@ public final class BackgroundUpdateHighlightersUtil { List fileLevelHighlights = new ArrayList<>(); List infosToCreateHighlightersFor = new ArrayList<>(filteredInfos.size()); SweepProcessor.sweep(generator, (__, info, atStart, overlappingIntervals) -> { + //if (info.isFromAnnotator()) { + // // annotator infos are handled by HighlightInfoUpdater separately + // return true; + //} if (!atStart) { return true; } @@ -293,7 +297,7 @@ public final class BackgroundUpdateHighlightersUtil { infoStartOffset = Math.min(infoStartOffset, infoEndOffset); } if (infoEndOffset == infoStartOffset && !info.isAfterEndOfLine()) { - if (infoEndOffset == docLength) return; // empty highlighter beyond file boundaries + if (infoEndOffset == docLength) return; // empty highlighter beyond file boundaries infoEndOffset++; //show something in case of empty HighlightInfo } diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightVisitor.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightVisitor.java index bbc0b267eba3..01502f45e630 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightVisitor.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightVisitor.java @@ -5,14 +5,7 @@ import com.intellij.codeInsight.daemon.AnnotatorStatisticsCollector; import com.intellij.codeInsight.daemon.impl.analysis.ErrorQuickFixProvider; import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder; import com.intellij.codeInsight.highlighting.HighlightErrorFilter; -import com.intellij.diagnostic.PluginException; -import com.intellij.lang.Language; -import com.intellij.lang.LanguageAnnotators; import com.intellij.lang.LanguageUtil; -import com.intellij.lang.annotation.Annotation; -import com.intellij.lang.annotation.Annotator; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; @@ -22,41 +15,27 @@ import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiErrorElement; import com.intellij.psi.PsiFile; -import com.intellij.util.ReflectionUtil; -import com.intellij.util.containers.ConcurrentFactoryMap; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Map; final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware { - private static final Logger LOG = Logger.getInstance(DefaultHighlightVisitor.class); - private AnnotationHolderImpl myAnnotationHolder; - private final Map> myAnnotators = ConcurrentFactoryMap.createMap(language -> createAnnotators(language)); private final Project myProject; private final boolean myHighlightErrorElements; - private final boolean myRunAnnotators; - private final DumbService myDumbService; private HighlightInfoHolder myHolder; private final boolean myBatchMode; - private boolean myDumb; private final AnnotatorStatisticsCollector myAnnotatorStatisticsCollector = new AnnotatorStatisticsCollector(); @SuppressWarnings("UnusedDeclaration") DefaultHighlightVisitor(@NotNull Project project) { - this(project, true, true, false); + this(project, true, false); } DefaultHighlightVisitor(@NotNull Project project, boolean highlightErrorElements, - boolean runAnnotators, boolean batchMode) { myProject = project; myHighlightErrorElements = highlightErrorElements; - myRunAnnotators = runAnnotators; - myDumbService = DumbService.getInstance(project); myBatchMode = batchMode; } @@ -70,40 +49,19 @@ final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware { boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable action) { - myDumb = myDumbService.isDumb(); myHolder = holder; - myAnnotationHolder = new AnnotationHolderImpl(holder.getAnnotationSession(), myBatchMode) { - @Override - void queueToUpdateIncrementally() { - if (!isEmpty()) { - myAnnotatorStatisticsCollector.reportAnnotationProduced(myCurrentAnnotator, get(0)); - for (int i = 0; i < size(); i++) { - Annotation annotation = get(i); - holder.add(HighlightInfo.fromAnnotation(myCurrentAnnotator.getClass(), annotation, myBatchMode)); - } - clear(); - } - } - }; try { action.run(); - myAnnotationHolder.assertAllAnnotationsCreated(); } finally { - myAnnotators.clear(); myHolder = null; - myAnnotationHolder = null; - myAnnotatorStatisticsCollector.reportAnalysisFinished(myProject, holder.getAnnotationSession(), file); } return true; } @Override public void visit(@NotNull PsiElement element) { - if (myRunAnnotators) { - runAnnotators(element); - } if (element instanceof PsiErrorElement && myHighlightErrorElements) { visitErrorElement((PsiErrorElement)element); } @@ -112,26 +70,7 @@ final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware { @SuppressWarnings("CloneDoesntCallSuperClone") @Override public @NotNull HighlightVisitor clone() { - return new DefaultHighlightVisitor(myProject, myHighlightErrorElements, myRunAnnotators, myBatchMode); - } - - private void runAnnotators(@NotNull PsiElement element) { - List annotators = myAnnotators.get(element.getLanguage()); - if (!annotators.isEmpty()) { - AnnotationHolderImpl holder = myAnnotationHolder; - holder.myCurrentElement = element; - for (Annotator annotator : annotators) { - if (!myDumb || DumbService.isDumbAware(annotator)) { - ProgressManager.checkCanceled(); - holder.myCurrentAnnotator = annotator; - annotator.annotate(element, holder); - // assume that annotator is done messing with just created annotations after its annotate() method completed, - // so we can start applying them incrementally at last - // (but not sooner, thanks to awfully racey Annotation.setXXX() API) - holder.queueToUpdateIncrementally(); - } - } - } + return new DefaultHighlightVisitor(myProject, myHighlightErrorElements, myBatchMode); } private void visitErrorElement(@NotNull PsiErrorElement element) { @@ -159,27 +98,6 @@ final class DefaultHighlightVisitor implements HighlightVisitor, DumbAware { return info; } - private @NotNull List cloneTemplates(@NotNull Collection templates) { - List result = new ArrayList<>(templates.size()); - for (Annotator template : templates) { - Annotator annotator; - try { - annotator = ReflectionUtil.newInstance(template.getClass()); - } - catch (Exception e) { - LOG.error(PluginException.createByClass(e, template.getClass())); - continue; - } - result.add(annotator); - myAnnotatorStatisticsCollector.reportNewAnnotatorCreated(annotator); - } - return result; - } - - private @NotNull List createAnnotators(@NotNull Language language) { - return cloneTemplates(LanguageAnnotators.INSTANCE.allForLanguageOrAny(language)); - } - private static @NotNull HighlightInfo.Builder createErrorElementInfoWithoutFixes(@NotNull PsiErrorElement element) { TextRange range = element.getTextRange(); String errorDescription = element.getErrorDescription(); diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/GeneralHighlightingPass.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/GeneralHighlightingPass.java index 9793a9fdc394..82b990b9a8d5 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/GeneralHighlightingPass.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/GeneralHighlightingPass.java @@ -56,6 +56,7 @@ import org.jetbrains.annotations.TestOnly; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; import java.util.function.Predicate; import java.util.function.Supplier; @@ -75,12 +76,14 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP final @NotNull ProperTextRange myPriorityRange; final List myHighlights = new ArrayList<>(); + final List myAnnotatorHighlights = Collections.synchronizedList(new ArrayList<>()); // have to store them separately to avoid double call setToEditor() protected volatile boolean myHasErrorElement; private volatile boolean myHasErrorSeverity; private volatile boolean myErrorFound; final EditorColorsScheme myGlobalScheme; private volatile @NotNull Supplier> myHighlightVisitorProducer = this::cloneHighlightVisitors; + private boolean myRunAnnotators = true; GeneralHighlightingPass(@NotNull PsiFile file, @NotNull Document document, @@ -286,35 +289,47 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP int chunkSize = Math.max(1, (elements1.size()+elements2.size()) / 100); // one percent precision is enough - boolean success = analyzeByVisitors(visitors, holder, 0, () -> { - LongStack nestedRange = new LongStack(); - Stack> nestedInfos = new Stack<>(); + BooleanSupplier runnable = () -> { + boolean success = analyzeByVisitors(visitors, holder, 0, () -> { + LongStack nestedRange = new LongStack(); + Stack> nestedInfos = new Stack<>(); - try (var ignored = HIGHLIGHTING_PERFORMANCE_ASSERT.runPass()) { - runVisitors(elements1, ranges1, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors, + try (var ignored = HIGHLIGHTING_PERFORMANCE_ASSERT.runPass()) { + runVisitors(elements1, ranges1, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors, + nestedRange, nestedInfos); + } + + boolean priorityIntersectionHasElements = myPriorityRange.intersectsStrict(myRestrictRange); + if ((!elements1.isEmpty() || !insideResult.isEmpty()) || priorityIntersectionHasElements) { // do not apply when there were no elements to highlight + myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), insideResult, myPriorityRange, myRestrictRange, getId()); + } + runVisitors(elements2, ranges2, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors, nestedRange, nestedInfos); + }); + // there can be extra highlights generated in PostHighlightVisitor + List postInfos; + synchronized (holder) { + postInfos = new ArrayList<>(holder.size()); + for (int j = 0; j < holder.size(); j++) { + HighlightInfo info = holder.get(j); + postInfos.add(info); + insideResult.add(info); + } } - - boolean priorityIntersectionHasElements = myPriorityRange.intersectsStrict(myRestrictRange); - if ((!elements1.isEmpty() || !insideResult.isEmpty()) || priorityIntersectionHasElements) { // do not apply when there were no elements to highlight - myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), insideResult, myPriorityRange, myRestrictRange, getId()); - } - runVisitors(elements2, ranges2, chunkSize, skipParentsSet, holder, insideResult, outsideResult, forceHighlightParents, visitors, - nestedRange, nestedInfos); - }); - // there can be extra highlights generated in PostHighlightVisitor - List postInfos; - synchronized (holder) { - postInfos = new ArrayList<>(holder.size()); - for (int j = 0; j < holder.size(); j++) { - HighlightInfo info = holder.get(j); - postInfos.add(info); - insideResult.add(info); - } + myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), + postInfos, getFile().getTextRange(), getFile().getTextRange(), POST_UPDATE_ALL); + return success; + }; + AnnotatorRunner annotatorRunner = myRunAnnotators ? new AnnotatorRunner(getFile(), false, holder, myHighlightingSession) : null; + boolean result; + if (annotatorRunner == null) { + result = runnable.getAsBoolean(); } - myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), - postInfos, getFile().getTextRange(), getFile().getTextRange(), POST_UPDATE_ALL); - return success; + else { + result = annotatorRunner.runAnnotatorsAsync(elements1, elements2, runnable); + outsideResult.addAll(annotatorRunner.getResults()); + } + return result; } private boolean analyzeByVisitors(HighlightVisitor @NotNull [] visitors, @@ -399,7 +414,7 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP // if this highlight info range is contained inside the current element range we are visiting // that means we can clear this highlight as soon as visitors won't produce any highlights during visiting the same range next time. // We also know that we can remove a syntax error element. - info.setVisitingTextRange(myFile, myDocument, elementRange); + info.setVisitingTextRange(getFile(), myDocument, elementRange); infosForThisRange.add(info); } holder.clear(); @@ -487,7 +502,8 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP synchronized (this) { added = super.add(info); } - if (info != null && added) { + // annotator infos are handled by HighlightInfoUpdater separately + if (info != null && added/* && !info.isFromAnnotator()*/) { queueInfoToUpdateIncrementally(info, info.getGroup() == 0 ? Pass.UPDATE_ALL : info.getGroup()); } return added; @@ -617,4 +633,7 @@ public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingP private static @Nls String getPresentableNameText() { return AnalysisBundle.message("pass.syntax"); } + void setRunAnnotators(boolean run) { + myRunAnnotators = run; + } } diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfo.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfo.java index 42d69ef0e082..bcdf09838e55 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfo.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfo.java @@ -13,10 +13,7 @@ import com.intellij.codeInspection.ex.InspectionToolWrapper; import com.intellij.codeInspection.ex.LocalInspectionToolWrapper; import com.intellij.injected.editor.DocumentWindow; import com.intellij.lang.ASTNode; -import com.intellij.lang.annotation.Annotation; -import com.intellij.lang.annotation.ExternalAnnotator; -import com.intellij.lang.annotation.HighlightSeverity; -import com.intellij.lang.annotation.ProblemGroup; +import com.intellij.lang.annotation.*; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.modcommand.ModCommandAction; import com.intellij.openapi.diagnostic.Logger; @@ -322,7 +319,7 @@ public class HighlightInfo implements Segment { } public @Nullable @NonNls String getInspectionToolId() { - return toolId instanceof String ? (String)toolId : null; + return toolId instanceof String inspectionToolShortName ? inspectionToolShortName : null; } private boolean isFlagSet(@FlagConstant byte mask) { @@ -1074,4 +1071,7 @@ public class HighlightInfo implements Segment { void setUnresolvedReferenceQuickFixesComputed() { setFlag(UNRESOLVED_REFERENCE_QUICK_FIXES_COMPUTED_MASK, true); } + boolean isFromAnnotator() { + return toolId instanceof Class c && Annotator.class.isAssignableFrom(c); + } } diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfoUpdater.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfoUpdater.java index 72a4129bcd8c..320446f8536e 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfoUpdater.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightInfoUpdater.java @@ -26,6 +26,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; +import com.intellij.psi.impl.source.tree.injected.InjectedFileViewProvider; import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.SearchScope; @@ -181,16 +182,15 @@ final class HighlightInfoUpdater { @NotNull HighlightersRecycler invalidElementRecycler, @NotNull HighlightingSession session) { List oldInfos = getInfosForVisitedPsi(psiFile, toolId, visitedPsiElement); - synchronized (oldInfos) { - if (LOG.isDebugEnabled()) { - LOG.debug("psiElementVisited: " + visitedPsiElement+ " in "+psiFile+" injected in "+InjectedLanguageManager.getInstance(project).injectedToHost(psiFile, psiFile.getTextRange())+ - "; tool:" + toolId + "; infos:" + newInfos+ "; oldInfos:" + oldInfos + "; document:" + hostDocument); - } - - if (!oldInfos.isEmpty() || !newInfos.isEmpty()) { - MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(hostDocument, project, true); - setHighlightersInRange(newInfos, oldInfos, markup, session, invalidElementRecycler); - } + if (LOG.isDebugEnabled()) { + LOG.debug("psiElementVisited: " + visitedPsiElement+ " in "+psiFile+ + (psiFile.getViewProvider() instanceof InjectedFileViewProvider ? + " injected in " + InjectedLanguageManager.getInstance(project).injectedToHost(psiFile, psiFile.getTextRange()) : "") + + "; tool:" + toolId + "; infos:" + newInfos + "; oldInfos:" + oldInfos + "; document:" + hostDocument); + } + if (!oldInfos.isEmpty() || !newInfos.isEmpty()) { + MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(hostDocument, project, true); + setHighlightersInRange(newInfos, oldInfos, markup, session, invalidElementRecycler); } // store back only after markup model changes are applied to avoid PCE thrown in the middle leaving corrupted data behind putInfosForVisitedPsi(psiFile, toolId, visitedPsiElement, newInfos); diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightVisitorBasedInspection.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightVisitorBasedInspection.java index cdd3dc0cea9d..a6a1e8a37ed0 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightVisitorBasedInspection.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightVisitorBasedInspection.java @@ -116,8 +116,9 @@ public final class HighlightVisitorBasedInspection extends GlobalSimpleInspectio for (GeneralHighlightingPass gpass : gpasses) { gpass.setHighlightVisitorProducer(() -> { gpass.incVisitorUsageCount(1); - return List.of(new DefaultHighlightVisitor(project, highlightErrorElements, runAnnotators, true)); + return List.of(new DefaultHighlightVisitor(project, highlightErrorElements, true)); }); + gpass.setRunAnnotators(runAnnotators); } } diff --git a/platform/core-impl/src/com/intellij/concurrency/JobLauncher.java b/platform/core-impl/src/com/intellij/concurrency/JobLauncher.java index 5220d45f7756..cb2cd906e9d9 100644 --- a/platform/core-impl/src/com/intellij/concurrency/JobLauncher.java +++ b/platform/core-impl/src/com/intellij/concurrency/JobLauncher.java @@ -6,6 +6,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.util.PairProcessor; import com.intellij.util.Processor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -89,4 +90,18 @@ public abstract class JobLauncher { * Use {@link com.intellij.openapi.application.Application#executeOnPooledThread(Runnable)} instead */ public abstract @NotNull Job submitToJobThread(final @NotNull Runnable action, @Nullable Consumer> onDoneCallback); + + @ApiStatus.Internal + public boolean procInOrderAsync(@NotNull ProgressIndicator progress, + int maxQueueSize, + @NotNull PairProcessor> thingProcessor, + @NotNull Processor> otherActions) throws ProcessCanceledException { + return false; + } + + public interface QueueController { + void enqueue(T element); + void dropEverythingAndPanic(); + void finish(); + } } diff --git a/platform/core-impl/src/com/intellij/core/CoreJobLauncher.java b/platform/core-impl/src/com/intellij/core/CoreJobLauncher.java index cf66a8ef23e1..1f407d1a9aa1 100644 --- a/platform/core-impl/src/com/intellij/core/CoreJobLauncher.java +++ b/platform/core-impl/src/com/intellij/core/CoreJobLauncher.java @@ -3,7 +3,9 @@ package com.intellij.core; import com.intellij.concurrency.Job; import com.intellij.concurrency.JobLauncher; +import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.util.PairProcessor; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; @@ -34,4 +36,5 @@ public class CoreJobLauncher extends JobLauncher { } return Job.nullJob(); } + } diff --git a/platform/editor-ui-ex/src/com/intellij/openapi/editor/impl/RangeHighlighterImpl.java b/platform/editor-ui-ex/src/com/intellij/openapi/editor/impl/RangeHighlighterImpl.java index a7aee38b5655..d45c14bd9e9d 100644 --- a/platform/editor-ui-ex/src/com/intellij/openapi/editor/impl/RangeHighlighterImpl.java +++ b/platform/editor-ui-ex/src/com/intellij/openapi/editor/impl/RangeHighlighterImpl.java @@ -3,6 +3,7 @@ package com.intellij.openapi.editor.impl; import com.intellij.codeInsight.daemon.GutterMark; import com.intellij.openapi.Disposable; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.EditorColorsScheme; @@ -12,6 +13,7 @@ import com.intellij.openapi.editor.ex.RangeHighlighterEx; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.TextRange; import com.intellij.util.BitUtil; import com.intellij.util.Consumer; import org.intellij.lang.annotations.MagicConstant; @@ -79,6 +81,13 @@ sealed class RangeHighlighterImpl extends RangeMarkerImpl implements RangeHighli myModel = model; registerInTree(start, end, greedyToLeft, greedyToRight, layer); + if (LOG.isDebugEnabled()) { + LOG.debug("create: " + this); + if (getTextRange().equals(new TextRange(16, 22))) { + //new RuntimeException().printStackTrace(); + int i = 0; + } + } } private boolean isFlagSet(@Flag byte mask) { @@ -443,8 +452,16 @@ sealed class RangeHighlighterImpl extends RangeMarkerImpl implements RangeHighli getMarkupModel().removeHighlighter(this); } + private static final Logger LOG = Logger.getInstance(RangeHighlighterImpl.class); @Override public void dispose() { + if (LOG.isDebugEnabled()) { + if (getTextRange().equals(new TextRange(16, 22))) { + //new RuntimeException().printStackTrace(); + int i = 0; + } + LOG.debug("dispose: "+this); + } super.dispose(); GutterIconRenderer renderer = getGutterIconRenderer(); if (renderer instanceof Disposable disposableRenderer) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DaemonCodeAnalyzerImpl.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DaemonCodeAnalyzerImpl.java index 439a0cc7f3c7..f503e7e6c980 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DaemonCodeAnalyzerImpl.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DaemonCodeAnalyzerImpl.java @@ -528,13 +528,16 @@ public final class DaemonCodeAnalyzerImpl extends DaemonCodeAnalyzerEx } catch (Throwable e) { Throwable unwrapped = ExceptionUtilRt.unwrapException(e, ExecutionException.class); + LOG.debug("doRunPasses() thrown " + ExceptionUtil.getThrowableText(unwrapped)); if (progress.isCanceled() && progress.isRunning()) { unwrapped.addSuppressed(new RuntimeException("Daemon progress was canceled unexpectedly: " + progress)); } ExceptionUtil.rethrow(unwrapped); } finally { - progress.cancel(); + if (!progress.isCanceled()) { + ((DaemonProgressIndicator)progress).cancel("Cancel after highlighting. threads:\n"+ThreadDumper.dumpThreadsToString()); + } waitForTermination(); } return null; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightInfoProcessor.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightInfoProcessor.java index f1561d82860c..32bf4bcc7596 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightInfoProcessor.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DefaultHighlightInfoProcessor.java @@ -44,11 +44,11 @@ public final class DefaultHighlightInfoProcessor extends HighlightInfoProcessor Document document = session.getDocument(); long modificationStamp = document.getModificationStamp(); TextRange priorityIntersection = priorityRange.intersection(restrictRange); - TextEditorHighlightingPass showAutoImportPass = editor == null ? null : getOrCreateShowAutoImportPass(editor, psiFile, session.getProgressIndicator()); - MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true); if (priorityIntersection != null) { + MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, project, true); BackgroundUpdateHighlightersUtil.setHighlightersInRange(priorityIntersection, new ArrayList(infos), markupModel, groupId, session); } + TextEditorHighlightingPass showAutoImportPass = editor == null ? null : getOrCreateShowAutoImportPass(editor, psiFile, session.getProgressIndicator()); ApplicationManager.getApplication().invokeLater(() -> { if (editor != null && !editor.isDisposed() && modificationStamp == document.getModificationStamp()) { // usability: show auto import popup as soon as possible @@ -132,7 +132,7 @@ public final class DefaultHighlightInfoProcessor extends HighlightInfoProcessor @NotNull HighlightingSession session) { List toRemove = new ArrayList<>(); DaemonCodeAnalyzerEx.processHighlights(document, project, null, TextRangeScalarUtil.startOffset(range), TextRangeScalarUtil.endOffset(range), existing -> { - if (existing.getGroup() == Pass.UPDATE_ALL && TextRangeScalarUtil.startOffset(range) == existing.getVisitingTextRange().getStartOffset() && TextRangeScalarUtil.endOffset(range) == existing.getVisitingTextRange().getEndOffset()) { + if (existing.getGroup() == Pass.UPDATE_ALL && !existing.isFromAnnotator() && TextRangeScalarUtil.startOffset(range) == existing.getVisitingTextRange().getStartOffset() && TextRangeScalarUtil.endOffset(range) == existing.getVisitingTextRange().getEndOffset()) { if (infos != null) { for (HighlightInfo created : infos) { if (existing.equalsByActualOffset(created)) return true; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InjectedGeneralHighlightingPass.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InjectedGeneralHighlightingPass.java index ce86bf604617..7193783d8e5d 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InjectedGeneralHighlightingPass.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InjectedGeneralHighlightingPass.java @@ -57,6 +57,13 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass { myReducedRanges = reducedRanges; } + @Override + public @NotNull List getInfos() { + synchronized (myHighlights) { + return ContainerUtil.concat(myHighlights, myAnnotatorHighlights); + } + } + @Override protected @NotNull String getPresentableName() { return IdeBundle.message("highlighting.pass.injected.presentable.name"); @@ -92,10 +99,10 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass { })); synchronized (myHighlights) { - // all infos for the "injected fragment for the host which is inside" are indeed inside + // all infos for the "injected fragment for the host which is inside" are inside indeed, // but some infos for the "injected fragment for the host which is outside" can be still inside if (resultOutside.isEmpty()) { - // apply only result (by default apply command) and only within inside + // apply only the result inside myHighlights.addAll(resultInside); myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, getEditor(), myHighlights, myRestrictRange, myRestrictRange, getId()); } @@ -230,16 +237,23 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass { } HighlightInfoHolder holder = createInfoHolder(injectedPsi, documentWindow, injectedLanguageManager, outInfos); - runHighlightVisitorsForInjected(injectedPsi, holder); - highlightInjectedSyntax(injectedPsi, places, outInfos); + List elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength()); - if (!isDumbMode()) { - List todos = new ArrayList<>(); - highlightTodos(injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), myPriorityRange, todos, todos); - for (HighlightInfo info : todos) { - addPatchedInfos(info, injectedPsi, documentWindow, injectedLanguageManager, outInfos); + AnnotatorRunner annotatorRunner = new AnnotatorRunner(injectedPsi, false, holder, myHighlightingSession); + annotatorRunner.runAnnotatorsAsync(elements, List.of(), () -> { + runHighlightVisitorsForInjected(injectedPsi, holder, elements); + highlightInjectedSyntax(injectedPsi, places, outInfos); + + if (!isDumbMode()) { + List todos = new ArrayList<>(); + highlightTodos(injectedPsi, injectedPsi.getText(), 0, injectedPsi.getTextLength(), myPriorityRange, todos, todos); + for (HighlightInfo info : todos) { + addPatchedInfos(info, injectedPsi, documentWindow, injectedLanguageManager, outInfos); + } } - } + return true; + }); + myAnnotatorHighlights.addAll(annotatorRunner.getResults()); } private static void addPatchedInfos(@NotNull HighlightInfo info, @@ -285,10 +299,9 @@ final class InjectedGeneralHighlightingPass extends GeneralHighlightingPass { } } - private void runHighlightVisitorsForInjected(@NotNull PsiFile injectedPsi, @NotNull HighlightInfoHolder holder) { + private void runHighlightVisitorsForInjected(@NotNull PsiFile injectedPsi, @NotNull HighlightInfoHolder holder, List elements) { HighlightVisitor[] filtered = getHighlightVisitors(injectedPsi); try { - List elements = CollectHighlightsUtil.getElementsInRange(injectedPsi, 0, injectedPsi.getTextLength()); for (HighlightVisitor visitor : filtered) { visitor.analyze(injectedPsi, true, holder, () -> { for (PsiElement element : elements) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InspectionRunner.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InspectionRunner.java index ea4415e60843..0dac52e81ff2 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InspectionRunner.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/InspectionRunner.java @@ -179,7 +179,7 @@ class InspectionRunner { }); return true; }; - if (!((JobLauncherImpl)JobLauncher.getInstance()).procInOrderAsync(new SensitiveProgressWrapper(myProgress), initSize, contextProcessor, addToQueue -> { + if (!JobLauncher.getInstance().procInOrderAsync(new SensitiveProgressWrapper(myProgress), initSize, contextProcessor, addToQueue -> { // have to do all this even for empty elements, to perform correct cleanup/inspectionFinished if (init.isEmpty()) { addToQueue.finish(); @@ -189,15 +189,16 @@ class InspectionRunner { addToQueue.enqueue(context); } } + reportIdsOfInspectionsReportedAnyProblemToFUS(init); if (myInspectInjected && InjectionUtils.shouldInspectInjectedFiles(myPsiFile)) { // we don't run whole-file tools on injected fragments List localTools = ContainerUtil.filter(toolWrappers, t -> !t.runForWholeFile()); - inspectInjectedPsi(session, localTools, injectedContexts, applyIncrementallyCallback, - contextFinishedCallback, enabledToolsPredicate, addToInjectedQueue -> + return inspectInjectedPsi(session, localTools, injectedContexts, applyIncrementallyCallback, + contextFinishedCallback, enabledToolsPredicate, addToInjectedQueue -> getInjectedWithHosts(ContainerUtil.concat(restrictedInside, restrictedOutside), addToInjectedQueue)); } - reportIdsOfInspectionsReportedAnyProblemToFUS(init); + return true; })) { throw new ProcessCanceledException(); } @@ -418,7 +419,7 @@ class InspectionRunner { @NotNull ApplyIncrementallyCallback addDescriptorIncrementallyCallback, @NotNull Consumer contextFinishedCallback, @Nullable Condition enabledToolsPredicate, - @NotNull Consumer>> otherActions) { + @NotNull Processor>> otherActions) { PairProcessor, JobLauncherImpl.QueueController>> injectedProcessor = (pair,__) -> { executeInImpatientReadAction(() -> { PsiFile injectedPsi = pair.getFirst(); @@ -495,8 +496,8 @@ class InspectionRunner { } } - private void getInjectedWithHosts(@NotNull List elements, - @NotNull JobLauncherImpl.QueueController> addToQueue) { + private boolean getInjectedWithHosts(@NotNull List elements, + @NotNull JobLauncherImpl.QueueController> addToQueue) { Map injectedToHost = createInjectedFileMap(); Project project = myPsiFile.getProject(); for (PsiElement element : elements) { @@ -510,6 +511,7 @@ class InspectionRunner { }); } addToQueue.finish(); // no more injections + return true; } interface ApplyIncrementallyCallback { diff --git a/platform/platform-impl/src/com/intellij/concurrency/JobLauncherImpl.java b/platform/platform-impl/src/com/intellij/concurrency/JobLauncherImpl.java index 3a701df9ef1a..4aeea0350308 100644 --- a/platform/platform-impl/src/com/intellij/concurrency/JobLauncherImpl.java +++ b/platform/platform-impl/src/com/intellij/concurrency/JobLauncherImpl.java @@ -22,7 +22,10 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -381,12 +384,6 @@ public final class JobLauncherImpl extends JobLauncher { return (T)TOMBSTONE; } - public interface QueueController { - void enqueue(T element); - void dropEverythingAndPanic(); - void finish(); - } - /** * Method for producing some elements (to process via {@code thingProcessor}), and scheduling their processing along with ongoing calculations. * It has three parts: @@ -402,11 +399,12 @@ public final class JobLauncherImpl extends JobLauncher { * Guarantees all tasks are completed in the method end, or runtime exception is thrown, in which case some elements might be in-flight. * @return true if all processors returned true */ + @Override @ApiStatus.Internal public boolean procInOrderAsync(@NotNull ProgressIndicator progress, int maxQueueSize, @NotNull PairProcessor> thingProcessor, - @NotNull Consumer> otherActions) throws ProcessCanceledException { + @NotNull Processor> otherActions) throws ProcessCanceledException { progress.checkCanceled(); // do not start up expensive threads if there's no need to // optimization: if we know the max number of elements in the queue, use the cheaper ABQ BlockingQueue things = maxQueueSize < Integer.MAX_VALUE ? @@ -536,7 +534,9 @@ public final class JobLauncherImpl extends JobLauncher { try { // execute other actions while we are processing enqueued elements - otherActions.accept(addToQueue); + if (!otherActions.process(addToQueue)) { + futureResult.set(false); + } } catch (Exception e) { // in case of exception in normal flow, terminate background tasks diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/DocumentMarkupModelTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/DocumentMarkupModelTest.java index 77cf5a93ab13..1f1e1dc00846 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/DocumentMarkupModelTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/DocumentMarkupModelTest.java @@ -21,6 +21,7 @@ import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.extensions.DefaultPluginDescriptor; import com.intellij.openapi.fileTypes.PlainTextFileType; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.testFramework.ExtensionTestUtil; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import org.jetbrains.annotations.NotNull; @@ -117,7 +118,9 @@ public class DocumentMarkupModelTest extends BasePlatformTestCase { public static class TestAnnotator implements Annotator { @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { - holder.newSilentAnnotation(HighlightSeverity.INFORMATION).create(); + if (element instanceof PsiFile) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION).create(); + } } } } diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java index cd0f4c8a64f6..d7d22cc2b9a9 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java @@ -262,7 +262,6 @@ public final class GroovyAnnotator extends GroovyElementVisitor { @Override public void visitVariableDeclaration(@NotNull GrVariableDeclaration variableDeclaration) { - checkDuplicateModifiers(myHolder, variableDeclaration.getModifierList(), null); if (variableDeclaration.isTuple()) { final GrModifierList list = variableDeclaration.getModifierList(); @@ -385,7 +384,10 @@ public final class GroovyAnnotator extends GroovyElementVisitor { } checkTypeDefinition(myHolder, typeDefinition); - checkImplementedMethodsOfClass(myHolder, typeDefinition); + // enum constants are not handled here because their getTextOffset() is crazy - outside their own getTextRange() + if (!(typeDefinition instanceof PsiEnumConstantInitializer)) { + checkImplementedMethodsOfClass(myHolder, typeDefinition); + } checkConstructors(myHolder, typeDefinition); checkAnnotationCollector(myHolder, typeDefinition); @@ -993,6 +995,8 @@ public final class GroovyAnnotator extends GroovyElementVisitor { } else { checkVariableModifiers(myHolder, declaration); + GrVariable[] variables = declaration.getVariables(); + checkDuplicateModifiers(myHolder, modifierList, variables.length == 0 ? null : variables[0]); } } else if (parent instanceof GrClassInitializer) { @@ -1731,7 +1735,7 @@ public final class GroovyAnnotator extends GroovyElementVisitor { final TextRange range = GrHighlightUtil.getClassHeaderTextRange(typeDefinition); String message = GroovyBundle.message("method.is.not.implemented", notImplementedMethodName); - AnnotationBuilder builder = + AnnotationBuilder builder = holder.newAnnotation(HighlightSeverity.ERROR, message) .range(range); registerImplementsMethodsFix(typeDefinition, abstractMethod, builder, message, range).create(); @@ -1944,8 +1948,9 @@ public final class GroovyAnnotator extends GroovyElementVisitor { } } - private static void checkDuplicateModifiers(AnnotationHolder holder, @NotNull GrModifierList list, PsiMember member) { + private static void checkDuplicateModifiers(AnnotationHolder holder, @NotNull GrModifierList list, PsiElement member) { final PsiElement[] modifiers = list.getModifiers(); + if (modifiers.length <= 1) return; Set set = new HashSet<>(modifiers.length); for (PsiElement modifier : modifiers) { if (modifier instanceof GrAnnotation) continue; @@ -1953,10 +1958,12 @@ public final class GroovyAnnotator extends GroovyElementVisitor { if (set.contains(name)) { String message = GroovyBundle.message("duplicate.modifier", name); AnnotationBuilder builder = - holder.newAnnotation(HighlightSeverity.ERROR, message).range(list); + holder.newAnnotation(HighlightSeverity.ERROR, message).range(modifier); + GrModifierFix fix = member instanceof PsiMember ? new GrModifierFix((PsiMember)member, name, false, false, GrModifierFix.MODIFIER_LIST) : + member instanceof GrVariable ? new GrModifierFix((GrVariable)member, name, false, GrModifierFix.MODIFIER_LIST) : + null; if (member != null) { - builder = registerLocalFix(builder, new GrModifierFix(member, name, false, false, GrModifierFix.MODIFIER_LIST), list, message, - ProblemHighlightType.ERROR, list.getTextRange()); + builder = registerLocalFix(builder, fix, list, message, ProblemHighlightType.ERROR, list.getTextRange()); } builder.create(); } diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/GroovyHighlightingTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/GroovyHighlightingTest.groovy index d76119580ed0..c8c47bf1da47 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/GroovyHighlightingTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/highlighting/GroovyHighlightingTest.groovy @@ -1274,7 +1274,7 @@ class B extends A { void testUnresolvedQualifierHighlighting() { doTestHighlighting('''\ -Abc.Cde abc +Abc.Cde abc ''') } @@ -1422,7 +1422,7 @@ class X{} @A('ab') class Y{} -public public class Z {} +public public class Z {} ''') } diff --git a/plugins/groovy/testdata/highlighting/AnonymousClassAbstractMethod.groovy b/plugins/groovy/testdata/highlighting/AnonymousClassAbstractMethod.groovy index 3b47ccce558d..ad6e3a11294d 100644 --- a/plugins/groovy/testdata/highlighting/AnonymousClassAbstractMethod.groovy +++ b/plugins/groovy/testdata/highlighting/AnonymousClassAbstractMethod.groovy @@ -1,7 +1,7 @@ import java.awt.event.ActionEvent import java.awt.event.ActionListener -def x=new ActionListener() { +def x=new ActionListener() { def actionPerformed(ActionEvent e) { } diff --git a/plugins/groovy/testdata/highlighting/DefaultInitializersAreNotAllowedInAbstractMethods.groovy b/plugins/groovy/testdata/highlighting/DefaultInitializersAreNotAllowedInAbstractMethods.groovy index 0e7b0aafede6..567adb41e647 100644 --- a/plugins/groovy/testdata/highlighting/DefaultInitializersAreNotAllowedInAbstractMethods.groovy +++ b/plugins/groovy/testdata/highlighting/DefaultInitializersAreNotAllowedInAbstractMethods.groovy @@ -22,7 +22,7 @@ enum E { String bar(x = 2) } -def a = new Runnable() { +def a = new Runnable() { void run() {} abstract foo(x = 5); } diff --git a/plugins/groovy/testdata/highlighting/FieldModifiers.groovy b/plugins/groovy/testdata/highlighting/FieldModifiers.groovy index e68c9e3d8604..34aca1927fe7 100644 --- a/plugins/groovy/testdata/highlighting/FieldModifiers.groovy +++ b/plugins/groovy/testdata/highlighting/FieldModifiers.groovy @@ -20,7 +20,7 @@ class Combinations { } class Duplicates { - public public a + public public a } interface I { diff --git a/plugins/groovy/testdata/highlighting/ScriptFieldModifiers.groovy b/plugins/groovy/testdata/highlighting/ScriptFieldModifiers.groovy index 30acb221a456..f4e67f3c1720 100644 --- a/plugins/groovy/testdata/highlighting/ScriptFieldModifiers.groovy +++ b/plugins/groovy/testdata/highlighting/ScriptFieldModifiers.groovy @@ -17,4 +17,4 @@ import groovy.transform.Field @Field protected public c @Field volatile final g -@Field public public d +@Field public public d diff --git a/plugins/groovy/testdata/highlighting/VariableDeclarationDuplicateModifiers.groovy b/plugins/groovy/testdata/highlighting/VariableDeclarationDuplicateModifiers.groovy index 78b0a3e634cf..fddd077ca770 100644 --- a/plugins/groovy/testdata/highlighting/VariableDeclarationDuplicateModifiers.groovy +++ b/plugins/groovy/testdata/highlighting/VariableDeclarationDuplicateModifiers.groovy @@ -1 +1 @@ -def final def foo = 44 \ No newline at end of file +def final def foo = 44 \ No newline at end of file diff --git a/plugins/groovy/testdata/highlighting/pre30/typeAnnotations.groovy b/plugins/groovy/testdata/highlighting/pre30/typeAnnotations.groovy index d0e064600ef9..2ea0f9c85799 100644 --- a/plugins/groovy/testdata/highlighting/pre30/typeAnnotations.groovy +++ b/plugins/groovy/testdata/highlighting/pre30/typeAnnotations.groovy @@ -12,11 +12,11 @@ class JSR308BaseClass {} interface JSR308Interface1 {} interface JSR308Interface2@JSR308 CharSequence> {} -class JSR308Class extends @JSR308 JSR308BaseClass<@JSR308 List> implements @JSR308 JSR308Interface1<@JSR308 String>, @JSR308 JSR308Interface2<@JSR308 String> { +class JSR308Class extends @JSR308 JSR308BaseClass<@JSR308 List> implements @JSR308 JSR308Interface1<@JSR308 String>, @JSR308 JSR308Interface2<@JSR308 String> { @JSR308 private String name; @JSR308 List<@JSR308 String> test(@JSR308 List<@JSR308 ? extends @JSR308 Object> list) throws @JSR308 IOException, @JSR308 java.sql.SQLException { - @JSR308 List<@JSR308 String> localVar = new @JSR308 ArrayList<@JSR308 String>(); + @JSR308 List<@JSR308 String> localVar = new @JSR308 ArrayList<@JSR308 String>(); try { for (e in list) { @@ -27,9 +27,9 @@ class JSR308Class extends @JSR308 JSR308BaseClass<@JSR308 [] strs = new String @JSR308 [] { 'a' } - String @JSR308 [] @JSR308 [] strs2 = new String @JSR308 [] @JSR308 [] { new String[] {'a', 'b'} } + String @JSR308 [] @JSR308 [] strs2 = new String @JSR308 [] @JSR308 [] { new String[] {'a', 'b'} } String [][] @JSR308 [] strs3 = new String [1][2] @JSR308 [] - String [] @JSR308 [] @JSR308 [] @JSR308 [] strs4 = new String [1] @JSR308 [2] @JSR308 [] @JSR308 [] + String [] @JSR308 [] @JSR308 [] @JSR308 [] strs4 = new String [1] @JSR308 [2] @JSR308 [] @JSR308 [] localVar.add(strs[0]) localVar.add(strs2[0][1]) diff --git a/plugins/javaFX/testData/highlighting/defaultTagInList.fxml b/plugins/javaFX/testData/highlighting/defaultTagInList.fxml index b4bfe857567d..055450393fb8 100644 --- a/plugins/javaFX/testData/highlighting/defaultTagInList.fxml +++ b/plugins/javaFX/testData/highlighting/defaultTagInList.fxml @@ -9,6 +9,6 @@ id/> -