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

#2249 #2049 rebuild Twig constant completion and navigation and supporting enums #2346

Merged
merged 1 commit into from
Apr 7, 2024
Merged
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,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;
}
}
Loading