Files
openide/java/java-impl/src/com/intellij/codeInsight/ExternalAnnotationsManagerImpl.java
Tagir Valeev 2c486b2d0a Improved type annotation support
PsiTypeElementImpl#addAnnotation implemented
AddAnnotationPsiFix, NullableNotNullManager adjusted
Fixes IDEA-232258 "Annotate overridden method parameters @NotNull" erroneously adds notNull to the param, not its type
Fixes IDEA-232565 Intention "Annotate overriding methods as NotNull" doesn't respect "type use"
Also, AddAnnotationPsiFix can be applied in batch now when annotations are not external.

GitOrigin-RevId: 0b652d3b032ed0d1c701beeda102c5e3c841762c
2020-02-12 06:13:13 +00:00

1053 lines
40 KiB
Java

// Copyright 2000-2019 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;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.CommonBundle;
import com.intellij.ProjectTopics;
import com.intellij.codeInsight.highlighting.HighlightManager;
import com.intellij.diagnostic.AttachmentFactory;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.undo.BasicUndoableAction;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.command.undo.UndoUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectBundle;
import com.intellij.openapi.projectRoots.SdkModificator;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlDocument;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.OptionsMessageDialog;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* @author anna
*/
public final class ExternalAnnotationsManagerImpl extends ReadableExternalAnnotationsManager {
private static final Logger LOG = Logger.getInstance(ExternalAnnotationsManagerImpl.class);
private final MessageBus myBus;
public ExternalAnnotationsManagerImpl(@NotNull Project project) {
super(PsiManager.getInstance(project));
myBus = project.getMessageBus();
MessageBusConnection connection = myBus.connect();
connection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
@Override
public void rootsChanged(@NotNull ModuleRootEvent event) {
dropAnnotationsCache();
}
});
connection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
for (VFileEvent event : events) {
if (!event.isFromRefresh()) {
continue;
}
String name;
if (event instanceof VFileCreateEvent) {
name = ((VFileCreateEvent)event).getChildName();
}
else {
VirtualFile file = event.getFile();
if (file == null) {
continue;
}
name = file.getName();
}
if (event.isFromRefresh() && ANNOTATIONS_XML.equals(name)) {
dropAnnotationsCache();
notifyChangedExternally();
}
}
}
});
EditorFactory.getInstance().getEventMulticaster().addDocumentListener(new MyDocumentListener(), project);
}
private void notifyAfterAnnotationChanging(@NotNull PsiModifierListOwner owner, @NotNull String annotationFQName, boolean successful) {
myBus.syncPublisher(TOPIC).afterExternalAnnotationChanging(owner, annotationFQName, successful);
myPsiManager.dropPsiCaches();
}
private void notifyChangedExternally() {
myBus.syncPublisher(TOPIC).externalAnnotationsChangedExternally();
myPsiManager.dropPsiCaches();
}
@Override
public void annotateExternally(@NotNull final PsiModifierListOwner listOwner,
@NotNull final String annotationFQName,
@NotNull final PsiFile fromFile,
final PsiNameValuePair @Nullable [] value) throws CanceledConfigurationException {
Application application = ApplicationManager.getApplication();
application.assertIsDispatchThread();
LOG.assertTrue(!application.isWriteAccessAllowed());
final Project project = myPsiManager.getProject();
final PsiFile containingFile = listOwner.getOriginalElement().getContainingFile();
if (!(containingFile instanceof PsiJavaFile)) {
notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
return;
}
final VirtualFile containingVirtualFile = containingFile.getVirtualFile();
LOG.assertTrue(containingVirtualFile != null);
final List<OrderEntry> entries = ProjectRootManager.getInstance(project).getFileIndex().getOrderEntriesForFile(containingVirtualFile);
if (entries.isEmpty()) {
notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
return;
}
ExternalAnnotation annotation = new ExternalAnnotation(listOwner, annotationFQName, value);
for (final OrderEntry entry : entries) {
if (entry instanceof ModuleOrderEntry) continue;
VirtualFile[] roots = AnnotationOrderRootType.getFiles(entry);
roots = filterByReadOnliness(roots);
if (roots.length > 0) {
chooseRootAndAnnotateExternally(roots, annotation);
}
else {
if (application.isUnitTestMode() || application.isHeadlessEnvironment()) {
notifyAfterAnnotationChanging(listOwner, annotationFQName, false);
return;
}
DumbService.getInstance(project).setAlternativeResolveEnabled(true);
try {
if (!setupRootAndAnnotateExternally(entry, project, annotation)) {
throw new CanceledConfigurationException();
}
}
finally {
DumbService.getInstance(project).setAlternativeResolveEnabled(false);
}
}
break;
}
}
private void annotateExternally(@NotNull VirtualFile root, @NotNull ExternalAnnotation annotation) {
annotateExternally(root, Collections.singletonList(annotation));
}
/**
* Tries to add external annotations into given root if possible.
* Notifies about each addition result separately.
*/
private void annotateExternally(@NotNull VirtualFile root, @NotNull List<? extends ExternalAnnotation> annotations) {
Project project = myPsiManager.getProject();
Map<Optional<XmlFile>, List<ExternalAnnotation>> annotationsByFiles = annotations.stream()
.collect(Collectors.groupingBy(annotation -> Optional.ofNullable(getFileForAnnotations(root, annotation.getOwner(), project))));
List<VirtualFile> files = StreamEx.ofKeys(annotationsByFiles).flatMap(StreamEx::of).map(XmlFile::getVirtualFile).nonNull().toList();
ReadonlyStatusHandler.OperationStatus status = ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(files);
if (status.hasReadonlyFiles()) {
VirtualFile[] readonlyFiles = status.getReadonlyFiles();
annotationsByFiles.keySet()
.removeIf(opt -> opt.map(XmlFile::getVirtualFile).filter(f -> ArrayUtil.contains(f, readonlyFiles)).isPresent());
}
if (annotationsByFiles.isEmpty()) return;
WriteCommandAction.writeCommandAction(project).run(() -> {
try {
for (Map.Entry<Optional<XmlFile>, List<ExternalAnnotation>> entry : annotationsByFiles.entrySet()) {
XmlFile annotationsFile = entry.getKey().orElse(null);
List<ExternalAnnotation> fileAnnotations = entry.getValue();
annotateExternally(annotationsFile, fileAnnotations);
}
UndoManager.getInstance(project).undoableActionPerformed(new BasicUndoableAction() {
@Override
public void undo() {
dropAnnotationsCache();
notifyChangedExternally();
}
@Override
public void redo() {
dropAnnotationsCache();
notifyChangedExternally();
}
});
} finally {
dropAnnotationsCache();
}
});
}
private void dropAnnotationsCache() {
dropCache();
}
private void annotateExternally(@Nullable XmlFile annotationsFile, @NotNull List<ExternalAnnotation> annotations) {
XmlTag rootTag = extractRootTag(annotationsFile);
TreeMap<String, List<ExternalAnnotation>> ownerToAnnotations = StreamEx.of(annotations)
.mapToEntry(annotation -> StringUtil.escapeXmlEntities(getExternalName(annotation.getOwner())), Function.identity())
.distinct()
.grouping(() -> new TreeMap<>(Comparator.nullsFirst(Comparator.naturalOrder())));
if (rootTag == null) {
ownerToAnnotations.values().stream().flatMap(List::stream).forEach(annotation ->
notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false));
return;
}
List<ExternalAnnotation> savedAnnotations = new ArrayList<>();
XmlTag startTag = null;
for (Map.Entry<String, List<ExternalAnnotation>> entry : ownerToAnnotations.entrySet()) {
@NonNls String ownerName = entry.getKey();
List<ExternalAnnotation> annotationList = entry.getValue();
for (ExternalAnnotation annotation : annotationList) {
if (ownerName == null) {
notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
continue;
}
try {
startTag = addAnnotation(rootTag, ownerName, annotation, startTag);
savedAnnotations.add(annotation);
}
catch (IncorrectOperationException e) {
LOG.error(e);
notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
}
finally {
dropAnnotationsCache();
markForUndo(annotation.getOwner().getContainingFile());
}
}
}
commitChanges(annotationsFile);
savedAnnotations.forEach(annotation ->
notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), true));
}
@Contract("null -> null")
private static XmlTag extractRootTag(XmlFile annotationsFile) {
if (annotationsFile == null) {
return null;
}
XmlDocument document = annotationsFile.getDocument();
if (document == null) {
return null;
}
return document.getRootTag();
}
private static void markForUndo(@Nullable PsiFile containingFile) {
if (containingFile == null) {
return;
}
VirtualFile virtualFile = containingFile.getVirtualFile();
if (virtualFile != null && virtualFile.isInLocalFileSystem()) {
UndoUtil.markPsiFileForUndo(containingFile);
}
}
/**
* Adds annotation sub tag after startTag.
* If startTag is {@code null} searches for all sub tags of rootTag and starts from the first.
*
* @param rootTag root tag to insert subtag into
* @param ownerName annotations owner name
* @param annotation external annotation
* @param startTag start tag
* @return added sub tag
*/
@NotNull
private XmlTag addAnnotation(@NotNull XmlTag rootTag, @NotNull String ownerName,
@NotNull ExternalAnnotation annotation, @Nullable XmlTag startTag) {
if (startTag == null) {
startTag = PsiTreeUtil.findChildOfType(rootTag, XmlTag.class);
}
XmlTag prevItem = null;
XmlTag curItem = startTag;
while (curItem != null) {
XmlTag addedItem = addAnnotation(rootTag, ownerName, annotation, curItem, prevItem);
if (addedItem != null) {
return addedItem;
}
prevItem = curItem;
curItem = PsiTreeUtil.getNextSiblingOfType(curItem, XmlTag.class);
}
return addItemTag(rootTag, prevItem, ownerName, annotation);
}
/**
* Adds annotation sub tag into curItem or between prevItem and curItem.
* Adds into curItem if curItem contains external annotations for owner.
* Adds between curItem and prevItem if owner's external name < cur item owner external name.
* Otherwise does nothing, returns null.
*
* @param rootTag root tag to insert sub tag into
* @param ownerName annotation owner
* @param annotation external annotation
* @param curItem current item with annotations
* @param prevItem previous item with annotations
* @return added tag
*/
@Nullable
private XmlTag addAnnotation(@NotNull XmlTag rootTag, @NotNull String ownerName, @NotNull ExternalAnnotation annotation,
@NotNull XmlTag curItem, @Nullable XmlTag prevItem) {
@NonNls String curItemName = curItem.getAttributeValue("name");
if (curItemName == null) {
curItem.delete();
return null;
}
int compare = ownerName.compareTo(curItemName);
if (compare == 0) {
//already have external annotations for owner
return appendItemAnnotation(curItem, annotation);
}
if (compare < 0) {
return addItemTag(rootTag, prevItem, ownerName, annotation);
}
return null;
}
@NotNull
private XmlTag addItemTag(@NotNull XmlTag rootTag,
@Nullable XmlTag anchor,
@NotNull String ownerName,
@NotNull ExternalAnnotation annotation) {
XmlElementFactory elementFactory = XmlElementFactory.getInstance(myPsiManager.getProject());
XmlTag newItemTag = elementFactory.createTagFromText(createItemTag(ownerName, annotation));
PsiElement addedElement;
if (anchor != null) {
addedElement = rootTag.addAfter(newItemTag, anchor);
}
else {
addedElement = rootTag.addSubTag(newItemTag, true);
}
if (!(addedElement instanceof XmlTag)) {
throw new IncorrectOperationException("Failed to add annotation " + annotation + " after " + anchor);
}
return (XmlTag)addedElement;
}
/**
* Appends annotation sub tag into itemTag. It can happen only if item tag belongs to annotation owner.
*
* @param itemTag item tag with annotations
* @param annotation external annotation
*/
private XmlTag appendItemAnnotation(@NotNull XmlTag itemTag, @NotNull ExternalAnnotation annotation) {
@NonNls String annotationFQName = annotation.getAnnotationFQName();
PsiNameValuePair[] values = annotation.getValues();
XmlElementFactory elementFactory = XmlElementFactory.getInstance(myPsiManager.getProject());
XmlTag anchor = null;
for (XmlTag itemAnnotation : itemTag.getSubTags()) {
String curAnnotationName = itemAnnotation.getAttributeValue("name");
if (curAnnotationName == null) {
itemAnnotation.delete();
continue;
}
if (annotationFQName.equals(curAnnotationName)) {
// found tag for same annotation, replacing
itemAnnotation.delete();
break;
}
anchor = itemAnnotation;
}
XmlTag newAnnotationTag = elementFactory.createTagFromText(createAnnotationTag(annotationFQName, values));
PsiElement addedElement = itemTag.addAfter(newAnnotationTag, anchor);
if (!(addedElement instanceof XmlTag)) {
throw new IncorrectOperationException("Failed to add annotation " + annotation + " after " + anchor);
}
return itemTag;
}
@Nullable
private List<XmlFile> findExternalAnnotationsXmlFiles(@NotNull PsiModifierListOwner listOwner) {
List<PsiFile> psiFiles = findExternalAnnotationsFiles(listOwner);
if (psiFiles == null) {
return null;
}
List<XmlFile> xmlFiles = new ArrayList<>();
for (PsiFile psiFile : psiFiles) {
if (psiFile instanceof XmlFile) {
xmlFiles.add((XmlFile)psiFile);
}
}
return xmlFiles;
}
private boolean setupRootAndAnnotateExternally(@NotNull final OrderEntry entry,
@NotNull final Project project,
@NotNull final ExternalAnnotation annotation) {
final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor();
descriptor.setTitle(ProjectBundle.message("external.annotations.root.chooser.title", entry.getPresentableName()));
descriptor.setDescription(ProjectBundle.message("external.annotations.root.chooser.description"));
final VirtualFile newRoot = FileChooser.chooseFile(descriptor, project, null);
if (newRoot == null) {
notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
return false;
}
WriteCommandAction.writeCommandAction(project).run(() -> appendChosenAnnotationsRoot(entry, newRoot));
annotateExternally(newRoot, annotation);
return true;
}
@Nullable
private static XmlFile findXmlFileInRoot(@Nullable List<? extends XmlFile> xmlFiles, @NotNull VirtualFile root) {
if (xmlFiles != null) {
for (XmlFile xmlFile : xmlFiles) {
VirtualFile vf = xmlFile.getVirtualFile();
if (vf != null) {
if (VfsUtilCore.isAncestor(root, vf, false)) {
return xmlFile;
}
}
}
}
return null;
}
private void chooseRootAndAnnotateExternally(VirtualFile @NotNull [] roots, @NotNull ExternalAnnotation annotation) {
if (roots.length > 1) {
JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<VirtualFile>("Annotation Roots", roots) {
@Override
public void canceled() {
notifyAfterAnnotationChanging(annotation.getOwner(), annotation.getAnnotationFQName(), false);
}
@Override
public PopupStep onChosen(@NotNull final VirtualFile file, final boolean finalChoice) {
annotateExternally(file, annotation);
return FINAL_CHOICE;
}
@NotNull
@Override
public String getTextFor(@NotNull final VirtualFile value) {
return value.getPresentableUrl();
}
@Override
public Icon getIconFor(final VirtualFile aValue) {
return AllIcons.Modules.Annotation;
}
}).showInBestPositionFor(DataManager.getInstance().getDataContext());
}
else {
annotateExternally(roots[0], annotation);
}
}
private static VirtualFile @NotNull [] filterByReadOnliness(VirtualFile @NotNull [] files) {
List<VirtualFile> result = ContainerUtil.filter(files, VirtualFile::isInLocalFileSystem);
return VfsUtilCore.toVirtualFileArray(result);
}
@Override
public boolean deannotate(@NotNull final PsiModifierListOwner listOwner, @NotNull final String annotationFQN) {
ApplicationManager.getApplication().assertIsDispatchThread();
return processExistingExternalAnnotations(listOwner, annotationFQN, annotationTag -> {
PsiElement parent = annotationTag.getParent();
annotationTag.delete();
if (parent instanceof XmlTag) {
if (((XmlTag)parent).getSubTags().length == 0) {
parent.delete();
}
}
return true;
});
}
@Override
public void elementRenamedOrMoved(@NotNull PsiModifierListOwner element, @NotNull String oldExternalName) {
ApplicationManager.getApplication().assertIsDispatchThread();
try {
final List<XmlFile> files = findExternalAnnotationsXmlFiles(element);
if (files == null) {
return;
}
for (final XmlFile file : files) {
if (!file.isValid()) {
continue;
}
final XmlDocument document = file.getDocument();
if (document == null) {
continue;
}
final XmlTag rootTag = document.getRootTag();
if (rootTag == null) {
continue;
}
for (XmlTag tag : rootTag.getSubTags()) {
String nameValue = tag.getAttributeValue("name");
String className = nameValue == null ? null : StringUtil.unescapeXmlEntities(nameValue);
if (Comparing.strEqual(className, oldExternalName)) {
WriteCommandAction
.runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
try {
String name = getExternalName(element);
tag.setAttribute("name", name == null ? null : StringUtil.escapeXmlEntities(name));
commitChanges(file);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}, file);
}
}
}
}
finally {
dropAnnotationsCache();
}
}
@Override
public boolean editExternalAnnotation(@NotNull PsiModifierListOwner listOwner,
@NotNull final String annotationFQN,
final PsiNameValuePair @Nullable [] value) {
ApplicationManager.getApplication().assertIsDispatchThread();
return processExistingExternalAnnotations(listOwner, annotationFQN, annotationTag -> {
annotationTag.replace(XmlElementFactory.getInstance(myPsiManager.getProject()).createTagFromText(
createAnnotationTag(annotationFQN, value)));
return true;
});
}
private boolean processExistingExternalAnnotations(@NotNull final PsiModifierListOwner listOwner,
@NotNull final String annotationFQN,
@NotNull final Processor<? super XmlTag> annotationTagProcessor) {
try {
final List<XmlFile> files = findExternalAnnotationsXmlFiles(listOwner);
if (files == null) {
notifyAfterAnnotationChanging(listOwner, annotationFQN, false);
return false;
}
boolean processedAnything = false;
for (final XmlFile file : files) {
if (!file.isValid()) {
continue;
}
final XmlDocument document = file.getDocument();
if (document == null) {
continue;
}
final XmlTag rootTag = document.getRootTag();
if (rootTag == null) {
continue;
}
final String externalName = getExternalName(listOwner);
final List<XmlTag> tagsToProcess = new ArrayList<>();
for (XmlTag tag : rootTag.getSubTags()) {
String nameValue = tag.getAttributeValue("name");
String className = nameValue == null ? null : StringUtil.unescapeXmlEntities(nameValue);
if (!Comparing.strEqual(className, externalName)) {
continue;
}
for (XmlTag annotationTag : tag.getSubTags()) {
if (!Comparing.strEqual(annotationTag.getAttributeValue("name"), annotationFQN)) {
continue;
}
tagsToProcess.add(annotationTag);
processedAnything = true;
}
}
if (tagsToProcess.isEmpty()) {
continue;
}
if (ReadonlyStatusHandler.getInstance(myPsiManager.getProject())
.ensureFilesWritable(Collections.singletonList(file.getVirtualFile())).hasReadonlyFiles()) {
continue;
}
WriteCommandAction.runWriteCommandAction(myPsiManager.getProject(), ExternalAnnotationsManagerImpl.class.getName(), null, () -> {
PsiDocumentManager.getInstance(myPsiManager.getProject()).commitAllDocuments();
try {
for (XmlTag annotationTag : tagsToProcess) {
annotationTagProcessor.process(annotationTag);
}
commitChanges(file);
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
});
}
notifyAfterAnnotationChanging(listOwner, annotationFQN, processedAnything);
return processedAnything;
}
finally {
dropAnnotationsCache();
}
}
@Override
@NotNull
public AnnotationPlace chooseAnnotationsPlaceNoUi(@NotNull final PsiElement element) {
return chooseAnnotationsPlace(element, () -> AnnotationPlace.NEED_ASK_USER);
}
@Override
@NotNull
public AnnotationPlace chooseAnnotationsPlace(@NotNull final PsiElement element) {
ApplicationManager.getApplication().assertIsDispatchThread();
return chooseAnnotationsPlace(element, () -> confirmNewExternalAnnotationRoot(element));
}
@NotNull
private AnnotationPlace chooseAnnotationsPlace(@NotNull final PsiElement element,
@NotNull Supplier<AnnotationPlace> confirmNewExternalAnnotationRoot) {
if (!element.isPhysical() && !(element.getOriginalElement() instanceof PsiCompiledElement)) {
return AnnotationPlace.IN_CODE; //element just created
}
if (!element.getManager().isInProject(element)) return AnnotationPlace.EXTERNAL;
final Project project = myPsiManager.getProject();
//choose external place iff USE_EXTERNAL_ANNOTATIONS option is on,
//otherwise external annotations should be read-only
final PsiFile containingFile = element.getContainingFile();
if (JavaCodeStyleSettings.getInstance(containingFile).USE_EXTERNAL_ANNOTATIONS) {
final VirtualFile virtualFile = containingFile.getVirtualFile();
LOG.assertTrue(virtualFile != null);
final List<OrderEntry> entries = ProjectRootManager.getInstance(project).getFileIndex().getOrderEntriesForFile(virtualFile);
if (!entries.isEmpty()) {
for (OrderEntry entry : entries) {
if (!(entry instanceof ModuleOrderEntry)) {
if (AnnotationOrderRootType.getUrls(entry).length > 0) {
return AnnotationPlace.EXTERNAL;
}
break;
}
}
}
return confirmNewExternalAnnotationRoot.get();
}
return AnnotationPlace.IN_CODE;
}
private static @NotNull AnnotationPlace confirmNewExternalAnnotationRoot(@NotNull PsiElement element) {
PsiFile containingFile = element.getContainingFile();
Project project = containingFile.getProject();
final MyExternalPromptDialog dialog = ApplicationManager.getApplication().isUnitTestMode() ||
ApplicationManager.getApplication().isHeadlessEnvironment()
? null
: new MyExternalPromptDialog(project);
if (dialog != null && dialog.isToBeShown()) {
final PsiElement highlightElement = element instanceof PsiNameIdentifierOwner
? ((PsiNameIdentifierOwner)element).getNameIdentifier()
: element.getNavigationElement();
LOG.assertTrue(highlightElement != null);
final Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
final List<RangeHighlighter> highlighters = new ArrayList<>();
final boolean highlight =
editor != null && editor.getDocument() == PsiDocumentManager.getInstance(project).getDocument(containingFile);
try {
if (highlight) { //do not highlight for batch inspections
final EditorColorsManager colorsManager = EditorColorsManager.getInstance();
final TextAttributes attributes = colorsManager.getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES);
final TextRange textRange = highlightElement.getTextRange();
HighlightManager.getInstance(project).addRangeHighlight(editor,
textRange.getStartOffset(), textRange.getEndOffset(),
attributes, true, highlighters);
final LogicalPosition logicalPosition = editor.offsetToLogicalPosition(textRange.getStartOffset());
editor.getScrollingModel().scrollTo(logicalPosition, ScrollType.CENTER);
}
dialog.show();
if (dialog.getExitCode() == 2) {
return AnnotationPlace.EXTERNAL;
}
else if (dialog.getExitCode() == 1) {
return AnnotationPlace.NOWHERE;
}
}
finally {
if (highlight) {
HighlightManager.getInstance(project).removeSegmentHighlighter(editor, highlighters.get(0));
}
}
}
else if (dialog != null) {
dialog.close(DialogWrapper.OK_EXIT_CODE);
}
return AnnotationPlace.IN_CODE;
}
private void appendChosenAnnotationsRoot(@NotNull final OrderEntry entry, @NotNull final VirtualFile vFile) {
if (entry instanceof LibraryOrderEntry) {
Library library = ((LibraryOrderEntry)entry).getLibrary();
LOG.assertTrue(library != null);
final Library.ModifiableModel model = library.getModifiableModel();
model.addRoot(vFile, AnnotationOrderRootType.getInstance());
model.commit();
}
else if (entry instanceof ModuleSourceOrderEntry) {
final ModifiableRootModel model = ModuleRootManager.getInstance(entry.getOwnerModule()).getModifiableModel();
final JavaModuleExternalPaths extension = model.getModuleExtension(JavaModuleExternalPaths.class);
extension.setExternalAnnotationUrls(ArrayUtil.mergeArrays(extension.getExternalAnnotationsUrls(), vFile.getUrl()));
model.commit();
}
else if (entry instanceof JdkOrderEntry) {
final SdkModificator sdkModificator = ((JdkOrderEntry)entry).getJdk().getSdkModificator();
sdkModificator.addRoot(vFile, AnnotationOrderRootType.getInstance());
sdkModificator.commitChanges();
}
dropAnnotationsCache();
}
private static void sortItems(@NotNull XmlFile xmlFile) {
XmlDocument document = xmlFile.getDocument();
if (document == null) {
return;
}
XmlTag rootTag = document.getRootTag();
if (rootTag == null) {
return;
}
List<XmlTag> itemTags = new ArrayList<>();
for (XmlTag item : rootTag.getSubTags()) {
if (item.getAttributeValue("name") != null) {
itemTags.add(item);
}
else {
item.delete();
}
}
List<XmlTag> sorted = new ArrayList<>(itemTags);
sorted.sort((item1, item2) -> {
String externalName1 = item1.getAttributeValue("name");
String externalName2 = item2.getAttributeValue("name");
assert externalName1 != null && externalName2 != null; // null names were not added
return externalName1.compareTo(externalName2);
});
if (!sorted.equals(itemTags)) {
for (XmlTag item : sorted) {
rootTag.addAfter(item, null);
item.delete();
}
}
}
private void commitChanges(XmlFile xmlFile) {
sortItems(xmlFile);
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myPsiManager.getProject());
Document doc = documentManager.getDocument(xmlFile);
assert doc != null;
documentManager.doPostponedOperationsAndUnblockDocument(doc);
FileDocumentManager.getInstance().saveDocument(doc);
}
@NonNls
@NotNull
private static String createItemTag(@NotNull String ownerName, @NotNull ExternalAnnotation annotation) {
String annotationTag = createAnnotationTag(annotation.getAnnotationFQName(), annotation.getValues());
return String.format("<item name='%s'>%s</item>", ownerName, annotationTag);
}
@NonNls
@NotNull
@VisibleForTesting
public static String createAnnotationTag(@NotNull String annotationFQName, PsiNameValuePair @Nullable [] values) {
@NonNls String text;
if (values != null && values.length != 0) {
text = " <annotation name='" + annotationFQName + "'>\n";
text += StringUtil.join(values, pair -> "<val" +
(pair.getName() != null ? " name=\"" + pair.getName() + "\"" : "") +
" val=\"" + StringUtil.escapeXmlEntities(pair.getValue().getText()) + "\"/>", " \n");
text += " </annotation>";
}
else {
text = " <annotation name='" + annotationFQName + "'/>\n";
}
return text;
}
@Nullable
private XmlFile createAnnotationsXml(@NotNull VirtualFile root, @NonNls @NotNull String packageName) {
return createAnnotationsXml(root, packageName, myPsiManager);
}
@Nullable
@VisibleForTesting
public static XmlFile createAnnotationsXml(@NotNull VirtualFile root, @NonNls @NotNull String packageName, PsiManager manager) {
final String[] dirs = packageName.split("\\.");
for (String dir : dirs) {
if (dir.isEmpty()) break;
VirtualFile subdir = root.findChild(dir);
if (subdir == null) {
try {
subdir = root.createChildDirectory(null, dir);
}
catch (IOException e) {
LOG.error(e);
return null;
}
}
root = subdir;
}
final PsiDirectory directory = manager.findDirectory(root);
if (directory == null) return null;
final PsiFile psiFile = directory.findFile(ANNOTATIONS_XML);
if (psiFile instanceof XmlFile) {
return (XmlFile)psiFile;
}
try {
final PsiFileFactory factory = PsiFileFactory.getInstance(manager.getProject());
return (XmlFile)directory.add(factory.createFileFromText(ANNOTATIONS_XML, XmlFileType.INSTANCE, "<root></root>"));
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
return null;
}
@Nullable
private XmlFile getFileForAnnotations(@NotNull VirtualFile root, @NotNull PsiModifierListOwner owner, Project project) {
final PsiFile containingFile = owner.getOriginalElement().getContainingFile();
String packageName = owner instanceof PsiPackage
? ((PsiPackage)owner).getQualifiedName()
: containingFile instanceof PsiJavaFile
? ((PsiJavaFile)containingFile).getPackageName() : null;
if (packageName == null) {
return null;
}
List<XmlFile> annotationsFiles = findExternalAnnotationsXmlFiles(owner);
XmlFile fileInRoot = findXmlFileInRoot(annotationsFiles, root);
if (fileInRoot != null) {
return fileInRoot;
}
return WriteCommandAction.writeCommandAction(project).compute(() -> {
XmlFile newAnnotationsFile = createAnnotationsXml(root, packageName);
if (newAnnotationsFile == null) {
return null;
}
Object key = owner instanceof PsiPackage ? owner : containingFile.getVirtualFile();
if (key != null) {
registerExternalAnnotations(key, newAnnotationsFile);
}
return newAnnotationsFile;
});
}
@Override
public boolean hasAnnotationRootsForFile(@NotNull VirtualFile file) {
if (hasAnyAnnotationsRoots()) {
ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myPsiManager.getProject()).getFileIndex();
for (OrderEntry entry : fileIndex.getOrderEntriesForFile(file)) {
if (!(entry instanceof ModuleOrderEntry) && AnnotationOrderRootType.getUrls(entry).length > 0) {
return true;
}
}
}
return false;
}
@Override
protected void duplicateError(@NotNull PsiFile file, @NotNull String externalName, @NotNull String text) {
String message = text + "; for signature: '" + externalName + "' in the file " + file.getName();
LOG.error(message, new Throwable(), AttachmentFactory.createAttachment(file.getVirtualFile()));
}
public static boolean areExternalAnnotationsApplicable(@NotNull PsiModifierListOwner owner) {
if (!owner.isPhysical()) {
PsiElement originalElement = owner.getOriginalElement();
if (!(originalElement instanceof PsiCompiledElement)) {
return false;
}
}
if (owner instanceof PsiLocalVariable) return false;
if (owner instanceof PsiParameter) {
PsiElement parent = owner.getParent();
if (parent == null || !(parent.getParent() instanceof PsiMethod)) return false;
}
if (!owner.getManager().isInProject(owner)) return true;
return JavaCodeStyleSettings.getInstance(owner.getContainingFile()).USE_EXTERNAL_ANNOTATIONS;
}
private static class MyExternalPromptDialog extends OptionsMessageDialog {
private final Project myProject;
MyExternalPromptDialog(final Project project) {
super(project, getMessage(), ProjectBundle.message("external.annotation.prompt"), Messages.getQuestionIcon());
myProject = project;
init();
}
@Override
protected String getOkActionName() {
return getAddInCode();
}
@Override
@NotNull
protected String getCancelActionName() {
return CommonBundle.getCancelButtonText();
}
@Override
protected Action @NotNull [] createActions() {
final Action okAction = getOKAction();
assignMnemonic(getAddInCode(), okAction);
final String externalName = ProjectBundle.message("external.annotations.external.option");
return new Action[]{okAction, new AbstractAction(externalName) {
{
assignMnemonic(externalName, this);
}
@Override
public void actionPerformed(final ActionEvent e) {
if (canBeHidden()) {
setToBeShown(toBeShown(), true);
}
close(2);
}
}, getCancelAction()};
}
@Override
protected boolean isToBeShown() {
return CodeStyleSettingsManager.getSettings(myProject).getCustomSettings(JavaCodeStyleSettings.class).USE_EXTERNAL_ANNOTATIONS;
}
@Override
protected void setToBeShown(boolean value, boolean onOk) {
CodeStyleSettingsManager.getSettings(myProject).getCustomSettings(JavaCodeStyleSettings.class).USE_EXTERNAL_ANNOTATIONS = value;
}
@NotNull
@Override
protected JComponent createNorthPanel() {
final JPanel northPanel = (JPanel)super.createNorthPanel();
northPanel.add(new JLabel(getMessage()), BorderLayout.CENTER);
return northPanel;
}
@Override
protected boolean shouldSaveOptionsOnCancel() {
return true;
}
private static String getAddInCode() {
return ProjectBundle.message("external.annotations.in.code.option");
}
private static String getMessage() {
return ProjectBundle.message("external.annotations.suggestion.message");
}
}
private class MyDocumentListener implements DocumentListener {
final FileDocumentManager myFileDocumentManager = FileDocumentManager.getInstance();
@Override
public void documentChanged(@NotNull DocumentEvent event) {
final VirtualFile file = myFileDocumentManager.getFile(event.getDocument());
if (file != null && ANNOTATIONS_XML.equals(file.getName()) && isUnderAnnotationRoot(file)) {
dropAnnotationsCache();
}
}
}
}