Skip to content

Commit

Permalink
#2148 Support for Twig Component HTML Syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
Haehnchen committed May 5, 2023
1 parent aaa3f17 commit 7aad331
Show file tree
Hide file tree
Showing 15 changed files with 730 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;

import com.intellij.util.indexing.*;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.KeyDescriptor;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.stubs.indexes.PhpConstantNameIndex;
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class UxTemplateStubIndex extends FileBasedIndexExtension<String, String> {
public static final ID<String, String> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.ux_template_index");

private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
@Override
public @NotNull ID<String, String> getName() {
return KEY;
}

@Override
public @NotNull DataIndexer<String, String, FileContent> getIndexer() {
return inputData -> {
Map<String, String> map = new HashMap<>();

if(inputData.getPsiFile() instanceof PhpFile phpFile) {
UxUtil.visitAsTwigComponent(phpFile, pair -> map.put(pair.getFirst(), pair.getSecond().getFQN()));
}

return map;
};
}

@Override
public @NotNull KeyDescriptor<String> getKeyDescriptor() {
return this.myKeyDescriptor;
}

@Override
public @NotNull DataExternalizer<String> getValueExternalizer() {
return EnumeratorStringDescriptor.INSTANCE;
}

@Override
public int getVersion() {
return 1;
}

public FileBasedIndex.@NotNull InputFilter getInputFilter() {
return PhpConstantNameIndex.PHP_INPUT_FILTER;
}

@Override
public boolean dependsOnFileContent() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package fr.adrienbrault.idea.symfony2plugin.templating;

import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.xml.XmlElementType;
import com.intellij.psi.xml.XmlToken;
import com.intellij.psi.xml.XmlTokenType;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigHtmlCompletionUtil;
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import org.apache.commons.lang.StringUtils;

import java.util.ArrayList;
import java.util.Collection;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class HtmlTemplateGoToDeclarationHandler implements GotoDeclarationHandler {
public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int offset, Editor editor) {
if (!Symfony2ProjectComponent.isEnabled(psiElement)) {
return null;
}

Collection<PsiElement> targets = new ArrayList<>();

// <twig:a
if (psiElement instanceof XmlToken && psiElement.getNode().getElementType() == XmlTokenType.XML_NAME && psiElement.getText().startsWith("twig:")) {
String text = psiElement.getText();
if (!text.startsWith("twig:")) {
return null;
}


int calulatedOffset = offset - psiElement.getTextRange().getStartOffset();
if (calulatedOffset < 0) {
calulatedOffset = 5;
}

// <twig:a
if (calulatedOffset > 5) {
if (TwigHtmlCompletionUtil.getTwigNamespacePattern().accepts(psiElement)) {

String componentName = StringUtils.stripStart(text, "twig:");
if (!componentName.isBlank()) {
targets.addAll(UxUtil.getTwigComponentNameTargets(psiElement.getProject(), componentName));
}
}
} else {
// <twig
targets.addAll(UxUtil.getTwigComponentAllTargets(psiElement.getProject()));
}
}

// <twig:Foo :message="" message="">
if (psiElement instanceof XmlToken) {
PsiElement parent = psiElement.getParent();
if (parent.getNode().getElementType() == XmlElementType.XML_ATTRIBUTE) {
if (parent.getParent() instanceof HtmlTag htmlTag && htmlTag.getName().startsWith("twig:")) {
String text = psiElement.getText();

for (PhpClass phpClass : UxUtil.getTwigComponentNameTargets(psiElement.getProject(), htmlTag.getName().substring(5))) {
Field fieldByName = phpClass.findFieldByName(StringUtils.stripStart(text, ":"), false);
if (fieldByName != null) {
targets.add(fieldByName);
}
}
};
}
}

return targets.toArray(new PsiElement[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package fr.adrienbrault.idea.symfony2plugin.templating;

import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInspection.XmlSuppressionProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.impl.source.xml.SchemaPrefix;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlToken;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.xml.XmlExtension;
import com.intellij.xml.XmlTagNameProvider;
import com.jetbrains.twig.TwigFile;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil;
import fr.adrienbrault.idea.symfony2plugin.util.UxUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.List;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class TwigComponentHtmlTagExtensions {
public static class TwigTemplateTagNameProvider implements XmlTagNameProvider {
@Override
public void addTagNameVariants(List<LookupElement> elements, @NotNull XmlTag tag, String prefix) {
PsiElement elementOnTwigViewProvider = TwigUtil.getElementOnTwigViewProvider(tag);

if (elementOnTwigViewProvider != null && !(elementOnTwigViewProvider.getContainingFile() instanceof TwigFile)) {
return;
}

for (String twigComponentName : UxUtil.getTwigComponentNames(tag.getProject())) {
elements.add(LookupElementBuilder.create("twig:" + twigComponentName).withIcon(Symfony2Icons.SYMFONY));
}
}
}

public static class TwigTemplateXmlExtension extends XmlExtension {
@Override
public boolean isAvailable(PsiFile file) {
return true;
}

@Override
public @NotNull List<TagInfo> getAvailableTagNames(@NotNull XmlFile file, @NotNull XmlTag context) {
return Collections.emptyList();
}

@Override
public @Nullable SchemaPrefix getPrefixDeclaration(XmlTag context, String namespacePrefix) {
if (namespacePrefix.equals("twig") && context instanceof HtmlTag && context.getName().startsWith("twig")) {
return new NullableParentShouldOverwriteSchemaPrefix(
context.getProject(),
context.getContainingFile(),
new TextRange(0, 4),
"twig"
);
}

return null;
}

private static class NullableParentShouldOverwriteSchemaPrefix extends SchemaPrefix {
private final Project project;
private final PsiFile psiFile;

public NullableParentShouldOverwriteSchemaPrefix(@NotNull Project project, @NotNull PsiFile psiFile, TextRange range, String name) {
super(null, range, name);
this.project = project;
this.psiFile = psiFile;
}

@Override
public PsiFile getContainingFile() {
return this.psiFile;
}

@Override
public @NotNull Project getProject() {
return this.project;
}
}
}

public static class TwigTemplateXmlSuppressionProvider extends XmlSuppressionProvider {

@Override
public boolean isProviderAvailable(@NotNull PsiFile file) {
return true;
}

@Override
public boolean isSuppressedFor(@NotNull PsiElement element, @NotNull String inspectionId) {
if (inspectionId.equals("HtmlUnknownTag") && element instanceof XmlToken xmlToken && element.getNode().getElementType() == XmlTokenType.XML_NAME) {
String text = xmlToken.getText();

return text.startsWith("twig:")
&& UxUtil.getTwigComponentNames(element.getProject()).contains(text.substring(5));
}

return false;
}

@Override
public void suppressForFile(@NotNull PsiElement element, @NotNull String inspectionId) {

}

@Override
public void suppressForTag(@NotNull PsiElement element, @NotNull String inspectionId) {

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,44 @@ public static ElementPattern<PsiElement> getStringAfterTagNamePattern(@NotNull S
.withLanguage(TwigLanguage.INSTANCE);
}

/**
* "{% tagName foo"
* "{% tagName 'foo'"
*/
public static ElementPattern<PsiElement> getArgumentAfterTagNamePattern(@NotNull String tagName) {
return PlatformPatterns.or(
PlatformPatterns
.psiElement(TwigTokenTypes.IDENTIFIER)
.withParent(
PlatformPatterns.psiElement(TwigElementTypes.TAG)
)
.afterLeafSkipping(
PlatformPatterns.or(
PlatformPatterns.psiElement(TwigTokenTypes.LBRACE),
PlatformPatterns.psiElement(PsiWhiteSpace.class),
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE),
PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE),
PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE)
),
PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(tagName)
).withLanguage(TwigLanguage.INSTANCE),
PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT)
.withParent(
PlatformPatterns.psiElement(TwigElementTypes.TAG)
)
.afterLeafSkipping(
PlatformPatterns.or(
PlatformPatterns.psiElement(TwigTokenTypes.LBRACE),
PlatformPatterns.psiElement(PsiWhiteSpace.class),
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE),
PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE),
PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE)
),
PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(tagName)
).withLanguage(TwigLanguage.INSTANCE)
);
}

/**
* Check for {% if foo is "foo" %}
*/
Expand Down Expand Up @@ -733,38 +771,7 @@ public static ElementPattern<PsiElement> getFilterTagPattern() {
* {% trans_default_domain <carpet> %}
*/
public static ElementPattern<PsiElement> getTransDefaultDomainPattern() {
//noinspection unchecked
return PlatformPatterns.or(
PlatformPatterns
.psiElement(TwigTokenTypes.IDENTIFIER)
.withParent(
PlatformPatterns.psiElement(TwigElementTypes.TAG)
)
.afterLeafSkipping(
PlatformPatterns.or(
PlatformPatterns.psiElement(TwigTokenTypes.LBRACE),
PlatformPatterns.psiElement(PsiWhiteSpace.class),
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE),
PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE),
PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE)
),
PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("trans_default_domain")
).withLanguage(TwigLanguage.INSTANCE),
PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT)
.withParent(
PlatformPatterns.psiElement(TwigElementTypes.TAG)
)
.afterLeafSkipping(
PlatformPatterns.or(
PlatformPatterns.psiElement(TwigTokenTypes.LBRACE),
PlatformPatterns.psiElement(PsiWhiteSpace.class),
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE),
PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE),
PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE)
),
PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("trans_default_domain")
).withLanguage(TwigLanguage.INSTANCE)
);
return getArgumentAfterTagNamePattern("trans_default_domain");
}

/**
Expand Down Expand Up @@ -958,6 +965,27 @@ public static ElementPattern<PsiElement> getAutocompletableRoutePattern() {
;
}

/**
* "{{ component('<caret>'}) }}"
*/
public static ElementPattern<PsiElement> getComponentPattern() {
return PlatformPatterns
.psiElement(TwigTokenTypes.STRING_TEXT)
.afterLeafSkipping(
PlatformPatterns.or(
PlatformPatterns.psiElement(TwigTokenTypes.LBRACE),
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE),
PlatformPatterns.psiElement(PsiWhiteSpace.class),
PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE),
PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE)
),
PlatformPatterns.or(
PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("component")
)
)
.withLanguage(TwigLanguage.INSTANCE);
}

/**
* {{ asset('<caret>') }}
* {{ asset("<caret>") }}
Expand Down
Loading

0 comments on commit 7aad331

Please sign in to comment.