partially support model branches in rename refactoring (only for modifications inside files so far)

GitOrigin-RevId: ec98bd14a8eaa9b6127ffbc637704f2457b36a00
This commit is contained in:
Peter Gromov
2020-07-07 06:51:14 +00:00
committed by intellij-monorepo-bot
parent 91c13f98ec
commit c2e584b074
10 changed files with 241 additions and 40 deletions
@@ -187,7 +187,7 @@ public class MoveMembersProcessor extends BaseRefactoringProcessor {
PsiClass targetCopy = branch.obtainPsiCopy(targetClass);
Set<PsiMember> membersToMove = new LinkedHashSet<>(ContainerUtil.map(myMembersToMove, branch::obtainPsiCopy));
List<MoveMembersUsageInfo> usages = ContainerUtil.map(originalUsages, u -> ((MoveMembersUsageInfo)u).branched(branch));
List<MoveMembersUsageInfo> usages = ContainerUtil.map(originalUsages, u -> (MoveMembersUsageInfo)((MoveMembersUsageInfo)u).branched(branch));
Map<PsiMember, SmartPsiElementPointer<PsiMember>> movedMembers = performMove(targetCopy, membersToMove, usages);
@@ -421,17 +421,5 @@ public class MoveMembersProcessor extends BaseRefactoringProcessor {
reference = element;
}
@NotNull
MoveMembersProcessor.MoveMembersUsageInfo branched(ModelBranch branch) {
PsiElement element = branch.obtainPsiCopy(reference);
PsiElement highlightElement = getElement();
PsiReference psiReference = getReference();
return new MoveMembersUsageInfo(
branch.findPsiCopy(member),
element,
qualifierClass == null ? null : branch.findPsiCopy(qualifierClass),
highlightElement == null ? null : branch.findPsiCopy(highlightElement),
psiReference == null ? null : branch.findReferenceCopy(psiReference));
}
}
}
@@ -9,6 +9,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pass;
import com.intellij.psi.*;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.impl.light.LightRecordMethod;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.search.SearchScope;
@@ -112,9 +113,9 @@ public class RenameJavaMethodProcessor extends RenameJavaMemberProcessor {
qualifyOuterMemberReferences(outerHides);
qualifyStaticImportReferences(staticImportHides);
if (!method.isConstructor() && method.isPhysical() && method.findDeepestSuperMethods().length == 0) {
if (!method.isConstructor() && !(method instanceof LightElement) && method.findDeepestSuperMethods().length == 0) {
PsiAnnotation annotation = AnnotationUtil.findAnnotation(method, true, CommonClassNames.JAVA_LANG_OVERRIDE);
if (annotation != null && annotation.isPhysical()) {
if (annotation != null) {
annotation.delete();
}
}
@@ -19,6 +19,14 @@ public class JavaBranchRefactoringTest {
public static Test suite() {
TestSuite suite = new TestSuite();
List<Class<? extends TestCase>> testsToWrap = Arrays.asList(
//RenameClassTest.class,
//RenameCollisionsTest.class,
//RenameDirectoryTest.class,
RenameFieldMultiTest.class,
RenameFieldTest.class,
RenameLocalTest.class,
RenameMethodMultiTest.class,
MoveMembersTest.class,
ConvertToInstanceMethodTest.class,
ConvertToInstance8MethodTest.class
@@ -15,17 +15,27 @@
*/
package com.intellij.refactoring.util;
import com.intellij.model.ModelBranch;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.BitUtil;
import com.intellij.util.Function;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MoveRenameUsageInfo extends UsageInfo{
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Objects;
public class MoveRenameUsageInfo extends UsageInfo implements Cloneable {
private static final Logger LOG = Logger.getInstance(MoveRenameUsageInfo.class);
private SmartPsiElementPointer myReferencedElementPointer = null;
private PsiElement myReferencedElement;
@@ -126,4 +136,64 @@ public class MoveRenameUsageInfo extends UsageInfo{
}
return reference;
}
@NotNull
@ApiStatus.Experimental
public MoveRenameUsageInfo branched(@NotNull ModelBranch branch) {
try {
MoveRenameUsageInfo copy = (MoveRenameUsageInfo)clone();
Class<?> aClass = copy.getClass();
while (aClass != null) {
for (Field field : aClass.getDeclaredFields()) {
if (BitUtil.isSet(field.getModifiers(), Modifier.STATIC)) continue;
field.setAccessible(true);
Object valueCopy = obtainBranchCopy(branch, field.get(copy));
if (valueCopy != null) {
field.set(copy, valueCopy);
}
}
aClass = aClass.getSuperclass();
}
return copy;
}
catch (CloneNotSupportedException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private Object obtainBranchCopy(ModelBranch branch, Object fieldValue) {
if (fieldValue instanceof PsiElement) {
return branch.obtainPsiCopy((PsiElement)fieldValue);
}
if (fieldValue instanceof PsiReference) {
return branch.obtainReferenceCopy((PsiReference)fieldValue);
}
if (fieldValue instanceof SmartPsiFileRange) {
return SmartPointerManager.getInstance(getProject())
.createSmartPsiFileRangePointer(
branch.obtainPsiCopy(Objects.requireNonNull(((SmartPsiFileRange)fieldValue).getContainingFile())),
TextRange.create(Objects.requireNonNull(((SmartPsiFileRange)fieldValue).getRange())));
}
if (fieldValue instanceof SmartPsiElementPointer) {
return SmartPointerManager.createPointer(
branch.obtainPsiCopy(Objects.requireNonNull(((SmartPsiElementPointer<?>)fieldValue).getElement())));
}
if (fieldValue instanceof RangeMarker) {
return obtainMarkerCopy(branch, (RangeMarker)fieldValue);
}
return null;
}
private static RangeMarker obtainMarkerCopy(@NotNull ModelBranch branch, RangeMarker original) {
Document document = original.getDocument();
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
VirtualFile fileCopy = branch.findFileCopy(Objects.requireNonNull(file));
Document docCopy = FileDocumentManager.getInstance().getDocument(Objects.requireNonNull(fileCopy));
assert docCopy != null;
RangeMarker marker = docCopy.createRangeMarker(original.getStartOffset(), original.getEndOffset());
marker.setGreedyToLeft(original.isGreedyToLeft());
marker.setGreedyToRight(original.isGreedyToRight());
return marker;
}
}
@@ -60,6 +60,13 @@ public interface ModelBranch {
*/
<T extends PsiElement> @NotNull T obtainPsiCopy(@NotNull T original);
/**
* Finds or creates a non-physical copy of the given PSI reference in this branch.
* This may only be called for references which occur in {@link PsiElement#getReferences()},
* and the corresponding document should be committed.
*/
<T extends PsiSymbolReference> @NotNull T obtainReferenceCopy(@NotNull T original);
// ----------------- find originals by branched model
@@ -13,9 +13,11 @@ import com.intellij.openapi.util.SimpleModificationTracker;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.file.PsiFileImplUtil;
import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
@@ -23,6 +25,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
@@ -129,15 +132,31 @@ public final class ModelBranchImpl implements ModelBranch {
return fileCopy == null ? null : PsiTreeUtil.findSameElementInCopy(original, fileCopy);
}
@Override
@NotNull
public <T extends PsiSymbolReference> T obtainReferenceCopy(@NotNull T original) {
PsiElement psiCopy = obtainPsiCopy(original.getElement());
TextRange range = original.getRangeInElement();
PsiReference[] refs = psiCopy.getReferences();
T found = findSimilarReference(original, range, refs);
if (found == null) throw new AssertionError("Cannot find " + original +
" of " + original.getClass() +
" at " + range +
" in the copy, where references are " + Arrays.toString(refs));
return found;
}
@Override
@Nullable
public <T extends PsiSymbolReference> T findReferenceCopy(@NotNull T original) {
PsiElement psiCopy = findPsiCopy(original.getElement());
if (psiCopy == null) return null;
return psiCopy == null ? null : findSimilarReference(original, original.getRangeInElement(), psiCopy.getReferences());
}
TextRange range = original.getRangeInElement();
@Nullable
private static <T> T findSimilarReference(@NotNull T original, TextRange range, PsiReference[] references) {
//noinspection unchecked
return (T)ContainerUtil.find(psiCopy.getReferences(), r -> r.getClass() == original.getClass() && range.equals(r.getRangeInElement()));
return (T)ContainerUtil.find(references, r -> r.getClass() == original.getClass() && range.equals(r.getRangeInElement()));
}
@Override
@@ -171,6 +190,20 @@ public final class ModelBranchImpl implements ModelBranch {
assert !myMerged;
myMerged = true;
for (Map.Entry<VirtualFile, VirtualFile> entry : myVFileCopies.entrySet()) {
VirtualFile original = entry.getKey();
String copyName = entry.getValue().getName();
if (!original.getName().equals(copyName)) {
PsiFileImplUtil.saveDocumentIfFileWillBecomeBinary(original, copyName);
try {
original.rename(this, copyName);
}
catch (IOException e) {
throw new IncorrectOperationException(e);
}
}
}
for (Document document : myDocumentChanges.keySet()) {
VirtualFile file = Objects.requireNonNull(FileDocumentManager.getInstance().getFile(document));
DocumentImpl original = (DocumentImpl) FileDocumentManager.getInstance().getDocument(findOriginalFile(file));
@@ -11,6 +11,7 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.PsiManagerImpl;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
@@ -19,20 +20,26 @@ public final class PsiFileImplUtil {
private PsiFileImplUtil() {
}
// before the file becomes non-openable in the editor, save it to prevent data loss
@ApiStatus.Internal
public static void saveDocumentIfFileWillBecomeBinary(VirtualFile vFile, @NotNull String newName) {
final FileType newFileType = FileTypeRegistry.getInstance().getFileTypeByFileName(newName);
if (UnknownFileType.INSTANCE.equals(newFileType) || newFileType.isBinary()) {
final FileDocumentManager fdm = FileDocumentManager.getInstance();
final Document doc = fdm.getCachedDocument(vFile);
if (doc != null) {
fdm.saveDocumentAsIs(doc);
}
}
}
public static PsiFile setName(@NotNull PsiFile file, @NotNull String newName) throws IncorrectOperationException {
VirtualFile vFile = file.getViewProvider().getVirtualFile();
PsiManagerImpl manager = (PsiManagerImpl)file.getManager();
try{
final FileType newFileType = FileTypeRegistry.getInstance().getFileTypeByFileName(newName);
if (UnknownFileType.INSTANCE.equals(newFileType) || newFileType.isBinary()) {
// before the file becomes unknown or a binary (thus, not openable in the editor), save it to prevent data loss
final FileDocumentManager fdm = FileDocumentManager.getInstance();
final Document doc = fdm.getCachedDocument(vFile);
if (doc != null) {
fdm.saveDocumentAsIs(doc);
}
}
saveDocumentIfFileWillBecomeBinary(vFile, newName);
vFile.rename(manager, newName);
}
@@ -8,6 +8,7 @@ import com.intellij.internal.statistic.eventLog.VarargEventId;
import com.intellij.internal.statistic.utils.PluginInfoDetectorKt;
import com.intellij.lang.LangBundle;
import com.intellij.lang.findUsages.DescriptiveNameUtil;
import com.intellij.model.ModelBranch;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
@@ -45,6 +46,7 @@ import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewDescriptor;
import com.intellij.usageView.UsageViewUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
@@ -369,24 +371,73 @@ public class RenameProcessor extends BaseRefactoringProcessor {
@Override
public void performRefactoring(UsageInfo @NotNull [] usages) {
doPerformRefactoring(usages, null);
}
@Override
protected boolean canPerformRefactoringInBranch() {
return true;
}
@Override
protected void performRefactoringInBranch(UsageInfo @NotNull [] usages, @NotNull ModelBranch branch) {
Class<?> syncDefinition = ReflectionUtil.getMethodDeclaringClass(getClass(), "performRefactoring", UsageInfo[].class);
Class<?> asyncDefinition = ReflectionUtil.getMethodDeclaringClass(getClass(), "performRefactoringInBranch", UsageInfo[].class, ModelBranch.class);
if (asyncDefinition != RenameProcessor.class && syncDefinition != asyncDefinition) {
throw new UnsupportedOperationException("performRefactoringInBranch should be implemented in " + syncDefinition);
}
doPerformRefactoring(usages, branch);
}
private void doPerformRefactoring(UsageInfo @NotNull [] usages, @Nullable ModelBranch branch) {
logScopeStatistics(RenameUsagesCollector.executed);
List<Runnable> postRenameCallbacks = new ArrayList<>();
final MultiMap<PsiElement, UsageInfo> classified = classifyUsages(myAllRenames.keySet(), usages);
Map<PsiElement, PsiElement> elementsToChange = new IdentityHashMap<>();
for (PsiElement element : myAllRenames.keySet()) {
elementsToChange.put(element, branch != null ? branch.obtainPsiCopy(element) : element);
}
MultiMap<RefactoringElementListener, SmartPsiElementPointer<PsiElement>> renameEvents = MultiMap.createLinked();
MultiMap<PsiElement, UsageInfo> classified = classifyUsages(
elementsToChange.values(),
branch == null ? Arrays.asList(usages)
: ContainerUtil.mapNotNull(usages, info -> shouldSkip(info) ? null : ((MoveRenameUsageInfo)info).branched(branch)));
for (final PsiElement element : myAllRenames.keySet()) {
PsiElement toChange = elementsToChange.get(element);
if (!element.isValid()) {
LOG.error(new PsiInvalidElementAccessException(element));
continue;
}
if (!toChange.isValid()) {
LOG.error(new PsiInvalidElementAccessException(toChange));
continue;
}
String newName = myAllRenames.get(element);
final RefactoringElementListener elementListener = getTransaction().getElementListener(element);
final RenamePsiElementProcessor renamePsiElementProcessor = RenamePsiElementProcessor.forElement(element);
Runnable postRenameCallback = renamePsiElementProcessor.getPostRenameCallback(element, newName, elementListener);
final Collection<UsageInfo> infos = classified.get(element);
Collection<UsageInfo> infos = classified.get(toChange);
try {
RenameUtil.doRename(element, newName, infos.toArray(UsageInfo.EMPTY_ARRAY), myProject, elementListener);
nowOrAfterMerge(branch, () -> RenameUtil.registerUndoableRename(element, elementListener));
renamePsiElementProcessor.renameElement(toChange, newName, infos.toArray(UsageInfo.EMPTY_ARRAY), new RefactoringElementListener() {
@Override
public void elementMoved(@NotNull PsiElement newElement) {
throw new UnsupportedOperationException();
}
@Override
public void elementRenamed(@NotNull PsiElement newElement) {
if (!newElement.isValid()) return;
if (branch != null) {
assert branch == ModelBranch.getPsiBranch(newElement);
}
renameEvents.putValue(elementListener, SmartPointerManager.createPointer(newElement));
}
});
}
catch (final IncorrectOperationException e) {
RenameUtil.showErrorMessage(e, element, myProject);
@@ -397,6 +448,34 @@ public class RenameProcessor extends BaseRefactoringProcessor {
}
}
nowOrAfterMerge(branch, () -> afterRename(postRenameCallbacks, usages, renameEvents, branch));
}
private static void nowOrAfterMerge(@Nullable ModelBranch branch, Runnable runnable) {
if (branch == null) {
runnable.run();
} else {
branch.runAfterMerge(runnable);
}
}
private void afterRename(List<Runnable> postRenameCallbacks,
UsageInfo[] usages,
MultiMap<RefactoringElementListener, SmartPsiElementPointer<PsiElement>> renameEvents,
@Nullable ModelBranch branch) {
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
for (Map.Entry<RefactoringElementListener, Collection<SmartPsiElementPointer<PsiElement>>> entry : renameEvents.entrySet()) {
for (SmartPsiElementPointer<PsiElement> pointer : entry.getValue()) {
PsiElement element = pointer.getElement();
if (branch != null && element != null) {
element = branch.findOriginalPsi(element);
}
if (element != null) {
entry.getKey().elementRenamed(element);
}
}
}
for (Runnable runnable : postRenameCallbacks) {
runnable.run();
}
@@ -439,12 +518,12 @@ public class RenameProcessor extends BaseRefactoringProcessor {
return myCommandName;
}
public static MultiMap<PsiElement, UsageInfo> classifyUsages(Collection<? extends PsiElement> elements, UsageInfo[] usages) {
public static MultiMap<PsiElement, UsageInfo> classifyUsages(Collection<? extends PsiElement> elements, Collection<UsageInfo> usages) {
final MultiMap<PsiElement, UsageInfo> result = new MultiMap<>();
for (UsageInfo usage : usages) {
LOG.assertTrue(usage instanceof MoveRenameUsageInfo);
if (usage.getReference() instanceof LightElement) {
continue; //filter out implicit references (e.g. from derived class to super class' default constructor)
if (shouldSkip(usage)) {
continue;
}
MoveRenameUsageInfo usageInfo = (MoveRenameUsageInfo)usage;
if (usage instanceof RelatedUsageInfo) {
@@ -468,6 +547,11 @@ public class RenameProcessor extends BaseRefactoringProcessor {
return result;
}
//filter out implicit references (e.g. from derived class to super class' default constructor)
private static boolean shouldSkip(UsageInfo usage) {
return usage.getReference() instanceof LightElement;
}
public Collection<String> getNewNames() {
return myAllRenames.values();
}
@@ -219,9 +219,7 @@ public final class RenameUtil {
}
}
public static void doRename(final PsiElement element, String newName, UsageInfo[] usages, final Project project,
@Nullable final RefactoringElementListener listener) throws IncorrectOperationException{
final RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(element);
static void registerUndoableRename(PsiElement element, @Nullable RefactoringElementListener listener) {
final String fqn = element instanceof PsiFile ? ((PsiFile)element).getVirtualFile().getPath() : CopyReferenceAction.elementToFqn(element);
if (fqn != null) {
UndoableAction action = new BasicUndoableAction() {
@@ -236,8 +234,14 @@ public final class RenameUtil {
public void redo() {
}
};
UndoManager.getInstance(project).undoableActionPerformed(action);
UndoManager.getInstance(element.getProject()).undoableActionPerformed(action);
}
}
public static void doRename(final PsiElement element, String newName, UsageInfo[] usages, final Project project,
@Nullable final RefactoringElementListener listener) throws IncorrectOperationException{
registerUndoableRename(element, listener);
RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(element);
processor.renameElement(element, newName, usages, listener);
}
@@ -263,8 +263,7 @@ public class VariableInplaceRenamer extends InplaceRefactoring {
if (!CommonRefactoringUtil.checkReadOnlyStatus(myProject, PsiUtilCore.toPsiElementArray(renamer.getElements()))) return;
final ThrowableRunnable<RuntimeException> performAutomaticRename = () -> {
CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject);
final UsageInfo[] usageInfos = usages.toArray(UsageInfo.EMPTY_ARRAY);
final MultiMap<PsiElement, UsageInfo> classified = RenameProcessor.classifyUsages(renamer.getElements(), usageInfos);
MultiMap<PsiElement, UsageInfo> classified = RenameProcessor.classifyUsages(renamer.getElements(), usages);
for (final PsiNamedElement element : renamer.getElements()) {
final String newElementName = renamer.getNewName(element);
if (newElementName != null) {