From 37158871a35298ca708ef29c651ef289a60057c5 Mon Sep 17 00:00:00 2001 From: Artemiy Sartakov Date: Tue, 31 Mar 2020 16:54:56 +0700 Subject: [PATCH] Problems view: store file state(changes and snapshot) and inlays separately GitOrigin-RevId: 5f4ba1f78fa99016c2ba65869c1c598c5a1c6875 --- .../daemon/problems/FileStateUpdater.kt | 144 +++++++++++++ .../codeInsight/daemon/problems/Member.kt | 5 +- .../daemon/problems/SnapshotUpdater.kt | 194 ------------------ .../ProjectProblemFileSelectionListener.java | 77 +------ .../problems/pass/ProjectProblemPass.java | 141 ++++++------- .../pass/ProjectProblemPassUtils.java | 110 +++------- .../daemon/problems/FieldProblemsTest.kt | 3 +- .../problems/ProjectProblemsViewTest.kt | 7 +- .../ProjectProblemsViewPropertyTest.kt | 16 +- 9 files changed, 257 insertions(+), 440 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/daemon/problems/FileStateUpdater.kt delete mode 100644 java/java-impl/src/com/intellij/codeInsight/daemon/problems/SnapshotUpdater.kt diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/FileStateUpdater.kt b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/FileStateUpdater.kt new file mode 100644 index 000000000000..22e834e9d454 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/FileStateUpdater.kt @@ -0,0 +1,144 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.codeInsight.daemon.problems + +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.util.Key +import com.intellij.psi.* + +internal typealias Snapshot = Map, ScopedMember> + +internal data class FileState(val snapshot: Snapshot, val changes: Map) + +internal class FileStateUpdater(private val prevSnapshot: Snapshot?) : JavaElementVisitor() { + + private val snapshot = mutableMapOf, ScopedMember>() + private val changes = mutableMapOf() + + override fun visitEnumConstant(psiEnumConstant: PsiEnumConstant) = visitMember(psiEnumConstant) + + override fun visitClass(psiClass: PsiClass) = visitMember(psiClass) + + override fun visitField(psiField: PsiField) = visitMember(psiField) + + override fun visitMethod(psiMethod: PsiMethod) = visitMember(psiMethod) + + private fun visitMember(psiMember: PsiMember) { + val member = ScopedMember.create(psiMember) ?: return + val pointer = SmartPointerManager.createPointer(psiMember) + snapshot[pointer] = member + if (prevSnapshot == null) return + val prevMember = prevSnapshot[pointer] + if (prevMember != null && !member.hasChanged(prevMember)) return + changes[psiMember] = prevMember + collectRelatedChanges(psiMember, member, prevMember, changes) + } + + companion object { + + private val FILE_STATE_KEY = Key.create("ProjectProblemFileStateKey") + + @JvmStatic + @JvmName("getState") + internal fun getState(psiFile: PsiFile): FileState? { + val storedState = psiFile.getUserData(FILE_STATE_KEY)?.toFileState() + if (storedState != null || DumbService.isDumb(psiFile.project)) return storedState + val updater = FileStateUpdater(null) + publicApi(psiFile).forEach { it.accept(updater) } + val snapshot = updater.snapshot + return FileState(snapshot, emptyMap()) + } + + @JvmStatic + @JvmName("findState") + internal fun findState(psiFile: PsiFile, prevSnapshot: Snapshot): FileState { + val updater = FileStateUpdater(prevSnapshot) + publicApi(psiFile).forEach { it.accept(updater) } + val snapshot = updater.snapshot + val changes = updater.changes + for ((memberPointer, prevMember) in prevSnapshot) { + if (memberPointer in snapshot) continue + val psiMember = memberPointer.element ?: continue + val member = ScopedMember.create(psiMember) ?: continue + changes[psiMember] = prevMember + collectRelatedChanges(psiMember, member, prevMember, changes) + } + return FileState(snapshot, changes) + } + + @JvmStatic + @JvmName("setPreviousState") + internal fun setPreviousState(psiFile: PsiFile) { + val (snapshot, changes) = psiFile.getUserData(FILE_STATE_KEY) ?: return + val oldSnapshot = snapshot.toMutableMap() + changes.forEach { (memberPointer, prevMember) -> + if (memberPointer.element == null) return@forEach + if (prevMember == null) oldSnapshot.remove(memberPointer) + else oldSnapshot[memberPointer] = prevMember + } + psiFile.putUserData(FILE_STATE_KEY, PrivateFileState(oldSnapshot, emptyMap())) + } + + @JvmStatic + @JvmName("updateState") + internal fun updateState(psiFile: PsiFile, fileState: FileState) { + psiFile.putUserData(FILE_STATE_KEY, PrivateFileState.create(fileState.snapshot, fileState.changes)) + } + + private fun collectRelatedChanges( + psiMember: PsiMember, + member: ScopedMember, + prevMember: ScopedMember?, + changes: MutableMap + ) { + when (psiMember) { + is PsiMethod -> { + val containingClass = psiMember.containingClass ?: return + // anonymous classes and lambdas creation might be broken, need to check class usages + changes.putIfAbsent(containingClass, null) + } + is PsiClass -> { + val prevClass = prevMember?.member as? Member.Class ?: return + val curClass = member.member as? Member.Class ?: return + when { + prevClass.isInterface != psiMember.isInterface -> { + // members usages might be broken, need to check them all + publicApi(psiMember).forEach { changes.putIfAbsent(it, null) } + } + prevClass.extendsList != curClass.extendsList || prevClass.implementsList != curClass.implementsList -> { + // maybe some parent members were referenced instead of current class overrides + publicApi(psiMember).filter { it is PsiMethod && it.isOverride() }.forEach { changes.putIfAbsent(it, null) } + } + } + } + } + } + + private data class PrivateFileState( + val snapshot: Snapshot, + val changePointers: Map, ScopedMember?> + ) { + fun toFileState(): FileState { + val changes: Map = changePointers.asSequence() + .mapNotNull { (memberPointer, prevMember) -> memberPointer.element?.let { it to prevMember } } + .toMap() + return FileState(snapshot, changes) + } + + companion object { + fun create(snapshot: Snapshot, changes: Map): PrivateFileState { + val changePointers: Map, ScopedMember?> = changes.entries.asSequence() + .map { (psiMember, prevMember) -> SmartPointerManager.createPointer(psiMember) to prevMember } + .toMap() + return PrivateFileState(snapshot, changePointers) + } + } + } + + private fun publicApi(psiElement: PsiElement) = MemberCollector.collectMembers(psiElement) { !it.hasModifier(PsiModifier.PRIVATE) } + + private fun PsiMethod.isOverride() = hasAnnotation(CommonClassNames.JAVA_LANG_OVERRIDE) + + private fun PsiMember.hasModifier(modifier: String) = modifierList?.hasModifierProperty(modifier) ?: false + } + +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/Member.kt b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/Member.kt index bdcde1b5e2bb..73404bbb37c3 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/Member.kt +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/Member.kt @@ -98,7 +98,7 @@ internal sealed class Member(open val name: String, open val modifiers: Set, - val returnType: String, + val returnType: String?, val paramTypes: List) : Member(name, modifiers) { override fun hasChanged(other: Member): Boolean { @@ -110,7 +110,8 @@ internal sealed class Member(open val name: String, open val modifiers: Set, ScopedMember> - -internal data class ChangeSet(val newSnapshot: Snapshot, val changes: Map) - -internal class SnapshotUpdater(project: Project, private val prevSnapshot: Snapshot) : JavaElementVisitor() { - - private val pointerManager = SmartPointerManager.getInstance(project) - - private val snapshot = mutableMapOf, ScopedMember>() - private val changes = mutableMapOf() - - override fun visitEnumConstant(psiEnumConstant: PsiEnumConstant) { - val member = ScopedMember.create(psiEnumConstant) ?: return - val (psiMember, prevMember) = visitMember(member, psiEnumConstant) ?: return - changes[psiMember] = prevMember - } - - override fun visitClass(psiClass: PsiClass) { - val member = ScopedMember.create(psiClass) ?: return - val (psiMember, prevMember) = visitMember(member, psiClass) ?: return - changes[psiMember] = prevMember - collectRelatedChanges(psiClass, member, prevMember, changes) - } - - override fun visitField(psiField: PsiField) { - val member = ScopedMember.create(psiField) ?: return - val (psiMember, prevMember) = visitMember(member, psiField) ?: return - changes[psiMember] = prevMember - } - - override fun visitMethod(psiMethod: PsiMethod) { - val member = ScopedMember.create(psiMethod) ?: return - val (psiMember, prevMember) = visitMember(member, psiMethod) ?: return - changes[psiMember] = prevMember - collectRelatedChanges(psiMethod, member, prevMember, changes) - } - - private fun visitMember(member: ScopedMember, psiMember: PsiMember): Pair? { - val pointer = pointerManager.createSmartPsiElementPointer(psiMember) - snapshot[pointer] = member - val prevMember = prevSnapshot[pointer] - if (prevMember != null && !member.hasChanged(prevMember)) return null - return psiMember to prevMember - } - - companion object { - private val OLD_CONTENT_KEY = Key.create("OLD_CONTENT_KEY") - private val SNAPSHOT_KEY = Key.create("SNAPSHOT_KEY") - - @JvmStatic - @JvmName("storeContent") - internal fun storeContent(psiFile: PsiFile, content: CharSequence) { - if (psiFile.getUserData(SNAPSHOT_KEY) != null || psiFile.getUserData(OLD_CONTENT_KEY) != null) return - psiFile.putUserData(OLD_CONTENT_KEY, content) - } - - @JvmStatic - @JvmName("updateSnapshot") - internal fun updateSnapshot(psiFile: PsiClassOwner, snapshot: Snapshot) { - psiFile.putUserData(OLD_CONTENT_KEY, null) - psiFile.putUserData(SNAPSHOT_KEY, snapshot) - } - - @JvmStatic - @JvmName("collectChanges") - internal fun collectChanges(psiFile: PsiClassOwner): ChangeSet? { - val snapshot = psiFile.getUserData(SNAPSHOT_KEY) - if (snapshot != null) return update(psiFile, snapshot) - val content = psiFile.getUserData(OLD_CONTENT_KEY) - if (content != null) return update(psiFile, content) - return null - } - - private fun update(psiFile: PsiClassOwner, prevSnapshot: Snapshot): ChangeSet { - val updater = SnapshotUpdater(psiFile.project, prevSnapshot) - publicApi(psiFile).forEach { it.accept(updater) } - val snapshot = updater.snapshot - val changes = updater.changes - for ((memberPointer, prevMember) in prevSnapshot) { - if (memberPointer in snapshot) continue - val psiMember = memberPointer.element ?: continue - val member = ScopedMember.create(psiMember) ?: continue - changes[psiMember] = prevMember - collectRelatedChanges(psiMember, member, prevMember, changes) - } - return ChangeSet(snapshot, changes) - } - - private fun update(psiFile: PsiClassOwner, oldContent: CharSequence): ChangeSet? { - val content = psiFile.viewProvider.contents - if (content == oldContent) return null - val project = psiFile.project - val module = ModuleUtilCore.findModuleForFile(psiFile) ?: return null - val scope = module.moduleScope - val oldFile = parseFile(project, oldContent) ?: return null - val publicMembers = constructMembers(psiFile) { !it.hasModifier(PsiModifier.PRIVATE) } - val oldPsiMembers = publicApi(oldFile) - val prevSnapshot: MutableMap, ScopedMember> = mutableMapOf() - val removedMembers = constructSnapshot(project, scope, publicMembers, oldPsiMembers, prevSnapshot) - if (removedMembers.size == 1) { - val removedMember = removedMembers[0] - val replacement = findReplacement(removedMember, publicMembers, psiFile) - if (replacement != null) prevSnapshot[SmartPointerManager.createPointer(replacement)] = removedMember - } - return update(psiFile, prevSnapshot) - } - - private fun constructSnapshot(project: Project, - scope: GlobalSearchScope, - publicMembers: MutableMap, - oldPsiMembers: List, - snapshot: MutableMap, ScopedMember>): List { - val pointerManager = SmartPointerManager.getInstance(project) - val removedMembers = mutableListOf() - for (oldPsiMember in oldPsiMembers) { - val member = ScopedMember.create(oldPsiMember, scope) ?: continue - val psiMember = publicMembers.remove(member) - if (psiMember == null) { - removedMembers.add(member) - continue - } - val memberPointer = pointerManager.createSmartPsiElementPointer(psiMember) - snapshot[memberPointer] = member - } - return removedMembers - } - - private fun findReplacement(removedMember: ScopedMember, - publicMembers: MutableMap, psiFile: PsiClassOwner): PsiMember? { - if (publicMembers.size == 1) return publicMembers.entries.first().value - val removedPrivate = removedMember.asPrivate() - val privateMembers = constructMembers(psiFile) { it.hasModifier(PsiModifier.PRIVATE) } - return privateMembers[removedPrivate] - } - - private fun parseFile(project: Project, content: CharSequence) = - PsiFileFactory.getInstance(project).createFileFromText(JavaLanguage.INSTANCE, content) as? PsiClassOwner - - private fun constructMembers(psiFile: PsiClassOwner, filter: (PsiMember) -> Boolean): MutableMap { - val psiMembers = MemberCollector.collectMembers(psiFile, filter) - val members = mutableMapOf() - for (psiMember in psiMembers) { - val member = ScopedMember.create(psiMember) ?: continue - members[member] = psiMember - } - return members - } - - private fun collectRelatedChanges(psiMember: PsiMember, - curMember: ScopedMember, - prevMember: ScopedMember?, - changes: MutableMap) { - when (psiMember) { - is PsiMethod -> { - val containingClass = psiMember.containingClass ?: return - // anonymous classes and lambdas creation might be broken, need to check class usages - changes.putIfAbsent(containingClass, null) - } - is PsiClass -> { - val prevClass = prevMember?.member as? Member.Class ?: return - val curClass = curMember.member as? Member.Class ?: return - when { - prevClass.isInterface != psiMember.isInterface -> { - // members usages might be broken, need to check them all - publicApi(psiMember).forEach { changes.putIfAbsent(it, null) } - } - prevClass.extendsList != curClass.extendsList || prevClass.implementsList != curClass.implementsList -> { - // maybe some parent members were referenced instead of current class overrides - publicApi(psiMember).filter { it is PsiMethod && it.isOverride() }.forEach { changes.putIfAbsent(it, null) } - } - } - } - } - } - - private fun publicApi(psiElement: PsiElement) = MemberCollector.collectMembers(psiElement) { !it.hasModifier(PsiModifier.PRIVATE) } - - private fun PsiMethod.isOverride() = hasAnnotation(CommonClassNames.JAVA_LANG_OVERRIDE) - - private fun PsiMember.hasModifier(modifier: String) = modifierList?.hasModifierProperty(modifier) ?: false - } - -} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemFileSelectionListener.java b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemFileSelectionListener.java index e9a003bfab1f..5ad1c1c6c2e2 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemFileSelectionListener.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemFileSelectionListener.java @@ -1,35 +1,22 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.codeInsight.daemon.problems.pass; -import com.intellij.codeInsight.daemon.problems.SnapshotUpdater; +import com.intellij.codeInsight.daemon.problems.FileStateUpdater; import com.intellij.injected.editor.VirtualFileWindow; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.event.BulkAwareDocumentListener; -import com.intellij.openapi.editor.event.DocumentEvent; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.FileEditorManagerEvent; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.project.ProjectManagerListener; import com.intellij.openapi.startup.StartupActivity; -import com.intellij.openapi.util.Key; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiManager; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Arrays; import static com.intellij.util.ObjectUtils.tryCast; -class ProjectProblemFileSelectionListener implements FileEditorManagerListener, BulkAwareDocumentListener, ProjectManagerListener { - - private static final Key PROJECT_PROBLEM_FILE_CHANGE_LISTENER_KEY = Key.create("ProjectProblemFileChangeListenerKey"); +class ProjectProblemFileSelectionListener implements FileEditorManagerListener { private final Project myProject; @@ -40,60 +27,12 @@ class ProjectProblemFileSelectionListener implements FileEditorManagerListener, @Override public void selectionChanged(@NotNull FileEditorManagerEvent event) { VirtualFile oldFile = event.getOldFile(); - if (oldFile != null) removeListener(oldFile); VirtualFile newFile = event.getNewFile(); - if (newFile == null) return; - PsiJavaFile psiFile = tryCast(PsiManager.getInstance(myProject).findFile(newFile), PsiJavaFile.class); - if (psiFile == null) return; - ProjectProblemPassUtils.removeOldInlays(psiFile); - addListener(newFile); - } - - @Override - public void projectClosingBeforeSave(@NotNull Project project) { - Arrays.stream(FileEditorManager.getInstance(project).getSelectedFiles()).forEach(vf -> removeListener(vf)); - } - - private void addListener(VirtualFile virtualFile) { - if (virtualFile instanceof VirtualFileWindow || !virtualFile.isValid()) return; - PsiJavaFile psiFile = tryCast(PsiManager.getInstance(myProject).findFile(virtualFile), PsiJavaFile.class); - if (psiFile == null) return; - Document document = getDocument(virtualFile); - if (document == null) return; - if (Boolean.TRUE.equals(document.getUserData(PROJECT_PROBLEM_FILE_CHANGE_LISTENER_KEY))) return; - document.addDocumentListener(this); - document.putUserData(PROJECT_PROBLEM_FILE_CHANGE_LISTENER_KEY, true); - } - - private void removeListener(VirtualFile virtualFile) { - if (virtualFile == null || virtualFile instanceof VirtualFileWindow || !virtualFile.isValid()) return; - if (!(PsiManager.getInstance(myProject).findFile(virtualFile) instanceof PsiJavaFile)) return; - Document document = getDocument(virtualFile); - if (document == null) return; - if (!Boolean.TRUE.equals(document.getUserData(PROJECT_PROBLEM_FILE_CHANGE_LISTENER_KEY))) return; - document.removeDocumentListener(this); - document.putUserData(PROJECT_PROBLEM_FILE_CHANGE_LISTENER_KEY, null); - } - - @Nullable - private static Document getDocument(@NotNull VirtualFile virtualFile) { - FileDocumentManager documentManager = FileDocumentManager.getInstance(); - Document document = documentManager.getDocument(virtualFile); - if (document == null) return null; - return document; - } - - @Override - public void beforeDocumentChange(@NotNull DocumentEvent event) { - Document document = event.getDocument(); - FileDocumentManager documentManager = FileDocumentManager.getInstance(); - VirtualFile virtualFile = documentManager.getFile(document); - if (virtualFile == null) return; - CharSequence content = document.getImmutableCharSequence(); - PsiJavaFile psiFile = tryCast(PsiManager.getInstance(myProject).findFile(virtualFile), PsiJavaFile.class); - if (psiFile == null) return; - SnapshotUpdater.storeContent(psiFile, content); - removeListener(virtualFile); + if (oldFile == null || oldFile instanceof VirtualFileWindow || !oldFile.isValid() || oldFile.equals(newFile)) return; + PsiJavaFile psiJavaFile = tryCast(PsiManager.getInstance(myProject).findFile(oldFile), PsiJavaFile.class); + if (psiJavaFile == null) return; + ProjectProblemPassUtils.removeInlays(psiJavaFile); + FileStateUpdater.setPreviousState(psiJavaFile); } public static class MyStartupActivity implements StartupActivity { @@ -101,9 +40,7 @@ class ProjectProblemFileSelectionListener implements FileEditorManagerListener, public void runActivity(@NotNull Project project) { if (!Registry.is("project.problems.view") && !ApplicationManager.getApplication().isUnitTestMode()) return; ProjectProblemFileSelectionListener listener = new ProjectProblemFileSelectionListener(project); - Arrays.stream(FileEditorManager.getInstance(project).getSelectedFiles()).forEach(vf -> listener.addListener(vf)); project.getMessageBus().connect().subscribe(FILE_EDITOR_MANAGER, listener); - ProjectManager.getInstance().addProjectManagerListener(project, listener); } } } diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPass.java b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPass.java index d03c4c920894..98ce45f75c7d 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPass.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPass.java @@ -2,26 +2,33 @@ package com.intellij.codeInsight.daemon.problems.pass; import com.intellij.codeHighlighting.TextEditorHighlightingPass; -import com.intellij.codeInsight.daemon.problems.ChangeSet; +import com.intellij.codeInsight.daemon.problems.FileState; +import com.intellij.codeInsight.daemon.problems.FileStateUpdater; import com.intellij.codeInsight.daemon.problems.ProblemCollector; import com.intellij.codeInsight.daemon.problems.ScopedMember; import com.intellij.codeInsight.hints.BlockInlayRenderer; import com.intellij.codeInsight.hints.presentation.InlayPresentation; import com.intellij.codeInsight.hints.presentation.PresentationFactory; -import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.editor.*; +import com.intellij.codeInspection.SmartHashMap; +import com.intellij.openapi.editor.BlockInlayPriority; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.psi.*; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiMember; +import com.intellij.psi.SmartPsiElementPointer; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Collections; +import java.util.Map; +import java.util.Set; -import static com.intellij.codeInsight.daemon.problems.SnapshotUpdater.collectChanges; -import static com.intellij.codeInsight.daemon.problems.SnapshotUpdater.updateSnapshot; import static com.intellij.codeInsight.daemon.problems.pass.ProjectProblemPassUtils.*; public class ProjectProblemPass extends TextEditorHighlightingPass { @@ -29,9 +36,7 @@ public class ProjectProblemPass extends TextEditorHighlightingPass { private final Editor myEditor; private final PsiJavaFile myFile; - private final SmartPointerManager myPointerManager = SmartPointerManager.getInstance(myProject); - - private Map>> myProblems = null; + private Map myProblems = null; private Map, ScopedMember> mySnapshot = null; ProjectProblemPass(@NotNull Project project, @NotNull Editor editor, @NotNull PsiJavaFile file) { @@ -42,88 +47,68 @@ public class ProjectProblemPass extends TextEditorHighlightingPass { @Override public void doCollectInformation(@NotNull ProgressIndicator progress) { - ChangeSet changeSet = collectChanges(myFile); - if (changeSet == null) return; - Map changes = mergeWithOldChanges(myFile, changeSet.getChanges()); - myProblems = ReadAction.nonBlocking(() -> collectProblems(changes)).executeSynchronously(); - mySnapshot = changeSet.getNewSnapshot(); - } - - private @Nullable Map>> collectProblems(@NotNull Map changes) { - if (changes.isEmpty()) return Collections.emptyMap(); - Map>> problems = new HashMap<>(); - for (Map.Entry entry : changes.entrySet()) { - if (myProject.isDisposed()) return null; - PsiMember curMember = entry.getKey(); - ScopedMember prevMember = entry.getValue(); - Set memberProblems = ProblemCollector.collect(prevMember, curMember); - if (memberProblems == null) memberProblems = Collections.emptySet(); - SmartPsiElementPointer curMemberPointer = myPointerManager.createSmartPsiElementPointer(curMember); - Change change = new Change(curMemberPointer, prevMember); - problems.put(change, ContainerUtil.map(memberProblems, p -> myPointerManager.createSmartPsiElementPointer(p))); - } - return problems; + FileState prevState = FileStateUpdater.getState(myFile); + if (prevState == null) return; + FileState curState = FileStateUpdater.findState(myFile, prevState.getSnapshot()); + myProblems = collectProblems(curState.getChanges(), prevState.getChanges()); + mySnapshot = curState.getSnapshot(); } @Override public void doApplyInformationToEditor() { - Document document = myDocument; - if (document == null) return; Map, ScopedMember> snapshot = mySnapshot; if (snapshot == null) return; - Map>> problems = myProblems; + Map problems = myProblems; if (problems == null) return; - InlayModel inlayModel = myEditor.getInlayModel(); + PresentationFactory factory = new PresentationFactory((EditorImpl)myEditor); - Map, ReportedChange> reportedChanges = new HashMap<>(); - problems.forEach((change, changeProblems) -> { - ScopedMember prevMember = change.prevMember; - SmartPsiElementPointer memberPointer = change.curMemberPointer; - PsiMember member = memberPointer.getElement(); - if (member == null) return; - reportedChanges.computeIfAbsent(memberPointer, (k) -> { - if (changeProblems.isEmpty()) return new ReportedChange(prevMember, null); - int offset = getMemberOffset(member); - InlayPresentation presentation = getPresentation(myProject, myEditor, document, factory, offset, member, changeProblems); - BlockInlayRenderer renderer = createBlockRenderer(presentation); - Inlay newInlay = inlayModel.addBlockElement(offset, true, true, BlockInlayPriority.PROBLEMS, renderer); - return new ReportedChange(prevMember, newInlay); - }); + Map> inlays = getInlays(myFile); + Map changes = new SmartHashMap<>(); + problems.forEach((curMember, problem) -> { + ScopedMember prevMember = problem.prevMember; + Set brokenUsages = problem.brokenUsages; + changes.put(curMember, prevMember); + if (brokenUsages != null) addInlay(factory, curMember, brokenUsages, inlays); }); - reportChanges(myFile, reportedChanges); - updateSnapshot(myFile, snapshot); + updateInlays(myFile, inlays); + + FileState fileState = new FileState(snapshot, changes); + FileStateUpdater.updateState(myFile, fileState); } - private static class Change { + private void addInlay(PresentationFactory factory, + PsiMember psiMember, + @NotNull Set brokenUsages, + @NotNull Map> inlays) { + Inlay oldInlay = inlays.remove(psiMember); + if (oldInlay != null) Disposer.dispose(oldInlay); + if (brokenUsages.isEmpty()) return; + int offset = getMemberOffset(psiMember); + InlayPresentation presentation = getPresentation(myProject, myEditor, myEditor.getDocument(), factory, offset, psiMember, brokenUsages); + BlockInlayRenderer renderer = createBlockRenderer(presentation); + Inlay newInlay = myEditor.getInlayModel().addBlockElement(offset, true, true, BlockInlayPriority.PROBLEMS, renderer); + inlays.put(psiMember, newInlay); + } - private final SmartPsiElementPointer curMemberPointer; + private static @NotNull Map collectProblems(@NotNull Map curChanges, + @NotNull Map oldChanges) { + Map problems = ContainerUtil.map2Map(oldChanges.entrySet(), + e -> Pair.create(e.getKey(), new Problem(e.getValue(), null))); + curChanges.forEach((curMember, prevMember) -> { + Set changeProblems = ProblemCollector.collect(prevMember, curMember); + if (changeProblems == null) changeProblems = Collections.emptySet(); + problems.put(curMember, new Problem(prevMember, changeProblems)); + }); + return problems; + } + + private static class Problem { private final ScopedMember prevMember; + private final Set brokenUsages; - private Change(SmartPsiElementPointer curMemberPointer, ScopedMember prevMember) { - this.curMemberPointer = curMemberPointer; + private Problem(ScopedMember prevMember, Set brokenUsages) { this.prevMember = prevMember; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Change change = (Change)o; - return Objects.equals(curMemberPointer, change.curMemberPointer) && - Objects.equals(prevMember, change.prevMember); - } - - @Override - public int hashCode() { - return Objects.hash(curMemberPointer, prevMember); - } - - @Override - public String toString() { - return "Change{" + - "curMemberPointer=" + curMemberPointer + - ", prevMember=" + prevMember + - '}'; + this.brokenUsages = brokenUsages; } } } diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPassUtils.java b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPassUtils.java index 5a449377aeb0..e8b3477d1d77 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPassUtils.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/problems/pass/ProjectProblemPassUtils.java @@ -2,7 +2,6 @@ package com.intellij.codeInsight.daemon.problems.pass; import com.intellij.codeInsight.daemon.JavaErrorBundle; -import com.intellij.codeInsight.daemon.problems.ScopedMember; import com.intellij.codeInsight.hints.BlockConstrainedPresentation; import com.intellij.codeInsight.hints.BlockConstraints; import com.intellij.codeInsight.hints.BlockInlayRenderer; @@ -10,6 +9,7 @@ import com.intellij.codeInsight.hints.presentation.InlayPresentation; import com.intellij.codeInsight.hints.presentation.PresentationFactory; import com.intellij.codeInsight.hints.presentation.RecursivelyUpdatingRootPresentation; import com.intellij.codeInsight.hints.presentation.SpacePresentation; +import com.intellij.codeInspection.SmartHashMap; import com.intellij.find.FindUtil; import com.intellij.openapi.editor.BlockInlayPriority; import com.intellij.openapi.editor.Document; @@ -19,19 +19,21 @@ import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; import com.intellij.pom.Navigatable; import com.intellij.psi.*; import com.intellij.psi.javadoc.PsiDocComment; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; public class ProjectProblemPassUtils { - private static final Key, ReportedChange>> REPORTED_CHANGES = Key.create("REPORTED_CHANGES"); - private static final Key FILE_OPENED_KEY = Key.create("FILE_OPENED"); + private static final Key, Inlay>> PROBLEM_INLAY_HINTS = Key.create("ProjectProblemInlayHintsKey"); static @NotNull InlayPresentation getPresentation(@NotNull Project project, @NotNull Editor editor, @@ -39,21 +41,20 @@ public class ProjectProblemPassUtils { @NotNull PresentationFactory factory, int offset, @NotNull PsiElement element, - @NotNull List> problems) { + @NotNull Set brokenUsages) { int line = document.getLineNumber(offset); int column = offset - document.getLineStartOffset(line); int columnWidth = EditorUtil.getPlainSpaceWidth(editor); SpacePresentation spacePresentation = new SpacePresentation(column * columnWidth, 0); - InlayPresentation textPresentation = factory.smallText(JavaErrorBundle.message("project.problems.broken.usages", problems.size())); + InlayPresentation textPresentation = factory.smallText(JavaErrorBundle.message("project.problems.broken.usages", brokenUsages.size())); InlayPresentation presentation = factory.seq(spacePresentation, textPresentation); return factory.referenceOnHover(presentation, (e, p) -> { - List elements = ContainerUtil.mapNotNull(problems, pointer -> pointer.getElement()); - if (elements.size() == 1) { - PsiElement problem = elements.get(0); - if (problem instanceof Navigatable) ((Navigatable)problem).navigate(true); + if (brokenUsages.size() == 1) { + PsiElement usage = brokenUsages.iterator().next(); + if (usage instanceof Navigatable) ((Navigatable)usage).navigate(true); } else { - FindUtil.showInUsageView(element, elements.toArray(PsiElement.EMPTY_ARRAY), + FindUtil.showInUsageView(element, brokenUsages.toArray(PsiElement.EMPTY_ARRAY), JavaErrorBundle.message("project.problems.title"), project); } }); @@ -74,79 +75,26 @@ public class ProjectProblemPassUtils { .getTextRange().getStartOffset(); } - static void removeOldInlays(@NotNull PsiFile psiFile) { - Map, ReportedChange> changes = getReportedChanges(psiFile); - changes.values().forEach(c -> { - Inlay inlay = c.myInlay; - if (inlay != null) Disposer.dispose(inlay); + public static @NotNull Map> getInlays(@NotNull PsiFile psiFile) { + Map, Inlay> oldInlays = psiFile.getUserData(PROBLEM_INLAY_HINTS); + Map> inlays = new SmartHashMap<>(); + if (oldInlays == null) return inlays; + oldInlays.forEach((pointer, inlay) -> { + PsiMember member = pointer.getElement(); + if (member == null) Disposer.dispose(inlay); + else inlays.put(member, inlay); }); - psiFile.putUserData(FILE_OPENED_KEY, true); + return inlays; } - static @NotNull Map mergeWithOldChanges(@NotNull PsiFile psiFile, - @NotNull Map newChanges) { - Map, ReportedChange> oldChanges = Boolean.TRUE.equals(psiFile.getUserData(FILE_OPENED_KEY)) ? - getReportedChanges(psiFile) : Collections.emptyMap(); - if (oldChanges.isEmpty()) return newChanges; - Map changes = new HashMap<>(newChanges); - oldChanges.forEach((memberPointer, reportedChange) -> { - PsiMember psiMember = memberPointer.getElement(); - if (psiMember != null) changes.putIfAbsent(psiMember, reportedChange.getPrevMember()); - }); - return changes; + static void updateInlays(@NotNull PsiFile psiFile, @NotNull Map> inlays) { + Map, Inlay> newInlays = + ContainerUtil.map2Map(inlays.entrySet(), e -> Pair.create(SmartPointerManager.createPointer(e.getKey()), e.getValue())); + psiFile.putUserData(PROBLEM_INLAY_HINTS, newInlays); } - static void reportChanges(@NotNull PsiFile psiFile, @NotNull Map, ReportedChange> newChanges) { - Map, ReportedChange> reportedChanges = new HashMap<>(newChanges); - Map, ReportedChange> oldChanges = getReportedChanges(psiFile); - oldChanges.forEach((memberPointer, oldChange) -> { - PsiMember member = memberPointer.getElement(); - if (member == null) { - Inlay inlay = oldChange.myInlay; - if (inlay != null) Disposer.dispose(inlay); - return; - } - ReportedChange newChange = reportedChanges.putIfAbsent(memberPointer, oldChange); - // we have new change for this element - if (newChange != null && newChange != oldChange) { - Inlay oldInlay = oldChange.myInlay; - if (oldInlay != null) Disposer.dispose(oldInlay); - if (newChange.myInlay == null) reportedChanges.remove(memberPointer); - } - }); - psiFile.putUserData(REPORTED_CHANGES, reportedChanges); - psiFile.putUserData(FILE_OPENED_KEY, false); - } - - public static @NotNull Map, ReportedChange> getReportedChanges(@NotNull PsiFile psiFile) { - Map, ReportedChange> changes = psiFile.getUserData(REPORTED_CHANGES); - return changes == null ? Collections.emptyMap() : changes; - } - - public static class ReportedChange { - - private final ScopedMember myPrevMember; - private final Inlay myInlay; - - ReportedChange(@Nullable ScopedMember prevMember, @Nullable Inlay inlay) { - myPrevMember = prevMember; - myInlay = inlay; - } - - public Inlay getInlay() { - return myInlay; - } - - public ScopedMember getPrevMember() { - return myPrevMember; - } - - @Override - public String toString() { - return "ReportedChange{" + - "myPrevMember=" + myPrevMember + - ", myInlay=" + myInlay + - '}'; - } + static void removeInlays(@NotNull PsiFile psiFile) { + getInlays(psiFile).values().forEach(inlay -> Disposer.dispose(inlay)); + psiFile.putUserData(PROBLEM_INLAY_HINTS, null); } } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/FieldProblemsTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/FieldProblemsTest.kt index bad022dde1ce..ea19d006ff7a 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/FieldProblemsTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/FieldProblemsTest.kt @@ -183,11 +183,12 @@ internal class FieldProblemsTest : ProjectProblemsViewTest() { val factory = JavaPsiFacade.getElementFactory(project) WriteCommandAction.runWriteCommandAction(project) { val psiField = factory.createFieldFromText("static final String field = \"foo\";", null) - targetClass.add(psiField) as PsiField + targetClass.add(psiField) } myFixture.checkHighlighting() assertFalse(hasReportedProblems(targetClass, outsideRefClass)) + myFixture.checkHighlighting() assertTrue(hasReportedProblems(targetClass, packageRefClass)) changeField(targetClass) { field -> field.modifierList?.setModifierProperty(PsiModifier.PUBLIC, true) } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/ProjectProblemsViewTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/ProjectProblemsViewTest.kt index fc999d57a114..b94102158be7 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/ProjectProblemsViewTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/problems/ProjectProblemsViewTest.kt @@ -2,12 +2,12 @@ package com.intellij.java.codeInsight.daemon.problems import com.intellij.codeInsight.daemon.problems.pass.ProjectProblemPassUtils -import com.intellij.codeInsight.daemon.problems.pass.ProjectProblemPassUtils.ReportedChange import com.intellij.codeInsight.hints.BlockInlayRenderer import com.intellij.codeInsight.hints.presentation.DynamicDelegatePresentation import com.intellij.codeInsight.hints.presentation.OnClickPresentation import com.intellij.codeInsight.hints.presentation.OnHoverPresentation import com.intellij.codeInsight.hints.presentation.RecursivelyUpdatingRootPresentation +import com.intellij.openapi.editor.Inlay import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.psi.* @@ -30,7 +30,7 @@ internal abstract class ProjectProblemsViewTest : LightJavaCodeInsightFixtureTes } protected fun getProblems(psiFile: PsiFile): List { - val reportedChanges: Map, ReportedChange> = ProjectProblemPassUtils.getReportedChanges(psiFile) + val reportedChanges: MutableMap> = ProjectProblemPassUtils.getInlays(psiFile) val targetFile = psiFile.virtualFile val problems: MutableList = mutableListOf() @@ -38,8 +38,7 @@ internal abstract class ProjectProblemsViewTest : LightJavaCodeInsightFixtureTes val editorManager = FileEditorManager.getInstance(project) val clickEvent = MouseEvent(JPanel(), 0, 0, 0, 0, 0, 0, true, MouseEvent.BUTTON1) val point = Point(0, 0) - for (reportedChange in reportedChanges.values) { - val inlay = reportedChange.inlay ?: continue + for (inlay in reportedChanges.values) { val renderer = inlay.renderer as BlockInlayRenderer val presentation = renderer.getConstrainedPresentations()[0] val rootPresentation = presentation.root as RecursivelyUpdatingRootPresentation diff --git a/java/java-tests/testSrc/com/intellij/java/propertyBased/ProjectProblemsViewPropertyTest.kt b/java/java-tests/testSrc/com/intellij/java/propertyBased/ProjectProblemsViewPropertyTest.kt index 5a7f922fd22e..940e295dc4bc 100644 --- a/java/java-tests/testSrc/com/intellij/java/propertyBased/ProjectProblemsViewPropertyTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/propertyBased/ProjectProblemsViewPropertyTest.kt @@ -4,8 +4,7 @@ package com.intellij.java.propertyBased import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.daemon.problems.MemberCollector import com.intellij.codeInsight.daemon.problems.MemberUsageCollector -import com.intellij.codeInsight.daemon.problems.pass.ProjectProblemPassUtils.ReportedChange -import com.intellij.codeInsight.daemon.problems.pass.ProjectProblemPassUtils.getReportedChanges +import com.intellij.codeInsight.daemon.problems.pass.ProjectProblemPassUtils import com.intellij.codeInsight.hints.BlockInlayRenderer import com.intellij.codeInsight.hints.presentation.DynamicDelegatePresentation import com.intellij.codeInsight.hints.presentation.OnClickPresentation @@ -95,14 +94,12 @@ class ProjectProblemsViewPropertyTest : BaseUnivocityTest() { for (changedFile in changedFiles) { val psiFile = psiManager.findFile(changedFile)!! rehighlight(psiFile) - val reportedChanges = getReportedChanges(psiFile) - for ((pointer, reportedChange) in reportedChanges) { - val inlay = reportedChange.inlay + val reportedChanges = ProjectProblemPassUtils.getInlays(psiFile) + for ((member, inlay) in reportedChanges) { if (inlay != null) { TestCase.fail("Problems are still reported even after the fix. " + "File: ${changedFile.name}, " + - "Member: ${JavaDocUtil.getReferenceText(myProject, pointer.element)}, " + - "Previous member: ${reportedChange.prevMember}, " + + "Member: ${JavaDocUtil.getReferenceText(myProject, member)}, " + "Reported problems: ${extractProblems(changedFile, inlay)}") } } @@ -281,11 +278,10 @@ class ProjectProblemsViewPropertyTest : BaseUnivocityTest() { } private fun getFilesReportedByProblemSearch(psiFile: PsiFile): Set { - val reportedChanges: Map, ReportedChange> = getReportedChanges(psiFile) + val reportedChanges: Map> = ProjectProblemPassUtils.getInlays(psiFile) val virtualFile = psiFile.virtualFile val filesWithProblems = mutableSetOf() - for (change in reportedChanges.values) { - val inlay = change.inlay ?: continue + for (inlay in reportedChanges.values) { clickOnInlay(inlay) val openedFile = FileEditorManager.getInstance(myProject).selectedEditor!!.file!! if (openedFile != virtualFile) {