Skip to content

Commit dfa4f0b

Browse files
committed
Added dedicated index of files that contain fragment definitions to provide completion of fragment names (#164)
1 parent 53f98cf commit dfa4f0b

File tree

3 files changed

+178
-21
lines changed

3 files changed

+178
-21
lines changed

resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102

103103
<!-- Indexing -->
104104
<fileBasedIndex implementation="com.intellij.lang.jsgraphql.ide.project.indexing.GraphQLIdentifierIndex" />
105+
<fileBasedIndex implementation="com.intellij.lang.jsgraphql.ide.project.indexing.GraphQLFragmentNameIndex" />
105106

106107
<!-- Startup -->
107108
<postStartupActivity implementation="com.intellij.lang.jsgraphql.endpoint.ide.startup.GraphQLStartupActivity" />

src/main/com/intellij/lang/jsgraphql/ide/project/GraphQLPsiSearchHelper.java

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.intellij.lang.jsgraphql.GraphQLLanguage;
2020
import com.intellij.lang.jsgraphql.GraphQLSettings;
2121
import com.intellij.lang.jsgraphql.ide.project.graphqlconfig.GraphQLConfigManager;
22+
import com.intellij.lang.jsgraphql.ide.project.indexing.GraphQLFragmentNameIndex;
2223
import com.intellij.lang.jsgraphql.ide.project.indexing.GraphQLIdentifierIndex;
2324
import com.intellij.lang.jsgraphql.ide.project.scopes.ConditionalGlobalSearchScope;
2425
import com.intellij.lang.jsgraphql.ide.references.GraphQLFindUsagesUtil;
@@ -42,10 +43,7 @@
4243
import com.intellij.psi.impl.PsiManagerImpl;
4344
import com.intellij.psi.search.GlobalSearchScope;
4445
import com.intellij.psi.search.GlobalSearchScopesCore;
45-
import com.intellij.psi.search.PsiSearchHelper;
46-
import com.intellij.psi.search.UsageSearchContext;
4746
import com.intellij.psi.search.scope.packageSet.NamedScope;
48-
import com.intellij.psi.util.PsiTreeUtil;
4947
import com.intellij.testFramework.LightVirtualFile;
5048
import com.intellij.util.Processor;
5149
import com.intellij.util.indexing.FileBasedIndex;
@@ -73,7 +71,6 @@ public class GraphQLPsiSearchHelper {
7371
private final Project myProject;
7472
private final GraphQLSettings mySettings;
7573
private final PluginDescriptor pluginDescriptor;
76-
private final Map<String, GraphQLFragmentDefinition> fragmentDefinitionsByName = Maps.newConcurrentMap();
7774
private final Map<String, GlobalSearchScope> fileNameToSchemaScope = Maps.newConcurrentMap();
7875
private final GlobalSearchScope searchScope;
7976
private final GlobalSearchScope allBuiltInSchemaScopes;
@@ -115,7 +112,6 @@ public GraphQLPsiSearchHelper(@NotNull final Project project) {
115112
@Override
116113
public void beforePsiChanged(boolean isPhysical) {
117114
// clear the cache on each PSI change
118-
fragmentDefinitionsByName.clear();
119115
fileNameToSchemaScope.clear();
120116
}
121117
});
@@ -190,23 +186,56 @@ public List<GraphQLFragmentDefinition> getKnownFragmentDefinitions(PsiElement sc
190186
// include the fragments in the currently edited scratch file
191187
schemaScope = schemaScope.union(GlobalSearchScope.fileScope(scopedElement.getContainingFile()));
192188
}
193-
// TODO JKM create fragment index
194-
PsiSearchHelper.getInstance(myProject).processElementsWithWord((psiElement, offsetInElement) -> {
195-
if (psiElement.getNode().getElementType() == GraphQLElementTypes.FRAGMENT_KEYWORD) {
196-
final GraphQLFragmentDefinition fragmentDefinition = PsiTreeUtil.getParentOfType(psiElement, GraphQLFragmentDefinition.class);
197-
if (fragmentDefinition != null && fragmentDefinition.getNameIdentifier() != null) {
198-
fragmentDefinitions.add(fragmentDefinition);
199-
}
189+
190+
final PsiManager psiManager = PsiManager.getInstance(myProject);
191+
192+
FileBasedIndex.getInstance().processFilesContainingAllKeys(GraphQLFragmentNameIndex.NAME, Collections.singleton(GraphQLFragmentNameIndex.HAS_FRAGMENTS), schemaScope, null, virtualFile -> {
193+
194+
final PsiFile psiFile = psiManager.findFile(virtualFile);
195+
if (psiFile != null) {
196+
final Ref<PsiRecursiveElementVisitor> identifierVisitor = Ref.create();
197+
identifierVisitor.set(new PsiRecursiveElementVisitor() {
198+
@Override
199+
public void visitElement(PsiElement element) {
200+
if (element instanceof GraphQLDefinition) {
201+
if (element instanceof GraphQLFragmentDefinition) {
202+
fragmentDefinitions.add((GraphQLFragmentDefinition) element);
203+
}
204+
return; // no need to visit deeper than definitions since fragments are top level
205+
} else if (element instanceof PsiLanguageInjectionHost) {
206+
if (visitLanguageInjectionHost((PsiLanguageInjectionHost) element, identifierVisitor)) {
207+
return;
208+
}
209+
}
210+
super.visitElement(element);
211+
}
212+
});
213+
psiFile.accept(identifierVisitor.get());
200214
}
201-
return true;
202-
}, schemaScope, "fragment", UsageSearchContext.IN_CODE, true, true);
215+
216+
return true; // process all known fragments
217+
});
203218
return fragmentDefinitions;
204219
} catch (IndexNotReadyException e) {
205220
// can't search yet (e.g. during project startup)
206221
}
207222
return Collections.emptyList();
208223
}
209224

225+
/**
226+
* Visits the potential GraphQL injection inside an injection host
227+
* @return true if the host contained GraphQL and was visited, false otherwise
228+
*/
229+
private boolean visitLanguageInjectionHost(PsiLanguageInjectionHost element, Ref<PsiRecursiveElementVisitor> identifierVisitor) {
230+
if (graphQLInjectionSearchHelper != null && graphQLInjectionSearchHelper.isJSGraphQLLanguageInjectionTarget(element)) {
231+
injectedLanguageManager.enumerateEx(element, element.getContainingFile(), false, (injectedPsi, places) -> {
232+
injectedPsi.accept(identifierVisitor.get());
233+
});
234+
return true;
235+
}
236+
return false;
237+
}
238+
210239
/**
211240
* Gets a resolved reference or null if no reference or resolved element is found
212241
*
@@ -230,8 +259,8 @@ public static GraphQLIdentifier getResolvedReference(GraphQLNamedElement psiElem
230259
/**
231260
* Processes GraphQL identifiers whose name matches the specified word within the given schema scope.
232261
* @param schemaScope the schema scope which limits the processing
233-
* @param word the word to match identifiers for
234-
* @param processor processor called for all GraphQL identifiers whose name match the specified word
262+
* @param word the word to match identifiers for
263+
* @param processor processor called for all GraphQL identifiers whose name match the specified word
235264
* @see GraphQLIdentifierIndex
236265
*/
237266
private void processElementsWithWordUsingIdentifierIndex(GlobalSearchScope schemaScope, String word, Processor<PsiNamedElement> processor) {
@@ -263,11 +292,8 @@ public void visitElement(PsiElement element) {
263292
graphQLFile.accept(identifierVisitor.get());
264293
}
265294
return; // no need to visit deeper
266-
} else if (element instanceof PsiLanguageInjectionHost && graphQLInjectionSearchHelper != null) {
267-
if (graphQLInjectionSearchHelper.isJSGraphQLLanguageInjectionTarget(element)) {
268-
injectedLanguageManager.enumerateEx(element, element.getContainingFile(), false, (injectedPsi, places) -> {
269-
injectedPsi.accept(identifierVisitor.get());
270-
});
295+
} else if (element instanceof PsiLanguageInjectionHost) {
296+
if (visitLanguageInjectionHost((PsiLanguageInjectionHost) element, identifierVisitor)) {
271297
return;
272298
}
273299
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) 2019-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
* <p>
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
package com.intellij.lang.jsgraphql.ide.project.indexing;
9+
10+
import com.intellij.lang.jsgraphql.GraphQLFileType;
11+
import com.intellij.lang.jsgraphql.ide.project.GraphQLInjectionSearchHelper;
12+
import com.intellij.lang.jsgraphql.ide.references.GraphQLFindUsagesUtil;
13+
import com.intellij.lang.jsgraphql.psi.GraphQLDefinition;
14+
import com.intellij.lang.jsgraphql.psi.GraphQLFragmentDefinition;
15+
import com.intellij.openapi.components.ServiceManager;
16+
import com.intellij.openapi.fileTypes.FileType;
17+
import com.intellij.openapi.util.Ref;
18+
import com.intellij.psi.*;
19+
import com.intellij.util.indexing.*;
20+
import com.intellij.util.io.BooleanDataDescriptor;
21+
import com.intellij.util.io.DataExternalizer;
22+
import com.intellij.util.io.EnumeratorStringDescriptor;
23+
import com.intellij.util.io.KeyDescriptor;
24+
import org.apache.commons.lang.StringUtils;
25+
import org.jetbrains.annotations.NotNull;
26+
27+
import java.util.Collections;
28+
import java.util.Set;
29+
30+
/**
31+
* Index for processing files that contain one or more GraphQL fragment definitions
32+
*/
33+
public class GraphQLFragmentNameIndex extends FileBasedIndexExtension<String, Boolean> {
34+
35+
public static final ID<String, Boolean> NAME = ID.create("GraphQLFragmentNameIndex");
36+
37+
public static final String HAS_FRAGMENTS = "fragments";
38+
39+
40+
private final GraphQLInjectionSearchHelper graphQLInjectionSearchHelper;
41+
42+
private final Set<FileType> includedFileTypes;
43+
44+
private final DataIndexer<String, Boolean, FileContent> myDataIndexer;
45+
46+
public GraphQLFragmentNameIndex() {
47+
myDataIndexer = inputData -> {
48+
49+
final Ref<Boolean> hasFragments = Ref.create(false);
50+
51+
final Ref<PsiRecursiveElementVisitor> identifierVisitor = Ref.create();
52+
identifierVisitor.set(new PsiRecursiveElementVisitor() {
53+
@Override
54+
public void visitElement(PsiElement element) {
55+
if (hasFragments.get()) {
56+
// done
57+
return;
58+
}
59+
if (element instanceof GraphQLDefinition) {
60+
if (element instanceof GraphQLFragmentDefinition) {
61+
hasFragments.set(true);
62+
}
63+
return; // no need to visit deeper than definitions since fragments are top level
64+
} else if (element instanceof PsiLanguageInjectionHost && graphQLInjectionSearchHelper != null) {
65+
if (graphQLInjectionSearchHelper.isJSGraphQLLanguageInjectionTarget(element)) {
66+
final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(element.getProject());
67+
final String graphqlBuffer = StringUtils.strip(element.getText(), "` \t\n");
68+
final PsiFile graphqlInjectedPsiFile = psiFileFactory.createFileFromText("", GraphQLFileType.INSTANCE, graphqlBuffer, 0, false, false);
69+
graphqlInjectedPsiFile.accept(identifierVisitor.get());
70+
return;
71+
}
72+
}
73+
super.visitElement(element);
74+
}
75+
});
76+
77+
inputData.getPsiFile().accept(identifierVisitor.get());
78+
79+
if (hasFragments.get()) {
80+
return Collections.singletonMap(HAS_FRAGMENTS, true);
81+
} else {
82+
return Collections.emptyMap();
83+
}
84+
85+
};
86+
includedFileTypes = GraphQLFindUsagesUtil.getService().getIncludedFileTypes();
87+
graphQLInjectionSearchHelper = ServiceManager.getService(GraphQLInjectionSearchHelper.class);
88+
}
89+
90+
@NotNull
91+
@Override
92+
public ID<String, Boolean> getName() {
93+
return NAME;
94+
}
95+
96+
@NotNull
97+
@Override
98+
public DataIndexer<String, Boolean, FileContent> getIndexer() {
99+
return myDataIndexer;
100+
}
101+
102+
@NotNull
103+
@Override
104+
public KeyDescriptor<String> getKeyDescriptor() {
105+
return new EnumeratorStringDescriptor();
106+
}
107+
108+
@NotNull
109+
@Override
110+
public DataExternalizer<Boolean> getValueExternalizer() {
111+
return BooleanDataDescriptor.INSTANCE;
112+
}
113+
114+
@Override
115+
public int getVersion() {
116+
return 0;
117+
}
118+
119+
@NotNull
120+
@Override
121+
public FileBasedIndex.InputFilter getInputFilter() {
122+
return file -> includedFileTypes.contains(file.getFileType());
123+
}
124+
125+
@Override
126+
public boolean dependsOnFileContent() {
127+
return true;
128+
}
129+
130+
}

0 commit comments

Comments
 (0)