Skip to content

Commit

Permalink
#266 provide attribute alias support for completion
Browse files Browse the repository at this point in the history
  • Loading branch information
Haehnchen committed Mar 24, 2023
1 parent 496cb30 commit c771e1d
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 3 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,31 @@ class NotBlank extends Constraint {}

`Languages & Framework > PHP > Annotations -> Use Alias`

##### Annotations

```php

use Doctrine\ORM\Mapping as ORM;

/**
* @Id() -> @ORM\Id()
* @NotBlank() -> @Assert\NotBlank()
*/
class Foo {}
```

##### Attributes


```php

use Doctrine\ORM\Mapping as ORM;

#[Id] -> #[ORM\Id()]
#[NotBlank] -> #[Assert\NotBlank()]
class Foo {}
```

#### Class LineMarker

LineMarker which provide navigation to annotation class usages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ProcessingContext;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.php.PhpIcons;
import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.inspections.attributes.PhpClassCantBeUsedAsAttributeInspection;
import com.jetbrains.php.lang.inspections.attributes.PhpInapplicableAttributeTargetDeclarationInspection;
import com.jetbrains.php.lang.lexer.PhpTokenTypes;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import com.jetbrains.php.lang.psi.stubs.indexes.PhpAttributesFQNsIndex;
import de.espend.idea.php.annotation.ApplicationSettings;
import de.espend.idea.php.annotation.completion.insert.AnnotationTagInsertHandler;
import de.espend.idea.php.annotation.completion.insert.AttributeAliasInsertHandler;
import de.espend.idea.php.annotation.completion.lookupelements.PhpAnnotationPropertyLookupElement;
import de.espend.idea.php.annotation.completion.lookupelements.PhpClassAnnotationLookupElement;
import de.espend.idea.php.annotation.dict.*;
Expand All @@ -32,8 +38,8 @@
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
Expand All @@ -46,6 +52,9 @@ public AnnotationCompletionContributor() {
// * @<caret>
extend(CompletionType.BASIC, AnnotationPattern.getDocBlockTag(), new PhpDocBlockTagAnnotations());

// #[<caret>] but only provide alias feature
extend(CompletionType.BASIC, AnnotationPattern.getAttributeNamePattern(), new PhpAttributeAlias());

// @Callback("", <caret>)
extend(CompletionType.BASIC, AnnotationPattern.getDocAttribute(), new PhpDocAttributeList());
extend(CompletionType.BASIC, AnnotationPattern.getTextIdentifier(), new PhpDocAttributeValue());
Expand Down Expand Up @@ -372,6 +381,85 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters
}
}

/**
* Extends attribute completion, but only for alias
*/
private static class PhpAttributeAlias extends CompletionProvider<CompletionParameters> {
protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) {
PsiElement psiElement = completionParameters.getOriginalPosition();
if(psiElement == null) {
return;
}

PhpAttributesList parentOfType = PsiTreeUtil.getParentOfType(psiElement, PhpAttributesList.class);
if(parentOfType == null) {
return;
}

attachLookupElements(psiElement.getProject(), parentOfType, completionResultSet);
}

private void attachLookupElements(@NotNull Project project, @NotNull PhpAttributesList phpAttributesList, @NotNull CompletionResultSet completionResultSet) {
Map<String, String> items = new HashMap<>();
for (UseAliasOption useAliasOption : ApplicationSettings.getUseAliasOptionsWithDefaultFallback()) {
items.put(useAliasOption.getAlias(), useAliasOption.getClassName());
}

items.putAll(getUseAsMap(phpAttributesList));

for (String fqnClass: FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) {
if(!fqnClass.startsWith("\\")) {
fqnClass = "\\" + fqnClass;
}

// attach class also "@ORM\Entity" if there is not import but an alias via settings
for (Map.Entry<String, String> aliasFqn : items.entrySet()) {
String className = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\") + "\\";

if (!fqnClass.startsWith(className)) {
continue;
}

String substring = fqnClass.substring(className.length());

String lookupString = aliasFqn.getKey() + "\\" + substring;

PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass);
if (underlyingClass != null) {
// check if Attribute is target allowed for context
// @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement
List<PhpAttribute> rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList());
if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) {
PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString);
phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance());
completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement);
}
}
}
}
}

private static Map<String, String> getUseAsMap(@NotNull PsiElement phpDocComment) {
PhpPsiElement scope = PhpCodeInsightUtil.findScopeForUseOperator(phpDocComment);
if(scope == null) {
return Collections.emptyMap();
}

Map<String, String> useImports = new HashMap<>();

for (PhpUseList phpUseList : PhpCodeInsightUtil.collectImports(scope)) {
for(PhpUse phpUse : phpUseList.getDeclarations()) {
String alias = phpUse.getAliasName();
if (alias != null) {
useImports.put(alias, phpUse.getFQN());
}
}
}

return useImports;
}
}

private static class PhpDocBlockTagAnnotations extends CompletionProvider<CompletionParameters> {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupEleme
/**
* Insert class alias before PhpStorm tries to import a new use statement "\Foo\Bar as Car"
*/
private void preAliasInsertion(@NotNull InsertionContext context, @NotNull LookupElement lookupElement) {
public static void preAliasInsertion(@NotNull InsertionContext context, @NotNull LookupElement lookupElement) {
Collection<UseAliasOption> importsAliases = AnnotationUtil.getActiveImportsAliasesFromSettings();
if(importsAliases.size() == 0) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package de.espend.idea.php.annotation.completion.insert;

import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiUtilCore;
import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
import com.jetbrains.php.completion.insert.PhpInsertHandlerUtil;
import com.jetbrains.php.completion.insert.PhpReferenceInsertHandler;
import com.jetbrains.php.lang.psi.elements.PhpPsiElement;
import de.espend.idea.php.annotation.ApplicationSettings;
import de.espend.idea.php.annotation.completion.lookupelements.PhpClassAnnotationLookupElement;
import de.espend.idea.php.annotation.util.PhpElementsUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AttributeAliasInsertHandler implements InsertHandler<LookupElement> {

private static final AttributeAliasInsertHandler instance = new AttributeAliasInsertHandler();

public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement lookupElement) {
// "ORM\Entity"
if (lookupElement instanceof PhpClassAnnotationLookupElement lookupElement1 && ((PhpClassAnnotationLookupElement) lookupElement).getAlias() != null) {
PsiElement element = PsiUtilCore.getElementAtOffset(context.getFile(), context.getStartOffset());
PhpPsiElement scopeForUseOperator = PhpCodeInsightUtil.findScopeForUseOperator(element);

PhpElementsUtil.insertUseIfNecessary(scopeForUseOperator, "\\" + StringUtils.stripStart(lookupElement1.getAlias().getClassName(), "\\"), lookupElement1.getAlias().getAlias());
PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(context.getDocument());
} else {

// find alias in settings "\Foo\Bar as Car" for given PhpClass insertion context
AnnotationTagInsertHandler.preAliasInsertion(context, lookupElement);

// reuse jetbrains "use importer": this is private only so we need some workaround
// to not implement your own algo for that
PhpReferenceInsertHandler.getInstance().handleInsert(context, lookupElement);
}

// force "#[Foo]" => "#[Foo(<caret>)]"
if(ApplicationSettings.getInstance().appendRoundBracket && !PhpInsertHandlerUtil.isStringAtCaret(context.getEditor(), "(")) {
PhpInsertHandlerUtil.insertStringAtCaret(context.getEditor(), "()");
context.getEditor().getCaretModel().moveCaretRelatively(-1, 0, false, false, true);
}
}

public static AttributeAliasInsertHandler getInstance(){
return instance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public static ElementPattern<PsiElement> getDocBlockTag() {
);
}

public static ElementPattern<PsiElement> getAttributeNamePattern() {
return PlatformPatterns.psiElement(PhpTokenTypes.IDENTIFIER)
.withParent(
PlatformPatterns.psiElement(ClassReference.class).withParent(PhpAttribute.class)
);
}

/**
* fire on: @Callback(<completion>), @Callback("", <completion>)
* * @ORM\Column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
import com.jetbrains.php.completion.PhpLanguagesAttributeCompletionContributor;
import com.jetbrains.php.lang.PhpLanguage;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.elements.*;
import com.jetbrains.php.lang.psi.stubs.indexes.PhpAttributeIndex;
import com.jetbrains.php.refactoring.PhpAliasImporter;
import de.espend.idea.php.annotation.dict.AnnotationTarget;
import org.apache.commons.lang.StringUtils;
Expand Down Expand Up @@ -64,6 +66,23 @@ static public AnnotationTarget findAnnotationTarget(@Nullable PhpDocComment phpD
return null;
}

static public AnnotationTarget findAttributeTarget(@NotNull PhpAttributesList phpAttributesList) {
PsiElement parent = phpAttributesList.getParent();
if (parent instanceof Method) {
return AnnotationTarget.METHOD;
}

if(parent.getNode().getElementType() == PhpElementTypes.CLASS_FIELDS) {
return AnnotationTarget.PROPERTY;
}

if(parent instanceof PhpClass) {
return AnnotationTarget.CLASS;
}

return null;
}

@Nullable
public static <T extends PsiElement> T getPrevSiblingOfPatternMatch(@Nullable PsiElement sibling, ElementPattern<T> pattern) {
if (sibling == null) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,4 +445,26 @@ public void testTheInternalAliasProvideCompletionAndImportsWithAlreadyImported()
lookupElement -> "ORM\\Entity".equals(lookupElement.getLookupString())
);
}

public void testTheInternalAliasProvideCompletionAndImportsForAttributes() {
assertCompletionResultEquals(PhpFileType.INSTANCE, "<?php\n" +
"namespace {\n" +
" class Foo {\n" +
" #[<caret>]\n" +
" function foo() {}\n" +
" }\n" +
"}",
"<?php\n" +
"namespace {\n" +
"\n" +
" use Doctrine\\ORM\\Mapping as ORM;\n" +
"\n" +
" class Foo {\n" +
" #[ORM\\Entity()]\n" +
" function foo() {}\n" +
" }\n" +
"}",
lookupElement -> "ORM\\Entity".equals(lookupElement.getLookupString())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @Attribute("has_access", type="bool"),
* )
*/
#[\Attribute(\Attribute::TARGET_ALL)]
class All
{
/**
Expand Down Expand Up @@ -53,6 +54,7 @@ class All
* @Target("CLASS")
* @deprecated
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AClazzDeprecated
{
}
Expand All @@ -61,6 +63,7 @@ class AClazzDeprecated
* @Annotation
* @Target("CLASS")
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class Clazz
{
}
Expand All @@ -70,6 +73,7 @@ class Clazz
* @Target("CLASS")
* @deprecated
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class ClazzDeprecated
{
}
Expand All @@ -78,6 +82,7 @@ class ClazzDeprecated
* @Annotation
* @Target("PROPERTY")
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Property
{
}
Expand All @@ -86,6 +91,7 @@ class Property
* @Annotation
* @Target("METHOD")
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
class Method
{
}
Expand All @@ -101,5 +107,6 @@ class Constants
/**
* @Annotation
*/
#[\Attribute]
class Entity {}
}

0 comments on commit c771e1d

Please sign in to comment.