Problems view: store file state(changes and snapshot) and inlays separately

GitOrigin-RevId: 5f4ba1f78fa99016c2ba65869c1c598c5a1c6875
This commit is contained in:
Artemiy Sartakov
2020-04-01 04:02:21 +00:00
committed by intellij-monorepo-bot
parent 04afc69c01
commit 37158871a3
9 changed files with 257 additions and 440 deletions
@@ -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<SmartPsiElementPointer<PsiMember>, ScopedMember>
internal data class FileState(val snapshot: Snapshot, val changes: Map<PsiMember, ScopedMember?>)
internal class FileStateUpdater(private val prevSnapshot: Snapshot?) : JavaElementVisitor() {
private val snapshot = mutableMapOf<SmartPsiElementPointer<PsiMember>, ScopedMember>()
private val changes = mutableMapOf<PsiMember, ScopedMember?>()
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<PrivateFileState>("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<PsiMember, ScopedMember?>
) {
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<SmartPsiElementPointer<PsiMember>, ScopedMember?>
) {
fun toFileState(): FileState {
val changes: Map<PsiMember, ScopedMember?> = changePointers.asSequence()
.mapNotNull { (memberPointer, prevMember) -> memberPointer.element?.let { it to prevMember } }
.toMap()
return FileState(snapshot, changes)
}
companion object {
fun create(snapshot: Snapshot, changes: Map<PsiMember, ScopedMember?>): PrivateFileState {
val changePointers: Map<SmartPsiElementPointer<PsiMember>, 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
}
}
@@ -98,7 +98,7 @@ internal sealed class Member(open val name: String, open val modifiers: Set<Stri
internal data class Method(override val name: String,
override val modifiers: Set<String>,
val returnType: String,
val returnType: String?,
val paramTypes: List<String>) : 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<Stri
companion object {
internal fun create(psiMethod: PsiMethod): Method? {
val returnType = psiMethod.returnType?.canonicalText ?: return null
val returnType = psiMethod.returnType?.canonicalText
if (returnType == null && !psiMethod.isConstructor) return null
val name = psiMethod.name
val modifiers = extractModifiers(psiMethod.modifierList)
val paramTypes = psiMethod.parameterList.parameters.map { it.type.canonicalText }
@@ -1,194 +0,0 @@
// 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.lang.java.JavaLanguage
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.*
import com.intellij.psi.search.GlobalSearchScope
internal typealias Snapshot = Map<SmartPsiElementPointer<PsiMember>, ScopedMember>
internal data class ChangeSet(val newSnapshot: Snapshot, val changes: Map<PsiMember, ScopedMember?>)
internal class SnapshotUpdater(project: Project, private val prevSnapshot: Snapshot) : JavaElementVisitor() {
private val pointerManager = SmartPointerManager.getInstance(project)
private val snapshot = mutableMapOf<SmartPsiElementPointer<PsiMember>, ScopedMember>()
private val changes = mutableMapOf<PsiMember, ScopedMember?>()
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<PsiMember, ScopedMember?>? {
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<CharSequence>("OLD_CONTENT_KEY")
private val SNAPSHOT_KEY = Key.create<Snapshot>("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<SmartPsiElementPointer<PsiMember>, 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<ScopedMember, PsiMember>,
oldPsiMembers: List<PsiMember>,
snapshot: MutableMap<SmartPsiElementPointer<PsiMember>, ScopedMember>): List<ScopedMember> {
val pointerManager = SmartPointerManager.getInstance(project)
val removedMembers = mutableListOf<ScopedMember>()
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<ScopedMember,
PsiMember>, 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<ScopedMember, PsiMember> {
val psiMembers = MemberCollector.collectMembers(psiFile, filter)
val members = mutableMapOf<ScopedMember, PsiMember>()
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<PsiMember, ScopedMember?>) {
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
}
}
@@ -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<Boolean> 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);
}
}
}
@@ -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<Change, List<SmartPsiElementPointer<PsiElement>>> myProblems = null;
private Map<PsiMember, Problem> myProblems = null;
private Map<SmartPsiElementPointer<PsiMember>, 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<PsiMember, ScopedMember> changes = mergeWithOldChanges(myFile, changeSet.getChanges());
myProblems = ReadAction.nonBlocking(() -> collectProblems(changes)).executeSynchronously();
mySnapshot = changeSet.getNewSnapshot();
}
private @Nullable Map<Change, List<SmartPsiElementPointer<PsiElement>>> collectProblems(@NotNull Map<PsiMember, ScopedMember> changes) {
if (changes.isEmpty()) return Collections.emptyMap();
Map<Change, List<SmartPsiElementPointer<PsiElement>>> problems = new HashMap<>();
for (Map.Entry<PsiMember, ScopedMember> entry : changes.entrySet()) {
if (myProject.isDisposed()) return null;
PsiMember curMember = entry.getKey();
ScopedMember prevMember = entry.getValue();
Set<PsiElement> memberProblems = ProblemCollector.collect(prevMember, curMember);
if (memberProblems == null) memberProblems = Collections.emptySet();
SmartPsiElementPointer<PsiMember> 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<SmartPsiElementPointer<PsiMember>, ScopedMember> snapshot = mySnapshot;
if (snapshot == null) return;
Map<Change, List<SmartPsiElementPointer<PsiElement>>> problems = myProblems;
Map<PsiMember, Problem> problems = myProblems;
if (problems == null) return;
InlayModel inlayModel = myEditor.getInlayModel();
PresentationFactory factory = new PresentationFactory((EditorImpl)myEditor);
Map<SmartPsiElementPointer<PsiMember>, ReportedChange> reportedChanges = new HashMap<>();
problems.forEach((change, changeProblems) -> {
ScopedMember prevMember = change.prevMember;
SmartPsiElementPointer<PsiMember> 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<PsiMember, Inlay<?>> inlays = getInlays(myFile);
Map<PsiMember, ScopedMember> changes = new SmartHashMap<>();
problems.forEach((curMember, problem) -> {
ScopedMember prevMember = problem.prevMember;
Set<PsiElement> 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<PsiElement> brokenUsages,
@NotNull Map<PsiMember, Inlay<?>> 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<PsiMember> curMemberPointer;
private static @NotNull Map<PsiMember, Problem> collectProblems(@NotNull Map<PsiMember, ScopedMember> curChanges,
@NotNull Map<PsiMember, ScopedMember> oldChanges) {
Map<PsiMember, Problem> problems = ContainerUtil.map2Map(oldChanges.entrySet(),
e -> Pair.create(e.getKey(), new Problem(e.getValue(), null)));
curChanges.forEach((curMember, prevMember) -> {
Set<PsiElement> 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<PsiElement> brokenUsages;
private Change(SmartPsiElementPointer<PsiMember> curMemberPointer, ScopedMember prevMember) {
this.curMemberPointer = curMemberPointer;
private Problem(ScopedMember prevMember, Set<PsiElement> 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;
}
}
}
@@ -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<Map<SmartPsiElementPointer<PsiMember>, ReportedChange>> REPORTED_CHANGES = Key.create("REPORTED_CHANGES");
private static final Key<Boolean> FILE_OPENED_KEY = Key.create("FILE_OPENED");
private static final Key<Map<SmartPsiElementPointer<PsiMember>, 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<SmartPsiElementPointer<PsiElement>> problems) {
@NotNull Set<PsiElement> 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<PsiElement> 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<SmartPsiElementPointer<PsiMember>, ReportedChange> changes = getReportedChanges(psiFile);
changes.values().forEach(c -> {
Inlay<?> inlay = c.myInlay;
if (inlay != null) Disposer.dispose(inlay);
public static @NotNull Map<PsiMember, Inlay<?>> getInlays(@NotNull PsiFile psiFile) {
Map<SmartPsiElementPointer<PsiMember>, Inlay<?>> oldInlays = psiFile.getUserData(PROBLEM_INLAY_HINTS);
Map<PsiMember, Inlay<?>> 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<PsiMember, ScopedMember> mergeWithOldChanges(@NotNull PsiFile psiFile,
@NotNull Map<PsiMember, ScopedMember> newChanges) {
Map<SmartPsiElementPointer<PsiMember>, ReportedChange> oldChanges = Boolean.TRUE.equals(psiFile.getUserData(FILE_OPENED_KEY)) ?
getReportedChanges(psiFile) : Collections.emptyMap();
if (oldChanges.isEmpty()) return newChanges;
Map<PsiMember, ScopedMember> 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<PsiMember, Inlay<?>> inlays) {
Map<SmartPsiElementPointer<PsiMember>, 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<SmartPsiElementPointer<PsiMember>, ReportedChange> newChanges) {
Map<SmartPsiElementPointer<PsiMember>, ReportedChange> reportedChanges = new HashMap<>(newChanges);
Map<SmartPsiElementPointer<PsiMember>, 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<SmartPsiElementPointer<PsiMember>, ReportedChange> getReportedChanges(@NotNull PsiFile psiFile) {
Map<SmartPsiElementPointer<PsiMember>, 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);
}
}
@@ -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<PsiStatement>(targetClass, outsideRefClass))
myFixture.checkHighlighting()
assertTrue(hasReportedProblems<PsiAssignmentExpression>(targetClass, packageRefClass))
changeField(targetClass) { field -> field.modifierList?.setModifierProperty(PsiModifier.PUBLIC, true) }
@@ -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<PsiElement> {
val reportedChanges: Map<SmartPsiElementPointer<PsiMember>, ReportedChange> = ProjectProblemPassUtils.getReportedChanges(psiFile)
val reportedChanges: MutableMap<PsiMember, Inlay<*>> = ProjectProblemPassUtils.getInlays(psiFile)
val targetFile = psiFile.virtualFile
val problems: MutableList<PsiElement> = 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
@@ -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<VirtualFile> {
val reportedChanges: Map<SmartPsiElementPointer<PsiMember>, ReportedChange> = getReportedChanges(psiFile)
val reportedChanges: Map<PsiMember, Inlay<*>> = ProjectProblemPassUtils.getInlays(psiFile)
val virtualFile = psiFile.virtualFile
val filesWithProblems = mutableSetOf<VirtualFile>()
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) {