|
3 | 3 | import com.intellij.codeInsight.completion.*; |
4 | 4 | import com.intellij.codeInsight.lookup.LookupElementBuilder; |
5 | 5 | import com.intellij.openapi.project.Project; |
| 6 | +import com.intellij.openapi.util.Key; |
6 | 7 | import com.intellij.patterns.ElementPattern; |
7 | 8 | import com.intellij.patterns.PlatformPatterns; |
8 | 9 | import com.intellij.psi.PsiElement; |
9 | 10 | import com.intellij.psi.PsiWhiteSpace; |
| 11 | +import com.intellij.psi.util.CachedValue; |
| 12 | +import com.intellij.psi.util.CachedValueProvider; |
| 13 | +import com.intellij.psi.util.CachedValuesManager; |
10 | 14 | import com.intellij.psi.util.PsiTreeUtil; |
11 | 15 | import com.intellij.util.ProcessingContext; |
12 | 16 | import com.intellij.util.indexing.FileBasedIndex; |
|
46 | 50 | * @author Daniel Espendiller <daniel@espendiller.net> |
47 | 51 | */ |
48 | 52 | public class AnnotationCompletionContributor extends CompletionContributor { |
| 53 | + private static final Key<CachedValue<Map<String, Collection<String>>>> ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE = new Key<>("ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE"); |
49 | 54 |
|
50 | 55 | private static final ElementPattern<PsiElement> DOC_IDENTIFIER_PATTERN = |
51 | 56 | PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_IDENTIFIER); |
@@ -411,36 +416,73 @@ private void attachLookupElements(@NotNull Project project, @NotNull PhpAttribut |
411 | 416 |
|
412 | 417 | items.putAll(getUseAsMap(phpAttributesList)); |
413 | 418 |
|
414 | | - for (String fqnClass: FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) { |
415 | | - if(!fqnClass.startsWith("\\")) { |
416 | | - fqnClass = "\\" + fqnClass; |
| 419 | + Map<String, Collection<String>> attributesByNamespace = getAttributeFqnsByNamespace(project); |
| 420 | + |
| 421 | + for (Map.Entry<String, String> aliasFqn : items.entrySet()) { |
| 422 | + String namespace = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\"); |
| 423 | + Collection<String> fqns = attributesByNamespace.get(namespace); |
| 424 | + if (fqns == null) { |
| 425 | + continue; |
417 | 426 | } |
418 | 427 |
|
419 | | - // attach class also "@ORM\Entity" if there is not import but an alias via settings |
420 | | - for (Map.Entry<String, String> aliasFqn : items.entrySet()) { |
421 | | - String className = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\") + "\\"; |
| 428 | + for (String fqnClass : fqns) { |
| 429 | + String substring = fqnClass.substring(namespace.length() + 1); |
| 430 | + String lookupString = aliasFqn.getKey() + "\\" + substring; |
422 | 431 |
|
423 | | - if (!fqnClass.startsWith(className)) { |
| 432 | + PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass); |
| 433 | + if (underlyingClass == null) { |
424 | 434 | continue; |
425 | 435 | } |
426 | 436 |
|
427 | | - String substring = fqnClass.substring(className.length()); |
| 437 | + // check if Attribute is target allowed for context |
| 438 | + // @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement |
| 439 | + List<PhpAttribute> rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList()); |
| 440 | + if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) { |
| 441 | + PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString); |
| 442 | + phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance()); |
| 443 | + completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement); |
| 444 | + } |
| 445 | + } |
| 446 | + } |
| 447 | + } |
428 | 448 |
|
429 | | - String lookupString = aliasFqn.getKey() + "\\" + substring; |
| 449 | + /** |
| 450 | + * Cache attribute FQNs by namespace prefix so alias completion can query only relevant branches. |
| 451 | + * |
| 452 | + * Example key: "\Doctrine\ORM\Mapping" => ["\Doctrine\ORM\Mapping\Entity", ...] |
| 453 | + */ |
| 454 | + @NotNull |
| 455 | + private static Map<String, Collection<String>> getAttributeFqnsByNamespace(@NotNull Project project) { |
| 456 | + return CachedValuesManager.getManager(project).getCachedValue( |
| 457 | + project, |
| 458 | + ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE, |
| 459 | + () -> { |
| 460 | + Map<String, Collection<String>> items = new HashMap<>(); |
| 461 | + |
| 462 | + for (String fqnClass : FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) { |
| 463 | + if (!fqnClass.startsWith("\\")) { |
| 464 | + fqnClass = "\\" + fqnClass; |
| 465 | + } |
430 | 466 |
|
431 | | - PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass); |
432 | | - if (underlyingClass != null) { |
433 | | - // check if Attribute is target allowed for context |
434 | | - // @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement |
435 | | - List<PhpAttribute> rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList()); |
436 | | - if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) { |
437 | | - PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString); |
438 | | - phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance()); |
439 | | - completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement); |
| 467 | + int index = fqnClass.indexOf("\\", 1); |
| 468 | + while (index > 0) { |
| 469 | + items.computeIfAbsent(fqnClass.substring(0, index), key -> new ArrayList<>()).add(fqnClass); |
| 470 | + index = fqnClass.indexOf("\\", index + 1); |
440 | 471 | } |
441 | 472 | } |
442 | | - } |
443 | | - } |
| 473 | + |
| 474 | + Map<String, Collection<String>> immutableItems = new HashMap<>(); |
| 475 | + for (Map.Entry<String, Collection<String>> entry : items.entrySet()) { |
| 476 | + immutableItems.put(entry.getKey(), List.copyOf(entry.getValue())); |
| 477 | + } |
| 478 | + |
| 479 | + return CachedValueProvider.Result.create( |
| 480 | + Collections.unmodifiableMap(immutableItems), |
| 481 | + AnnotationUtil.getModificationTrackerForIndexId(project, PhpAttributesFQNsIndex.KEY) |
| 482 | + ); |
| 483 | + }, |
| 484 | + false |
| 485 | + ); |
444 | 486 | } |
445 | 487 |
|
446 | 488 | private static Map<String, String> getUseAsMap(@NotNull PsiElement phpDocComment) { |
|
0 commit comments