Skip to content

Commit 1b508b3

Browse files
committed
Merge branch 'issue-74-relay-modern'
2 parents d295b28 + 8e5252f commit 1b508b3

File tree

6 files changed

+218
-2
lines changed

6 files changed

+218
-2
lines changed

resources/META-INF/plugin.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<idea-plugin version="2">
1111
<id>com.intellij.lang.jsgraphql</id>
1212
<name>JS GraphQL</name>
13-
<version>1.5.4</version>
13+
<version>1.6.0</version>
1414
<vendor>Jim Kynde Meyer - [email protected]</vendor>
1515

1616
<description><![CDATA[
@@ -29,6 +29,7 @@
2929

3030
<change-notes><![CDATA[
3131
<ul>
32+
<li>1.6.0: Support for Relay Modern fragments.</li>
3233
<li>1.5.4: Only show the error console automatically on the first error in the project. Fixes Int variables being sent as floats. Fixes auto-import is not placed on a new line in JS files with GraphQL templates.</li>
3334
<li>1.5.3: Support Relay Modern graphql.experimental tag.</li>
3435
<li>1.5.2: Pass "variables" in payload to GraphQL server as JSON.</li>
@@ -68,6 +69,7 @@
6869
<projectService serviceInterface="com.intellij.lang.jsgraphql.schema.ide.project.JSGraphQLSchemaLanguageProjectService" serviceImplementation="com.intellij.lang.jsgraphql.schema.ide.project.JSGraphQLSchemaLanguageProjectService" />
6970
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.configuration.JSGraphQLConfigurationProvider" serviceImplementation="com.intellij.lang.jsgraphql.ide.configuration.JSGraphQLConfigurationProvider" />
7071
<projectService serviceInterface="com.intellij.lang.jsgraphql.endpoint.ide.project.JSGraphQLEndpointNamedTypeRegistry" serviceImplementation="com.intellij.lang.jsgraphql.endpoint.ide.project.JSGraphQLEndpointNamedTypeRegistry" />
72+
<projectService serviceInterface="com.intellij.lang.jsgraphql.ide.project.JSGraphQLPsiSearchHelper" serviceImplementation="com.intellij.lang.jsgraphql.ide.project.JSGraphQLPsiSearchHelper" />
7173

7274

7375
<!-- Spellchecking and to-do view-->

src/main/com/intellij/lang/jsgraphql/ide/annotator/JSGraphQLAnnotator.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.intellij.lang.annotation.AnnotationHolder;
1212
import com.intellij.lang.annotation.ExternalAnnotator;
1313
import com.intellij.lang.annotation.HighlightSeverity;
14+
import com.intellij.lang.injection.InjectedLanguageManager;
1415
import com.intellij.lang.javascript.psi.JSFile;
1516
import com.intellij.lang.javascript.psi.ecma6.JSStringTemplateExpression;
1617
import com.intellij.lang.jsgraphql.ide.project.JSGraphQLLanguageUIProjectService;
@@ -20,7 +21,9 @@
2021
import com.intellij.lang.jsgraphql.languageservice.api.Annotation;
2122
import com.intellij.lang.jsgraphql.languageservice.api.AnnotationsResponse;
2223
import com.intellij.lang.jsgraphql.languageservice.api.Pos;
24+
import com.intellij.lang.jsgraphql.psi.JSGraphQLErrorContextAware;
2325
import com.intellij.lang.jsgraphql.psi.JSGraphQLFile;
26+
import com.intellij.lang.jsgraphql.psi.JSGraphQLPsiElement;
2427
import com.intellij.lang.jsgraphql.schema.psi.JSGraphQLSchemaFile;
2528
import com.intellij.openapi.diagnostic.Logger;
2629
import com.intellij.openapi.editor.Editor;
@@ -109,6 +112,12 @@ public void visitElement(PsiElement element) {
109112
HighlightSeverity severity = "error".equals(annotation.getSeverity()) ? HighlightSeverity.ERROR : HighlightSeverity.WARNING;
110113
if (fromOffset < toOffset) {
111114
final String message = StringUtils.substringBefore(annotation.getMessage(), "\n");
115+
final PsiElement errorElement = getPsiElementAtErrorOffset(file, fromOffset);
116+
if(errorElement instanceof JSGraphQLErrorContextAware) {
117+
if(!((JSGraphQLErrorContextAware) errorElement).isErrorInContext(message)) {
118+
continue;
119+
}
120+
}
112121
holder.createAnnotation(severity, TextRange.create(fromOffset, toOffset), message);
113122
errors.add(new JSGraphQLErrorResult(message, fileName, annotation.getSeverity(), from.line+1, from.column+1)); // +1 is for UI lines/columns
114123
}
@@ -128,6 +137,22 @@ public void visitElement(PsiElement element) {
128137

129138
// --- implementation ----
130139

140+
private PsiElement getPsiElementAtErrorOffset(PsiFile psiFile, int offset) {
141+
PsiElement element = psiFile.findElementAt(offset);
142+
if(element instanceof JSGraphQLPsiElement) {
143+
return element;
144+
} else if(element != null) {
145+
// error is potentially inside injected GraphQL
146+
final PsiElement injectedElement = InjectedLanguageManager.getInstance(psiFile.getProject()).findInjectedElementAt(psiFile, offset);
147+
if(injectedElement instanceof JSGraphQLPsiElement) {
148+
return injectedElement;
149+
} else if(injectedElement != null && injectedElement.getParent() instanceof JSGraphQLPsiElement) {
150+
return injectedElement.getParent();
151+
}
152+
}
153+
return null;
154+
}
155+
131156
private CharSequence getWhitespacePaddedGraphQL(PsiFile psiFile, CharSequence buffer) {
132157
// find the template expressions in the file
133158
Collection<JSStringTemplateExpression> stringTemplateExpressions = PsiTreeUtil.collectElementsOfType(psiFile, JSStringTemplateExpression.class);

src/main/com/intellij/lang/jsgraphql/ide/completion/JSGraphQLCompletionContributor.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.intellij.lang.jsgraphql.JSGraphQLTokenTypes;
1717
import com.intellij.lang.jsgraphql.icons.JSGraphQLIcons;
1818
import com.intellij.lang.jsgraphql.ide.injection.JSGraphQLLanguageInjectionUtil;
19+
import com.intellij.lang.jsgraphql.ide.project.JSGraphQLPsiSearchHelper;
1920
import com.intellij.lang.jsgraphql.languageservice.JSGraphQLNodeLanguageServiceClient;
2021
import com.intellij.lang.jsgraphql.languageservice.api.Hint;
2122
import com.intellij.lang.jsgraphql.languageservice.api.HintsResponse;
@@ -144,6 +145,21 @@ protected void addCompletions(@NotNull final CompletionParameters parameters, Pr
144145

145146
result.addElement(element);
146147
}
148+
149+
if(isFragmentSpreadCompletion && (hints.getHints() == null || hints.getHints().isEmpty())) {
150+
151+
// also complete on fragments across files
152+
final List<JSGraphQLFragmentDefinitionPsiElement> knownFragmentDefinitions = JSGraphQLPsiSearchHelper.getService(project).getKnownFragmentDefinitions();
153+
for (JSGraphQLFragmentDefinitionPsiElement fragmentDefinition : knownFragmentDefinitions) {
154+
final String fragmentName = fragmentDefinition.getName();
155+
if(fragmentName != null) {
156+
final JSGraphQLNamedTypePsiElement fragmentOnType = fragmentDefinition.getFragmentOnType();
157+
final String tailText = fragmentOnType != null ? " - fragment " + fragmentName + " on " + fragmentOnType.getName() : "";
158+
result.addElement(LookupElementBuilder.create(fragmentName).bold().withIcon(JSGraphQLIcons.Schema.Fragment).withTailText(tailText));
159+
}
160+
}
161+
162+
}
147163
}
148164

149165
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Copyright (c) 2015-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
*
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;
9+
10+
11+
import com.google.common.collect.Lists;
12+
import com.google.common.collect.Maps;
13+
import com.intellij.lang.jsgraphql.JSGraphQLTokenTypes;
14+
import com.intellij.lang.jsgraphql.ide.findUsages.JSGraphQLFindUsagesUtil;
15+
import com.intellij.lang.jsgraphql.psi.JSGraphQLFragmentDefinitionPsiElement;
16+
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedTypePsiElement;
17+
import com.intellij.openapi.components.ServiceManager;
18+
import com.intellij.openapi.fileTypes.FileType;
19+
import com.intellij.openapi.project.IndexNotReadyException;
20+
import com.intellij.openapi.project.Project;
21+
import com.intellij.openapi.util.Ref;
22+
import com.intellij.psi.impl.AnyPsiChangeListener;
23+
import com.intellij.psi.impl.PsiManagerImpl;
24+
import com.intellij.psi.search.GlobalSearchScope;
25+
import com.intellij.psi.search.PsiSearchHelper;
26+
import com.intellij.psi.search.UsageSearchContext;
27+
import org.jetbrains.annotations.NotNull;
28+
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Map;
32+
33+
/**
34+
* Enables cross-file searches for PSI references and fragment completion
35+
*/
36+
public class JSGraphQLPsiSearchHelper {
37+
38+
private static final FileType[] FILE_TYPES = JSGraphQLFindUsagesUtil.INCLUDED_FILE_TYPES.toArray(new FileType[JSGraphQLFindUsagesUtil.INCLUDED_FILE_TYPES.size()]);
39+
40+
private final Project myProject;
41+
private final Map<String, JSGraphQLNamedTypePsiElement> fragmentDefinitionsByName = Maps.newConcurrentMap();
42+
private final GlobalSearchScope searchScope;
43+
44+
public static JSGraphQLPsiSearchHelper getService(@NotNull Project project) {
45+
return ServiceManager.getService(project, JSGraphQLPsiSearchHelper.class);
46+
}
47+
48+
49+
public JSGraphQLPsiSearchHelper(@NotNull final Project project) {
50+
myProject = project;
51+
searchScope = GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.projectScope(myProject), FILE_TYPES);
52+
project.getMessageBus().connect().subscribe(PsiManagerImpl.ANY_PSI_CHANGE_TOPIC, new AnyPsiChangeListener.Adapter() {
53+
@Override
54+
public void beforePsiChanged(boolean isPhysical) {
55+
// clear the cache on each PSI change
56+
fragmentDefinitionsByName.clear();
57+
}
58+
});
59+
}
60+
61+
/**
62+
* Gets the fragment name element that is the source of a fragment usage by searching across files
63+
*
64+
* @param fragmentUsage a specific fragment usage, e.g. '...FragmentName'
65+
* @return the fragment definition that the usage references, e.g. 'fragment FragmentName'
66+
*/
67+
public JSGraphQLNamedTypePsiElement resolveFragmentReference(@NotNull JSGraphQLNamedTypePsiElement fragmentUsage) {
68+
final String fragmentName = fragmentUsage.getName();
69+
if (fragmentName != null) {
70+
final JSGraphQLNamedTypePsiElement cachedResult = fragmentDefinitionsByName.get(fragmentName);
71+
if (cachedResult != null) {
72+
return cachedResult;
73+
}
74+
final Ref<JSGraphQLNamedTypePsiElement> fragmentDefinitionRef = new Ref<>();
75+
try {
76+
PsiSearchHelper.SERVICE.getInstance(myProject).processElementsWithWord((element, offsetInElement) -> {
77+
if (element instanceof JSGraphQLNamedTypePsiElement && element.getParent() instanceof JSGraphQLFragmentDefinitionPsiElement) {
78+
if (!element.equals(fragmentUsage)) {
79+
// only consider as a reference if the element is not the usage element
80+
final JSGraphQLNamedTypePsiElement fragmentDefinition = (JSGraphQLNamedTypePsiElement) element;
81+
fragmentDefinitionsByName.put(fragmentName, fragmentDefinition);
82+
fragmentDefinitionRef.set(fragmentDefinition);
83+
}
84+
return false;
85+
}
86+
return true;
87+
}, searchScope, fragmentName, UsageSearchContext.IN_CODE, true, true);
88+
} catch (IndexNotReadyException e) {
89+
// can't search yet (e.g. during project startup)
90+
}
91+
return fragmentDefinitionRef.get();
92+
}
93+
return null;
94+
}
95+
96+
/**
97+
* Finds all fragment definition across files in the project
98+
*
99+
* @return a list of known fragment definitions, or an empty list if the index is not yet ready
100+
*/
101+
public List<JSGraphQLFragmentDefinitionPsiElement> getKnownFragmentDefinitions() {
102+
try {
103+
final List<JSGraphQLFragmentDefinitionPsiElement> fragmentDefinitions = Lists.newArrayList();
104+
PsiSearchHelper.SERVICE.getInstance(myProject).processElementsWithWord((psiElement, offsetInElement) -> {
105+
if (psiElement.getNode().getElementType() == JSGraphQLTokenTypes.KEYWORD && psiElement.getParent() instanceof JSGraphQLFragmentDefinitionPsiElement) {
106+
final JSGraphQLFragmentDefinitionPsiElement fragmentDefinition = (JSGraphQLFragmentDefinitionPsiElement) psiElement.getParent();
107+
final String fragmentName = fragmentDefinition.getName();
108+
if (fragmentName != null) {
109+
fragmentDefinitions.add(fragmentDefinition);
110+
}
111+
}
112+
return true;
113+
}, searchScope, "fragment", UsageSearchContext.IN_CODE, true, true);
114+
return fragmentDefinitions;
115+
} catch (IndexNotReadyException e) {
116+
// can't search yet (e.g. during project startup)
117+
}
118+
return Collections.emptyList();
119+
}
120+
121+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2015-present, Jim Kynde Meyer
3+
* All rights reserved.
4+
*
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.psi;
9+
10+
11+
/**
12+
* Implemented by PSI elements that are able to "mute" traditional GraphQL errors based on the context, e.g. Relay fragments
13+
*/
14+
public interface JSGraphQLErrorContextAware {
15+
16+
/**
17+
* Gets whether the errors should be reported in this element context
18+
* @param errorMessage the error message from the traditional GraphQL validation
19+
* @return whether the error should be shown in this context
20+
*/
21+
boolean isErrorInContext(String errorMessage);
22+
23+
}

src/main/com/intellij/lang/jsgraphql/psi/JSGraphQLNamedTypePsiElement.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import com.intellij.lang.jsgraphql.JSGraphQLKeywords;
1212
import com.intellij.lang.jsgraphql.JSGraphQLTokenTypes;
1313
import com.intellij.lang.jsgraphql.icons.JSGraphQLIcons;
14+
import com.intellij.lang.jsgraphql.ide.project.JSGraphQLPsiSearchHelper;
1415
import com.intellij.lang.jsgraphql.schema.psi.JSGraphQLSchemaFile;
1516
import com.intellij.navigation.ItemPresentation;
1617
import com.intellij.openapi.util.TextRange;
1718
import com.intellij.psi.PsiElement;
19+
import com.intellij.psi.PsiLanguageInjectionHost;
1820
import com.intellij.psi.PsiReference;
1921
import com.intellij.psi.PsiReferenceBase;
2022
import com.intellij.psi.util.PsiTreeUtil;
@@ -25,7 +27,7 @@
2527
import java.util.Objects;
2628
import java.util.Optional;
2729

28-
public class JSGraphQLNamedTypePsiElement extends JSGraphQLNamedPsiElement {
30+
public class JSGraphQLNamedTypePsiElement extends JSGraphQLNamedPsiElement implements JSGraphQLErrorContextAware {
2931

3032
public JSGraphQLNamedTypePsiElement(@NotNull ASTNode node) {
3133
super(node);
@@ -66,6 +68,17 @@ public PsiReference getReference() {
6668
}
6769
}
6870
}
71+
72+
// also search for the fragment definition in other files
73+
final JSGraphQLNamedTypePsiElement definitionType = JSGraphQLPsiSearchHelper.getService(getProject()).resolveFragmentReference(this); // fragmentDefinitionName.get();
74+
if(definitionType != null) {
75+
if(this.equals(definitionType)) {
76+
// this element is the fragment definition name element
77+
return null;
78+
}
79+
return new PsiReferenceBase.Immediate<>(this, TextRange.from(0, definitionType.getTextLength()), definitionType);
80+
}
81+
6982
}
7083
// null for named type in the schema, e.g. 'Node' which means that ctrl+click is find usages
7184
return null;
@@ -126,4 +139,20 @@ public Icon getElementIcon(final int flags) {
126139
return super.getElementIcon(flags);
127140
}
128141

142+
@Override
143+
public boolean isErrorInContext(String errorMessage) {
144+
if(errorMessage.startsWith("Unknown fragment")) {
145+
// GraphQL doesn't known this fragment, but if it's defined elsewhere, check for a valid reference
146+
final PsiReference reference = this.getReference();
147+
if(reference != null && reference.resolve() != null) {
148+
return false;
149+
}
150+
} else if(errorMessage.startsWith("Fragment") && errorMessage.endsWith("is never used.")) {
151+
// fragments are not considered unused inside an injected element (e.g. Relay graphql, Apollo gql)
152+
if(getContainingFile().getContext() instanceof PsiLanguageInjectionHost) {
153+
return false;
154+
}
155+
}
156+
return true;
157+
}
129158
}

0 commit comments

Comments
 (0)