[mod-commands] IDEA-333301 Support suggested refactoring in ModCommands

GitOrigin-RevId: dbd1401630a68e3e6ca932c5671f428f81e8324d
This commit is contained in:
Tagir Valeev
2023-09-26 09:19:50 +00:00
committed by intellij-monorepo-bot
parent d3f9b07ae2
commit 320c3f2442
16 changed files with 184 additions and 21 deletions
@@ -48,6 +48,20 @@ class JavaSuggestedRefactoringAvailability(refactoringSupport: SuggestedRefactor
}
}
override fun isAvailable(state: SuggestedRefactoringState): Boolean {
val method = state.declaration as? PsiMethod
val declarationCopy = state.restoredDeclarationCopy()
if (method != null && method.canHaveOverrides(state.oldSignature)) {
val restoredMethod = declarationCopy as PsiMethod
if (OverridingMethodsSearch.search(restoredMethod, false).findFirst() != null) return true
}
if (declarationCopy != null) {
if (ReferencesSearch.search(declarationCopy, LocalSearchScope(declarationCopy.containingFile)).findFirst() != null) return true
if (ReferencesSearch.search(declarationCopy, declarationCopy.useScope).findFirst() != null) return true
}
return false
}
private fun callStateToDeclarationState(state: SuggestedRefactoringState): SuggestedRefactoringState? {
val anchor = state.anchor as? PsiCallExpression ?: return null
val resolveResult = anchor.resolveMethodGenerics()
@@ -5,10 +5,7 @@ import com.intellij.openapi.application.runUndoTransparentWriteAction
import com.intellij.openapi.command.executeCommand
import com.intellij.psi.*
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.refactoring.changeSignature.ChangeSignatureProcessor
import com.intellij.refactoring.changeSignature.JavaThrownExceptionInfo
import com.intellij.refactoring.changeSignature.ParameterInfoImpl
import com.intellij.refactoring.changeSignature.ThrownExceptionInfo
import com.intellij.refactoring.changeSignature.*
import com.intellij.refactoring.suggested.SuggestedChangeSignatureData
import com.intellij.refactoring.suggested.SuggestedRefactoringExecution
import com.intellij.refactoring.suggested.SuggestedRefactoringSupport
@@ -103,17 +100,18 @@ class JavaSuggestedRefactoringExecution(refactoringSupport: SuggestedRefactoring
val exceptionInfos = prepareExceptionInfos(newExceptionTypes, oldExceptionTypes)
val processor = ChangeSignatureProcessor(
project,
val changeInfo = JavaChangeInfoImpl.generateChangeInfo(
declaration,
false,
false,
data.newSignature.visibility?.takeIf { it != data.oldSignature.visibility },
data.newSignature.name,
returnType,
returnType?.let(CanonicalTypes::createTypeWrapper),
newParameters.toTypedArray(),
exceptionInfos.toTypedArray()
)
processor.run()
exceptionInfos.toTypedArray(),
null,
null)
ChangeSignatureProcessor(project, changeInfo).run()
}
private fun typesEqualWithAnnotations(type1: PsiType?, type2: PsiType?) = type1?.getCanonicalText(true) == type2?.getCanonicalText(true)
@@ -72,7 +72,7 @@ public class ChangeSignatureProcessor extends ChangeSignatureProcessorBase {
ThrownExceptionInfo[] thrownExceptions,
Set<PsiMethod> propagateParametersMethods,
Set<PsiMethod> propagateExceptionsMethods) {
this(project, JavaChangeInfoImpl.generateChangeInfo(method, generateDelegate, newVisibility, newName, newType, parameterInfo, thrownExceptions,
this(project, JavaChangeInfoImpl.generateChangeInfo(method, generateDelegate, true, newVisibility, newName, newType, parameterInfo, thrownExceptions,
propagateParametersMethods, propagateExceptionsMethods));
}
@@ -1031,7 +1031,12 @@ public class JavaChangeSignatureUsageProcessor implements ChangeSignatureUsagePr
final List<PsiParameter> newParameters = ContainerUtil.map(newElements, element -> (PsiParameter)element.variable());
final List<String> newParameterNames = ContainerUtil.map(newElements, VariableWrapper::getName);
final boolean[] toRemove = myChangeInfo.toRemoveParm();
resolveVariableVsFieldsConflicts(newParameters, newParameterNames, myParent, toRemove, myMethodBody, ParameterList.INSTANCE);
if (myChangeInfo.isFixFieldConflicts()) {
resolveVariableVsFieldsConflicts(newParameters, newParameterNames, myParent, toRemove, myMethodBody, ParameterList.INSTANCE);
} else {
ChangeSignatureUtil.synchronizeList(myParent, newParameters, ParameterList.INSTANCE, toRemove);
JavaCodeStyleManager.getInstance(myParent.getProject()).shortenClassReferences(myParent);
}
}
}
@@ -151,7 +151,7 @@ public class JavaRefactoringFactoryImpl extends JavaRefactoringFactory {
Set<PsiMethod> propagateParametersMethods,
Set<PsiMethod> propagateExceptionsMethods,
Consumer<? super List<ParameterInfo>> callback) {
JavaChangeInfo changeInfo = JavaChangeInfoImpl.generateChangeInfo(method, generateDelegate, newVisibility,
JavaChangeInfo changeInfo = JavaChangeInfoImpl.generateChangeInfo(method, generateDelegate, true, newVisibility,
newName, newReturnType != null ? CanonicalTypes.createTypeWrapper(newReturnType) : null,
(ParameterInfoImpl[])parameterInfo,
thrownExceptions, propagateParametersMethods, propagateExceptionsMethods);
@@ -30,6 +30,13 @@ public interface JavaChangeInfo extends ChangeInfo {
return type != null ? type.getCanonicalText() : null;
}
/**
* @return true if it's desired to fix conflicts between new parameters and field names (adding {@code this.} qualifier to field accesses)
*/
default boolean isFixFieldConflicts() {
return true;
}
@Override
JavaParameterInfo @NotNull [] getNewParameters();
@@ -58,6 +58,7 @@ public class JavaChangeInfoImpl extends UserDataHolderBase implements JavaChange
private final PsiExpression[] defaultValues;
private final boolean isGenerateDelegate;
private boolean isFixFieldConflicts = true;
final Set<PsiMethod> propagateParametersMethods;
final Set<PsiMethod> propagateExceptionsMethods;
@@ -65,8 +66,8 @@ public class JavaChangeInfoImpl extends UserDataHolderBase implements JavaChange
public static JavaChangeInfo generateChangeInfo(PsiMethod method,
boolean generateDelegate,
@Nullable // null means unchanged
@PsiModifier.ModifierConstant String newVisibility,
boolean fixFieldConflicts,
@Nullable("Null means unchanged") @PsiModifier.ModifierConstant String newVisibility,
String newName,
CanonicalTypes.Type newType,
ParameterInfoImpl @NotNull [] parameterInfo,
@@ -91,6 +92,7 @@ public class JavaChangeInfoImpl extends UserDataHolderBase implements JavaChange
new JavaChangeInfoImpl(newVisibility, method, newName, newType, parameterInfo, thrownExceptions, generateDelegate,
propagateParametersMethods, propagateExceptionsMethods);
javaChangeInfo.setCheckUnusedParameter();
javaChangeInfo.setFixFieldConflicts(fixFieldConflicts);
return javaChangeInfo;
}
@@ -236,6 +238,15 @@ public class JavaChangeInfoImpl extends UserDataHolderBase implements JavaChange
myCheckUnusedParameter = true;
}
@Override
public boolean isFixFieldConflicts() {
return isFixFieldConflicts;
}
public void setFixFieldConflicts(boolean fixFieldConflicts) {
isFixFieldConflicts = fixFieldConflicts;
}
protected void fillOldParams(PsiMethod method) {
PsiParameter[] parameters = method.getParameterList().getParameters();
oldParameterNames = new String[parameters.length];
@@ -31,8 +31,8 @@ import java.util.function.Function;
*/
public sealed interface ModCommand
permits ModChooseAction, ModChooseMember, ModCompositeCommand, ModCopyToClipboard, ModCreateFile, ModDeleteFile, ModDisplayMessage,
ModHighlight, ModNavigate, ModNothing, ModRenameSymbol, ModShowConflicts, ModStartTemplate, ModUpdateFileText,
ModUpdateInspectionOptions {
ModHighlight, ModNavigate, ModNothing, ModRenameSymbol, ModShowConflicts, ModStartTemplate, ModUpdateReferences,
ModUpdateFileText, ModUpdateInspectionOptions {
/**
* @return true if the command does nothing
@@ -80,6 +80,18 @@ public interface ModPsiUpdater extends ModPsiNavigator {
*/
void rename(@NotNull PsiNameIdentifierOwner element, @NotNull List<@NotNull String> suggestedNames);
/**
* Tracks subsequent changes in a given declaration (e.g., method) and produce a command to
* update the references to the declaration (maybe displaying UI).
* The current file may be changed if the declaration is located in the different file.
* <p>
* This method must be called before you actually update the declaration (e.g., change method parameters).
* </p>
*
* @param declaration declaration to track
*/
void trackDeclaration(@NotNull PsiElement declaration);
/**
* @return a builder that allows you to create a template.
*/
@@ -0,0 +1,30 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.modcommand;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
/**
* A command to update the references to the declaration if possible. A common use-case is to
* invoke "change signature" refactoring automatically. May do nothing, if particular language,
* or particular kind of declaration change is not supported. The command should follow actual
* text update in the file via {@link ModUpdateFileText}.
*
* @implNote In IntelliJ IDEA interactive executor, it invokes the "suggested refactoring" mechanism.
*
* @param file virtual file where the declaration resides
* @param oldText the complete content of the virtual file before the declaration was updated
* @param oldRange the complete text range of the declaration within the oldText
* @param newRange the complete current text range of the declaration
*/
public record ModUpdateReferences(@NotNull VirtualFile file, @NotNull String oldText, @NotNull TextRange oldRange,
@NotNull TextRange newRange) implements ModCommand {
/**
* @param newRange updated new range
* @return the equivalent command but with updated newRange.
*/
public @NotNull ModUpdateReferences withNewRange(@NotNull TextRange newRange) {
return newRange.equals(newRange()) ? this : new ModUpdateReferences(file, oldText, oldRange, newRange);
}
}
@@ -219,6 +219,7 @@ final class PsiUpdateImpl {
private final List<ModHighlight.HighlightInfo> myHighlightInfos = new ArrayList<>();
private final List<ModStartTemplate.TemplateField> myTemplateFields = new ArrayList<>();
private @Nullable ModRenameSymbol myRenameSymbol;
private final List<ModUpdateReferences> myTrackedDeclarations = new ArrayList<>();
private boolean myPositionUpdated = false;
private @NlsContexts.Tooltip String myErrorMessage;
private @NlsContexts.Tooltip String myInfoMessage;
@@ -426,6 +427,16 @@ final class PsiUpdateImpl {
myRenameSymbol = new ModRenameSymbol(myNavigationFile, range, suggestedNames);
}
@Override
public void trackDeclaration(@NotNull PsiElement declaration) {
TextRange range = getRange(declaration);
if (range == null) {
throw new IllegalArgumentException("Element disappeared after postponed operations: " + declaration);
}
String oldText = myTracker.myCopyFile.getText();
myTrackedDeclarations.add(new ModUpdateReferences(myNavigationFile, oldText, range, range));
}
@Override
public void cancel(@NotNull @NlsContexts.Tooltip String errorMessage) {
if (myErrorMessage != null) {
@@ -481,6 +492,7 @@ final class PsiUpdateImpl {
mySelection = updateRange(event, mySelection);
myHighlightInfos.replaceAll(info -> info.withRange(updateRange(event, info.range())));
myTemplateFields.replaceAll(info -> info.withRange(updateRange(event, info.range())));
myTrackedDeclarations.replaceAll(range -> range.withNewRange(updateRange(event, range.newRange())));
if (myRenameSymbol != null) {
myRenameSymbol = myRenameSymbol.withRange(updateRange(event, myRenameSymbol.symbolRange()));
}
@@ -513,11 +525,13 @@ final class PsiUpdateImpl {
if (myErrorMessage != null) {
return error(myErrorMessage);
}
return myChangedFiles.values().stream().map(FileTracker::getUpdateCommand).reduce(nop(), ModCommand::andThen)
return myChangedFiles.values().stream()
.map(fileTracker -> fileTracker.getUpdateCommand()).reduce(nop(), ModCommand::andThen)
.andThen(myChangedDirectories.values().stream()
.flatMap(info -> info.createFileCommands(myTracker.myProject))
.reduce(nop(), ModCommand::andThen))
.andThen(getNavigateCommand()).andThen(getHighlightCommand()).andThen(getTemplateCommand())
.andThen(myTrackedDeclarations.stream().<ModCommand>map(c -> c).reduce(nop(), ModCommand::andThen))
.andThen(myRenameSymbol == null ? nop() : myRenameSymbol)
.andThen(myInfoMessage == null ? nop() : ModCommand.info(myInfoMessage));
}
@@ -554,7 +568,7 @@ final class PsiUpdateImpl {
private final TextRange myRange;
private final @NotNull PsiElement myElement;
public DummyContext(TextRange range, @NotNull PsiElement element) {
private DummyContext(TextRange range, @NotNull PsiElement element) {
myRange = range;
myElement = element;
}
@@ -593,3 +593,4 @@ executor.one.of.actions={0} of {1}: {2}
command.title.finishing.template=Finishing Template
notification.group.batch.quick.fix=Batch quick fix
command.check.availability.for=Check Availability for {0}
dialog.title.searching.for.usages=Searching for Usages
@@ -43,6 +43,11 @@ abstract class SuggestedRefactoringAvailability(protected val refactoringSupport
*/
open fun amendStateInBackground(state: SuggestedRefactoringState): Iterator<SuggestedRefactoringState> = iterator { }
/**
* Slow way to determine whether the action is available. Intended to be executed in background thread.
*/
open fun isAvailable(state: SuggestedRefactoringState): Boolean = true
/**
* Determines refactoring availability for a given state and returns instance of [SuggestedRefactoringData],
* providing information for presentation and execution of the refactoring.
@@ -46,7 +46,7 @@ abstract class SuggestedRefactoringStateChanges(protected val refactoringSupport
val signatureRange = refactoringSupport.signatureRange(anchor) ?: return null
val psiDocumentManager = PsiDocumentManager.getInstance(anchor.project)
val file = anchor.containingFile
val document = psiDocumentManager.getDocument(file)!!
val document = file.viewProvider.document!!
require(psiDocumentManager.isCommitted(document))
return SuggestedRefactoringState(
anchor,
@@ -163,7 +163,7 @@ abstract class SuggestedRefactoringStateChanges(protected val refactoringSupport
}
fun SuggestedRefactoringStateChanges.parameterMarkers(declaration: PsiElement, signature: Signature): List<ParameterMarker> {
val document = PsiDocumentManager.getInstance(declaration.project).getDocument(declaration.containingFile)!!
val document = declaration.containingFile.viewProvider.document!!
val markerRanges = parameterMarkerRanges(declaration)
require(markerRanges.size == signature.parameters.size)
return markerRanges.zip(signature.parameters)
@@ -24,6 +24,7 @@ import com.intellij.lang.LangBundle;
import com.intellij.modcommand.*;
import com.intellij.modcommand.ModChooseMember.SelectionMode;
import com.intellij.modcommand.ModUpdateFileText.Fragment;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
@@ -52,6 +53,7 @@ import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer;
import com.intellij.refactoring.suggested.*;
import com.intellij.refactoring.ui.ConflictsDialog;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.concurrency.annotations.RequiresEdt;
@@ -162,6 +164,9 @@ public class ModCommandExecutorImpl implements ModCommandExecutor {
if (command instanceof ModUpdateFileText upd) {
return executeUpdate(project, upd);
}
if (command instanceof ModUpdateReferences decl) {
return !executeTrackDeclaration(context, decl, editor);
}
if (command instanceof ModCompositeCommand cmp) {
return executeComposite(context, cmp, editor);
}
@@ -207,6 +212,54 @@ public class ModCommandExecutorImpl implements ModCommandExecutor {
throw new IllegalArgumentException("Unknown command: " + command);
}
@Nullable
private static PsiElement findElementAtRange(PsiFile psiFile, TextRange declarationRange) {
PsiElement element = psiFile.findElementAt(declarationRange.getStartOffset());
while (element != null && !element.getTextRange().contains(declarationRange)) {
element = element.getParent();
}
if (element == null || !element.getTextRange().equals(declarationRange)) return null;
return element;
}
private static boolean executeTrackDeclaration(@NotNull ActionContext context, @NotNull ModUpdateReferences decl, @Nullable Editor editor) {
// TODO: properly support multiple tracked declarations
VirtualFile file = decl.file();
Project project = context.project();
Callable<SuggestedRefactoringState> computeNewState = () -> {
PsiFile psiFile = PsiManagerEx.getInstanceEx(project).findFile(file);
if (psiFile == null) return null;
SuggestedRefactoringSupport support = SuggestedRefactoringSupport.Companion.forLanguage(psiFile.getLanguage());
if (support == null) return null;
PsiElement newElement = findElementAtRange(psiFile, decl.newRange());
if (newElement == null || !support.isAnchor(newElement)) return null;
SuggestedRefactoringStateChanges stateChanges = support.getStateChanges();
PsiFile fileCopy = (PsiFile)psiFile.copy();
Document documentCopy = fileCopy.getViewProvider().getDocument();
documentCopy.replaceString(0, documentCopy.getTextLength(), decl.oldText());
PsiDocumentManager.getInstance(project).commitDocument(documentCopy);
PsiElement element = findElementAtRange(fileCopy, decl.oldRange());
if (element == null) return null;
if (!support.isAnchor(element) || stateChanges.findDeclaration(element) != element) return null;
SuggestedRefactoringState state = stateChanges.createInitialState(element);
if (state == null) return null;
SuggestedRefactoringAvailability availability = support.getAvailability();
SuggestedRefactoringState newState = stateChanges.updateState(state, newElement);
if (newState.getErrorLevel() != SuggestedRefactoringState.ErrorLevel.NO_ERRORS) return null;
if (availability.detectAvailableRefactoring(newState) != null && availability.isAvailable(newState)) return newState;
return null;
};
SuggestedRefactoringState finalState = ProgressManager.getInstance().runProcessWithProgressSynchronously(
() -> ReadAction.nonBlocking(computeNewState).executeSynchronously(),
LangBundle.message("dialog.title.searching.for.usages"), true, project);
if (finalState == null) return false;
Editor finalEditor = getEditor(project, editor, decl.file());
if (finalEditor == null) return false;
PerformSuggestedRefactoringKt.performSuggestedRefactoring(
finalState, finalEditor, project, ActionPlaces.INTENTION_MENU, true, null, null);
return true;
}
private static boolean executeUpdateInspectionOptions(@NotNull ActionContext context, @NotNull ModUpdateInspectionOptions options) {
VirtualFile vFile = context.file().getVirtualFile();
Project project = context.project();
@@ -62,6 +62,19 @@ internal fun performSuggestedRefactoring(
?.let {
it.refactoringSupport.availability.refineSignaturesWithResolve(it)
} ?: return
performSuggestedRefactoring(state, originalEditor, project, actionPlace, showReviewBalloon, popupAnchorComponent, popupAnchorPoint)
}
/**
* Launch suggested refactoring based on the specified state
*/
fun performSuggestedRefactoring(state: SuggestedRefactoringState,
originalEditor: Editor,
project: Project,
actionPlace: String,
showReviewBalloon: Boolean,
popupAnchorComponent: JComponent?,
popupAnchorPoint: Point?) {
if (state.errorLevel != ErrorLevel.NO_ERRORS || state.oldSignature == state.newSignature) return
val refactoringSupport = state.refactoringSupport