-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
feat: add move prototype refactoring
- Loading branch information
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
package de.vette.idea.neos.lang.fusion.refactoring; | ||
|
||
import com.intellij.codeInsight.FileModificationService; | ||
import com.intellij.ide.util.EditorHelper; | ||
import com.intellij.notification.NotificationGroupManager; | ||
import com.intellij.notification.NotificationType; | ||
import com.intellij.openapi.application.WriteAction; | ||
import com.intellij.openapi.command.CommandProcessor; | ||
import com.intellij.openapi.project.Project; | ||
import com.intellij.openapi.util.NlsContexts; | ||
import com.intellij.openapi.util.Ref; | ||
import com.intellij.openapi.util.io.FileUtil; | ||
import com.intellij.openapi.vfs.LocalFileSystem; | ||
import com.intellij.openapi.vfs.VfsUtil; | ||
import com.intellij.openapi.vfs.VirtualFile; | ||
import com.intellij.psi.*; | ||
import com.intellij.psi.impl.source.tree.LeafPsiElement; | ||
import com.intellij.psi.util.PsiTreeUtil; | ||
import com.intellij.refactoring.BaseRefactoringProcessor; | ||
import com.intellij.refactoring.util.CommonRefactoringUtil; | ||
import com.intellij.usageView.BaseUsageViewDescriptor; | ||
import com.intellij.usageView.UsageInfo; | ||
import com.intellij.usageView.UsageViewDescriptor; | ||
import com.intellij.util.PathUtil; | ||
import de.vette.idea.neos.lang.fusion.FusionBundle; | ||
import de.vette.idea.neos.lang.fusion.psi.FusionFile; | ||
import de.vette.idea.neos.lang.fusion.psi.FusionPrototypeSignature; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
public class MovePrototypeProcessor extends BaseRefactoringProcessor { | ||
|
||
private final String myTitle; | ||
private final @Nullable PsiFile myTargetFile; | ||
private final PsiElement[] myAffectedElements; | ||
private final boolean myOpenInEditor; | ||
private @Nullable String myTargetFilePath = null; | ||
|
||
public MovePrototypeProcessor( | ||
@NotNull Project project, | ||
String title, | ||
@NotNull PsiFile targetFile, | ||
Iterable<FusionPrototypeSignature> signaturesToMove, | ||
boolean openInEditor | ||
) { | ||
super(project); | ||
this.myTitle = title; | ||
this.myTargetFile = targetFile; | ||
this.myAffectedElements = MovePrototypeProcessor.collectAffectedElements(signaturesToMove, project); | ||
this.myOpenInEditor = openInEditor; | ||
} | ||
|
||
public MovePrototypeProcessor( | ||
@NotNull Project project, | ||
String title, | ||
@NotNull String targetFilePath, | ||
Iterable<FusionPrototypeSignature> signaturesToMove, | ||
boolean openInEditor | ||
) { | ||
super(project); | ||
this.myTitle = title; | ||
this.myTargetFile = getOrCreateFileFromPath(targetFilePath); | ||
this.myTargetFilePath = targetFilePath; | ||
this.myAffectedElements = MovePrototypeProcessor.collectAffectedElements(signaturesToMove, project); | ||
this.myOpenInEditor = openInEditor; | ||
} | ||
|
||
private static boolean isMultilineComment(@Nullable PsiElement element) { | ||
return element instanceof PsiComment && element.getText().startsWith("/*"); | ||
} | ||
|
||
private @Nullable FusionFile getOrCreateFileFromPath(String targetFilePath) { | ||
String path = FileUtil.toSystemIndependentName(targetFilePath); | ||
VirtualFile file = LocalFileSystem.getInstance().findFileByPath(path); | ||
if (file != null) { | ||
PsiFile psiFile = PsiManager.getInstance(myProject).findFile(file); | ||
return psiFile instanceof FusionFile ? (FusionFile) psiFile : null; | ||
} | ||
|
||
String fileName = PathUtil.getFileName(path); | ||
Ref<VirtualFile> fileRef = Ref.create(); | ||
CommandProcessor.getInstance().executeCommand(myProject, () -> { | ||
try { | ||
WriteAction.run(() -> { | ||
VirtualFile parentDir = VfsUtil.createDirectories(PathUtil.getParentPath(path)); | ||
fileRef.set(parentDir.createChildData(this, fileName)); | ||
}); | ||
} catch (IOException e) { | ||
CommonRefactoringUtil.showErrorMessage(myTitle, FusionBundle.message("refactoring.move.prototype.error.creating.file", e.getMessage()), null, myProject); | ||
} | ||
}, FusionBundle.message("refactoring.move.prototype.create.file", fileName), "movePrototypeRefactoring"); | ||
Check warning on line 95 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeProcessor.java GitHub Actions / Qodana Community for JVMIncorrect string capitalization
|
||
|
||
if (fileRef.isNull()) { | ||
return null; | ||
} | ||
|
||
PsiFile psiFile = PsiManager.getInstance(myProject).findFile(fileRef.get()); | ||
return psiFile instanceof FusionFile ? (FusionFile) psiFile : null; | ||
} | ||
|
||
public static PsiElement[] collectAffectedElements(Iterable<FusionPrototypeSignature> prototypes, Project project) { | ||
List<PsiElement> elements = new ArrayList<>(); | ||
for (FusionPrototypeSignature prototype : prototypes) { | ||
elements.addAll(getPsiElementsForPrototypeSignature(prototype, project)); | ||
} | ||
return elements.toArray(PsiElement.EMPTY_ARRAY); | ||
} | ||
|
||
private static List<PsiElement> getPsiElementsForPrototypeSignature(FusionPrototypeSignature prototype, Project project) { | ||
PsiElement topLevelElement = prototype; | ||
while (!(topLevelElement.getParent() instanceof PsiFile) && topLevelElement.getParent() != null) { | ||
topLevelElement = topLevelElement.getParent(); | ||
} | ||
PsiElement firstElement = topLevelElement; | ||
|
||
// collect preceding comments | ||
do { | ||
PsiElement prevSibling = firstElement.getPrevSibling(); | ||
if (prevSibling instanceof PsiWhiteSpace) { | ||
// between multiline-comments and a prototype seems to be a whitespace | ||
if (isMultilineComment(prevSibling.getPrevSibling())) { | ||
firstElement = prevSibling.getPrevSibling(); | ||
} | ||
break; | ||
} else if (prevSibling instanceof PsiComment) { | ||
firstElement = prevSibling; | ||
} else { | ||
break; | ||
} | ||
} while (true); | ||
|
||
// collect elements to move in correct order | ||
List<PsiElement> elementsToMove = new ArrayList<>(); | ||
while (firstElement != null) { | ||
elementsToMove.add(firstElement); | ||
if (firstElement == topLevelElement) { | ||
break; | ||
} | ||
firstElement = firstElement.getNextSibling(); | ||
} | ||
elementsToMove.add(PsiParserFacade.getInstance(project).createWhiteSpaceFromText("\n")); | ||
|
||
return elementsToMove; | ||
} | ||
|
||
@Override | ||
protected @NotNull UsageViewDescriptor createUsageViewDescriptor(UsageInfo @NotNull [] usages) { | ||
return new BaseUsageViewDescriptor(myAffectedElements); | ||
} | ||
|
||
@Override | ||
protected UsageInfo @NotNull [] findUsages() { | ||
return new UsageInfo[0]; | ||
} | ||
|
||
@Override | ||
protected void performRefactoring(UsageInfo @NotNull [] usages) { | ||
PsiFile targetFile = myTargetFile != null | ||
? myTargetFile | ||
: myTargetFilePath != null ? getOrCreateFileFromPath(myTargetFilePath) : null; | ||
if (targetFile == null) { | ||
// there could be other errors as well, but we assume, they have been validated before | ||
if (myTargetFilePath != null) { | ||
CommonRefactoringUtil.showErrorMessage(myTitle, FusionBundle.message("refactoring.move.prototype.error.creating.file", myTargetFilePath), null, myProject); | ||
return; | ||
} | ||
return; | ||
} | ||
// we assume that everything is from the same file. we could alternatively go over everything by signature | ||
PsiFile originalFile = myAffectedElements[0].getContainingFile(); | ||
FileModificationService.getInstance().preparePsiElementsForWrite(originalFile, targetFile); | ||
|
||
if (targetFile.getChildren().length != 0) { | ||
targetFile.add(PsiParserFacade.getInstance(myProject).createWhiteSpaceFromText("\n")); | ||
} | ||
|
||
PsiElement firstElement = null; | ||
int movedElements = 0; | ||
|
||
for (PsiElement element : myAffectedElements) { | ||
PsiElement newElement = targetFile.add(element); | ||
if (PsiTreeUtil.findChildOfType(element, FusionPrototypeSignature.class) != null) { | ||
movedElements++; | ||
} | ||
if (firstElement == null) { | ||
firstElement = newElement; | ||
} | ||
} | ||
|
||
// deleting the elements when moving creates an error, as parent elements for subsequent elements may be deleted | ||
for (PsiElement element : myAffectedElements) { | ||
// there were a lot of issues with deleting elements, so we try to go over them kind of gracefully | ||
if (element instanceof PsiWhiteSpace) { | ||
continue; | ||
} | ||
if (element instanceof LeafPsiElement && ((LeafPsiElement) element).getTreeParent() == null) { | ||
continue; | ||
} | ||
try { | ||
element.delete(); | ||
} catch (Throwable e) { | ||
// ignore | ||
} | ||
} | ||
|
||
if (myOpenInEditor && firstElement != null) { | ||
EditorHelper.openInEditor(firstElement); | ||
} | ||
|
||
String message = FusionBundle.message("refactoring.move.prototype.0.moved.elements", movedElements); | ||
NotificationGroupManager.getInstance() | ||
.getNotificationGroup("Neos") | ||
.createNotification(message, NotificationType.INFORMATION) | ||
.notify(myProject); | ||
} | ||
|
||
@Override | ||
protected @NotNull @NlsContexts.Command String getCommandName() { | ||
String path = myTargetFile != null ? myTargetFile.getVirtualFile().getPath() : myTargetFilePath; | ||
return FusionBundle.message("refactoring.move.prototype.move.to", path); | ||
Check warning on line 224 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeProcessor.java GitHub Actions / Qodana Community for JVMIncorrect string capitalization
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package de.vette.idea.neos.lang.fusion.refactoring; | ||
|
||
import com.intellij.lang.Language; | ||
import com.intellij.openapi.actionSystem.DataContext; | ||
import com.intellij.openapi.editor.Editor; | ||
import com.intellij.openapi.project.Project; | ||
import com.intellij.psi.PsiElement; | ||
import com.intellij.psi.PsiFile; | ||
import com.intellij.psi.util.PsiTreeUtil; | ||
import com.intellij.refactoring.RefactoringActionHandler; | ||
import com.intellij.refactoring.actions.BaseRefactoringAction; | ||
import de.vette.idea.neos.NeosProjectService; | ||
import de.vette.idea.neos.lang.fusion.FusionLanguage; | ||
import de.vette.idea.neos.lang.fusion.psi.*; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
public class MovePrototypeToFile extends BaseRefactoringAction implements RefactoringActionHandler { | ||
|
||
@Override | ||
protected boolean isAvailableInEditorOnly() { | ||
return true; | ||
} | ||
|
||
@Override | ||
protected boolean isAvailableForFile(PsiFile file) { | ||
if (!(file instanceof FusionFile)) { | ||
return false; | ||
} | ||
|
||
if (findAllPrototypeSignatures(file).isEmpty()) { | ||
return false; | ||
} | ||
|
||
return super.isAvailableForFile(file); | ||
} | ||
|
||
@Override | ||
protected boolean isEnabledOnElements(PsiElement @NotNull [] psiElements) { | ||
for (PsiElement element : psiElements) { | ||
if (!(element instanceof FusionPrototypeSignature)) { | ||
Check warning on line 47 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java GitHub Actions / Qodana Community for JVMNullability and data flow problems
|
||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
protected boolean isAvailableForLanguage(Language language) { | ||
return language == FusionLanguage.INSTANCE; | ||
} | ||
|
||
@Override | ||
protected @Nullable RefactoringActionHandler getHandler(@NotNull DataContext dataContext) { | ||
return this; | ||
} | ||
|
||
@Override | ||
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile, DataContext dataContext) { | ||
List<FusionPrototypeSignature> selectedSignatures = new ArrayList<>(); | ||
editor.getCaretModel().getAllCarets().forEach(caret -> { | ||
PsiElement element = psiFile.findElementAt(caret.getOffset()); | ||
PsiElement signature = PsiTreeUtil.findFirstParent(element, e -> e instanceof FusionPrototypeSignature); | ||
Check warning on line 69 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java GitHub Actions / Qodana Community for JVMNullability and data flow problems
|
||
if (isTopLevelPrototype(signature)) { | ||
selectedSignatures.add((FusionPrototypeSignature) signature); | ||
} | ||
}); | ||
List<FusionPrototypeSignature> allSignatures = findAllPrototypeSignatures(psiFile); | ||
startRefactoring(project, selectedSignatures, allSignatures); | ||
} | ||
|
||
@Override | ||
public void invoke(@NotNull Project project, PsiElement @NotNull [] psiElements, DataContext dataContext) { | ||
// not sure when this is called | ||
|
||
List<FusionPrototypeSignature> selectedSignatures = new ArrayList<>(); | ||
List<FusionPrototypeSignature> allSignatures = new ArrayList<>(); | ||
Set<PsiFile> visitedFiles = new HashSet<>(); | ||
for (PsiElement element : psiElements) { | ||
if (!isTopLevelPrototype(element)) { | ||
continue; | ||
} | ||
selectedSignatures.add((FusionPrototypeSignature) element); | ||
PsiFile file = element.getContainingFile(); | ||
if (visitedFiles.contains(file)) { | ||
continue; | ||
} | ||
visitedFiles.add(file); | ||
allSignatures.addAll(findAllPrototypeSignatures(file)); | ||
} | ||
|
||
startRefactoring(project, selectedSignatures, allSignatures); | ||
} | ||
|
||
private void startRefactoring(Project project , List<FusionPrototypeSignature> selectedSignatures, List<FusionPrototypeSignature> allSignatures) { | ||
if (allSignatures.isEmpty()) { | ||
NeosProjectService.getLogger().debug("No prototypes found"); | ||
|
||
return; | ||
} | ||
|
||
MovePrototypeDialog dialog = new MovePrototypeDialog(project, allSignatures, selectedSignatures); | ||
dialog.show(); | ||
} | ||
|
||
public static List<FusionPrototypeSignature> findAllPrototypeSignatures(PsiFile psiFile) { | ||
List<FusionPrototypeSignature> signatures = new ArrayList<>(PsiTreeUtil.findChildrenOfType(psiFile, FusionPrototypeSignature.class)); | ||
Check warning on line 113 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java GitHub Actions / Qodana Community for JVMUnchecked warning
|
||
return signatures.stream().filter(MovePrototypeToFile::isTopLevelPrototype).collect(Collectors.toList()); | ||
} | ||
|
||
/** | ||
* Determines whether the prototype definition is an override on some path or not. | ||
*/ | ||
private static boolean isTopLevelPrototype(@Nullable PsiElement prototypeSignature) { | ||
if (!(prototypeSignature instanceof FusionPrototypeSignature)) { | ||
Check warning on line 121 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java GitHub Actions / Qodana Community for JVMNullability and data flow problems
|
||
return false; | ||
} | ||
|
||
PsiElement parent = prototypeSignature.getParent(); | ||
while (parent != null) { | ||
// TODO: there might be a better way to check this | ||
if (parent instanceof FusionBlock || parent instanceof FusionPrototypeSignature) { | ||
Check warning on line 128 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java GitHub Actions / Qodana Community for JVMCondition is covered by further condition
Check warning on line 128 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java GitHub Actions / Qodana Community for JVMConstant values
|
||
return false; | ||
} | ||
parent = parent.getParent(); | ||
} | ||
|
||
return true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,15 @@ | ||
usage.type.definition=Definition | ||
usage.type.deleted=Deleted | ||
usage.type.instance=Instance | ||
usage.type.inherited=Inherited | ||
usage.type.inherited=Inherited | ||
refactoring.move.prototype.title=Move Prototype | ||
refactoring.move.prototype.target.file=Move to | ||
refactoring.move.prototype.no.prototypes.selected=No prototype selected to be moved | ||
refactoring.move.prototype.source.target.files.should.be.different=Source and target files should be different. | ||
refactoring.move.prototype.target.file.not.specified=Target file not specified. | ||
refactoring.move.prototype.target.not.a.fusion.file=Target file is not a fusion file. | ||
refactoring.move.prototype.error.creating.file=Error creating file:\n{0} | ||
refactoring.move.prototype.create.file=Create file ''{0}'' | ||
refactoring.move.prototype.moving.between.packages=Target file seems to be a different package (moving from {0} to {1})! | ||
refactoring.move.prototype.move.to=Move prototypes to file ''{0}'' | ||
refactoring.move.prototype.0.moved.elements=Moved {0, choice, 1#prototype|2#{0} prototypes}. |