Skip to content

Commit

Permalink
Merge pull request #323 from PRGfx/fusion-refactoring
Browse files Browse the repository at this point in the history
feat: add move prototype refactoring
  • Loading branch information
cvette authored Dec 22, 2024
2 parents ec9894d + 8d9a606 commit 8e855f7
Show file tree
Hide file tree
Showing 10 changed files with 870 additions and 1 deletion.

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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Incorrect string capitalization

String 'Create file ''{0}''' is not properly capitalized. It should have title 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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Incorrect string capitalization

String 'Move prototypes to file ''{0}''' is not properly capitalized. It should have title 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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Nullability and data flow problems

Condition `element instanceof FusionPrototypeSignature` is redundant and can be replaced with a null check
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Nullability and data flow problems

Condition `e instanceof FusionPrototypeSignature` is redundant and can be replaced with a null check
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked call to 'ArrayList(Collection)' as a member of raw type 'java.util.ArrayList'

Check warning on line 113 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unchecked warning

Unchecked assignment: 'java.util.ArrayList' to 'java.util.List'
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Nullability and data flow problems

Condition `prototypeSignature instanceof FusionPrototypeSignature` is redundant and can be replaced with a null check
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

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Condition is covered by further condition

Condition 'parent instanceof FusionBlock' covered by subsequent condition 'parent instanceof FusionPrototypeSignature'

Check warning on line 128 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constant values

Condition `parent instanceof FusionBlock || parent instanceof FusionPrototypeSignature` is always `true`

Check warning on line 128 in src/main/java/de/vette/idea/neos/lang/fusion/refactoring/MovePrototypeToFile.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constant values

Condition `parent instanceof FusionBlock` is always `true`
return false;
}
parent = parent.getParent();
}

return true;
}
}
3 changes: 3 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,8 @@
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
<add-to-group group-id="ProjectViewPopupMenu" anchor="first"/>
</action>
<action id="de.vette.idea.neos.fusion.refactoring.MovePrototypeToFile" class="de.vette.idea.neos.lang.fusion.refactoring.MovePrototypeToFile" text="Move Prototype">
<add-to-group group-id="RefactoringMenu"/>
</action>
</actions>
</idea-plugin>
13 changes: 12 additions & 1 deletion src/main/resources/messages/FusionBundle.properties
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}.
Loading

0 comments on commit 8e855f7

Please sign in to comment.