Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fusion path intentions #325

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package de.vette.idea.neos.lang.fusion.codeInsight.intention;

import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import de.vette.idea.neos.lang.fusion.FusionBundle;
import de.vette.idea.neos.lang.fusion.psi.*;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.List;

public class GroupFusionPaths extends BaseElementAtCaretIntentionAction {
@Override
public boolean isAvailable(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement element) {
if (!(element.getContainingFile() instanceof FusionFile)) {
return false;
}
if (element.getNode().getElementType() != FusionTypes.PATH_SEPARATOR) {
return false;
}
var path = PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionPath);
if (path == null) {
return false;
}
var prefix = getPathPrefix((FusionPath) path, element);
var context = getContext(element);
var siblingCandidates = getElementsInPath(prefix, context);
this.setText(FusionBundle.message("intention.group.fusion.paths.of", prefix));
return siblingCandidates.size() > 1;
}

protected String getPathPrefix(FusionPath path, PsiElement separator) {
var child = path.getFirstChild();
StringBuilder prefix = new StringBuilder();
while (child != separator) {
prefix.append(child.getText());
child = child.getNextSibling();
}
return prefix.toString();
}

protected PsiElement getContext(PsiElement element) {
var closestBlock = PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionBlock);
if (closestBlock != null) {
return closestBlock;
}
// we expect a path always to be within a block or to be at the top level of the file
return element.getContainingFile();
}

protected @NotNull List<PsiElement> getElementsInPath(String prefix, PsiElement context) {
var prefixLike = prefix + ".";
var elements = new ArrayList<PsiElement>();

for (var child : context.getChildren()) {
if (child instanceof FusionPropertyBlock propertyBlock) {
var path = propertyBlock.getPath().getText();
if (path.equals(prefix) || path.startsWith(prefixLike)) {
elements.add(propertyBlock);
}
} else if (child instanceof FusionPropertyAssignment assignment) {
var path = assignment.getPath().getText();
if (path.equals(prefix)) {
// we can only merge into assignments that instantiate a prototype
if (assignment.getAssignmentValue() == null || assignment.getAssignmentValue().getPrototypeInstance() == null) {
return new ArrayList<>();
}
elements.add(assignment);
} else if (path.startsWith(prefixLike)) {
elements.add(assignment);
}
}
}

return elements;
}

@Override
public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
var path = PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionPath);
if (path == null) {
return;
}
var prefix = getPathPrefix((FusionPath) path, element);
var context = getContext(element);
var siblingCandidates = getElementsInPath(prefix, context);
if (siblingCandidates.size() < 2) {
return;
}

var prefixLength = 1;
var prefixStart = path.getFirstChild();
while (prefixStart != element) {
prefixLength++;
prefixStart = prefixStart.getNextSibling();
}

// TODO: check if this would merge into a prototype instantiation: in general that could work, but not with this
// code. Also for some prototypes we might want some order of properties, e.g. any other property before
// "renderer"

// I would have preferred working with the PSI tree, but I couldn't get correct line breaks within reasonable time
var newBlockCode = new StringBuilder();
newBlockCode.append(prefix);
newBlockCode.append(" {");

List<PsiElement> elementsToRemove = new ArrayList<>();
for (var sibling : siblingCandidates) {
FusionPath siblingPath;
if (sibling instanceof FusionPropertyBlock propertyBlock) {
siblingPath = propertyBlock.getPath();
} else if (sibling instanceof FusionPropertyAssignment assignment) {
siblingPath = assignment.getPath();
} else {
continue;
}

// drop prefix from path
var removedCount = prefixLength;
while (siblingPath.getFirstChild() != null && removedCount > 0) {
removedCount--;
siblingPath.getFirstChild().delete();
}

elementsToRemove.add(sibling);
if (sibling.getNextSibling() instanceof PsiWhiteSpace) {
sibling.getNextSibling().delete();
}

if (sibling instanceof FusionPropertyBlock propertyBlock) {
var openingBrace = propertyBlock.getBlock().getLeftBrace();
var closingBrace = propertyBlock.getBlock().getRightBrace();
var child = openingBrace.getNextSibling();
while (child != closingBrace) {
if (!(child instanceof PsiWhiteSpace)) {
newBlockCode.append("\n");
newBlockCode.append(child.getText());
}
child = child.getNextSibling();
}
} else {
newBlockCode.append("\n");
newBlockCode.append(sibling.getText());
}
}

newBlockCode.append("\n}\n");
var newBlock = FusionElementFactory.createFusionFile(project, newBlockCode.toString());
var addedBlock = (FusionPropertyBlock) siblingCandidates.get(0).getParent().addBefore(newBlock.getFirstChild(), siblingCandidates.get(0));
addedBlock.getParent().addAfter(newBlock.getLastChild(), addedBlock);

elementsToRemove.forEach(PsiElement::delete);

CodeStyleManager.getInstance(project).reformat(addedBlock);
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return FusionBundle.message("intention.group.fusion.paths");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package de.vette.idea.neos.lang.fusion.codeInsight.intention;

import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import de.vette.idea.neos.NeosProjectService;
import de.vette.idea.neos.lang.fusion.FusionBundle;
import de.vette.idea.neos.lang.fusion.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class MergeFusionPathUp extends BaseElementAtCaretIntentionAction {
@Override
public @NotNull @IntentionName String getText() {
return getFamilyName();
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return FusionBundle.message("intention.merge.fusion.path.up");
}

@Override
public boolean isAvailable(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement element) {
if (!(element.getContainingFile() instanceof FusionFile)) {
return false;
}

var path = (FusionPath) PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionPath);

var closestBlock = getClosestBlock(path);
return closestBlock != null;
}

protected @Nullable FusionPropertyBlock getClosestBlock(@Nullable FusionPath path) {
if (path == null || path.getParent() == null || !(path.getParent().getParent() instanceof FusionBlock)) {
return null;
}

FusionBlock block = (FusionBlock) path.getParent().getParent();
return block.getParent() instanceof FusionPropertyBlock ? (FusionPropertyBlock) block.getParent() : null;
}

protected int getChildrenCount(FusionPropertyBlock block) {
return block.getBlock().getChildren().length;
}

@Override
public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
var path = (FusionPath) PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionPath);
var block = getClosestBlock(path);
if (path == null || block == null) {
return;
}

int blockChildrenCount = getChildrenCount(block);
NeosProjectService.getLogger().debug("blockChildrenCount: " + blockChildrenCount);
var originalStatement = path.getParent();
var pathSeparator = FusionElementFactory.createFusionFile(project, "foo.bar").getFirstChild().getNextSibling();
var child = block.getPath().getFirstChild();
var anchor = path.getFirstChild();
while (child != null) {
path.addBefore(child.copy(), anchor);
child = child.getNextSibling();
}
path.addBefore(pathSeparator.copy(), anchor);
block.getParent().addAfter(originalStatement, block);

if (blockChildrenCount == 1) {
// block should now be empty
block.delete();
} else {
var nl = FusionElementFactory.createFusionFile(project, "\n").getFirstChild();
block.getParent().addAfter(nl, block);
// remove trailing line break
if (originalStatement.getNextSibling() instanceof PsiWhiteSpace) {
originalStatement.getNextSibling().delete();
}
originalStatement.delete();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package de.vette.idea.neos.lang.fusion.codeInsight.intention;

import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import de.vette.idea.neos.lang.fusion.FusionBundle;
import de.vette.idea.neos.lang.fusion.psi.*;
import org.jetbrains.annotations.NotNull;

public class SplitFusionPath extends BaseElementAtCaretIntentionAction {

@Override
public @NotNull @IntentionName String getText() {
return getFamilyName();
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return FusionBundle.message("intention.split.fusion.path");
}

@Override
public boolean isAvailable(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement element) {
if (!(element.getContainingFile() instanceof FusionFile)) {
return false;
}
if (element.getNode().getElementType() != FusionTypes.PATH_SEPARATOR) {
return false;
}
var path = PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionPath);
return path != null && path.getChildren().length > 1;
}

@Override
public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
var path = PsiTreeUtil.findFirstParent(element, true, e -> e instanceof FusionPath);
if (path == null) {
return;
}

var originalStatement = path.getParent();

var newBlock = (FusionPropertyBlock) FusionElementFactory.createFusionFile(project, "dummy {\n\n}").getFirstChild();
var newPath = newBlock.getPath();
var child = path.getFirstChild();
while (child != element) {
// this seems to create a copy and does not "remount" the element, so we need to delete the original
newPath.add(child);
var next = child.getNextSibling();
child.delete();
child = next;
}
element.delete();
newPath.getFirstChild().delete();

var insertedBlock = (FusionPropertyBlock) originalStatement.getParent().addBefore(newBlock, originalStatement);
insertedBlock.getBlock().addAfter(originalStatement, insertedBlock.getBlock().getLeftBrace().getNextSibling());

originalStatement.delete();

CodeStyleManager.getInstance(project).reformat(insertedBlock);
}
}
16 changes: 16 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@
<gotoSymbolContributor implementation="de.vette.idea.neos.search.NodeTypeContributor"/>
<gotoSymbolContributor implementation="de.vette.idea.neos.search.FusionPrototypeDeclarationContributor"/>

<intentionAction>
<language>NeosFusion</language>
<className>de.vette.idea.neos.lang.fusion.codeInsight.intention.SplitFusionPath</className>
<category>Neos Fusion</category>
</intentionAction>
<intentionAction>
<language>NeosFusion</language>
<className>de.vette.idea.neos.lang.fusion.codeInsight.intention.MergeFusionPathUp</className>
<category>Neos Fusion</category>
</intentionAction>
<intentionAction>
<language>NeosFusion</language>
<className>de.vette.idea.neos.lang.fusion.codeInsight.intention.GroupFusionPaths</className>
<category>Neos Fusion</category>
</intentionAction>

<!-- Line Marker Providers -->
<codeInsight.lineMarkerProvider language="NeosFusion" implementationClass="de.vette.idea.neos.lang.fusion.annotators.NodeTypeLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="yaml" implementationClass="de.vette.idea.neos.lang.yaml.annotators.PrototypeLineMarkerProvider"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
renderer.@process {
processor1 = Vendor.Package:StringProcessor
processor2 = ${props.prefix + value}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[email protected] = Vendor.Package:StringProcessor
[email protected] = ${props.prefix + value}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html lang="en">
<body>
Groups all fusion paths with the prefix before the caret into a single block.
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
foo.bar = ${value}

foo {
baz = ${value2}
}
foo.bar = ${value}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
foo {
bar = ${value}
}

foo {
bar = ${value1}
baz = ${value2}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html lang="en">
<body>
Merges a fusion path to the parent block.
<p>
If a fusion path (as part of an assignment or a block) is within a block, it will be moved to an assignment (or
block) with the merged path.
</p>
<p>
If a block contains multiple paths, only the current path will be pulled out and removed from the block.
</p>
</body>
</html>
Loading
Loading