package org.angularjs.index; import com.intellij.lang.javascript.DialectDetector; import com.intellij.lang.javascript.psi.JSImplicitElementProvider; import com.intellij.lang.javascript.psi.JSQualifiedNameImpl; import com.intellij.lang.javascript.psi.impl.JSOffsetBasedImplicitElement; import com.intellij.lang.javascript.psi.resolve.JSResolveResult; import com.intellij.lang.javascript.psi.stubs.JSElementIndexingData; import com.intellij.lang.javascript.psi.stubs.JSImplicitElement; import com.intellij.lang.javascript.psi.stubs.impl.JSImplicitElementImpl; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootModificationTracker; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.ResolveResult; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.psi.stubs.StubIndexKey; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; import com.intellij.psi.util.ParameterizedCachedValue; import com.intellij.psi.util.ParameterizedCachedValueProvider; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.Function; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.FileBasedIndex; import com.intellij.util.indexing.ID; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.ConcurrentMap; /** * @author Dennis.Ushakov */ public class AngularIndexUtil { public static final int BASE_VERSION = 56; private static final ConcurrentMap<String, Key<ParameterizedCachedValue<Collection<String>, Pair<Project, ID<String, ?>>>>> ourCacheKeys = ContainerUtil.newConcurrentMap(); private static final AngularKeysProvider PROVIDER = new AngularKeysProvider(); public static final Function<JSImplicitElement, ResolveResult> JS_IMPLICIT_TO_RESOLVE_RESULT = JSResolveResult::new; public static JSImplicitElement resolve(final Project project, final StubIndexKey<String, JSImplicitElementProvider> index, final String lookupKey) { final Ref<JSImplicitElement> result = new Ref<>(null); final Processor<JSImplicitElement> processor = element -> { result.set(element); if (DialectDetector.isTypeScript(element)) { return false; } return true; }; multiResolve(project, index, lookupKey, processor); return result.get(); } public static void multiResolve(Project project, final StubIndexKey<String, JSImplicitElementProvider> index, final String lookupKey, final Processor<JSImplicitElement> processor) { final GlobalSearchScope scope = GlobalSearchScope.allScope(project); StubIndex.getInstance().processElements( index, lookupKey, project, scope, JSImplicitElementProvider.class, provider -> { final JSElementIndexingData indexingData = provider.getIndexingData(); if (indexingData != null) { final Collection<JSImplicitElement> elements = indexingData.getImplicitElements(); if (elements != null) { for (JSImplicitElement element : elements) { if (element.getName().equals(lookupKey) && ((index != AngularDirectivesIndex.KEY && index != AngularDirectivesDocIndex.KEY) || AngularJSIndexingHandler.isAngularRestrictions(element.getTypeString()))) { if (!processor.process(element)) return false; } } } } return true; } ); } public static ResolveResult[] multiResolveAngularNamedDefinitionIndex(@NotNull final Project project, @NotNull final ID<String, AngularNamedItemDefinition> INDEX, @NotNull final String id, @NotNull final Condition<VirtualFile> filter, boolean dirtyResolve) { final FileBasedIndex instance = FileBasedIndex.getInstance(); Collection<VirtualFile> files = instance.getContainingFiles(INDEX, id, GlobalSearchScope.allScope(project)); if (files.isEmpty()) return ResolveResult.EMPTY_ARRAY; final List<VirtualFile> filtered = ContainerUtil.filter(files, filter); if (filtered.isEmpty()) { if (!dirtyResolve) return ResolveResult.EMPTY_ARRAY; } else { files = filtered; } final List<JSImplicitElement> elements = new ArrayList<>(); for (VirtualFile file : files) { final List<AngularNamedItemDefinition> values = instance.getValues(INDEX, id, GlobalSearchScope.fileScope(project, file)); for (AngularNamedItemDefinition value : values) { JSQualifiedNameImpl qName = JSQualifiedNameImpl.fromQualifiedName(id); JSImplicitElementImpl.Builder elementBuilder = new JSImplicitElementImpl.Builder(qName, null); final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); if (psiFile != null) { elements.add(new JSOffsetBasedImplicitElement(elementBuilder, (int)value.getStartOffset(), psiFile)); } } } final List<ResolveResult> list = ContainerUtil.map(elements, JS_IMPLICIT_TO_RESOLVE_RESULT); return list.toArray(new ResolveResult[list.size()]); } public static Collection<String> getAllKeys(final ID<String, ?> index, final Project project) { final String indexId = index.getName(); final Key<ParameterizedCachedValue<Collection<String>, Pair<Project, ID<String, ?>>>> key = ConcurrencyUtil.cacheOrGet(ourCacheKeys, indexId, Key.create("angularjs.index." + indexId)); final Pair<Project, ID<String, ?>> pair = Pair.create(project, index); return CachedValuesManager.getManager(project).getParameterizedCachedValue(project, key, PROVIDER, false, pair); } public static boolean hasAngularJS(final Project project) { if (ApplicationManager.getApplication().isUnitTestMode() && "disabled".equals(System.getProperty("angular.js"))) return false; return getAngularJSVersion(project) > 0; } public static boolean hasAngularJS2(final Project project) { if (ApplicationManager.getApplication().isUnitTestMode() && "disabled".equals(System.getProperty("angular.js"))) return false; return getAngularJSVersion(project) >= 20; } private static int getAngularJSVersion(final Project project) { if (DumbService.isDumb(project)) return -1; return CachedValuesManager.getManager(project).getCachedValue(project, () -> { int version = -1; PsiElement resolve; if ((resolve = resolve(project, AngularDirectivesIndex.KEY, "ngFor")) != null) { version = 20; } else if ((resolve = resolve(project, AngularDirectivesIndex.KEY, "ng-messages")) != null) { version = 13; } else if ((resolve = resolve(project, AngularDirectivesIndex.KEY, "ng-model")) != null) { version = 12; } if (resolve != null) { return CachedValueProvider.Result.create(version, resolve.getContainingFile()); } return CachedValueProvider.Result .create(version, VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS, ProjectRootModificationTracker.getInstance(project)); }); } public static String convertRestrictions(final Project project, String restrictions) { if (AngularJSIndexingHandler.DEFAULT_RESTRICTIONS.equals(restrictions)) { return getAngularJSVersion(project) >= 13 ? "AE" : "A"; } return restrictions; } private static class AngularKeysProvider implements ParameterizedCachedValueProvider<Collection<String>, Pair<Project, ID<String, ?>>> { @Nullable @Override public CachedValueProvider.Result<Collection<String>> compute(final Pair<Project, ID<String, ?>> projectAndIndex) { final Project project = projectAndIndex.first; final ID<String, ?> id = projectAndIndex.second; final GlobalSearchScope scope = GlobalSearchScope.allScope(project); final FileBasedIndex fileIndex = FileBasedIndex.getInstance(); final StubIndex stubIndex = StubIndex.getInstance(); final Collection<String> allKeys = id instanceof StubIndexKey ? stubIndex.getAllKeys((StubIndexKey<String, ?>)id, project) : fileIndex.getAllKeys(id, project); return CachedValueProvider.Result.create(ContainerUtil.filter(allKeys, key -> id instanceof StubIndexKey ? !stubIndex.processElements((StubIndexKey<String, PsiElement>)id, key, project, scope, PsiElement.class, element -> false) : !fileIndex.processValues(id, key, null, (FileBasedIndex.ValueProcessor)(file, value) -> false, scope)), PsiManager.getInstance(project).getModificationTracker()); } } }