Skip to content

Commit

Permalink
#2249 #2049 rebuild Twig constant completion and navigation and suppo…
Browse files Browse the repository at this point in the history
…rting enums
  • Loading branch information
Haehnchen committed Apr 7, 2024
1 parent cdd0eff commit b586f00
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package fr.adrienbrault.idea.symfony2plugin.completion.insertHandler;

import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.editor.Document;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPsiElementPointer;
import com.jetbrains.php.lang.psi.elements.PhpClassMember;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

/**
* "Foo\Foo" => "Foo\\Foo"
*
* @author Daniel Espendiller <[email protected]>
*/
public class TwigEscapedSlashInsertHandler implements InsertHandler<LookupElement> {
private static final TwigEscapedSlashInsertHandler instance = new TwigEscapedSlashInsertHandler();

@Override
public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
if (!(item.getObject() instanceof SmartPsiElementPointer<?> smartPsiElementPointer)) {
return;
}

String classFqn = null;
String fieldName = null;

if (smartPsiElementPointer.getElement() instanceof PhpClassMember phpClassMember) {
classFqn = phpClassMember.getContainingClass().getFQN();
fieldName = phpClassMember.getName();
}

Document document = context.getDocument();
document.deleteString(context.getStartOffset(), context.getTailOffset());

String s = StringUtils.stripStart(classFqn, "\\").replace("\\", "\\\\") + "::" + fieldName;
document.insertString(context.getStartOffset(), s);
context.commitDocument();

PsiElement elementAt = context.getFile().findElementAt(context.getEditor().getCaretModel().getOffset());
if (elementAt == null) {
return;
}

context.getEditor().getCaretModel().moveCaretRelatively(s.length(), 0, false, false, true);
}

public static TwigEscapedSlashInsertHandler getInstance(){
return instance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import fr.adrienbrault.idea.symfony2plugin.asset.AssetDirectoryReader;
import fr.adrienbrault.idea.symfony2plugin.asset.provider.AssetCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.assetMapper.AssetMapperUtil;
import fr.adrienbrault.idea.symfony2plugin.completion.insertHandler.TwigEscapedSlashInsertHandler;
import fr.adrienbrault.idea.symfony2plugin.dic.MethodReferenceBag;
import fr.adrienbrault.idea.symfony2plugin.dic.ServiceCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
Expand Down Expand Up @@ -434,32 +435,7 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr
extend(
CompletionType.BASIC,
TwigPattern.getPrintBlockOrTagFunctionPattern("constant"),
new CompletionProvider<>() {
public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
if (!Symfony2ProjectComponent.isEnabled(position)) {
return;
}

PhpIndex instance = PhpIndex.getInstance(position.getProject());
for (String constant : instance.getAllConstantNames(PrefixMatcher.ALWAYS_TRUE)) {
resultSet.addElement(LookupElementBuilder.create(constant).withIcon(PhpIcons.CONSTANT));
}

int foo = parameters.getOffset() - position.getTextRange().getStartOffset();
String before = position.getText().substring(0, foo);
String[] parts = before.split("::");

if (parts.length >= 1) {
PhpClass phpClass = PhpElementsUtil.getClassInterface(position.getProject(), parts[0].replace("\\\\", "\\"));
if (phpClass != null) {
phpClass.getFields().stream().filter(Field::isConstant).forEach(field ->
resultSet.addElement(LookupElementBuilder.create(phpClass.getPresentableFQN().replace("\\", "\\\\") + "::" + field.getName()).withIcon(PhpIcons.CONSTANT))
);
}
}
}
}
new ConstantCompletionParametersCompletionProvider()
);

// {% e => {% extends '...'
Expand Down Expand Up @@ -1574,6 +1550,98 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull
}
}

/**
* {% constant('<caret>') %}
* {% constant('Foo\\<caret>') %}
* {% constant('FOO::<caret>') %}
*/
private static class ConstantCompletionParametersCompletionProvider extends CompletionProvider<CompletionParameters> {
public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
if (!Symfony2ProjectComponent.isEnabled(position)) {
return;
}

Project project = position.getProject();
PhpIndex instance = PhpIndex.getInstance(project);

PrefixMatcher prefixMatcher = resultSet.getPrefixMatcher();
String prefix = prefixMatcher.getPrefix();
if (prefix.contains(":")) {
// 'FOO::foo<caret>'
String[] parts = prefix.replace("::", ":").split(":");
String substring = prefix.substring(prefix.lastIndexOf(":") + 1);
CompletionResultSet completionResultSet = resultSet.withPrefixMatcher(substring);

PhpClass phpClass = PhpElementsUtil.getClassInterface(project, parts[0].replace("\\\\", "\\"));
if (phpClass != null) {
String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\");

for (PhpNamedElement item : getFieldsAndEnums(phpClass)) {
LookupElementBuilder element = LookupElementBuilder
.createWithSmartPointer(item.getName(), item)
.withTypeText(fqnNoLeadingSlash, true)
.withIcon(item.getIcon());

completionResultSet.addElement(element);
}
}
} else if (prefix.contains("\\")) {
// 'FOO\\Foo<caret>'
int i = prefix.lastIndexOf("\\");
String substring = "\\" + StringUtils.stripStart(prefix.substring(0, i).replace("\\\\", "\\"), "\\");
String pre = prefix.substring(prefix.lastIndexOf("\\") + 1);
CompletionResultSet completionResultSet = resultSet.withPrefixMatcher(pre);

for (PhpClass phpClass: PhpIndexUtil.getPhpClassInsideNamespace(project, substring)) {
String fqn = phpClass.getFQN().substring(substring.length());
String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\");

for (PhpNamedElement item : getFieldsAndEnums(phpClass)) {
LookupElementBuilder element = LookupElementBuilder
.create(fqn.replace("\\", "\\\\") + "::" + item.getName())
.withTypeText(fqnNoLeadingSlash, true)
.withIcon(item.getIcon());

completionResultSet.addElement(element);
}
}
} else {
// '<caret>'
for (String constant : instance.getAllConstantNames(prefixMatcher)) {
resultSet.addElement(LookupElementBuilder.create(constant).withIcon(PhpIcons.CONSTANT));
}

Collection<PhpClass> phpClasses = new ArrayList<>();
for (String className : instance.getAllClassNames(resultSet.getPrefixMatcher())) {
phpClasses.addAll(instance.getClassesByName(className));
}

for (PhpClass phpClass : phpClasses) {
String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\");
for (PhpNamedElement field : getFieldsAndEnums(phpClass)) {
LookupElementBuilder element = LookupElementBuilder
.createWithSmartPointer(phpClass.getName() + "::" + field.getName(), field)
.withInsertHandler(TwigEscapedSlashInsertHandler.getInstance())
.withTypeText(fqnNoLeadingSlash, true)
.withIcon(field.getIcon());

resultSet.addElement(element);
}
}
}
}

private static @NotNull Collection<PhpNamedElement> getFieldsAndEnums(@NotNull PhpClass phpClass) {
Collection<PhpNamedElement> items = new ArrayList<>();

items.addAll(Arrays.stream(phpClass.getOwnFields()).filter(field -> field.isConstant() && field.getModifier().isPublic()).toList());
items.addAll(phpClass.getEnumCases());

return items;
}
}

@NotNull
private Collection<LookupElement> processVariables(@NotNull PsiElement psiElement, @NotNull Predicate<PhpType> filter, @NotNull Function<Map.Entry<String, Pair<String, LookupElement>>, String> map) {
Project project = psiElement.getProject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpEnumCase;
import com.jetbrains.twig.TwigLanguage;
import com.jetbrains.twig.TwigTokenTypes;
import com.jetbrains.twig.elements.TwigBlockTag;
Expand All @@ -39,6 +40,7 @@
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -346,35 +348,35 @@ private Collection<PsiElement> getTranslationDomainGoto(@NotNull PsiElement psiE

@NotNull
private Collection<PsiElement> getConstantGoto(@NotNull PsiElement psiElement) {
Collection<PsiElement> targetPsiElements = new ArrayList<>();

String contents = psiElement.getText();
if(StringUtils.isBlank(contents)) {
return targetPsiElements;
return Collections.emptyList();
}

// global constant
if(!contents.contains(":")) {
targetPsiElements.addAll(PhpIndex.getInstance(psiElement.getProject()).getConstantsByName(contents));
return targetPsiElements;
return new ArrayList<>(PhpIndex.getInstance(psiElement.getProject()).getConstantsByName(contents));
}

// resolve class constants
String[] parts = contents.split("::");
if(parts.length != 2) {
return targetPsiElements;
return Collections.emptyList();
}

PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), parts[0].replace("\\\\", "\\"));
if(phpClass == null) {
return targetPsiElements;
if (phpClass == null) {
return Collections.emptyList();
}

Collection<PsiElement> targetPsiElements = new ArrayList<>();
Field field = phpClass.findFieldByName(parts[1], true);
if(field != null) {
targetPsiElements.add(field);
}

targetPsiElements.addAll(phpClass.getEnumCases().stream().filter(e -> parts[1].equals(e.getName())).toList());

return targetPsiElements;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,24 @@ public void testThatInlineVarProvidesClassCompletionDeprecated() {
assertCompletionContains(TwigFileType.INSTANCE, "{# bar F<caret> #}", "Foobar");
}

public void testThatConstantProvidesCompletionForClassAndDefine() {
public void testThatConstantProvidesCompletionForClassConstant() {
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('<caret>') }}", "CONST_FOO");
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('<caret>') }}", "FooConst::CAR", "FooEnum::FOOBAR");

assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\<caret>') }}", "\\\\Bike\\\\FooConst::CAR", "\\\\Bike\\\\FooEnum::FOOBAR");
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\<caret>') }}", "FooConst::CAR", "FooEnum::FOOBAR");
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\Foo<caret>') }}", "FooEnum::FOOBAR");

assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('\\\\App\\\\Bike\\\\Foo<caret>') }}", "FooEnum::FOOBAR");

assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\FooConst::C<caret>') }}", "CAR");
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\FooEnum::F<caret>') }}", "FOOBAR");
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('\\\\App\\\\Bike\\\\FooEnum::F<caret>') }}", "FOOBAR");

assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('<caret>') }}", "{{ constant('App\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "FooEnum::FOOBAR".equals(l.getLookupString()));
assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('App\\<caret>') }}", "{{ constant('App\\\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "\\\\Bike\\\\FooEnum::FOOBAR".equals(l.getLookupString()));
assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\Foo<caret>') }}", "{{ constant('App\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "FooEnum::FOOBAR".equals(l.getLookupString()));
assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\FooEnum::F<caret>') }}", "{{ constant('App\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "FOOBAR".equals(l.getLookupString()));
}

public void testCompletionForRoutingParameter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,14 @@ public void testSeeTagGotoRegexMatch() {
}
}

public void testThatConstantProvidesNavigation() {
public void testThatConstantAndEnumProvidesNavigation() {
assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('\\Foo\\ConstantBar\\Foo::F<caret>OO') }}", PlatformPatterns.psiElement(Field.class).withName("FOO"));
assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('\\\\Foo\\\\ConstantBar\\\\Foo::F<caret>OO') }}", PlatformPatterns.psiElement(Field.class).withName("FOO"));

assertNavigationMatch(TwigFileType.INSTANCE, "{% if foo == constant('\\Foo\\ConstantBar\\Foo::F<caret>OO') %}", PlatformPatterns.psiElement(Field.class).withName("FOO"));
assertNavigationMatch(TwigFileType.INSTANCE, "{% set foo == constant('\\Foo\\ConstantBar\\Foo::F<caret>OO') %}", PlatformPatterns.psiElement(Field.class).withName("FOO"));

assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('CONST<caret>_FOO') }}", PlatformPatterns.psiElement());
assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('\\\\App\\\\FooEnum::F<caret>OO') }}", PlatformPatterns.psiElement());
}

public void testTestControllerActionsProvidesReferences() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,23 @@ class Foobar2
}
}

namespace App\Bike
{
enum FooEnum
{
case FOOBAR;
case FOOBAR1;
case FOOBAR2;
case FOOBAR3;
}

class FooConst
{
public const CAR = '';
public const CAR1 = '';
public const CAR2 = '';
public const CAR3 = '';
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,16 @@ public function getTag();
class Alert
{
}
}
}

namespace App
{
enum FooEnum
{
case FOO;
case FOO1;

case BAR;
case BAR1;
}
}

0 comments on commit b586f00

Please sign in to comment.