Skip to content

Commit 66a18de

Browse files
committed
Parser support for object values and lists (#35)
Proper indentation of input type fields Go to declaration and find usages for input type fields
1 parent c62fdea commit 66a18de

17 files changed

+683
-229
lines changed

src/main/com/intellij/lang/jsgraphql/ide/documentation/JSGraphQLDocumentationProvider.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.intellij.lang.jsgraphql.languageservice.JSGraphQLNodeLanguageServiceClient;
1515
import com.intellij.lang.jsgraphql.languageservice.api.TokenDocumentationResponse;
1616
import com.intellij.lang.jsgraphql.languageservice.api.TypeDocumentationResponse;
17+
import com.intellij.lang.jsgraphql.psi.JSGraphQLAttributePsiElement;
1718
import com.intellij.lang.jsgraphql.psi.JSGraphQLFragmentDefinitionPsiElement;
1819
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedPropertyPsiElement;
1920
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedPsiElement;
@@ -24,10 +25,12 @@
2425
import com.intellij.openapi.editor.colors.EditorColorsManager;
2526
import com.intellij.openapi.editor.colors.EditorColorsScheme;
2627
import com.intellij.openapi.project.Project;
28+
import com.intellij.psi.PsiComment;
2729
import com.intellij.psi.PsiElement;
2830
import com.intellij.psi.PsiFile;
2931
import com.intellij.psi.PsiManager;
3032
import com.intellij.psi.PsiReference;
33+
import com.intellij.psi.PsiWhiteSpace;
3134
import com.intellij.psi.util.PsiTreeUtil;
3235
import com.intellij.ui.GuiUtils;
3336
import com.intellij.util.containers.ContainerUtil;
@@ -197,6 +200,27 @@ private String createQuickNavigateDocumentation(PsiElement element, boolean full
197200
doc += "</code>";
198201
return getDocTemplate(fullDocumentation).replace("${body}", doc);
199202
}
203+
} else if(element instanceof JSGraphQLAttributePsiElement) {
204+
String doc = "";
205+
PsiElement prevLeaf = PsiTreeUtil.prevLeaf(element);
206+
String documentation = "";
207+
while (prevLeaf instanceof PsiWhiteSpace || prevLeaf instanceof PsiComment) {
208+
documentation = StringUtils.removeStart(prevLeaf.getText(), "# ") + documentation;
209+
prevLeaf = PsiTreeUtil.prevLeaf(prevLeaf);
210+
}
211+
documentation = documentation.trim();
212+
if(StringUtils.isNotBlank(documentation)) {
213+
doc += "<div style=\"margin-bottom: 4px\">" + StringEscapeUtils.escapeHtml(documentation) + "</div>";
214+
}
215+
doc += "<code>" + element.getText();
216+
PsiElement nextLeaf = PsiTreeUtil.nextLeaf(element);
217+
// include the attribute type (stopping at newline, ",", and ")")
218+
while(nextLeaf != null && !nextLeaf.getText().contains("\n") && !nextLeaf.getText().contains(",") && !nextLeaf.getText().contains(")")) {
219+
doc += nextLeaf.getText();
220+
nextLeaf = PsiTreeUtil.nextLeaf(nextLeaf);
221+
}
222+
doc += "</code>";
223+
return getDocTemplate(fullDocumentation).replace("${body}", doc);
200224
}
201225

202226
}

src/main/com/intellij/lang/jsgraphql/ide/findUsages/JSGraphQLFindUsagesProvider.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@
99

1010
import com.intellij.lang.cacheBuilder.WordsScanner;
1111
import com.intellij.lang.findUsages.FindUsagesProvider;
12+
import com.intellij.lang.jsgraphql.JSGraphQLKeywords;
13+
import com.intellij.lang.jsgraphql.psi.JSGraphQLAttributePsiElement;
1214
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedPropertyPsiElement;
1315
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedPsiElement;
1416
import com.intellij.lang.jsgraphql.psi.JSGraphQLNamedTypePsiElement;
17+
import com.intellij.lang.jsgraphql.psi.JSGraphQLPsiElement;
1518
import com.intellij.lang.jsgraphql.schema.psi.JSGraphQLSchemaFile;
1619
import com.intellij.openapi.util.text.StringUtil;
1720
import com.intellij.psi.PsiElement;
1821
import com.intellij.psi.PsiNamedElement;
22+
import com.intellij.psi.util.PsiTreeUtil;
23+
1924
import org.jetbrains.annotations.NotNull;
2025
import org.jetbrains.annotations.Nullable;
2126

@@ -52,6 +57,22 @@ public String getType(@NotNull PsiElement element) {
5257
final boolean atom = element.getContainingFile() instanceof JSGraphQLSchemaFile || ((JSGraphQLNamedTypePsiElement) element).isAtom();
5358
return atom ? "type" : "definition";
5459
}
60+
if(element instanceof JSGraphQLAttributePsiElement) {
61+
JSGraphQLPsiElement parent = PsiTreeUtil.getParentOfType(element, JSGraphQLPsiElement.class);
62+
while (parent != null) {
63+
final JSGraphQLPsiElement ancestor = PsiTreeUtil.getParentOfType(parent, JSGraphQLPsiElement.class);
64+
if(ancestor == null) {
65+
break;
66+
}
67+
parent = ancestor;
68+
}
69+
if(parent != null && JSGraphQLKeywords.INPUT.equals(parent.getKeyword())) {
70+
// field of an input type
71+
return "input field";
72+
}
73+
// field argument
74+
return "argument";
75+
}
5576
return "unknown";
5677
}
5778

src/main/com/intellij/lang/jsgraphql/ide/formatter/JSGraphQLBlock.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,24 @@ public Indent getIndent() {
121121
if(parent != null) {
122122
JSGraphQLElementType astNode = getAstNode(parent);
123123
if (astNode != null) {
124-
if (JSGraphQLElementType.SELECTION_SET_KIND.equals(astNode.getKind())) {
124+
final String kind = astNode.getKind();
125+
if (JSGraphQLElementType.SELECTION_SET_KIND.equals(kind)) {
125126
// this block is inside a selection set: '{ ... }', so indent it
126127
return Indent.getNormalIndent();
127128
}
128-
if (JSGraphQLElementType.ARGUMENTS_KIND.equals(astNode.getKind())) {
129+
if (JSGraphQLElementType.ARGUMENTS_KIND.equals(kind)) {
129130
// this block is inside a () arguments list, so indent it
130131
return Indent.getNormalIndent();
131132
}
132-
if (JSGraphQLElementType.OBJECT_VALUE_KIND.equals(astNode.getKind())) {
133+
if (JSGraphQLElementType.OBJECT_VALUE_KIND.equals(kind)) {
133134
// this block is inside a {} object value, so indent it
134135
return Indent.getNormalIndent();
135136
}
136-
if (JSGraphQLElementType.LIST_VALUE_KIND.equals(astNode.getKind())) {
137+
if (JSGraphQLElementType.LIST_VALUE_KIND.equals(kind)) {
137138
// this block is inside a [] list value, so indent it
138139
return Indent.getNormalIndent();
139140
}
140-
if(JSGraphQLElementType.SCHEMA_DEF_KIND.equals(astNode.getKind())) {
141+
if(JSGraphQLElementType.SCHEMA_DEF_KIND.equals(kind)) {
141142
// inside schema {}
142143
if(myNode.getText().equals(JSGraphQLKeywords.QUERY) || myNode.getText().equals(JSGraphQLKeywords.MUTATION) || myNode.getText().equals(JSGraphQLKeywords.SUBSCRIPTION)) {
143144
return Indent.getNormalIndent();
@@ -146,7 +147,7 @@ public Indent getIndent() {
146147
return Indent.getNormalIndent();
147148
}
148149
}
149-
if(myNode.getElementType() != JSGraphQLTokenTypes.KEYWORD && INDENT_PARENT_KINDS.contains(astNode.getKind())) {
150+
if(myNode.getElementType() != JSGraphQLTokenTypes.KEYWORD && INDENT_PARENT_KINDS.contains(kind)) {
150151
// properties inside schema definitions
151152
return Indent.getNormalIndent();
152153
}

src/main/com/intellij/lang/jsgraphql/parser/JSGraphQLParser.java

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ public ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder) {
9090

9191
final PropertyScope propertyScope = tokenToPropertyScope.get(currentToken);
9292
if(propertyScope != null) {
93+
94+
if(scopes.isEmpty()) {
95+
if(currentToken.tokenType == JSGraphQLTokenTypes.RBRACE || currentToken.tokenType == JSGraphQLTokenTypes.RBRACKET || currentToken.tokenType == JSGraphQLTokenTypes.RPAREN) {
96+
// closing scope without an open scope, so skip ahead to continue parsing
97+
builder.advanceLexer();
98+
tokenIndex.set(tokenIndex.get()+1);
99+
continue;
100+
}
101+
}
102+
93103
if (currentToken.tokenType == JSGraphQLTokenTypes.PROPERTY || currentToken.tokenType == JSGraphQLTokenTypes.KEYWORD/* query etc.*/) {
94104
if (propertyScope.lbrace != null) {
95105
// Field property token with selection set is considered a scope
@@ -107,6 +117,9 @@ public ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder) {
107117
if(JSGraphQLElementType.OBJECT_VALUE_KIND.equals(propertyScope.lbrace.sourceToken.getKind())) {
108118
// close object value
109119
endScope(builder, tokenIndex, scopes, true);
120+
if(propertyScope.parentToClose != null) {
121+
endScope(builder, tokenIndex, scopes, false);
122+
}
110123
continue;
111124
} else {
112125
// close selection set
@@ -128,6 +141,14 @@ public ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder) {
128141
}
129142
} else if(currentToken.tokenType == JSGraphQLTokenTypes.RBRACKET) {
130143
endScope(builder, tokenIndex, scopes, true);
144+
if(propertyScope.parentToClose != null) {
145+
endScope(builder, tokenIndex, scopes, false);
146+
}
147+
continue;
148+
} else if(currentToken.tokenType == JSGraphQLTokenTypes.ATTRIBUTE) {
149+
// atribute with list/object value, so it's a scope for indentation, folding etc.
150+
startScope(builder, scopes, currentToken, false);
151+
markCurrentToken(builder, tokenIndex, JSGraphQLElementType.ATTRIBUTE_KIND);
131152
continue;
132153
}
133154
} else if(currentToken.tokenType == JSGraphQLTokenTypes.PROPERTY) {
@@ -139,6 +160,10 @@ public ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder) {
139160
} else if(currentToken.tokenType == JSGraphQLTokenTypes.DEF) {
140161
markCurrentToken(builder, tokenIndex, JSGraphQLElementType.DEFINITION_KIND);
141162
continue;
163+
} else if(currentToken.tokenType == JSGraphQLTokenTypes.ATTRIBUTE) {
164+
// attribute with literal value (not a scope)
165+
markCurrentToken(builder, tokenIndex, JSGraphQLElementType.ATTRIBUTE_KIND);
166+
continue;
142167
}
143168

144169
// ---- template fragment ----
@@ -197,15 +222,11 @@ private List<PropertyScope> getPropertyScopes(List<JSGraphQLToken> tokens) {
197222
.filter((token) -> token.tokenType != JSGraphQLTokenTypes.WHITESPACE && token.tokenType != JSGraphQLTokenTypes.COMMENT)
198223
.collect(Collectors.toList());
199224

200-
final Set<String> closeScopeKinds = Sets.newHashSet(
201-
JSGraphQLElementType.SELECTION_SET_KIND,
202-
JSGraphQLElementType.DOCUMENT_KIND,
203-
JSGraphQLElementType.OBJECT_VALUE_KIND,
204-
JSGraphQLElementType.ARGUMENTS_KIND
205-
);
206-
207225
final List<PropertyScope> ret = Lists.newArrayList();
208226
final Stack<PropertyScope> scopes = new Stack<>();
227+
228+
boolean parseArguments = false;
229+
209230
for (int i = 0; i < astTokens.size(); i++) {
210231
JSGraphQLToken token = astTokens.get(i);
211232
if(token.tokenType == JSGraphQLTokenTypes.KEYWORD) {
@@ -224,54 +245,111 @@ private List<PropertyScope> getPropertyScopes(List<JSGraphQLToken> tokens) {
224245
}
225246
} else if (token.tokenType == JSGraphQLTokenTypes.RBRACE) {
226247
if(!scopes.isEmpty()) {
227-
final String kind = token.sourceToken.getKind();
228-
if(closeScopeKinds.contains(kind) || isSchemaDefWithLBrace(kind)) {
229-
PropertyScope propertyScope = scopes.pop();
230-
if(propertyScope.lbrace == null) {
231-
// closing the parent scope
232-
if(!scopes.isEmpty()) {
233-
propertyScope = scopes.pop();
248+
if(parseArguments) {
249+
scopes.pop().rbrace = token;
250+
} else {
251+
final String kind = token.sourceToken.getKind();
252+
if (JSGraphQLElementType.SELECTION_SET_KIND.equals(kind) || JSGraphQLElementType.DOCUMENT_KIND.equals(kind) || isSchemaDefWithLBrace(kind)) {
253+
PropertyScope propertyScope = scopes.pop();
254+
if (propertyScope.lbrace == null) {
255+
// closing the parent scope
256+
if (!scopes.isEmpty()) {
257+
propertyScope = scopes.pop();
258+
}
234259
}
260+
propertyScope.rbrace = token;
235261
}
236-
propertyScope.rbrace = token;
237262
}
238263
}
239264
} else if(token.tokenType == JSGraphQLTokenTypes.LBRACE) {
240-
// if top level scope, it's shorthand for a query
241-
if(scopes.isEmpty()) {
265+
if(parseArguments) {
242266
PropertyScope propertyScope = new PropertyScope(token, token);
243267
scopes.add(propertyScope);
244268
ret.add(propertyScope);
245269
} else {
246-
final String kind = token.sourceToken.getKind();
247-
if (JSGraphQLElementType.OBJECT_VALUE_KIND.equals(kind)) {
270+
// if top level scope, it's shorthand for a query
271+
if(scopes.isEmpty()) {
248272
PropertyScope propertyScope = new PropertyScope(token, token);
249273
scopes.add(propertyScope);
250274
ret.add(propertyScope);
251275
}
252276
}
253277
} else if(token.tokenType == JSGraphQLTokenTypes.LPAREN) {
254-
PropertyScope propertyScope = new PropertyScope(token, token);
255-
scopes.add(propertyScope);
256-
ret.add(propertyScope);
278+
if(!schema) {
279+
parseArguments = true;
280+
PropertyScope propertyScope = new PropertyScope(token, token);
281+
scopes.add(propertyScope);
282+
ret.add(propertyScope);
283+
}
257284
} else if(token.tokenType == JSGraphQLTokenTypes.RPAREN) {
258-
if(!scopes.isEmpty()) {
259-
scopes.pop().rbrace = token;
285+
if(!schema) {
286+
if (!scopes.isEmpty()) {
287+
scopes.pop().rbrace = token;
288+
}
289+
parseArguments = false;
260290
}
261291
} else if (token.tokenType == JSGraphQLTokenTypes.LBRACKET) {
262-
PropertyScope propertyScope = new PropertyScope(token, token);
263-
scopes.add(propertyScope);
264-
ret.add(propertyScope);
292+
if(!schema && parseArguments) {
293+
PropertyScope propertyScope = new PropertyScope(token, token);
294+
scopes.add(propertyScope);
295+
ret.add(propertyScope);
296+
}
265297
} else if (token.tokenType == JSGraphQLTokenTypes.RBRACKET) {
266-
if (!scopes.isEmpty()) {
298+
if (!schema && parseArguments && !scopes.isEmpty()) {
267299
scopes.pop().rbrace = token;
268300
}
301+
} else if(token.tokenType == JSGraphQLTokenTypes.ATTRIBUTE) {
302+
if(!schema && parseArguments) {
303+
PropertyScope propertyScope = new PropertyScope(token, null);
304+
propertyScope.astTokenStartIndex = i;
305+
ret.add(propertyScope);
306+
}
307+
}
308+
}
309+
310+
// associate the attribute scopes with their corresponding list/object values
311+
final Set<PropertyScope> literalAttributeValues = Sets.newHashSet();
312+
for (int i = 0; i < ret.size(); i++) {
313+
final PropertyScope attributeNameScope = ret.get(i);
314+
if(attributeNameScope.closedBy == null && attributeNameScope.propertyOrOperation.tokenType == JSGraphQLTokenTypes.ATTRIBUTE) {
315+
if(i + 1 < ret.size()) {
316+
final PropertyScope attributeValueScope = ret.get(i + 1);
317+
if(!isValueForAttribute(astTokens, attributeNameScope, attributeValueScope)) {
318+
literalAttributeValues.add(attributeNameScope);
319+
continue;
320+
}
321+
final JSGraphQLToken valueToken = attributeValueScope.lbrace;
322+
if(valueToken != null && (valueToken.tokenType == JSGraphQLTokenTypes.LBRACE || valueToken.tokenType == JSGraphQLTokenTypes.LBRACKET)) {
323+
attributeValueScope.parentToClose = attributeNameScope;
324+
attributeNameScope.closedBy = attributeValueScope;
325+
} else {
326+
literalAttributeValues.add(attributeNameScope);
327+
}
328+
} else {
329+
literalAttributeValues.add(attributeNameScope);
330+
}
269331
}
270332
}
333+
ret.removeAll(literalAttributeValues);
271334

272335
return ret;
273336
}
274337

338+
private boolean isValueForAttribute(List<JSGraphQLToken> astTokens, PropertyScope attributeNameScope, PropertyScope attributeValue) {
339+
for(int i = attributeNameScope.astTokenStartIndex + 1; i < astTokens.size(); i++) {
340+
final JSGraphQLToken token = astTokens.get(i);
341+
if(token == attributeValue.lbrace) {
342+
return true;
343+
}
344+
if(token.tokenType == JSGraphQLTokenTypes.PUNCTUATION) {
345+
// expecting colon before the attribute value
346+
continue;
347+
}
348+
break;
349+
}
350+
return false;
351+
}
352+
275353
private boolean isPropertyScopeDefinition(String text, int tokenIndex, List<JSGraphQLToken> astTokens, Stack<PropertyScope> scopes) {
276354
switch(text) {
277355
case JSGraphQLKeywords.TYPE:
@@ -370,6 +448,11 @@ private static class PropertyScope {
370448
JSGraphQLToken lbrace;
371449
JSGraphQLToken rbrace;
372450

451+
PropertyScope closedBy;
452+
PropertyScope parentToClose;
453+
454+
Integer astTokenStartIndex;
455+
373456
public PropertyScope(JSGraphQLToken propertyOrOperation, JSGraphQLToken lbrace) {
374457
this.propertyOrOperation = propertyOrOperation;
375458
this.lbrace = lbrace;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) 2015-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.psi;
9+
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import com.intellij.lang.ASTNode;
13+
import com.intellij.psi.util.PsiTreeUtil;
14+
15+
/**
16+
* Represents a field argument, eg. "id" in "{ node(id: 10) {}}"
17+
*/
18+
public class JSGraphQLArgumentPsiElement extends JSGraphQLPsiElement {
19+
20+
public JSGraphQLArgumentPsiElement(@NotNull ASTNode node) {
21+
super(node);
22+
}
23+
24+
@NotNull
25+
public JSGraphQLAttributePsiElement getAttribute() {
26+
return PsiTreeUtil.getRequiredChildOfType(this, JSGraphQLAttributePsiElement.class);
27+
}
28+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2015-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.psi;
9+
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import com.intellij.lang.ASTNode;
13+
14+
/**
15+
* The name of a field argument or input type field
16+
*/
17+
public class JSGraphQLAttributePsiElement extends JSGraphQLNamedPsiElement {
18+
19+
public JSGraphQLAttributePsiElement(@NotNull ASTNode node) {
20+
super(node);
21+
}
22+
}

0 commit comments

Comments
 (0)