Skip to content

Commit 25d1af1

Browse files
committed
Reintroduced support for contextual queries based on caret position (#243, #43, #94)
- Highlight current executable operation and included fragments - Automatically include referenced fragments, including across files based on resolving PSI references
1 parent 71fe9fd commit 25d1af1

File tree

2 files changed

+111
-83
lines changed

2 files changed

+111
-83
lines changed

resources/META-INF/plugin.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@
206206

207207

208208
<!-- Query context highlighter -->
209-
<!-- Fixme: Breaks internal annotators used in v2: <highlightVisitor implementation="com.intellij.lang.jsgraphql.v1.ide.editor.JSGraphQLQueryContextHighlightVisitor" />-->
209+
<highlightVisitor implementation="com.intellij.lang.jsgraphql.v1.ide.editor.JSGraphQLQueryContextHighlightVisitor" />
210210

211211

212212
<!-- Formatting -->

src/main/com/intellij/lang/jsgraphql/v1/ide/editor/JSGraphQLQueryContextHighlightVisitor.java

Lines changed: 110 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (c) 2015-present, Jim Kynde Meyer
33
* All rights reserved.
44
* <p>
@@ -8,6 +8,7 @@
88
package com.intellij.lang.jsgraphql.v1.ide.editor;
99

1010
import com.google.common.collect.Lists;
11+
import com.google.common.collect.Maps;
1112
import com.google.common.collect.Sets;
1213
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
1314
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
@@ -21,6 +22,7 @@
2122
import com.intellij.ide.util.PropertiesComponent;
2223
import com.intellij.lang.annotation.Annotation;
2324
import com.intellij.lang.annotation.HighlightSeverity;
25+
import com.intellij.lang.jsgraphql.ide.project.GraphQLPsiSearchHelper;
2426
import com.intellij.lang.jsgraphql.psi.*;
2527
import com.intellij.notification.Notification;
2628
import com.intellij.notification.NotificationType;
@@ -51,10 +53,7 @@
5153

5254
import javax.swing.*;
5355
import java.awt.*;
54-
import java.util.Collection;
55-
import java.util.Comparator;
56-
import java.util.HashSet;
57-
import java.util.Set;
56+
import java.util.*;
5857
import java.util.function.Function;
5958
import java.util.stream.Collectors;
6059
import java.util.stream.Stream;
@@ -104,6 +103,9 @@ public void visit(@NotNull PsiElement element) {
104103
@Override
105104
public boolean analyze(final @NotNull PsiFile file, boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable action) {
106105

106+
// run the default pass first (DefaultHighlightVisitor) which calls annotators etc.
107+
action.run();
108+
107109
final PsiElement operationAtCursor = getOperationAtCursor(file, null);
108110
if (operationAtCursor != null && hasMultipleVisibleTopLevelElement(file)) {
109111

@@ -112,12 +114,17 @@ public boolean analyze(final @NotNull PsiFile file, boolean updateWholeFile, @No
112114

113115
final Color borderColor = EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.TEARLINE_COLOR);
114116
final TextAttributes textAttributes = new TextAttributes(null, null, borderColor, EffectType.ROUNDED_BOX, Font.PLAIN);
115-
final HashSet<GraphQLFragmentDefinition> foundFragments = Sets.newHashSet();
117+
final Map<String, GraphQLFragmentDefinition> foundFragments = Maps.newHashMap();
116118
findFragmentsInsideOperation(operationAtCursor, foundFragments, null);
117119
for (PsiElement psiElement : file.getChildren()) {
118120
boolean showAsUsed = false;
119121
if (psiElement instanceof GraphQLFragmentDefinition) {
120-
showAsUsed = foundFragments.contains(psiElement);
122+
GraphQLFragmentDefinition definition = (GraphQLFragmentDefinition) psiElement;
123+
if (definition.getOriginalElement() instanceof GraphQLFragmentDefinition) {
124+
// use the original PSI to compare since a separate editor tab has its own version of the PSI
125+
definition = (GraphQLFragmentDefinition) definition.getOriginalElement();
126+
}
127+
showAsUsed = foundFragments.containsKey(getFragmentKey(definition));
121128
} else if (psiElement == operationAtCursor) {
122129
showAsUsed = true;
123130
}
@@ -188,8 +195,14 @@ public void caretPositionChanged(CaretEvent e) {
188195
return true;
189196
}
190197

198+
private static String getFragmentKey(GraphQLFragmentDefinition definition) {
199+
if (definition == null) {
200+
return "";
201+
}
202+
return GraphQLPsiSearchHelper.getFileName(definition.getContainingFile()) + ":" + definition.getName();
203+
}
204+
191205
private static void removeHighlights(Editor editor, Project project) {
192-
// TODO JKM This also clears error highlights from the other GraphQL annotators that are internal in v2!!
193206
HighlightManagerImpl highlightManager = (HighlightManagerImpl) HighlightManager.getInstance(project);
194207
for (RangeHighlighter rangeHighlighter : highlightManager.getHighlighters(editor)) {
195208
highlightManager.removeSegmentHighlighter(editor, rangeHighlighter);
@@ -328,11 +341,11 @@ public static JSGraphQLQueryContext getQueryContextBufferAndHighlightUnused(fina
328341

329342
// no selection -- see if the caret is inside an operation
330343

331-
final PsiElement operationAtCursor = getOperationAtCursor(psiFile, null);
344+
final GraphQLOperationDefinition operationAtCursor = getOperationAtCursor(psiFile, null);
332345
if (operationAtCursor != null) {
333-
final HashSet<GraphQLFragmentDefinition> foundFragments = Sets.newHashSet();
346+
final Map<String, GraphQLFragmentDefinition> foundFragments = Maps.newHashMap();
334347
findFragmentsInsideOperation(operationAtCursor, foundFragments, null);
335-
Set<PsiElement> queryElements = Sets.newHashSet(foundFragments);
348+
Set<PsiElement> queryElements = Sets.newHashSet(foundFragments.values());
336349
queryElements.add(operationAtCursor);
337350
final StringBuilder query = new StringBuilder(editorLength);
338351
final TextAttributes unusedTextAttributes = getUnusedTextAttributes();
@@ -343,8 +356,16 @@ public static JSGraphQLQueryContext getQueryContextBufferAndHighlightUnused(fina
343356
}
344357
} else {
345358
final TextRange textRange = psiElement.getTextRange();
346-
if (queryElements.contains(psiElement)) {
359+
String fragmentKey = "";
360+
if (psiElement instanceof GraphQLFragmentDefinition) {
361+
fragmentKey = getFragmentKey((GraphQLFragmentDefinition) psiElement);
362+
}
363+
if (queryElements.contains(psiElement) || foundFragments.containsKey(fragmentKey)) {
347364
queryElements.remove(psiElement);
365+
GraphQLFragmentDefinition fragmentDefinition = foundFragments.get(fragmentKey);
366+
if (fragmentDefinition != null) {
367+
queryElements.remove(fragmentDefinition);
368+
}
348369
query.append(editorBuffer.subSequence(textRange.getStartOffset(), textRange.getEndOffset()));
349370
} else {
350371
if (!queryElements.isEmpty()) {
@@ -365,12 +386,20 @@ public static JSGraphQLQueryContext getQueryContextBufferAndHighlightUnused(fina
365386
}
366387
}
367388
}
368-
final GraphQLIdentifier operationName = PsiTreeUtil.getChildOfType(operationAtCursor, GraphQLIdentifier.class);
369-
if (operationName != null) {
370-
showQueryContextHint(editor, "Executed " + getOperationKind(operationName) + " \"" + operationName.getText() + "\"");
371-
} else if (operationAtCursor instanceof GraphQLSelectionSetOperationDefinition) {
372-
// anonymous query
373-
showQueryContextHint(editor, "Executed anonymous query");
389+
390+
// include fragments from other PsiFiles
391+
for (PsiElement queryElement : queryElements) {
392+
query.append("\n\n# ---- fragment automatically included from \"");
393+
query.append(GraphQLPsiSearchHelper.getFileName(queryElement.getContainingFile())).append("\" ----\n");
394+
query.append(queryElement.getText());
395+
}
396+
397+
if (operationAtCursor.getNameIdentifier() != null) {
398+
// named operation
399+
showQueryContextHint(editor, "Executed " + getOperationKind(operationAtCursor) + " \"" + operationAtCursor.getNameIdentifier().getText() + "\"");
400+
} else {
401+
// anonymous operation
402+
showQueryContextHint(editor, "Executed anonymous " + getOperationKind(operationAtCursor));
374403
}
375404
return new JSGraphQLQueryContext(query.toString(), null);
376405
}
@@ -410,9 +439,18 @@ private static TextAttributes getUnusedTextAttributes() {
410439
}
411440

412441
/**
413-
* Determines the kind of operation keyword that preceeds an operation name
442+
* Determines the kind of operation keyword that defines an operation, e.g. "query" or "mutation"
414443
*/
415-
private static String getOperationKind(GraphQLIdentifier operationName) {
444+
private static String getOperationKind(GraphQLOperationDefinition operation) {
445+
if (operation instanceof GraphQLSelectionSetOperationDefinition) {
446+
return "query";
447+
}
448+
if (operation instanceof GraphQLTypedOperationDefinition) {
449+
final GraphQLOperationType operationType = ((GraphQLTypedOperationDefinition) operation).getOperationType();
450+
if (operationType != null) {
451+
return operationType.getText();
452+
}
453+
}
416454
return "operation";
417455
}
418456

@@ -434,10 +472,6 @@ private static void showQueryContextHint(Editor editor, String hintText) {
434472
* e.g. when outside any operation or inside a fragment definition
435473
*/
436474
private static GraphQLOperationDefinition getOperationAtCursor(PsiFile psiFile, CaretEvent caretEvent) {
437-
if (true) {
438-
// TODO refactor to v2 PSI
439-
return null;
440-
}
441475
final FileEditor fileEditor = FileEditorManager.getInstance(psiFile.getProject()).getSelectedEditor(psiFile.getVirtualFile());
442476
if (fileEditor instanceof TextEditor) {
443477
final Editor editor = ((TextEditor) fileEditor).getEditor();
@@ -466,21 +500,7 @@ private static GraphQLOperationDefinition getOperationAtCursor(PsiFile psiFile,
466500
* Returns the operation candidate if it's an operation, or <code>null</code>
467501
*/
468502
private static GraphQLOperationDefinition asOperationOrNull(PsiElement operationCandidate) {
469-
// if (operationCandidate instanceof JSGraphQLPsiElement) {
470-
// if (operationCandidate instanceof GraphQLFragmentDefinition) {
471-
// // fragments aren't operations
472-
// return null;
473-
// }
474-
// if (operationCandidate instanceof JSGraphQLSelectionSetPsiElement) {
475-
// if (!((JSGraphQLSelectionSetPsiElement) operationCandidate).isAnonymousQuery()) {
476-
// // selection set is not an anonymous query, so not considered an operation
477-
// return null;
478-
// }
479-
// }
480-
// // named queries, mutations, subscriptions
481-
// return operationCandidate;
482-
// }
483-
return null;
503+
return PsiTreeUtil.getParentOfType(operationCandidate, GraphQLOperationDefinition.class, false);
484504
}
485505

486506
/**
@@ -497,17 +517,23 @@ private static void placeCaretInsideFirstOperation(Editor editor, PsiFile psiFil
497517
}
498518
GraphQLOperationDefinition operationOrNull = asOperationOrNull(psiElement);
499519
if (operationOrNull != null) {
520+
PsiElement navigationTarget = operationOrNull;
500521
final Project project = editor.getProject();
501522
if (project != null) {
502523
// try to find the name of the operation
503-
// final JSGraphQLNamedTypePsiElement name = PsiTreeUtil.getChildOfType(operationOrNull, JSGraphQLNamedTypePsiElement.class);
504-
// if (name != null && name.isDefinition()) {
505-
// operationOrNull = name;
506-
// }
524+
if (operationOrNull instanceof GraphQLSelectionSetOperationDefinition) {
525+
// unnamed query
526+
final GraphQLSelectionSet selectionSet = ((GraphQLSelectionSetOperationDefinition) operationOrNull).getSelectionSet();
527+
if (selectionSet != null) {
528+
navigationTarget = selectionSet;
529+
}
530+
} else if (operationOrNull.getNameIdentifier() != null) {
531+
navigationTarget = operationOrNull.getNameIdentifier();
532+
}
507533
final FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
508534
fileEditorManager.openFile(psiFile.getVirtualFile(), true, true);
509535
editor.getSelectionModel().removeSelection();
510-
editor.getCaretModel().moveToOffset(operationOrNull.getTextOffset());
536+
editor.getCaretModel().moveToOffset(navigationTarget.getTextOffset());
511537
editor.getScrollingModel().scrollToCaret(ScrollType.CENTER);
512538
}
513539
return;
@@ -520,48 +546,50 @@ private static void placeCaretInsideFirstOperation(Editor editor, PsiFile psiFil
520546
* Locates the fragments used from inside an operation, and the fragments, if any, that are used from within those fragments
521547
*
522548
* @param operationOrFragment the operation to find used fragments for
523-
* @param foundFragments a set to add the found fragments to
549+
* @param foundFragments a map fragments map keyed by filename:fragment-name to add the found fragments to
524550
* @param findMore optional function to stop once a specific fragment has been found
525551
*/
526-
private static void findFragmentsInsideOperation(PsiElement operationOrFragment, Set<GraphQLFragmentDefinition> foundFragments,
552+
private static void findFragmentsInsideOperation(PsiElement operationOrFragment, Map<String, GraphQLFragmentDefinition> foundFragments,
527553
Function<GraphQLFragmentDefinition, Boolean> findMore) {
528554

529-
// operationOrFragment.accept(new PsiRecursiveElementVisitor() {
530-
//
531-
// private boolean done = false;
532-
//
533-
// @Override
534-
// public void visitElement(PsiElement element) {
535-
// if (done) {
536-
// return;
537-
// }
538-
// if (element instanceof JSGraphQLNamedTypePsiElement) {
539-
// if (((JSGraphQLNamedTypePsiElement) element).isDefinition()) {
540-
// final PsiReference reference = element.getReference();
541-
// if (reference != null) {
542-
// final PsiElement declaration = reference.resolve();
543-
// if (declaration instanceof JSGraphQLNamedTypePsiElement) {
544-
// JSGraphQLNamedTypePsiElement namedType = (JSGraphQLNamedTypePsiElement) declaration;
545-
// if (namedType.getParent() instanceof GraphQLFragmentDefinition) {
546-
// final GraphQLFragmentDefinition fragment = (GraphQLFragmentDefinition) namedType.getParent();
547-
// if (!foundFragments.contains(fragment)) {
548-
// foundFragments.add(fragment);
549-
// if (findMore != null && !findMore.apply(fragment)) {
550-
// // we're done
551-
// done = true;
552-
// return;
553-
// }
554-
// // also look for fragments inside this fragment
555-
// findFragmentsInsideOperation(fragment, foundFragments, findMore);
556-
// }
557-
// }
558-
// }
559-
// }
560-
// }
561-
// }
562-
// super.visitElement(element);
563-
// }
564-
// });
555+
operationOrFragment.accept(new PsiRecursiveElementVisitor() {
556+
557+
private boolean done = false;
558+
559+
@Override
560+
public void visitElement(PsiElement element) {
561+
if (done) {
562+
return;
563+
}
564+
if (element instanceof GraphQLFragmentSpread) {
565+
final GraphQLIdentifier fragmentSpreadName = ((GraphQLFragmentSpread) element).getNameIdentifier();
566+
final PsiReference reference = fragmentSpreadName != null ? fragmentSpreadName.getReference() : null;
567+
if (reference != null) {
568+
PsiElement fragmentDefinitionRef = reference.resolve();
569+
if (fragmentDefinitionRef instanceof GraphQLIdentifier) {
570+
if (fragmentDefinitionRef.getOriginalElement() instanceof GraphQLIdentifier) {
571+
fragmentDefinitionRef = fragmentDefinitionRef.getOriginalElement();
572+
}
573+
final GraphQLFragmentDefinition fragment = PsiTreeUtil.getParentOfType(fragmentDefinitionRef, GraphQLFragmentDefinition.class);
574+
final String fragmentKey = getFragmentKey(fragment);
575+
if (fragment != null && !foundFragments.containsKey(fragmentKey)) {
576+
foundFragments.put(fragmentKey, fragment);
577+
if (findMore != null && !findMore.apply(fragment)) {
578+
// we're done
579+
done = true;
580+
return;
581+
}
582+
// also look for fragments inside this fragment
583+
findFragmentsInsideOperation(fragment, foundFragments, findMore);
584+
}
585+
586+
}
587+
}
588+
589+
}
590+
super.visitElement(element);
591+
}
592+
});
565593
}
566594

567595
}

0 commit comments

Comments
 (0)